GUIDE 21 / 28 · NATIVE 2.0.7

Complete plugin examples

Download three runnable projects and follow concrete checks for counters, local journals and metadata webhooks.

Download and install the examples#

Download plugin examples

The archive contains Clip Counter, Clip Journal and Metadata Webhook, each with its own plugin.json and index.js. Extract the individual folders directly into %APPDATA%\Hydro Clips\plugins, then reload from the app's Plugins panel. Do not leave all three inside one extra wrapper directory.

The samples use the exact current sandbox globals and context methods. They do not require Electron or a separate npm installation. The webhook example starts with automatic sending off and an empty URL.

Clip Counter#

Demonstrates onInit, onClipSaved, onScreenshot, getConfig, saveConfig, tab registration, sidebar routing and a reset action. Save two videos and expect the counter to read 2. Enable screenshot counting and take one screenshot to reach 3.

Restart the app to check persistence. Opening the tab calls refresh; resetting changes the counter without deleting video files. The complete source appears in Your first plugin.

Clip Journal#

clip-journal/index.js
const journal = path.join(__dirname, 'clip-journal.jsonl');
async function append(clip, context) {
  const entry = { id: clip.id, name: clip.name, type: clip.type, duration: clip.duration, size: clip.size, createdAt: clip.createdAt };
  await fs.promises.appendFile(journal, JSON.stringify(entry) + '\n', 'utf8');
  context.log('Journal entry saved for ' + clip.id, 'success');
}
module.exports = {
  onInit(context) {
    context.ui.registerButton({ id: 'journal-info', label: 'Journal size', icon: 'terminal', location: 'sidebar_actions', actionId: 'info' });
    context.log('Journal: ' + journal);
  },
  onClipSaved: append,
  onScreenshot: append,
  async onAction(actionId, context) {
    if (actionId !== 'info') throw new Error('Unknown action: ' + actionId);
    let bytes = 0;
    try { bytes = (await fs.promises.stat(journal)).size; } catch (error) { if (error.code !== 'ENOENT') throw error; }
    context.showNotification('Clip Journal', bytes + ' bytes of metadata stored locally', 'info');
    return { bytes };
  }
};

Save a clip and inspect clip-journal.jsonl in the example's folder. Each new line is a JSON object with metadata; screenshots use the same append helper. The Journal size action reports bytes without parsing an unlimited file.

Metadata Webhook#

This example demonstrates optional networking outside the awaited save hook, a single-request busy guard, HTTP status checks and a bounded action wait. Configure a destination you control, invoke Test webhook, then enable automatic metadata delivery if desired.

metadata-webhook/index.js
let busy = false;
async function send(metadata, context) {
  const url = String(context.getConfig().url || '').trim();
  if (!/^https:\/\/[^\s]+$/i.test(url)) throw new Error('Configure an HTTPS endpoint first');
  if (busy) { context.log('Previous request is still pending; skipped this request', 'warn'); return { skipped: true }; }
  busy = true;
  let timer;
  try {
    const request = context.fetch(url, {
      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(metadata)
    }).then(response => { busy = false; return response; }, error => { busy = false; throw error; });
    const deadline = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Request wait exceeded 8 seconds; the request may still be pending')), 8000); });
    const response = await Promise.race([request, deadline]);
    if (!response.ok) throw new Error('HTTP ' + response.status);
    context.log('Metadata delivered', 'success');
    return { status: response.status };
  } finally { clearTimeout(timer); }
}
module.exports = {
  onInit(context) {
    context.ui.registerButton({ id: 'test-webhook', label: 'Test webhook', icon: 'cloud', location: 'sidebar_actions', actionId: 'test' });
  },
  onClipSaved(clip, context) {
    if (context.getConfig().sendOnSave !== true || !context.getConfig().url) return;
    // Return immediately so a network request does not hold up recording export.
    send({ id: clip.id, name: clip.name, type: clip.type, duration: clip.duration, createdAt: clip.createdAt }, context)
      .catch(error => context.log(error.message, 'error'));
  },
  onAction(actionId, context) {
    if (actionId !== 'test') throw new Error('Unknown action: ' + actionId);
    return send({ event: 'hydroclips-plugin-test' }, context);
  }
};

The timeout ends waiting; it does not cancel the underlying request. The busy guard stays active until that request settles. The example never uploads the video and does not log the endpoint URL.

Expected results and changes#

Start with one example at a time if you are diagnosing behavior. Check the Plugins console for one event per operation. Exercise reload and restart separately because persisted config and in-memory tab content have different lifetimes.

Use these as starting projects: add strict settings normalization, bounded data retention or service-specific payloads as needed. Read Debugging plugins when a sample does not behave as described.

Implementation reference: documentation/examples; src/main/plugins/PluginManager.ts · native 2.0.7.