1318 lines
59 KiB
JavaScript
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
|
|
}
|
|
});
|
|
})();
|