verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js

1318 lines
59 KiB
JavaScript

/* ===========================================================
Browser Plugin — Verstak v2 Frontend Bundle
Contract: window.VerstakPluginRegister(id, { components })
=========================================================== */
(function () {
'use strict';
var PLUGIN_ID = 'verstak.browser-inbox';
var CAPTURE_EVENTS = ['browser.capture.page', 'browser.capture.selection', 'browser.capture.link', 'browser.capture.file'];
var MAX_CAPTURES = 100;
var LEGACY_KEY = 'captures';
var GLOBAL_KEY = 'captures:global';
var WORKSPACE_PREFIX = 'captures:workspace:';
var MUTATION_EVENT = 'browser-inbox.storage.mutate';
function injectStyles() {
if (document.getElementById('browser-inbox-style-injected')) return;
var style = document.createElement('style');
style.id = 'browser-inbox-style-injected';
style.textContent = STYLES;
document.head.appendChild(style);
}
var STYLES = [
'.browser-inbox-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:var(--vt-color-text-primary,#f4f7fb);background:var(--vt-color-background,#101020)}',
'.browser-inbox-toolbar{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629);flex-shrink:0;flex-wrap:wrap}',
'.browser-inbox-title{font-size:.82rem;font-weight:600;color:var(--vt-color-text-primary,#f4f7fb)}',
'.browser-inbox-count{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3)}',
'.browser-inbox-filters{display:flex;align-items:center;gap:.35rem;min-width:0;flex:1;flex-wrap:wrap}',
'.browser-inbox-input,.browser-inbox-select{box-sizing:border-box;min-height:1.85rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-secondary,#b7c0d4);font:inherit;font-size:.76rem;padding:.25rem .42rem}',
'.browser-inbox-input{width:min(15rem,100%)}',
'.browser-inbox-select{max-width:12rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.65rem}',
'.browser-inbox-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}',
'.browser-inbox-input:focus,.browser-inbox-select:focus{outline:none;border-color:var(--vt-color-accent,#4ecca3);box-shadow:0 0 0 1px var(--vt-color-accent,#4ecca3)}',
'.browser-inbox-spacer{flex:1}',
'.browser-inbox-btn{font-size:.78rem;padding:.32rem .65rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);cursor:pointer}',
'.browser-inbox-btn:hover{background:var(--vt-color-surface-hover,#1b2440);border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}',
'.browser-inbox-btn:disabled{opacity:.45;cursor:default}',
'.browser-inbox-btn.danger{border-color:rgba(233,69,96,.42);color:#ff9a9a;background:var(--vt-color-danger-muted,rgba(233,69,96,.14))}',
'.browser-inbox-status{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3);white-space:nowrap}',
'.browser-inbox-status.error{display:inline-flex;border:1px solid rgba(233,69,96,.45);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-danger-muted,rgba(233,69,96,.14));color:#ffc6ce;padding:.18rem .4rem}',
'.browser-inbox-body{display:grid;grid-template-columns:minmax(260px,360px) minmax(0,1fr);flex:1;min-height:0}',
'.browser-inbox-list{min-height:0;overflow:auto;border-right:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629)}',
'.browser-inbox-empty{height:100%;display:flex;align-items:center;justify-content:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem;padding:2rem;text-align:center;line-height:1.45}',
'.browser-inbox-row{display:flex;flex-direction:column;gap:.22rem;padding:.65rem .75rem;border-bottom:1px solid rgba(32,43,70,.72);cursor:pointer}',
'.browser-inbox-row:hover{background:var(--vt-color-surface-hover,#1b2440)}',
'.browser-inbox-row.selected{background:var(--vt-color-surface-selected,rgba(78,204,163,.14));box-shadow:inset 2px 0 0 var(--vt-color-accent,#4ecca3)}',
'.browser-inbox-row-head{display:flex;align-items:center;gap:.45rem;min-width:0}',
'.browser-inbox-kind{font-size:.68rem;color:var(--vt-color-accent,#4ecca3);text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}',
'.browser-inbox-row-title{font-size:.86rem;color:var(--vt-color-text-primary,#f4f7fb);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.browser-inbox-row-url{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
'.browser-inbox-row-text{font-size:.76rem;color:var(--vt-color-text-secondary,#b7c0d4);line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}',
'.browser-inbox-row-meta{display:flex;align-items:center;gap:.35rem;min-width:0;font-size:.68rem;color:var(--vt-color-text-muted,#7f8aa3)}',
'.browser-inbox-badge{display:inline-flex;align-items:center;min-height:1.1rem;padding:0 .3rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-sm,4px);white-space:nowrap}',
'.browser-inbox-badge.unassigned{border-color:rgba(240,180,75,.5);color:#ffd37a}.browser-inbox-badge.processed{border-color:rgba(116,190,148,.45);color:#9fe0bc}',
'.browser-inbox-detail{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:auto;padding:1rem;gap:.75rem}',
'.browser-inbox-detail-empty{margin:auto;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem}',
'.browser-inbox-detail-title{font-size:1rem;font-weight:600;color:var(--vt-color-text-primary,#f4f7fb);word-break:break-word}',
'.browser-inbox-meta{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.35rem .75rem;font-size:.78rem}',
'.browser-inbox-meta-label{color:var(--vt-color-text-muted,#7f8aa3)}',
'.browser-inbox-meta-value{color:var(--vt-color-text-secondary,#b7c0d4);min-width:0;overflow-wrap:anywhere}',
'.browser-inbox-text{border:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface,#15152c);border-radius:var(--vt-radius-lg,8px);padding:.75rem;font-size:.85rem;line-height:1.5;color:var(--vt-color-text-primary,#f4f7fb);white-space:pre-wrap;overflow-wrap:anywhere}',
'.browser-inbox-detail-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
'.browser-inbox-assignment{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}',
'.browser-inbox-detail-note{font-size:.76rem;color:var(--vt-color-text-muted,#7f8aa3);line-height:1.4}',
'.browser-inbox-settings{display:grid;gap:.85rem;padding:1rem;max-width:560px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:var(--vt-color-text-primary,#f4f7fb)}',
'.browser-inbox-settings-field{display:grid;gap:.3rem}',
'.browser-inbox-settings-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}',
'.browser-inbox-settings-input{width:100%;box-sizing:border-box;padding:.48rem .58rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.78rem}',
'.browser-inbox-settings-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
'.browser-inbox-settings-status{min-height:1.15rem;font-size:.76rem;color:var(--vt-color-text-muted,#7f8aa3)}',
'.browser-inbox-settings-status.error{color:#ffc6ce}',
'@media(max-width:760px){.browser-inbox-body{grid-template-columns:1fr}.browser-inbox-list{border-right:0;border-bottom:1px solid #16213e;max-height:45vh}.browser-inbox-meta{grid-template-columns:1fr}}'
].join('\n');
function el(tag, attrs, children) {
var elem = document.createElement(tag);
if (attrs) {
Object.keys(attrs).forEach(function (k) {
if (k === 'className') elem.className = attrs[k];
else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(elem.style, attrs[k]);
else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
else if (k === 'textContent') elem.textContent = attrs[k];
else elem.setAttribute(k, attrs[k]);
});
}
if (children) {
(Array.isArray(children) ? children : [children]).forEach(function (child) {
if (child == null) return;
elem.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
});
}
return elem;
}
function text(value) {
return String(value == null ? '' : value);
}
function encodeKey(value) {
return encodeURIComponent(text(value).trim());
}
function cleanWorkspace(value) {
return text(value).trim().replace(/^\/+|\/+$/g, '');
}
function cleanDomain(value) {
return text(value).trim().toLowerCase().replace(/^\.+/, '');
}
function domainFromUrl(value) {
try {
return cleanDomain(new URL(text(value).trim()).hostname);
} catch (_) {
return '';
}
}
function domainFromCapture(capture) {
return cleanDomain(capture && capture.domain) || domainFromUrl(capture && capture.url);
}
function normalizeDomainBindings(value) {
var result = {};
if (!value || typeof value !== 'object' || Array.isArray(value)) return result;
Object.keys(value).forEach(function (domain) {
var normalizedDomain = cleanDomain(domain);
var workspaceRoot = cleanWorkspace(value[domain]);
if (normalizedDomain && workspaceRoot) result[normalizedDomain] = workspaceRoot;
});
return result;
}
function workspaceFromProps(props) {
var node = props && props.workspaceNode;
return cleanWorkspace((props && (props.workspaceRootPath || props.workspaceName || props.workspaceNodeId))
|| (node && (node.rootPath || node.name || node.id)));
}
function workspaceFromPayload(payload) {
return cleanWorkspace(payload && (payload.workspaceRootPath || payload.workspaceName || payload.workspaceNodeId));
}
function scopeFromProps(props) {
var workspaceRoot = workspaceFromProps(props);
if (!workspaceRoot) {
return { mode: 'global', key: GLOBAL_KEY, label: '', workspaceRoot: '' };
}
return {
mode: 'workspace',
key: WORKSPACE_PREFIX + encodeKey(workspaceRoot),
label: workspaceRoot,
workspaceRoot: workspaceRoot
};
}
function cleanKind(value) {
value = text(value).trim();
return value === 'selection' || value === 'link' || value === 'file' || value === 'page' ? value : 'page';
}
function displayTitle(capture, fallbackTitle) {
if (capture && capture.kind === 'file' && capture.fileName) return capture.fileName;
return capture.title || capture.url || capture.captureId || fallbackTitle || '';
}
function noteTitle(capture, fallbackTitle) {
return text((capture && (capture.title || capture.domain || capture.captureId)) || fallbackTitle).trim() || text(fallbackTitle).trim();
}
function safeNoteFilename(title) {
var base = text(title).trim()
.replace(/[\\/:*?"<>|#\[\]\r\n\t]+/g, '_')
.replace(/\s+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
return (base || 'Browser_Capture') + '.md';
}
function safeLinkFilename(title) {
return safeNoteFilename(title).replace(/\.md$/, '.url');
}
function numberedLinkFilename(title, number) {
var filename = safeLinkFilename(title);
if (number <= 1) return filename;
return filename.replace(/\.url$/, ' (' + number + ').url');
}
function safeFileFilename(name) {
var base = text(name).trim()
.replace(/[\\/:*?"<>|\r\n\t]+/g, '_')
.replace(/\s+/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
if (!base || base === '.' || base === '..') return 'browser-file.txt';
return base;
}
function captureToMarkdown(capture, fallbackTitle) {
var title = noteTitle(capture, fallbackTitle);
var lines = ['# ' + title, ''];
if (capture && capture.url) lines.push('Source: ' + capture.url);
if (capture && capture.capturedAt) lines.push('Captured: ' + capture.capturedAt);
if (capture && capture.kind) lines.push('Kind: ' + capture.kind);
if (lines[lines.length - 1] !== '') lines.push('');
if (capture && capture.text) {
lines.push(capture.text);
lines.push('');
}
return lines.join('\n');
}
function captureToUrlShortcut(capture) {
return '[InternetShortcut]\nURL=' + text(capture && capture.url).trim() + '\n';
}
function eventPayload(event) {
if (!event || !event.payload) return {};
return event.payload;
}
function captureFromEvent(event) {
var payload = eventPayload(event);
var captureId = text(payload.captureId).trim();
if (!captureId) {
captureId = 'capture-' + Date.now() + '-' + Math.random().toString(36).slice(2);
}
return {
captureId: captureId,
capturedAt: text(payload.capturedAt || event.timestamp || new Date().toISOString()),
receivedAt: new Date().toISOString(),
kind: cleanKind(payload.kind || text(event.name).replace('browser.capture.', '')),
url: text(payload.url).trim(),
title: text(payload.title).trim(),
domain: text(payload.domain).trim(),
text: text(payload.text).trim(),
fileName: text(payload.fileName).trim(),
fileMime: text(payload.fileMime).trim(),
fileSize: Number(payload.fileSize) || 0,
fileText: text(payload.fileText),
fileDataBase64: text(payload.fileDataBase64).trim(),
source: text(payload.source).trim(),
browserName: text(payload.browserName).trim(),
workspaceRootPath: workspaceFromPayload(payload),
workspaceId: text(payload.workspaceId).trim(),
workspaceState: text(payload.workspaceState || 'unassigned').trim(),
workspaceTrashId: text(payload.workspaceTrashId).trim()
};
}
function workspaceFromStorageKey(storageKey) {
var key = text(storageKey);
if (key.indexOf(WORKSPACE_PREFIX) !== 0) return '';
try {
return cleanWorkspace(decodeURIComponent(key.slice(WORKSPACE_PREFIX.length)));
} catch (_) {
return '';
}
}
function normalizeStoredCaptures(value, storageKey) {
if (!Array.isArray(value)) return [];
return value.filter(function (item) {
return item && typeof item === 'object' && item.captureId;
}).map(function (item) {
var workspaceRootPath = cleanWorkspace(item.workspaceRootPath) || workspaceFromStorageKey(storageKey);
return {
captureId: text(item.captureId),
capturedAt: text(item.capturedAt),
receivedAt: text(item.receivedAt),
kind: cleanKind(item.kind),
url: text(item.url),
title: text(item.title),
domain: text(item.domain),
text: text(item.text),
fileName: text(item.fileName),
fileMime: text(item.fileMime),
fileSize: Number(item.fileSize) || 0,
fileText: text(item.fileText),
fileDataBase64: text(item.fileDataBase64).trim(),
source: text(item.source),
browserName: text(item.browserName),
workspaceRootPath: workspaceRootPath,
workspaceName: cleanWorkspace(item.workspaceName || workspaceRootPath),
workspaceId: text(item.workspaceId),
workspaceState: text(item.workspaceState || (workspaceRootPath ? 'active' : 'unassigned')),
workspaceTrashId: text(item.workspaceTrashId),
globalState: text(item.globalState || 'inbox') === 'archived' ? 'archived' : 'inbox',
processed: item.processed === true,
_storageKey: storageKey || ''
};
}).slice(0, MAX_CAPTURES);
}
function storageCaptures(captureList) {
return captureList.map(function (item) {
return {
captureId: item.captureId,
capturedAt: item.capturedAt,
receivedAt: item.receivedAt,
kind: item.kind,
url: item.url,
title: item.title,
domain: item.domain,
text: item.text,
fileName: item.fileName,
fileMime: item.fileMime,
fileSize: item.fileSize,
fileText: item.fileText,
fileDataBase64: item.fileDataBase64,
source: item.source,
browserName: item.browserName,
workspaceRootPath: item.workspaceRootPath,
workspaceName: item.workspaceName || item.workspaceRootPath || '',
workspaceId: item.workspaceId || '',
workspaceState: item.workspaceState || (item.workspaceRootPath ? 'active' : 'unassigned'),
workspaceTrashId: item.workspaceTrashId || '',
globalState: item.globalState === 'archived' ? 'archived' : 'inbox',
processed: item.processed === true
};
});
}
function sortCaptures(captureList) {
var seen = {};
return captureList.filter(function (item) {
var key = item && item.captureId;
if (!key) return false;
if (seen[key]) return false;
seen[key] = true;
return true;
}).slice().sort(function (a, b) {
return text(b.capturedAt || b.receivedAt).localeCompare(text(a.capturedAt || a.receivedAt));
}).slice(0, MAX_CAPTURES);
}
function globalCaptureKeys(settings) {
var keys = [GLOBAL_KEY, LEGACY_KEY];
Object.keys(settings || {}).forEach(function (key) {
if (key.indexOf(WORKSPACE_PREFIX) === 0 && keys.indexOf(key) === -1) keys.push(key);
});
return keys;
}
function formatDate(value) {
if (!value) return '';
var date = new Date(value);
if (isNaN(date.getTime())) return text(value);
return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
function BrowserInboxView() {}
BrowserInboxView.mount = function (containerEl, props, api) {
injectStyles();
containerEl.innerHTML = '';
containerEl.className = 'browser-inbox-root';
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
var scope = scopeFromProps(props || {});
var captures = [];
var selectedId = '';
var statusText = '';
var statusClass = '';
var disposed = false;
var unsubscribers = [];
var domainBindings = {};
var workspaceOptions = [];
var statusFilter = 'all';
var workspaceFilter = '';
var searchQuery = '';
function tr(key, params, fallback) {
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
return fallback || key;
}
function localizedItemCount(count) {
var locale = api && api.i18n && typeof api.i18n.getLocale === 'function' ? api.i18n.getLocale() : 'en';
if (locale === 'ru') {
var mod10 = count % 10;
var mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return tr('ui.items.one', { count: count }, count + ' материал');
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return tr('ui.items.few', { count: count }, count + ' материала');
return tr('ui.items.many', { count: count }, count + ' материалов');
}
return tr(count === 1 ? 'ui.items.one' : 'ui.items.other', { count: count }, count + (count === 1 ? ' item' : ' items'));
}
function reportError(key, fallback, err) {
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
console.warn('[verstak.browser-inbox] ' + key, err);
}
statusText = tr(key, null, fallback);
statusClass = 'error';
render();
}
statusText = tr('ui.connecting', null, 'Connecting to receiver events...');
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Browser') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser · ' + scope.label) });
var countEl = el('span', { className: 'browser-inbox-count' });
var statusEl = el('span', { className: 'browser-inbox-status' });
var filtersEl = el('div', { className: 'browser-inbox-filters' });
var statusFilterEl = el('select', {
className: 'browser-inbox-select',
'data-browser-inbox-filter': 'status',
'aria-label': tr('ui.statusFilter', null, 'Material status filter'),
onChange: function (event) {
statusFilter = text(event && event.target && event.target.value) || 'all';
selectedId = '';
render();
}
}, [
el('option', { value: 'all', textContent: tr('ui.allCaptures', null, 'All captures') }),
el('option', { value: 'unassigned', textContent: tr('ui.unassigned', null, 'Unassigned') }),
el('option', { value: 'unprocessed', textContent: tr('ui.unprocessed', null, 'Unprocessed') }),
el('option', { value: 'processed', textContent: tr('ui.processed', null, 'Processed') }),
el('option', { value: 'archived', textContent: tr('ui.archive', null, 'Archive') })
]);
var workspaceFilterEl = el('select', {
className: 'browser-inbox-select',
'data-browser-inbox-filter': 'workspace',
'aria-label': tr('ui.workspaceFilter', null, 'Deal filter'),
onChange: function (event) {
workspaceFilter = cleanWorkspace(event && event.target && event.target.value);
selectedId = '';
render();
}
});
var searchInput = el('input', {
className: 'browser-inbox-input',
type: 'search',
placeholder: tr('ui.search', null, 'Search captures'),
'data-browser-inbox-filter': 'search',
'aria-label': tr('ui.search', null, 'Search captures'),
onInput: function (event) {
searchQuery = text(event && event.target && event.target.value).trim().toLowerCase();
selectedId = '';
renderList();
renderDetail();
renderCount();
}
});
var clearBtn = el('button', {
className: 'browser-inbox-btn danger',
'data-browser-inbox-action': 'clear',
textContent: tr('ui.clear', null, 'Clear'),
onClick: function () {
clearScope().then(render);
}
});
toolbar.appendChild(titleEl);
toolbar.appendChild(countEl);
filtersEl.appendChild(statusFilterEl);
if (scope.mode === 'global') {
filtersEl.appendChild(workspaceFilterEl);
} else {
filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: tr('ui.assignedHere', null, 'Assigned to this Deal') }));
}
filtersEl.appendChild(searchInput);
toolbar.appendChild(filtersEl);
toolbar.appendChild(el('span', { className: 'browser-inbox-spacer' }));
toolbar.appendChild(statusEl);
toolbar.appendChild(clearBtn);
var body = el('div', { className: 'browser-inbox-body' });
var listEl = el('div', { className: 'browser-inbox-list' });
var detailEl = el('div', { className: 'browser-inbox-detail' });
body.appendChild(listEl);
body.appendChild(detailEl);
containerEl.appendChild(toolbar);
containerEl.appendChild(body);
function option(value, label) {
return el('option', { value: value, textContent: label });
}
function workspaceRoots() {
var roots = workspaceOptions.slice();
captures.forEach(function (capture) {
var root = cleanWorkspace(capture && capture.workspaceRootPath);
if (root && roots.indexOf(root) === -1) roots.push(root);
});
if (scope.workspaceRoot && roots.indexOf(scope.workspaceRoot) === -1) roots.push(scope.workspaceRoot);
return roots.sort(function (a, b) {
return a.localeCompare(b, undefined, { sensitivity: 'base' });
});
}
function renderWorkspaceFilterOptions() {
if (scope.mode !== 'global') return;
workspaceFilterEl.innerHTML = '';
workspaceFilterEl.appendChild(option('', tr('ui.allDeals', null, 'All Deals')));
workspaceRoots().forEach(function (root) {
workspaceFilterEl.appendChild(option(root, root));
});
workspaceFilterEl.value = workspaceFilter;
}
function visibleCaptures() {
return captures.filter(function (capture) {
var workspaceRoot = cleanWorkspace(capture && capture.workspaceRootPath);
if (scope.mode === 'workspace' && workspaceRoot !== scope.workspaceRoot) return false;
if (scope.mode === 'global' && workspaceFilter && workspaceRoot !== workspaceFilter) return false;
if (statusFilter === 'archived' && capture.globalState !== 'archived') return false;
if (statusFilter !== 'archived' && capture.globalState === 'archived') return false;
if (statusFilter === 'unassigned' && workspaceRoot) return false;
if (statusFilter === 'unprocessed' && capture.processed === true) return false;
if (statusFilter === 'processed' && capture.processed !== true) return false;
if (!searchQuery) return true;
return [displayTitle(capture), capture.url, capture.domain, capture.text, workspaceRoot].join('\n').toLowerCase().indexOf(searchQuery) !== -1;
});
}
function publishMutation(action, payload, verify, verifySettings) {
if (!api || !api.events || typeof api.events.publish !== 'function') {
reportError('ui.saveError', 'Could not update browser materials. Please try again.');
return Promise.resolve(false);
}
return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () {
if (!api.settings || typeof api.settings.read !== 'function') throw new Error('settings API unavailable');
return api.settings.read();
}).then(function (settings) {
settings = settings || {};
if (typeof verifySettings === 'function' && !verifySettings(settings)) {
throw new Error('the stored inbox did not reach the expected state');
}
return loadStored(true);
}).then(function () {
if (typeof verify === 'function' && !verify()) {
throw new Error('the stored capture did not change');
}
return true;
}).catch(function (err) {
reportError('ui.saveError', 'Could not update browser materials. Please try again.', err);
return false;
});
}
function archiveCaptures(captureIds, successText) {
var ids = {};
captureIds.forEach(function (captureId) {
ids[captureId] = true;
});
return publishMutation('archive', { captureIds: captureIds }, function () {
return captures.every(function (capture) {
return !ids[capture.captureId] || capture.globalState === 'archived';
});
}).then(function (saved) {
if (!saved) return;
if (ids[selectedId]) selectedId = '';
statusText = successText;
statusClass = '';
render();
});
}
function clearScope() {
var ids = scope.mode === 'global'
? captures.map(function (capture) { return capture.captureId; })
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).map(function (capture) { return capture.captureId; });
return archiveCaptures(ids, scope.mode === 'global'
? tr('ui.inboxArchived', null, 'Inbox archived')
: tr('ui.workspaceCapturesArchived', null, 'Deal materials archived'));
}
function selectedCapture() {
var visible = visibleCaptures();
for (var i = 0; i < visible.length; i += 1) {
if (visible[i].captureId === selectedId) return visible[i];
}
return visible[0] || null;
}
function addCapture(capture) {
capture = applyDomainBinding(capture);
var existing = captures.some(function (item) {
return item.captureId === capture.captureId;
});
if (existing) return Promise.resolve();
selectedId = capture.captureId;
statusText = tr('ui.captureReceived', null, 'Material received');
statusClass = '';
render();
return Promise.resolve();
}
function applyDomainBinding(capture) {
if (!capture || capture.workspaceRootPath) return capture;
var workspaceRoot = domainBindings[domainFromCapture(capture)];
if (!workspaceRoot) return capture;
capture.workspaceRootPath = workspaceRoot;
capture.workspaceName = workspaceRoot;
return capture;
}
function assignWorkspace(captureId, workspaceRoot) {
workspaceRoot = cleanWorkspace(workspaceRoot);
return publishMutation('assign', {
captureId: captureId,
workspaceRootPath: workspaceRoot
}, function () {
return captures.some(function (capture) {
return capture.captureId === captureId && cleanWorkspace(capture.workspaceRootPath) === workspaceRoot;
});
}).then(function (saved) {
if (!saved) return;
if (workspaceRoot && workspaceOptions.indexOf(workspaceRoot) === -1) workspaceOptions.push(workspaceRoot);
statusText = workspaceRoot
? tr('ui.assignedToWorkspace', { workspace: workspaceRoot }, 'Material assigned to ' + workspaceRoot)
: tr('ui.captureUnassigned', null, 'Material is unassigned');
statusClass = '';
render();
});
}
function archiveCapture(captureId) {
return archiveCaptures([captureId], tr('ui.captureArchived', null, 'Material archived'));
}
function restoreCapture(captureId) {
return publishMutation('restore', { captureId: captureId }, function () {
return captures.some(function (capture) {
return capture.captureId === captureId && capture.globalState === 'inbox';
});
}).then(function (saved) {
if (!saved) return;
statusText = tr('ui.captureRestored', null, 'Material restored to Inbox');
statusClass = '';
render();
});
}
function permanentlyDeleteCapture(captureId) {
return publishMutation('delete', { captureId: captureId, permanent: true }, function () {
return !captures.some(function (capture) { return capture.captureId === captureId; });
}).then(function (saved) {
if (!saved) return;
if (selectedId === captureId) selectedId = '';
statusText = tr('ui.captureDeleted', null, 'Material permanently deleted');
statusClass = '';
render();
});
}
function setProcessed(captureId, processed) {
return publishMutation('processed', {
captureId: captureId,
processed: processed === true
}, function () {
return captures.some(function (capture) {
return capture.captureId === captureId && capture.processed === (processed === true);
});
}).then(function (saved) {
if (!saved) return;
statusText = processed
? tr('ui.captureProcessed', null, 'Material marked processed')
: tr('ui.captureUnprocessed', null, 'Material marked unprocessed');
statusClass = '';
render();
});
}
function createNoteFromCapture(capture) {
if (!capture || !capture.workspaceRootPath) return Promise.resolve();
if (!api || !api.files || typeof api.files.writeText !== 'function') {
reportError('ui.createNoteError', 'Could not create the note. Please try again.');
return Promise.resolve();
}
var title = noteTitle(capture, tr('ui.untitledCapture', null, 'Untitled material'));
var notePath = capture.workspaceRootPath + '/Notes/' + safeNoteFilename(title);
statusText = tr('ui.creatingNote', null, 'Creating note...');
statusClass = '';
render();
return api.files.writeText(notePath, captureToMarkdown(capture, tr('ui.untitledCapture', null, 'Untitled material')), {
createIfMissing: true,
overwrite: false
}).then(function () {
if (api.events && typeof api.events.publish === 'function') {
return api.events.publish('browser.capture.converted', {
captureId: capture.captureId,
conversionType: 'note',
notePath: notePath,
workspaceRootPath: capture.workspaceRootPath,
title: title,
url: capture.url || '',
sourcePluginId: PLUGIN_ID
});
}
return undefined;
}).then(function () {
statusText = tr('ui.noteCreated', { path: notePath }, 'Note created: ' + notePath);
statusClass = '';
return archiveCapture(capture.captureId);
}).catch(function (err) {
reportError('ui.createNoteError', 'Could not create the note. Please try again.', err);
});
}
function createLinkFromCapture(capture) {
if (!capture || !capture.workspaceRootPath || !capture.url) return Promise.resolve();
if (!api || !api.files || typeof api.files.writeText !== 'function') {
reportError('ui.createLinkError', 'Could not create the link. Please try again.');
return Promise.resolve();
}
var title = noteTitle(capture, tr('ui.untitledCapture', null, 'Untitled material'));
statusText = tr('ui.creatingLink', null, 'Creating link...');
statusClass = '';
render();
function writeLink(number) {
var linkPath = capture.workspaceRootPath + '/Links/' + numberedLinkFilename(title, number);
return api.files.writeText(linkPath, captureToUrlShortcut(capture), {
createIfMissing: true,
overwrite: false
}).then(function () {
return linkPath;
}).catch(function (error) {
if (number < 99 && /^conflict:/.test(text(error && error.message ? error.message : error))) {
return writeLink(number + 1);
}
throw error;
});
}
return writeLink(1).then(function (linkPath) {
if (api.events && typeof api.events.publish === 'function') {
return api.events.publish('browser.capture.converted', {
captureId: capture.captureId,
conversionType: 'link',
linkPath: linkPath,
workspaceRootPath: capture.workspaceRootPath,
title: title,
url: capture.url || '',
sourcePluginId: PLUGIN_ID
}).then(function () {
return linkPath;
});
}
return linkPath;
}).then(function () {
statusText = tr('ui.linkCreated', null, 'Link created');
statusClass = '';
return archiveCapture(capture.captureId);
}).catch(function (err) {
reportError('ui.createLinkError', 'Could not create the link. Please try again.', err);
});
}
function openCaptureURL(capture) {
if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve();
return api.files.openURL(capture.url).catch(function (err) {
reportError('ui.openLinkError', 'Could not open the link. Please try again.', err);
});
}
function createFileFromCapture(capture) {
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve();
if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
reportError('ui.createFileError', 'Could not create the file. Please try again.');
return Promise.resolve();
}
var fileName = safeFileFilename(capture.fileName);
var filePath = capture.workspaceRootPath + '/Files/' + fileName;
statusText = tr('ui.creatingFile', null, 'Creating file...');
statusClass = '';
render();
var writeOptions = {
createIfMissing: true,
overwrite: false
};
var writePromise = capture.fileDataBase64
? api.files.writeBytes(filePath, capture.fileDataBase64, writeOptions)
: api.files.writeText(filePath, capture.fileText, writeOptions);
return writePromise.then(function () {
if (api.events && typeof api.events.publish === 'function') {
return api.events.publish('browser.capture.converted', {
captureId: capture.captureId,
conversionType: 'file',
filePath: filePath,
workspaceRootPath: capture.workspaceRootPath,
title: displayTitle(capture),
url: capture.url || '',
fileName: capture.fileName || '',
fileMime: capture.fileMime || '',
fileSize: capture.fileSize || 0,
sourcePluginId: PLUGIN_ID
});
}
return undefined;
}).then(function () {
statusText = tr('ui.fileCreated', { path: filePath }, 'File created: ' + filePath);
statusClass = '';
return archiveCapture(capture.captureId);
}).catch(function (err) {
reportError('ui.createFileError', 'Could not create the file. Please try again.', err);
});
}
function renderList() {
listEl.innerHTML = '';
var visible = visibleCaptures();
if (visible.length === 0) {
var emptyText = captures.length === 0
? tr('ui.empty', null, 'No browser materials yet. Send a page, selection, or link from the extension.')
: tr('ui.emptyFiltered', null, 'No materials match the current filters.');
listEl.appendChild(el('div', { className: 'browser-inbox-empty', textContent: emptyText }));
return;
}
visible.forEach(function (capture) {
var workspaceRoot = cleanWorkspace(capture.workspaceRootPath);
var row = el('div', {
className: 'browser-inbox-row' + (capture.captureId === selectedId ? ' selected' : '') + (capture.processed ? ' processed' : ''),
'data-browser-capture-id': capture.captureId,
onClick: function () {
selectedId = capture.captureId;
render();
}
}, [
el('div', { className: 'browser-inbox-row-head' }, [
el('span', { className: 'browser-inbox-kind', textContent: capture.kind }),
el('span', { className: 'browser-inbox-row-title', textContent: displayTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')) })
]),
el('div', { className: 'browser-inbox-row-url', textContent: capture.url || capture.domain || capture.captureId })
]);
row.appendChild(el('div', { className: 'browser-inbox-row-meta' }, [
el('span', {
className: 'browser-inbox-badge' + (workspaceRoot ? '' : ' unassigned'),
textContent: workspaceRoot || tr('ui.unassigned', null, 'Unassigned')
}),
el('span', {
className: 'browser-inbox-badge' + (capture.processed ? ' processed' : ''),
textContent: capture.processed
? tr('ui.processed', null, 'Processed')
: tr('ui.unprocessed', null, 'Unprocessed')
}),
capture.globalState === 'archived' ? el('span', { className: 'browser-inbox-badge', textContent: tr('ui.archive', null, 'Archive') }) : null
]));
if (capture.text) {
row.appendChild(el('div', { className: 'browser-inbox-row-text', textContent: capture.text }));
}
listEl.appendChild(row);
});
}
function renderDetail() {
detailEl.innerHTML = '';
var capture = selectedCapture();
if (!capture) {
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-empty', textContent: tr('ui.selectCapture', null, 'Select a capture to inspect it.') }));
return;
}
selectedId = capture.captureId;
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')) }));
detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.kind', null, 'Kind') }),
el('div', { className: 'browser-inbox-meta-value', textContent: capture.kind }),
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.url', null, 'URL') }),
el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }),
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.domain', null, 'Domain') }),
el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }),
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.captured', null, 'Captured') }),
el('div', { className: 'browser-inbox-meta-value', textContent: formatDate(capture.capturedAt) || '-' }),
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.browser', null, 'Browser') }),
el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }),
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.workspace', null, 'Deal') }),
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || tr('ui.unassigned', null, 'Unassigned') }),
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.status', null, 'Status') }),
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed
? tr('ui.processed', null, 'Processed')
: tr('ui.unprocessed', null, 'Unprocessed') })
]));
var assignmentSelect = el('select', {
className: 'browser-inbox-select',
'data-browser-inbox-assignment': capture.captureId,
'aria-label': tr('ui.assignment', null, 'Assign to Deal'),
onChange: function (event) {
assignWorkspace(capture.captureId, event && event.target && event.target.value);
}
});
assignmentSelect.appendChild(option('', tr('ui.unassigned', null, 'Unassigned')));
workspaceRoots().forEach(function (workspaceRoot) {
assignmentSelect.appendChild(option(workspaceRoot, workspaceRoot));
});
assignmentSelect.value = capture.workspaceRootPath || '';
var assignmentControls = [
el('span', { className: 'browser-inbox-meta-label', textContent: tr('ui.assignment', null, 'Assign to Deal') }),
assignmentSelect
];
if (capture.workspaceRootPath) {
assignmentControls.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'clear-assignment',
textContent: tr('ui.clearAssignment', null, 'Clear assignment'),
onClick: function () {
assignWorkspace(capture.captureId, '');
}
}));
}
detailEl.appendChild(el('div', { className: 'browser-inbox-assignment' }, assignmentControls));
if (!capture.workspaceRootPath) {
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-note', textContent: tr('ui.assignBeforeCreation', null, 'Assign a Deal before creating a note, link, or file.') }));
}
if (capture.text) {
detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.text }));
}
if (capture.kind === 'file' && capture.fileText) {
detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.fileText }));
}
var actionButtons = [];
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'toggle-processed',
textContent: capture.processed
? tr('ui.markUnprocessed', null, 'Mark unprocessed')
: tr('ui.markProcessed', null, 'Mark processed'),
onClick: function () {
setProcessed(capture.captureId, !capture.processed);
}
}));
if (capture.url) {
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'open-link',
textContent: tr('ui.openLink', null, 'Open link'),
onClick: function () {
openCaptureURL(capture);
}
}));
}
if (capture.workspaceRootPath) {
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'create-note',
textContent: tr('ui.createNote', null, 'Create Note'),
onClick: function () {
createNoteFromCapture(capture);
}
}));
if (capture.url) {
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'create-link',
textContent: tr('ui.createLink', null, 'Create Link'),
onClick: function () {
createLinkFromCapture(capture);
}
}));
}
if (capture.kind === 'file' && capture.fileName && (capture.fileText || capture.fileDataBase64)) {
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'create-file',
textContent: tr('ui.createFile', null, 'Create File'),
onClick: function () {
createFileFromCapture(capture);
}
}));
}
}
if (capture.globalState === 'archived') {
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'restore',
textContent: tr('ui.restore', null, 'Restore to Inbox'),
onClick: function () {
restoreCapture(capture.captureId);
}
}));
} else {
actionButtons.push(el('button', {
className: 'browser-inbox-btn',
'data-browser-inbox-action': 'archive',
textContent: tr('ui.archive', null, 'Archive'),
onClick: function () {
archiveCapture(capture.captureId);
}
}));
}
actionButtons.push(el('button', {
className: 'browser-inbox-btn danger',
'data-browser-inbox-action': 'delete-permanently',
textContent: tr('ui.deletePermanently', null, 'Delete permanently'),
onClick: function () {
permanentlyDeleteCapture(capture.captureId);
}
}));
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-actions' }, actionButtons));
}
function renderCount() {
var visibleCount = visibleCaptures().length;
var total = captures.length;
countEl.textContent = visibleCount === total
? localizedItemCount(total)
: tr('ui.items.filtered', {
visible: localizedItemCount(visibleCount),
total: localizedItemCount(total)
}, localizedItemCount(visibleCount) + ' of ' + localizedItemCount(total));
var scopeCount = scope.mode === 'global'
? total
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).length;
clearBtn.disabled = scopeCount === 0;
}
function render() {
statusFilterEl.value = statusFilter;
searchInput.value = searchQuery;
renderWorkspaceFilterOptions();
renderCount();
statusEl.textContent = statusText;
statusEl.className = 'browser-inbox-status' + (statusClass ? ' ' + statusClass : '');
renderList();
renderDetail();
}
function loadStored(skipMigration) {
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
return api.settings.read().then(function (settings) {
domainBindings = normalizeDomainBindings((settings || {}).domainBindings);
var keys = globalCaptureKeys(settings || {});
var all = [];
var hasLegacyCaptures = false;
keys.forEach(function (key) {
var stored = normalizeStoredCaptures((settings || {})[key], key);
if (key !== GLOBAL_KEY && stored.length > 0) hasLegacyCaptures = true;
all = all.concat(stored);
});
captures = sortCaptures(all).map(applyDomainBinding);
if (!selectedId && captures[0]) selectedId = captures[0].captureId;
// Read legacy records once, then ask the backend to migrate them atomically.
if (!hasLegacyCaptures || skipMigration) return undefined;
var expectedIds = captures.map(function (capture) { return capture.captureId; });
return publishMutation('migrate', {}, function () {
return expectedIds.every(function (captureId) {
return captures.some(function (capture) { return capture.captureId === captureId; });
});
}, function (migratedSettings) {
var canonicalIds = normalizeStoredCaptures(migratedSettings[GLOBAL_KEY], GLOBAL_KEY).map(function (capture) {
return capture.captureId;
});
var legacyIsEmpty = globalCaptureKeys(migratedSettings).filter(function (key) {
return key !== GLOBAL_KEY;
}).every(function (key) {
return normalizeStoredCaptures(migratedSettings[key], key).length === 0;
});
return legacyIsEmpty && expectedIds.every(function (captureId) {
return canonicalIds.indexOf(captureId) !== -1;
});
});
}).catch(function (err) {
reportError('ui.loadError', 'Could not load browser materials. Please try again.', err);
});
}
function loadWorkspaceOptions() {
if (!api || !api.files || typeof api.files.list !== 'function') return Promise.resolve();
return api.files.list('').then(function (entries) {
workspaceOptions = (Array.isArray(entries) ? entries : []).filter(function (entry) {
return text(entry && entry.type).toLowerCase() === 'folder';
}).map(function (entry) {
return cleanWorkspace(entry.relativePath || entry.name);
}).filter(function (workspaceRoot) {
return workspaceRoot && workspaceRoot.indexOf('/') === -1;
});
}).catch(function () {
workspaceOptions = [];
});
}
function subscribeEvents() {
if (!api || !api.events || typeof api.events.subscribe !== 'function') return Promise.resolve();
return Promise.all(CAPTURE_EVENTS.map(function (eventName) {
return api.events.subscribe(eventName, function (event) {
var received = captureFromEvent(event);
return loadStored(true).then(function () {
var stored = captures.find(function (capture) {
return capture.captureId === received.captureId;
});
if (!stored) return addCapture(received);
if (!received.workspaceRootPath && stored.workspaceRootPath) {
return publishMutation('assign', {
captureId: stored.captureId,
workspaceRootPath: stored.workspaceRootPath
}, function () {
return captures.some(function (capture) {
return capture.captureId === stored.captureId && capture.workspaceRootPath === stored.workspaceRootPath;
});
}).then(function (saved) {
if (!saved) return;
selectedId = received.captureId;
statusText = tr('ui.captureReceived', null, 'Material received');
statusClass = '';
render();
});
}
selectedId = received.captureId;
statusText = tr('ui.captureReceived', null, 'Material received');
statusClass = '';
render();
return undefined;
});
}).then(function (unsubscribe) {
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
});
})).then(function () {
statusText = scope.mode === 'global'
? tr('ui.receiverReadyAll', null, 'Receiver ready for all Deals')
: tr('ui.receiverReadyWorkspace', null, 'Receiver ready for this Deal');
statusClass = '';
}).catch(function (err) {
reportError('ui.receiverError', 'The browser receiver is unavailable. Please try again.', err);
});
}
render();
Promise.all([loadStored(), loadWorkspaceOptions()]).then(function () {
if (disposed) return;
render();
return subscribeEvents();
}).then(function () {
if (!disposed) render();
});
if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
api.i18n.onDidChangeLocale(function () {
titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Browser') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser · ' + scope.label);
searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search captures'));
statusFilterEl.setAttribute('aria-label', tr('ui.statusFilter', null, 'Material status filter'));
workspaceFilterEl.setAttribute('aria-label', tr('ui.workspaceFilter', null, 'Deal filter'));
searchInput.setAttribute('aria-label', tr('ui.search', null, 'Search captures'));
clearBtn.textContent = tr('ui.clear', null, 'Clear');
render();
});
}
containerEl.__browserInboxUnmount = function () {
disposed = true;
while (unsubscribers.length > 0) {
try {
unsubscribers.pop()();
} catch (err) {
console.error('[BrowserInbox] unsubscribe error:', err);
}
}
};
};
BrowserInboxView.unmount = function (containerEl) {
if (containerEl && typeof containerEl.__browserInboxUnmount === 'function') {
containerEl.__browserInboxUnmount();
delete containerEl.__browserInboxUnmount;
}
};
var BrowserInboxSettings = {
mount: function (containerEl, props, api) {
injectStyles();
containerEl.innerHTML = '';
containerEl.className = 'browser-inbox-settings';
function tr(key, params, fallback) {
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
return fallback || key;
}
var receiverURLInput = el('input', {
className: 'browser-inbox-settings-input',
type: 'text',
readonly: 'readonly',
spellcheck: 'false',
autocomplete: 'off',
'data-browser-inbox-pairing-url': ''
});
var receiverTokenInput = el('input', {
className: 'browser-inbox-settings-input',
type: 'text',
readonly: 'readonly',
spellcheck: 'false',
autocomplete: 'off',
'data-browser-inbox-pairing-token': ''
});
var statusEl = el('div', { className: 'browser-inbox-settings-status' });
var copyURLButton = el('button', {
className: 'browser-inbox-btn',
type: 'button',
'data-browser-inbox-settings-action': 'copy-url',
textContent: tr('ui.copyUrl', null, 'Copy URL')
});
var copyTokenButton = el('button', {
className: 'browser-inbox-btn',
type: 'button',
'data-browser-inbox-settings-action': 'copy-token',
textContent: tr('ui.copyToken', null, 'Copy Token')
});
var rotateTokenButton = el('button', {
className: 'browser-inbox-btn danger',
type: 'button',
'data-browser-inbox-settings-action': 'rotate-token',
textContent: tr('ui.rotateToken', null, 'Rotate Token')
});
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
el('label', { className: 'browser-inbox-settings-label', textContent: tr('ui.receiverUrl', null, 'Receiver URL') }),
receiverURLInput
]));
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
el('label', { className: 'browser-inbox-settings-label', textContent: tr('ui.pairingToken', null, 'Pairing Token') }),
receiverTokenInput
]));
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-actions' }, [
copyURLButton,
copyTokenButton,
rotateTokenButton
]));
containerEl.appendChild(statusEl);
function setStatus(message, isError) {
statusEl.textContent = message || '';
statusEl.className = 'browser-inbox-settings-status' + (isError ? ' error' : '');
}
function reportError(key, fallback, err) {
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
console.warn('[verstak.browser-inbox.settings] ' + key, err);
}
setStatus(tr(key, null, fallback), true);
}
function setBusy(busy) {
copyURLButton.disabled = busy;
copyTokenButton.disabled = busy;
rotateTokenButton.disabled = busy;
}
function pairingAPI() {
if (!api || !api.browserReceiver) throw new Error('Browser receiver API is unavailable');
return api.browserReceiver;
}
function applyPairing(pairing) {
receiverURLInput.value = text(pairing && pairing.receiverUrl).trim();
receiverTokenInput.value = text(pairing && pairing.receiverToken).trim();
}
function loadPairing() {
setBusy(true);
setStatus(tr('ui.loading', null, 'Loading...'), false);
return Promise.resolve().then(function () {
return pairingAPI().pairing();
}).then(function (pairing) {
applyPairing(pairing);
setStatus('', false);
}).catch(function (err) {
reportError('ui.pairingLoadError', 'Could not load browser connection settings. Please try again.', err);
}).then(function () {
setBusy(false);
});
}
function copyValue(value, label) {
if (!value) return;
if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
setStatus(tr('ui.clipboardUnavailable', null, 'Clipboard unavailable'), true);
return;
}
navigator.clipboard.writeText(value).then(function () {
setStatus(tr('ui.copied', { label: label }, '{label} copied'), false);
}).catch(function (err) {
reportError('ui.clipboardError', 'Could not copy to the clipboard. Please try again.', err);
});
}
copyURLButton.addEventListener('click', function () {
copyValue(receiverURLInput.value, 'URL');
});
copyTokenButton.addEventListener('click', function () {
copyValue(receiverTokenInput.value, 'Token');
});
rotateTokenButton.addEventListener('click', function () {
if (typeof window.confirm === 'function' && !window.confirm(tr('ui.rotateConfirm', null, 'Rotate pairing token?'))) return;
setBusy(true);
setStatus(tr('ui.rotating', null, 'Rotating...'), false);
Promise.resolve().then(function () {
return pairingAPI().rotateToken();
}).then(function (pairing) {
applyPairing(pairing);
setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false);
}).catch(function (err) {
reportError('ui.tokenRotateError', 'Could not rotate the pairing token. Please try again.', err);
}).then(function () {
setBusy(false);
});
});
loadPairing();
if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
api.i18n.onDidChangeLocale(function () {
copyURLButton.textContent = tr('ui.copyUrl', null, 'Copy URL');
copyTokenButton.textContent = tr('ui.copyToken', null, 'Copy Token');
rotateTokenButton.textContent = tr('ui.rotateToken', null, 'Rotate Token');
});
}
},
unmount: function (containerEl) {
if (containerEl) containerEl.innerHTML = '';
}
};
window.VerstakPluginRegister(PLUGIN_ID, {
components: {
BrowserInboxView: BrowserInboxView,
BrowserInboxSettings: BrowserInboxSettings
}
});
})();