feat: localize official plugins in Russian
This commit is contained in:
parent
077de94e61
commit
3dd0ec9f1f
|
|
@ -409,19 +409,23 @@
|
||||||
var candidateSourceEvents = [];
|
var candidateSourceEvents = [];
|
||||||
var candidates = [];
|
var candidates = [];
|
||||||
var dismissedByWorkspace = {};
|
var dismissedByWorkspace = {};
|
||||||
var statusText = 'Loading activity...';
|
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 statusText = tr('ui.loading', null, 'Loading activity...');
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
var unsubscribers = [];
|
var unsubscribers = [];
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'activity-toolbar' });
|
var toolbar = el('div', { className: 'activity-toolbar' });
|
||||||
var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? 'Activity' : 'Activity · ' + scope.label });
|
var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label) });
|
||||||
var countEl = el('span', { className: 'activity-count' });
|
var countEl = el('span', { className: 'activity-count' });
|
||||||
var statusEl = el('span', { className: 'activity-status' });
|
var statusEl = el('span', { className: 'activity-status' });
|
||||||
var clearBtn = el('button', {
|
var clearBtn = el('button', {
|
||||||
className: 'activity-btn danger',
|
className: 'activity-btn danger',
|
||||||
'data-activity-action': 'clear',
|
'data-activity-action': 'clear',
|
||||||
textContent: 'Clear',
|
textContent: tr('ui.clear', null, 'Clear'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
clearGlobal().then(render);
|
clearGlobal().then(render);
|
||||||
|
|
@ -489,7 +493,7 @@
|
||||||
? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
||||||
: events;
|
: events;
|
||||||
return api.settings.write(scope.key, storageEvents(toStore)).then(persistCandidateCaches).catch(function (err) {
|
return api.settings.write(scope.key, storageEvents(toStore)).then(persistCandidateCaches).catch(function (err) {
|
||||||
statusText = 'Could not save activity: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save activity: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -513,10 +517,10 @@
|
||||||
return api.settings.write(key, []);
|
return api.settings.write(key, []);
|
||||||
}));
|
}));
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
statusText = 'Activity cleared';
|
statusText = tr('ui.cleared', null, 'Activity cleared');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not clear activity: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.clearError', { error: err && err.message ? err.message : String(err) }, 'Could not clear activity: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -525,8 +529,8 @@
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
listEl.appendChild(el('div', { className: 'activity-empty' }, [
|
listEl.appendChild(el('div', { className: 'activity-empty' }, [
|
||||||
el('div', { className: 'activity-empty-title', textContent: 'No activity events yet' }),
|
el('div', { className: 'activity-empty-title', textContent: tr('ui.empty', null, 'No activity events yet') }),
|
||||||
el('div', { textContent: 'File changes, browser captures, and conversions will appear here.' })
|
el('div', { textContent: tr('ui.emptyHint', null, 'File changes, browser captures, and conversions will appear here.') })
|
||||||
]));
|
]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -543,8 +547,8 @@
|
||||||
]),
|
]),
|
||||||
activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null,
|
activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null,
|
||||||
el('details', { className: 'activity-details' }, [
|
el('details', { className: 'activity-details' }, [
|
||||||
el('summary', {}, ['Details']),
|
el('summary', {}, [tr('ui.details', null, 'Details')]),
|
||||||
el('div', { className: 'activity-source', textContent: 'Event: ' + activity.type + (activity.sourcePluginId ? ' · Source: ' + activity.sourcePluginId : '') })
|
el('div', { className: 'activity-source', textContent: tr('ui.eventSource', { event: activity.type, source: activity.sourcePluginId ? ' · ' + activity.sourcePluginId : '' }, 'Event: ' + activity.type + (activity.sourcePluginId ? ' · Source: ' + activity.sourcePluginId : '')) })
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
|
|
@ -574,7 +578,7 @@
|
||||||
persistDismissals(candidate.workspaceRootPath),
|
persistDismissals(candidate.workspaceRootPath),
|
||||||
persistCandidateCache(candidate.workspaceRootPath)
|
persistCandidateCache(candidate.workspaceRootPath)
|
||||||
]).then(function () {
|
]).then(function () {
|
||||||
statusText = 'Candidate dismissed';
|
statusText = tr('ui.dismissed', null, 'Candidate dismissed');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not dismiss candidate: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Could not dismiss candidate: ' + (err && err.message ? err.message : String(err));
|
||||||
|
|
@ -590,14 +594,14 @@
|
||||||
}
|
}
|
||||||
if (typeof candidatesEl.removeAttribute === 'function') candidatesEl.removeAttribute('hidden');
|
if (typeof candidatesEl.removeAttribute === 'function') candidatesEl.removeAttribute('hidden');
|
||||||
else delete candidatesEl.attributes.hidden;
|
else delete candidatesEl.attributes.hidden;
|
||||||
candidatesEl.appendChild(el('div', { className: 'activity-candidates-title', textContent: 'Possible journal entries' }));
|
candidatesEl.appendChild(el('div', { className: 'activity-candidates-title', textContent: tr('ui.candidates', null, 'Possible journal entries') }));
|
||||||
candidates.forEach(function (candidate) {
|
candidates.forEach(function (candidate) {
|
||||||
candidatesEl.appendChild(el('div', {
|
candidatesEl.appendChild(el('div', {
|
||||||
className: 'activity-candidate',
|
className: 'activity-candidate',
|
||||||
'data-work-session-candidate': candidate.candidateId
|
'data-work-session-candidate': candidate.candidateId
|
||||||
}, [
|
}, [
|
||||||
el('div', {}, [
|
el('div', {}, [
|
||||||
el('div', { className: 'activity-candidate-title', textContent: 'Possible journal entry' }),
|
el('div', { className: 'activity-candidate-title', textContent: tr('ui.candidate', null, 'Possible journal entry') }),
|
||||||
el('div', { className: 'activity-candidate-facts' }, [
|
el('div', { className: 'activity-candidate-facts' }, [
|
||||||
el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }),
|
el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }),
|
||||||
el('div', { textContent: 'Time: ' + candidateTimeRange(candidate) }),
|
el('div', { textContent: 'Time: ' + candidateTimeRange(candidate) }),
|
||||||
|
|
@ -612,8 +616,8 @@
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'activity-candidate-actions' }, [
|
el('div', { className: 'activity-candidate-actions' }, [
|
||||||
el('div', { className: 'activity-candidate-duration', textContent: candidate.estimatedMinutes + ' min' }),
|
el('div', { className: 'activity-candidate-duration', textContent: candidate.estimatedMinutes + ' min' }),
|
||||||
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'review', textContent: 'Review', onClick: function () { reviewCandidate(candidate); } }),
|
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'review', textContent: tr('ui.review', null, 'Review'), onClick: function () { reviewCandidate(candidate); } }),
|
||||||
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'dismiss', textContent: 'Dismiss', onClick: function () { dismissCandidate(candidate); } })
|
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'dismiss', textContent: tr('ui.dismiss', null, 'Dismiss'), onClick: function () { dismissCandidate(candidate); } })
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
@ -696,6 +700,13 @@
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
if (!disposed) render();
|
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, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label);
|
||||||
|
clearBtn.textContent = tr('ui.clear', null, 'Clear');
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
containerEl.__activityUnmount = function () {
|
containerEl.__activityUnmount = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Activity",
|
||||||
|
"manifest.description": "Workspace-scoped activity log for public plugin events.",
|
||||||
|
"contributions.views.verstak.activity.view.title": "Activity",
|
||||||
|
"contributions.sidebarItems.verstak.activity.sidebar.title": "Activity",
|
||||||
|
"contributions.workspaceItems.verstak.activity.workspace.title": "Activity",
|
||||||
|
"contributions.commands.verstak.activity.suggestWorklog.title": "List Possible Journal Entries",
|
||||||
|
"ui.loading": "Loading activity...",
|
||||||
|
"ui.title": "Activity",
|
||||||
|
"ui.workspaceTitle": "Activity · {workspace}",
|
||||||
|
"ui.clear": "Clear",
|
||||||
|
"ui.saveError": "Could not save activity: {error}",
|
||||||
|
"ui.cleared": "Activity cleared",
|
||||||
|
"ui.clearError": "Could not clear activity: {error}",
|
||||||
|
"ui.empty": "No activity events yet",
|
||||||
|
"ui.emptyHint": "File changes, browser captures, and conversions will appear here.",
|
||||||
|
"ui.details": "Details",
|
||||||
|
"ui.eventSource": "Event: {event}{source}",
|
||||||
|
"ui.dismissed": "Candidate dismissed",
|
||||||
|
"ui.candidates": "Possible journal entries",
|
||||||
|
"ui.candidate": "Possible journal entry",
|
||||||
|
"ui.review": "Review",
|
||||||
|
"ui.dismiss": "Dismiss"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Активность",
|
||||||
|
"manifest.description": "Журнал публичных событий плагинов в рабочем пространстве.",
|
||||||
|
"contributions.views.verstak.activity.view.title": "Активность",
|
||||||
|
"contributions.sidebarItems.verstak.activity.sidebar.title": "Активность",
|
||||||
|
"contributions.workspaceItems.verstak.activity.workspace.title": "Активность",
|
||||||
|
"contributions.commands.verstak.activity.suggestWorklog.title": "Показать возможные записи журнала",
|
||||||
|
"ui.loading": "Загрузка активности...",
|
||||||
|
"ui.title": "Активность",
|
||||||
|
"ui.workspaceTitle": "Активность · {workspace}",
|
||||||
|
"ui.clear": "Очистить",
|
||||||
|
"ui.saveError": "Не удалось сохранить активность: {error}",
|
||||||
|
"ui.cleared": "Активность очищена",
|
||||||
|
"ui.clearError": "Не удалось очистить активность: {error}",
|
||||||
|
"ui.empty": "Событий активности пока нет",
|
||||||
|
"ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.",
|
||||||
|
"ui.details": "Подробнее",
|
||||||
|
"ui.eventSource": "Событие: {event}{source}",
|
||||||
|
"ui.dismissed": "Кандидат скрыт",
|
||||||
|
"ui.candidates": "Возможные записи журнала",
|
||||||
|
"ui.candidate": "Возможная запись журнала",
|
||||||
|
"ui.review": "Проверить",
|
||||||
|
"ui.dismiss": "Скрыть"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped activity log for public plugin events.",
|
"description": "Workspace-scoped activity log for public plugin events.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "activity",
|
"icon": "activity",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -354,9 +354,14 @@
|
||||||
var statusFilter = 'all';
|
var statusFilter = 'all';
|
||||||
var workspaceFilter = '';
|
var workspaceFilter = '';
|
||||||
var searchQuery = '';
|
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;
|
||||||
|
}
|
||||||
|
statusText = tr('ui.connecting', null, 'Connecting to receiver events...');
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
||||||
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? 'Browser Inbox' : 'Browser Inbox · ' + scope.label });
|
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Browser Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label) });
|
||||||
var countEl = el('span', { className: 'browser-inbox-count' });
|
var countEl = el('span', { className: 'browser-inbox-count' });
|
||||||
var statusEl = el('span', { className: 'browser-inbox-status' });
|
var statusEl = el('span', { className: 'browser-inbox-status' });
|
||||||
var filtersEl = el('div', { className: 'browser-inbox-filters' });
|
var filtersEl = el('div', { className: 'browser-inbox-filters' });
|
||||||
|
|
@ -370,10 +375,10 @@
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
el('option', { value: 'all', textContent: 'All captures' }),
|
el('option', { value: 'all', textContent: tr('ui.allCaptures', null, 'All captures') }),
|
||||||
el('option', { value: 'unassigned', textContent: 'Unassigned' }),
|
el('option', { value: 'unassigned', textContent: tr('ui.unassigned', null, 'Unassigned') }),
|
||||||
el('option', { value: 'unprocessed', textContent: 'Unprocessed' }),
|
el('option', { value: 'unprocessed', textContent: tr('ui.unprocessed', null, 'Unprocessed') }),
|
||||||
el('option', { value: 'processed', textContent: 'Processed' })
|
el('option', { value: 'processed', textContent: tr('ui.processed', null, 'Processed') })
|
||||||
]);
|
]);
|
||||||
var workspaceFilterEl = el('select', {
|
var workspaceFilterEl = el('select', {
|
||||||
className: 'browser-inbox-select',
|
className: 'browser-inbox-select',
|
||||||
|
|
@ -388,7 +393,7 @@
|
||||||
var searchInput = el('input', {
|
var searchInput = el('input', {
|
||||||
className: 'browser-inbox-input',
|
className: 'browser-inbox-input',
|
||||||
type: 'search',
|
type: 'search',
|
||||||
placeholder: 'Search captures',
|
placeholder: tr('ui.search', null, 'Search captures'),
|
||||||
'data-browser-inbox-filter': 'search',
|
'data-browser-inbox-filter': 'search',
|
||||||
'aria-label': 'Search captures',
|
'aria-label': 'Search captures',
|
||||||
onInput: function (event) {
|
onInput: function (event) {
|
||||||
|
|
@ -402,7 +407,7 @@
|
||||||
var clearBtn = el('button', {
|
var clearBtn = el('button', {
|
||||||
className: 'browser-inbox-btn danger',
|
className: 'browser-inbox-btn danger',
|
||||||
'data-browser-inbox-action': 'clear',
|
'data-browser-inbox-action': 'clear',
|
||||||
textContent: 'Clear',
|
textContent: tr('ui.clear', null, 'Clear'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
clearScope().then(render);
|
clearScope().then(render);
|
||||||
}
|
}
|
||||||
|
|
@ -413,7 +418,7 @@
|
||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
filtersEl.appendChild(workspaceFilterEl);
|
filtersEl.appendChild(workspaceFilterEl);
|
||||||
} else {
|
} else {
|
||||||
filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: 'Assigned to this workspace' }));
|
filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: tr('ui.assignedHere', null, 'Assigned to this workspace') }));
|
||||||
}
|
}
|
||||||
filtersEl.appendChild(searchInput);
|
filtersEl.appendChild(searchInput);
|
||||||
toolbar.appendChild(filtersEl);
|
toolbar.appendChild(filtersEl);
|
||||||
|
|
@ -748,25 +753,25 @@
|
||||||
detailEl.innerHTML = '';
|
detailEl.innerHTML = '';
|
||||||
var capture = selectedCapture();
|
var capture = selectedCapture();
|
||||||
if (!capture) {
|
if (!capture) {
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-empty', textContent: 'Select a capture to inspect it.' }));
|
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-empty', textContent: tr('ui.selectCapture', null, 'Select a capture to inspect it.') }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedId = capture.captureId;
|
selectedId = capture.captureId;
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture) }));
|
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture) }));
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [
|
detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Kind' }),
|
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-value', textContent: capture.kind }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'URL' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: 'URL' }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Domain' }),
|
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-value', textContent: capture.domain || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Captured' }),
|
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-value', textContent: formatDate(capture.capturedAt) || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Browser' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: 'Browser' }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Workspace' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.workspace', null, 'Workspace') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || 'Unassigned' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || 'Unassigned' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Status' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.status', null, 'Status') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed ? 'Processed' : 'Unprocessed' })
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed ? 'Processed' : 'Unprocessed' })
|
||||||
]));
|
]));
|
||||||
var assignmentSelect = el('select', {
|
var assignmentSelect = el('select', {
|
||||||
|
|
@ -819,7 +824,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'create-note',
|
'data-browser-inbox-action': 'create-note',
|
||||||
textContent: 'Create Note',
|
textContent: tr('ui.createNote', null, 'Create Note'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
createNoteFromCapture(capture);
|
createNoteFromCapture(capture);
|
||||||
}
|
}
|
||||||
|
|
@ -828,7 +833,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'create-link',
|
'data-browser-inbox-action': 'create-link',
|
||||||
textContent: 'Create Link',
|
textContent: tr('ui.createLink', null, 'Create Link'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
createLinkFromCapture(capture);
|
createLinkFromCapture(capture);
|
||||||
}
|
}
|
||||||
|
|
@ -838,7 +843,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'create-file',
|
'data-browser-inbox-action': 'create-file',
|
||||||
textContent: 'Create File',
|
textContent: tr('ui.createFile', null, 'Create File'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
createFileFromCapture(capture);
|
createFileFromCapture(capture);
|
||||||
}
|
}
|
||||||
|
|
@ -848,7 +853,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn danger',
|
className: 'browser-inbox-btn danger',
|
||||||
'data-browser-inbox-action': 'remove',
|
'data-browser-inbox-action': 'remove',
|
||||||
textContent: 'Delete',
|
textContent: tr('ui.delete', null, 'Delete'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
removeCapture(capture.captureId);
|
removeCapture(capture.captureId);
|
||||||
}
|
}
|
||||||
|
|
@ -941,6 +946,14 @@
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
if (!disposed) render();
|
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 Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label);
|
||||||
|
searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search captures'));
|
||||||
|
clearBtn.textContent = tr('ui.clear', null, 'Clear');
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
containerEl.__browserInboxUnmount = function () {
|
containerEl.__browserInboxUnmount = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
|
@ -967,6 +980,11 @@
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
containerEl.className = 'browser-inbox-settings';
|
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', {
|
var receiverURLInput = el('input', {
|
||||||
className: 'browser-inbox-settings-input',
|
className: 'browser-inbox-settings-input',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
@ -988,27 +1006,27 @@
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-browser-inbox-settings-action': 'copy-url',
|
'data-browser-inbox-settings-action': 'copy-url',
|
||||||
textContent: 'Copy URL'
|
textContent: tr('ui.copyUrl', null, 'Copy URL')
|
||||||
});
|
});
|
||||||
var copyTokenButton = el('button', {
|
var copyTokenButton = el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-browser-inbox-settings-action': 'copy-token',
|
'data-browser-inbox-settings-action': 'copy-token',
|
||||||
textContent: 'Copy Token'
|
textContent: tr('ui.copyToken', null, 'Copy Token')
|
||||||
});
|
});
|
||||||
var rotateTokenButton = el('button', {
|
var rotateTokenButton = el('button', {
|
||||||
className: 'browser-inbox-btn danger',
|
className: 'browser-inbox-btn danger',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-browser-inbox-settings-action': 'rotate-token',
|
'data-browser-inbox-settings-action': 'rotate-token',
|
||||||
textContent: 'Rotate Token'
|
textContent: tr('ui.rotateToken', null, 'Rotate Token')
|
||||||
});
|
});
|
||||||
|
|
||||||
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
|
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
|
||||||
el('label', { className: 'browser-inbox-settings-label', textContent: 'Receiver URL' }),
|
el('label', { className: 'browser-inbox-settings-label', textContent: tr('ui.receiverUrl', null, 'Receiver URL') }),
|
||||||
receiverURLInput
|
receiverURLInput
|
||||||
]));
|
]));
|
||||||
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
|
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [
|
||||||
el('label', { className: 'browser-inbox-settings-label', textContent: 'Pairing Token' }),
|
el('label', { className: 'browser-inbox-settings-label', textContent: tr('ui.pairingToken', null, 'Pairing Token') }),
|
||||||
receiverTokenInput
|
receiverTokenInput
|
||||||
]));
|
]));
|
||||||
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-actions' }, [
|
containerEl.appendChild(el('div', { className: 'browser-inbox-settings-actions' }, [
|
||||||
|
|
@ -1090,6 +1108,13 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
loadPairing();
|
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) {
|
unmount: function (containerEl) {
|
||||||
if (containerEl) containerEl.innerHTML = '';
|
if (containerEl) containerEl.innerHTML = '';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Browser Inbox",
|
||||||
|
"manifest.description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.",
|
||||||
|
"contributions.views.verstak.browser-inbox.view.title": "Browser Inbox",
|
||||||
|
"contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Browser Inbox",
|
||||||
|
"contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Browser Inbox",
|
||||||
|
"contributions.settingsPanels.verstak.browser-inbox.settings.title": "Browser Inbox",
|
||||||
|
"ui.connecting": "Connecting to receiver events...",
|
||||||
|
"ui.title": "Browser Inbox",
|
||||||
|
"ui.workspaceTitle": "Browser Inbox · {workspace}",
|
||||||
|
"ui.allCaptures": "All captures",
|
||||||
|
"ui.unassigned": "Unassigned",
|
||||||
|
"ui.unprocessed": "Unprocessed",
|
||||||
|
"ui.processed": "Processed",
|
||||||
|
"ui.search": "Search captures",
|
||||||
|
"ui.clear": "Clear",
|
||||||
|
"ui.assignedHere": "Assigned to this workspace",
|
||||||
|
"ui.selectCapture": "Select a capture to inspect it.",
|
||||||
|
"ui.kind": "Kind",
|
||||||
|
"ui.domain": "Domain",
|
||||||
|
"ui.captured": "Captured",
|
||||||
|
"ui.workspace": "Workspace",
|
||||||
|
"ui.status": "Status",
|
||||||
|
"ui.createNote": "Create Note",
|
||||||
|
"ui.createLink": "Create Link",
|
||||||
|
"ui.createFile": "Create File",
|
||||||
|
"ui.delete": "Delete",
|
||||||
|
"ui.copyUrl": "Copy URL",
|
||||||
|
"ui.copyToken": "Copy Token",
|
||||||
|
"ui.rotateToken": "Rotate Token",
|
||||||
|
"ui.receiverUrl": "Receiver URL",
|
||||||
|
"ui.pairingToken": "Pairing Token"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Входящие из браузера",
|
||||||
|
"manifest.description": "Общая очередь материалов из браузера с явным назначением рабочего пространства через локальный протокол приёма.",
|
||||||
|
"contributions.views.verstak.browser-inbox.view.title": "Входящие из браузера",
|
||||||
|
"contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Входящие из браузера",
|
||||||
|
"contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Входящие из браузера",
|
||||||
|
"contributions.settingsPanels.verstak.browser-inbox.settings.title": "Входящие из браузера",
|
||||||
|
"ui.connecting": "Подключение к событиям приёмника...",
|
||||||
|
"ui.title": "Входящие из браузера",
|
||||||
|
"ui.workspaceTitle": "Входящие из браузера · {workspace}",
|
||||||
|
"ui.allCaptures": "Все материалы",
|
||||||
|
"ui.unassigned": "Не назначено",
|
||||||
|
"ui.unprocessed": "Не обработано",
|
||||||
|
"ui.processed": "Обработано",
|
||||||
|
"ui.search": "Поиск материалов",
|
||||||
|
"ui.clear": "Очистить",
|
||||||
|
"ui.assignedHere": "Назначено этому рабочему пространству",
|
||||||
|
"ui.selectCapture": "Выберите материал для просмотра.",
|
||||||
|
"ui.kind": "Тип",
|
||||||
|
"ui.domain": "Домен",
|
||||||
|
"ui.captured": "Получено",
|
||||||
|
"ui.workspace": "Рабочее пространство",
|
||||||
|
"ui.status": "Состояние",
|
||||||
|
"ui.createNote": "Создать заметку",
|
||||||
|
"ui.createLink": "Создать ссылку",
|
||||||
|
"ui.createFile": "Создать файл",
|
||||||
|
"ui.delete": "Удалить",
|
||||||
|
"ui.copyUrl": "Копировать URL",
|
||||||
|
"ui.copyToken": "Копировать токен",
|
||||||
|
"ui.rotateToken": "Сменить токен",
|
||||||
|
"ui.receiverUrl": "URL приёмника",
|
||||||
|
"ui.pairingToken": "Токен сопряжения"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.",
|
"description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "inbox",
|
"icon": "inbox",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,10 @@
|
||||||
var linesEl = null;
|
var linesEl = null;
|
||||||
var previewEl = null;
|
var previewEl = null;
|
||||||
var secretLinksAvailable = false;
|
var secretLinksAvailable = false;
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
|
|
||||||
containerEl.setAttribute('data-editor-mode', editorMode);
|
containerEl.setAttribute('data-editor-mode', editorMode);
|
||||||
containerEl.setAttribute('data-resource-path', resourcePath);
|
containerEl.setAttribute('data-resource-path', resourcePath);
|
||||||
|
|
@ -298,13 +302,13 @@
|
||||||
|
|
||||||
var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]);
|
var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]);
|
||||||
var contextLabel = el('span', { className: 'de-toolbar-context', title: resourcePath }, [resourcePath || fileName(resourcePath)]);
|
var contextLabel = el('span', { className: 'de-toolbar-context', title: resourcePath }, [resourcePath || fileName(resourcePath)]);
|
||||||
var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, ['notes context']) : null;
|
var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, [tr('ui.notesContext', null, 'notes context')]) : null;
|
||||||
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
||||||
var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, ['Edit']) : null;
|
var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, [tr('ui.edit', null, 'Edit')]) : null;
|
||||||
var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, ['Preview']) : null;
|
var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, [tr('ui.preview', null, 'Preview')]) : null;
|
||||||
var splitBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'split' }, ['Split']) : null;
|
var splitBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'split' }, [tr('ui.split', null, 'Split')]) : null;
|
||||||
var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, ['Reload']);
|
var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, [tr('ui.reload', null, 'Reload')]);
|
||||||
var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, ['Save']);
|
var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, [tr('ui.save', null, 'Save')]);
|
||||||
var statusEl = el('span', { className: 'de-status', 'data-save-state': '' });
|
var statusEl = el('span', { className: 'de-status', 'data-save-state': '' });
|
||||||
var toolbarChildren = [modeLabel, contextLabel];
|
var toolbarChildren = [modeLabel, contextLabel];
|
||||||
if (notesBadge) toolbarChildren.push(notesBadge);
|
if (notesBadge) toolbarChildren.push(notesBadge);
|
||||||
|
|
@ -336,7 +340,7 @@
|
||||||
containerEl.appendChild(editorWrap);
|
containerEl.appendChild(editorWrap);
|
||||||
|
|
||||||
if (editorMode === 'notes-markdown') {
|
if (editorMode === 'notes-markdown') {
|
||||||
containerEl.appendChild(el('div', { className: 'de-notes-info' }, ['Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.']));
|
containerEl.appendChild(el('div', { className: 'de-notes-info' }, [tr('ui.notesInfo', null, 'Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.')]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLineNumbers() {
|
function updateLineNumbers() {
|
||||||
|
|
@ -349,16 +353,16 @@
|
||||||
|
|
||||||
function updateStatus() {
|
function updateStatus() {
|
||||||
if (saveState === 'saving') {
|
if (saveState === 'saving') {
|
||||||
statusEl.textContent = 'Saving...';
|
statusEl.textContent = tr('ui.saving', null, 'Saving...');
|
||||||
statusEl.className = 'de-status saving';
|
statusEl.className = 'de-status saving';
|
||||||
} else if (saveState === 'error') {
|
} else if (saveState === 'error') {
|
||||||
statusEl.textContent = 'Error saving';
|
statusEl.textContent = tr('ui.saveError', null, 'Error saving');
|
||||||
statusEl.className = 'de-status error';
|
statusEl.className = 'de-status error';
|
||||||
} else if (dirty) {
|
} else if (dirty) {
|
||||||
statusEl.textContent = 'Modified';
|
statusEl.textContent = tr('ui.modified', null, 'Modified');
|
||||||
statusEl.className = 'de-status dirty';
|
statusEl.className = 'de-status dirty';
|
||||||
} else if (lastSavedAt) {
|
} else if (lastSavedAt) {
|
||||||
statusEl.textContent = saveState === 'saved' ? 'Saved ' + lastSavedAt : 'Saved';
|
statusEl.textContent = saveState === 'saved' ? tr('ui.savedAt', { time: lastSavedAt }, 'Saved ' + lastSavedAt) : tr('ui.saved', null, 'Saved');
|
||||||
statusEl.className = 'de-status saved';
|
statusEl.className = 'de-status saved';
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
|
|
@ -475,9 +479,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadFromDisk() {
|
function reloadFromDisk() {
|
||||||
if (dirty && !window.confirm('Discard unsaved changes and reload from disk?')) return;
|
if (dirty && !window.confirm(tr('ui.discardConfirm', null, 'Discard unsaved changes and reload from disk?'))) return;
|
||||||
editorWrap.innerHTML = '';
|
editorWrap.innerHTML = '';
|
||||||
editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...']));
|
editorWrap.appendChild(el('div', { className: 'de-loading' }, [tr('ui.loading', null, 'Loading...')]));
|
||||||
var readPromise = api.files.readText(resourcePath);
|
var readPromise = api.files.readText(resourcePath);
|
||||||
readPromise.then(function (content) {
|
readPromise.then(function (content) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
|
@ -490,7 +494,7 @@
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
editorWrap.innerHTML = '';
|
editorWrap.innerHTML = '';
|
||||||
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
||||||
el('div', {}, ['Failed to load file']),
|
el('div', {}, [tr('ui.loadFailed', null, 'Failed to load file')]),
|
||||||
el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
@ -534,6 +538,17 @@
|
||||||
|
|
||||||
loadSecretProviderAvailability();
|
loadSecretProviderAvailability();
|
||||||
reloadFromDisk();
|
reloadFromDisk();
|
||||||
|
var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function'
|
||||||
|
? api.i18n.onDidChangeLocale(function () {
|
||||||
|
if (notesBadge) notesBadge.textContent = tr('ui.notesContext', null, 'notes context');
|
||||||
|
if (editBtn) editBtn.textContent = tr('ui.edit', null, 'Edit');
|
||||||
|
if (previewBtn) previewBtn.textContent = tr('ui.preview', null, 'Preview');
|
||||||
|
if (splitBtn) splitBtn.textContent = tr('ui.split', null, 'Split');
|
||||||
|
reloadBtn.textContent = tr('ui.reload', null, 'Reload');
|
||||||
|
saveBtn.textContent = tr('ui.save', null, 'Save');
|
||||||
|
updateStatus();
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
containerEl.addEventListener('click', function (event) {
|
containerEl.addEventListener('click', function (event) {
|
||||||
var secretLink = event.target.closest('.secret-link');
|
var secretLink = event.target.closest('.secret-link');
|
||||||
|
|
@ -582,6 +597,7 @@
|
||||||
|
|
||||||
containerEl.__deCleanup = function () {
|
containerEl.__deCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Default Editor",
|
||||||
|
"manifest.description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.",
|
||||||
|
"contributions.openProviders.verstak.default-editor.text.title": "Default Text Editor",
|
||||||
|
"contributions.openProviders.verstak.default-editor.markdown.title": "Default Markdown Editor",
|
||||||
|
"contributions.openProviders.verstak.default-editor.notes-markdown.title": "Default Notes Markdown Editor",
|
||||||
|
"ui.notesContext": "notes context",
|
||||||
|
"ui.edit": "Edit",
|
||||||
|
"ui.preview": "Preview",
|
||||||
|
"ui.split": "Split",
|
||||||
|
"ui.reload": "Reload",
|
||||||
|
"ui.save": "Save",
|
||||||
|
"ui.notesInfo": "Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.",
|
||||||
|
"ui.saving": "Saving...",
|
||||||
|
"ui.saveError": "Error saving",
|
||||||
|
"ui.modified": "Modified",
|
||||||
|
"ui.savedAt": "Saved {time}",
|
||||||
|
"ui.saved": "Saved",
|
||||||
|
"ui.discardConfirm": "Discard unsaved changes and reload from disk?",
|
||||||
|
"ui.loading": "Loading...",
|
||||||
|
"ui.loadFailed": "Failed to load file"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Стандартный редактор",
|
||||||
|
"manifest.description": "Встроенный редактор и просмотрщик текста и Markdown для Верстака.",
|
||||||
|
"contributions.openProviders.verstak.default-editor.text.title": "Стандартный текстовый редактор",
|
||||||
|
"contributions.openProviders.verstak.default-editor.markdown.title": "Стандартный редактор Markdown",
|
||||||
|
"contributions.openProviders.verstak.default-editor.notes-markdown.title": "Стандартный редактор Markdown-заметок",
|
||||||
|
"ui.notesContext": "контекст заметки",
|
||||||
|
"ui.edit": "Редактировать",
|
||||||
|
"ui.preview": "Просмотр",
|
||||||
|
"ui.split": "Разделить",
|
||||||
|
"ui.reload": "Перезагрузить",
|
||||||
|
"ui.save": "Сохранить",
|
||||||
|
"ui.notesInfo": "Активен контекст заметки. Действия, обратные ссылки и граф будут развиваться в плагине заметок.",
|
||||||
|
"ui.saving": "Сохранение...",
|
||||||
|
"ui.saveError": "Ошибка сохранения",
|
||||||
|
"ui.modified": "Изменён",
|
||||||
|
"ui.savedAt": "Сохранено {time}",
|
||||||
|
"ui.saved": "Сохранено",
|
||||||
|
"ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?",
|
||||||
|
"ui.loading": "Загрузка...",
|
||||||
|
"ui.loadFailed": "Не удалось загрузить файл"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.",
|
"description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "edit",
|
"icon": "edit",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@
|
||||||
var request = props && props.request || {};
|
var request = props && props.request || {};
|
||||||
var path = request.path || '';
|
var path = request.path || '';
|
||||||
var ext = extension(path, request.extension);
|
var ext = extension(path, request.extension);
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
containerEl.className = 'fp-root';
|
containerEl.className = 'fp-root';
|
||||||
containerEl.setAttribute('data-plugin-id', 'verstak.file-preview');
|
containerEl.setAttribute('data-plugin-id', 'verstak.file-preview');
|
||||||
|
|
@ -116,19 +120,19 @@
|
||||||
var openButton = el('button', {
|
var openButton = el('button', {
|
||||||
className: 'fp-btn',
|
className: 'fp-btn',
|
||||||
'data-action': 'open-external',
|
'data-action': 'open-external',
|
||||||
textContent: 'Open External',
|
textContent: tr('ui.openExternal', null, 'Open External'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
if (api.files.openExternal) api.files.openExternal(path).catch(function (err) { console.error('[file-preview] openExternal:', err); });
|
if (api.files.openExternal) api.files.openExternal(path).catch(function (err) { console.error('[file-preview] openExternal:', err); });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
containerEl.appendChild(el('div', { className: 'fp-toolbar' }, [
|
containerEl.appendChild(el('div', { className: 'fp-toolbar' }, [
|
||||||
el('span', { className: 'fp-mode' }, ['Preview']),
|
el('span', { className: 'fp-mode' }, [tr('ui.preview', null, 'Preview')]),
|
||||||
el('span', { className: 'fp-path' }, [path]),
|
el('span', { className: 'fp-path' }, [path]),
|
||||||
el('span', { className: 'fp-spacer' }),
|
el('span', { className: 'fp-spacer' }),
|
||||||
openButton
|
openButton
|
||||||
]));
|
]));
|
||||||
|
|
||||||
var body = el('div', { className: 'fp-loading' }, ['Loading...']);
|
var body = el('div', { className: 'fp-loading' }, [tr('ui.loading', null, 'Loading...')]);
|
||||||
containerEl.appendChild(body);
|
containerEl.appendChild(body);
|
||||||
|
|
||||||
api.files.metadata(path).then(function (meta) {
|
api.files.metadata(path).then(function (meta) {
|
||||||
|
|
@ -140,8 +144,13 @@
|
||||||
});
|
});
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
body.className = 'fp-error';
|
body.className = 'fp-error';
|
||||||
body.textContent = 'Preview error: ' + (err && err.message ? err.message : String(err));
|
body.textContent = tr('ui.error', { error: err && err.message ? err.message : String(err) }, 'Preview error: ' + (err && err.message ? err.message : String(err)));
|
||||||
});
|
});
|
||||||
|
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
|
api.i18n.onDidChangeLocale(function () {
|
||||||
|
openButton.textContent = tr('ui.openExternal', null, 'Open External');
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
unmount: function (containerEl) {
|
unmount: function (containerEl) {
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "File Preview",
|
||||||
|
"manifest.description": "Read-only inline image preview provider for vault files.",
|
||||||
|
"contributions.openProviders.verstak.file-preview.image.title": "Image Preview",
|
||||||
|
"ui.openExternal": "Open External",
|
||||||
|
"ui.preview": "Preview",
|
||||||
|
"ui.loading": "Loading...",
|
||||||
|
"ui.error": "Preview error: {error}"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Просмотр файлов",
|
||||||
|
"manifest.description": "Встроенный просмотр изображений из хранилища без редактирования.",
|
||||||
|
"contributions.openProviders.verstak.file-preview.image.title": "Просмотр изображения",
|
||||||
|
"ui.openExternal": "Открыть во внешнем приложении",
|
||||||
|
"ui.preview": "Просмотр",
|
||||||
|
"ui.loading": "Загрузка...",
|
||||||
|
"ui.error": "Ошибка просмотра: {error}"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Read-only inline image preview provider for vault files.",
|
"description": "Read-only inline image preview provider for vault files.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "eye",
|
"icon": "eye",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -369,6 +369,11 @@
|
||||||
}
|
}
|
||||||
var navigatingHistory = false;
|
var navigatingHistory = false;
|
||||||
|
|
||||||
|
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 scopedPath(local) {
|
function scopedPath(local) {
|
||||||
local = cleanPath(local);
|
local = cleanPath(local);
|
||||||
return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local;
|
return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local;
|
||||||
|
|
@ -390,26 +395,26 @@
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'files-toolbar' });
|
var toolbar = el('div', { className: 'files-toolbar' });
|
||||||
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
||||||
var backBtn = iconButton('back', 'Back', 'back', goBack);
|
var backBtn = iconButton('back', tr('ui.back', null, 'Back'), 'back', goBack);
|
||||||
var forwardBtn = iconButton('forward', 'Forward', 'forward', goForward);
|
var forwardBtn = iconButton('forward', tr('ui.forward', null, 'Forward'), 'forward', goForward);
|
||||||
var upBtn = iconButton('up', 'Up', 'up', goUp);
|
var upBtn = iconButton('up', tr('ui.up', null, 'Up'), 'up', goUp);
|
||||||
var refreshBtn = iconButton('refresh', 'Refresh', 'refresh', loadEntries);
|
var refreshBtn = iconButton('refresh', tr('ui.refresh', null, 'Refresh'), 'refresh', loadEntries);
|
||||||
var newFolderBtn = iconButton('new-folder', 'New folder', 'folderAdd', function () { startCreate('folder'); });
|
var newFolderBtn = iconButton('new-folder', tr('ui.newFolder', null, 'New folder'), 'folderAdd', function () { startCreate('folder'); });
|
||||||
var newMdBtn = iconButton('new-markdown', 'New markdown file', 'markdownAdd', function () { startCreate('markdown'); });
|
var newMdBtn = iconButton('new-markdown', tr('ui.newMarkdown', null, 'New markdown file'), 'markdownAdd', function () { startCreate('markdown'); });
|
||||||
var newTextBtn = iconButton('new-text', 'New text file', 'textAdd', function () { startCreate('text'); });
|
var newTextBtn = iconButton('new-text', tr('ui.newText', null, 'New text file'), 'textAdd', function () { startCreate('text'); });
|
||||||
var openBtn = iconButton('open', 'Open', 'open', function () { openEntry(selectedEntry()); });
|
var openBtn = iconButton('open', tr('ui.open', null, 'Open'), 'open', function () { openEntry(selectedEntry()); });
|
||||||
var renameBtn = iconButton('rename', 'Rename', 'rename', function () { beginRename(); });
|
var renameBtn = iconButton('rename', tr('ui.rename', null, 'Rename'), 'rename', function () { beginRename(); });
|
||||||
var trashBtn = iconButton('trash', 'Move to trash', 'trash', function () { trashEntry(); });
|
var trashBtn = iconButton('trash', tr('ui.trash', null, 'Move to trash'), 'trash', function () { trashEntry(); });
|
||||||
var cutBtn = iconButton('cut', 'Cut', 'cut', function () { cutSelection(); });
|
var cutBtn = iconButton('cut', tr('ui.cut', null, 'Cut'), 'cut', function () { cutSelection(); });
|
||||||
var copyBtn = iconButton('copy', 'Copy', 'copy', function () { copySelection(); });
|
var copyBtn = iconButton('copy', tr('ui.copy', null, 'Copy'), 'copy', function () { copySelection(); });
|
||||||
var pasteBtn = iconButton('paste', 'Paste', 'paste', function () { pasteEntry(); });
|
var pasteBtn = iconButton('paste', tr('ui.paste', null, 'Paste'), 'paste', function () { pasteEntry(); });
|
||||||
var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' });
|
var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: tr('ui.filter', null, 'Filter current folder') });
|
||||||
var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [
|
var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [
|
||||||
el('option', { value: 'folder-name' }, ['Folders + name']),
|
el('option', { value: 'folder-name' }, [tr('ui.sort.foldersName', null, 'Folders + name')]),
|
||||||
el('option', { value: 'name-asc' }, ['Name']),
|
el('option', { value: 'name-asc' }, [tr('ui.column.name', null, 'Name')]),
|
||||||
el('option', { value: 'type' }, ['Type']),
|
el('option', { value: 'type' }, [tr('ui.column.type', null, 'Type')]),
|
||||||
el('option', { value: 'modified-desc' }, ['Modified']),
|
el('option', { value: 'modified-desc' }, [tr('ui.column.modified', null, 'Modified')]),
|
||||||
el('option', { value: 'size-desc' }, ['Size'])
|
el('option', { value: 'size-desc' }, [tr('ui.column.size', null, 'Size')])
|
||||||
]);
|
]);
|
||||||
trashBtn.classList.add('danger');
|
trashBtn.classList.add('danger');
|
||||||
toolbar.appendChild(breadcrumb);
|
toolbar.appendChild(breadcrumb);
|
||||||
|
|
@ -430,8 +435,8 @@
|
||||||
var createField = el('div', { className: 'files-field-stack' });
|
var createField = el('div', { className: 'files-field-stack' });
|
||||||
var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' });
|
var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' });
|
||||||
var createError = el('div', { className: 'files-panel-error', 'data-files-create-error': '', role: 'alert' });
|
var createError = el('div', { className: 'files-panel-error', 'data-files-create-error': '', role: 'alert' });
|
||||||
var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, ['Create']);
|
var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, [tr('ui.create', null, 'Create')]);
|
||||||
var createCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']);
|
var createCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]);
|
||||||
createField.appendChild(createInput);
|
createField.appendChild(createInput);
|
||||||
createField.appendChild(createError);
|
createField.appendChild(createError);
|
||||||
createPanel.appendChild(createField);
|
createPanel.appendChild(createField);
|
||||||
|
|
@ -443,8 +448,8 @@
|
||||||
var renameField = el('div', { className: 'files-field-stack' });
|
var renameField = el('div', { className: 'files-field-stack' });
|
||||||
var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' });
|
var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' });
|
||||||
var renameError = el('div', { className: 'files-panel-error', 'data-files-rename-error': '', role: 'alert' });
|
var renameError = el('div', { className: 'files-panel-error', 'data-files-rename-error': '', role: 'alert' });
|
||||||
var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, ['Rename']);
|
var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, [tr('ui.rename', null, 'Rename')]);
|
||||||
var renameCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']);
|
var renameCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]);
|
||||||
renameField.appendChild(renameInput);
|
renameField.appendChild(renameInput);
|
||||||
renameField.appendChild(renameError);
|
renameField.appendChild(renameError);
|
||||||
renamePanel.appendChild(renameField);
|
renamePanel.appendChild(renameField);
|
||||||
|
|
@ -598,27 +603,27 @@
|
||||||
|
|
||||||
function renderEmptyFolderState() {
|
function renderEmptyFolderState() {
|
||||||
return el('div', { className: 'files-empty' }, [
|
return el('div', { className: 'files-empty' }, [
|
||||||
el('div', { className: 'files-empty-title' }, ['Empty folder']),
|
el('div', { className: 'files-empty-title' }, [tr('ui.emptyFolder', null, 'Empty folder')]),
|
||||||
el('div', { className: 'files-empty-actions' }, [
|
el('div', { className: 'files-empty-actions' }, [
|
||||||
emptyCreateAction('new-folder', 'New folder', 'folder', 'folderAdd'),
|
emptyCreateAction('new-folder', tr('ui.newFolder', null, 'New folder'), 'folder', 'folderAdd'),
|
||||||
emptyCreateAction('new-markdown', 'New markdown file', 'markdown', 'markdownAdd'),
|
emptyCreateAction('new-markdown', tr('ui.newMarkdown', null, 'New markdown file'), 'markdown', 'markdownAdd'),
|
||||||
emptyCreateAction('new-text', 'New text file', 'text', 'textAdd')
|
emptyCreateAction('new-text', tr('ui.newText', null, 'New text file'), 'text', 'textAdd')
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNoMatchesState() {
|
function renderNoMatchesState() {
|
||||||
return el('div', { className: 'files-empty' }, [
|
return el('div', { className: 'files-empty' }, [
|
||||||
el('div', { className: 'files-empty-title' }, ['No matches']),
|
el('div', { className: 'files-empty-title' }, [tr('ui.noMatches', null, 'No matches')]),
|
||||||
el('div', { className: 'files-empty-actions' }, [
|
el('div', { className: 'files-empty-actions' }, [
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'files-empty-btn',
|
className: 'files-empty-btn',
|
||||||
'data-files-empty-action': 'clear-filter',
|
'data-files-empty-action': 'clear-filter',
|
||||||
'data-files-icon': 'refresh',
|
'data-files-icon': 'refresh',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
title: 'Clear filter',
|
title: tr('ui.clearFilter', null, 'Clear filter'),
|
||||||
'aria-label': 'Clear filter',
|
'aria-label': tr('ui.clearFilter', null, 'Clear filter'),
|
||||||
innerHTML: svgIcon(ACTION_ICONS.refresh) + '<span>Clear filter</span>',
|
innerHTML: svgIcon(ACTION_ICONS.refresh) + '<span>' + tr('ui.clearFilter', null, 'Clear filter') + '</span>',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
filterText = '';
|
filterText = '';
|
||||||
filterInput.value = '';
|
filterInput.value = '';
|
||||||
|
|
@ -633,11 +638,11 @@
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
var header = el('div', { className: 'files-header' }, [
|
var header = el('div', { className: 'files-header' }, [
|
||||||
el('span', {}, ['Name']),
|
el('span', {}, [tr('ui.column.name', null, 'Name')]),
|
||||||
el('span', {}, ['Type']),
|
el('span', {}, [tr('ui.column.type', null, 'Type')]),
|
||||||
el('span', {}, ['Size']),
|
el('span', {}, [tr('ui.column.size', null, 'Size')]),
|
||||||
el('span', {}, ['Modified']),
|
el('span', {}, [tr('ui.column.modified', null, 'Modified')]),
|
||||||
el('span', {}, ['Actions'])
|
el('span', {}, [tr('ui.column.actions', null, 'Actions')])
|
||||||
]);
|
]);
|
||||||
listContainer.appendChild(header);
|
listContainer.appendChild(header);
|
||||||
|
|
||||||
|
|
@ -687,9 +692,9 @@
|
||||||
el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]),
|
el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]),
|
||||||
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]),
|
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]),
|
||||||
el('div', { className: 'files-row-actions' }, [
|
el('div', { className: 'files-row-actions' }, [
|
||||||
iconButton('row-open', 'Open', 'open', function (event) { event.stopPropagation(); openEntry(entry); }, 'files-row-btn'),
|
iconButton('row-open', tr('ui.open', null, 'Open'), 'open', function (event) { event.stopPropagation(); openEntry(entry); }, 'files-row-btn'),
|
||||||
iconButton('row-rename', 'Rename', 'rename', function (event) { event.stopPropagation(); beginRename(entry); }, 'files-row-btn'),
|
iconButton('row-rename', tr('ui.rename', null, 'Rename'), 'rename', function (event) { event.stopPropagation(); beginRename(entry); }, 'files-row-btn'),
|
||||||
iconButton('row-trash', 'Move to trash', 'trash', function (event) { event.stopPropagation(); trashEntry(entry); }, 'files-row-btn danger')
|
iconButton('row-trash', tr('ui.trash', null, 'Move to trash'), 'trash', function (event) { event.stopPropagation(); trashEntry(entry); }, 'files-row-btn danger')
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
listContainer.appendChild(row);
|
listContainer.appendChild(row);
|
||||||
|
|
@ -701,7 +706,7 @@
|
||||||
selectedPaths = {};
|
selectedPaths = {};
|
||||||
lastClickedPath = '';
|
lastClickedPath = '';
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...']));
|
listContainer.appendChild(el('div', { className: 'files-loading' }, [tr('ui.loading', null, 'Loading...')]));
|
||||||
updateBreadcrumb();
|
updateBreadcrumb();
|
||||||
api.files.list(scopedPath(currentPath)).then(function (result) {
|
api.files.list(scopedPath(currentPath)).then(function (result) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
|
@ -711,7 +716,7 @@
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
||||||
el('div', {}, ['Failed to load files']),
|
el('div', {}, [tr('ui.loadFailed', null, 'Failed to load files')]),
|
||||||
el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
@ -1478,6 +1483,29 @@
|
||||||
loadContributionActions();
|
loadContributionActions();
|
||||||
loadEntries();
|
loadEntries();
|
||||||
|
|
||||||
|
var localeUnsubscribe = null;
|
||||||
|
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
|
localeUnsubscribe = api.i18n.onDidChangeLocale(function () {
|
||||||
|
[
|
||||||
|
[backBtn, 'ui.back', 'Back'], [forwardBtn, 'ui.forward', 'Forward'], [upBtn, 'ui.up', 'Up'],
|
||||||
|
[refreshBtn, 'ui.refresh', 'Refresh'], [newFolderBtn, 'ui.newFolder', 'New folder'],
|
||||||
|
[newMdBtn, 'ui.newMarkdown', 'New markdown file'], [newTextBtn, 'ui.newText', 'New text file'],
|
||||||
|
[openBtn, 'ui.open', 'Open'], [renameBtn, 'ui.rename', 'Rename'], [trashBtn, 'ui.trash', 'Move to trash'],
|
||||||
|
[cutBtn, 'ui.cut', 'Cut'], [copyBtn, 'ui.copy', 'Copy'], [pasteBtn, 'ui.paste', 'Paste']
|
||||||
|
].forEach(function (item) {
|
||||||
|
var label = tr(item[1], null, item[2]);
|
||||||
|
item[0].setAttribute('title', label);
|
||||||
|
item[0].setAttribute('aria-label', label);
|
||||||
|
});
|
||||||
|
filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter current folder'));
|
||||||
|
createConfirm.textContent = tr('ui.create', null, 'Create');
|
||||||
|
createCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
||||||
|
renameConfirm.textContent = tr('ui.rename', null, 'Rename');
|
||||||
|
renameCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
||||||
|
renderList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var fileChangedUnsubscribe = null;
|
var fileChangedUnsubscribe = null;
|
||||||
if (api.events && typeof api.events.subscribe === 'function') {
|
if (api.events && typeof api.events.subscribe === 'function') {
|
||||||
api.events.subscribe('file.changed', function (event) {
|
api.events.subscribe('file.changed', function (event) {
|
||||||
|
|
@ -1492,6 +1520,7 @@
|
||||||
|
|
||||||
containerEl.__filesCleanup = function () {
|
containerEl.__filesCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
||||||
document.removeEventListener('click', onDocClick);
|
document.removeEventListener('click', onDocClick);
|
||||||
document.removeEventListener('keydown', onDocKeydown);
|
document.removeEventListener('keydown', onDocKeydown);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Files",
|
||||||
|
"manifest.description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
||||||
|
"contributions.workspaceItems.verstak.files.workspace.title": "Files",
|
||||||
|
"ui.back": "Back",
|
||||||
|
"ui.forward": "Forward",
|
||||||
|
"ui.up": "Up",
|
||||||
|
"ui.refresh": "Refresh",
|
||||||
|
"ui.newFolder": "New folder",
|
||||||
|
"ui.newMarkdown": "New markdown file",
|
||||||
|
"ui.newText": "New text file",
|
||||||
|
"ui.open": "Open",
|
||||||
|
"ui.rename": "Rename",
|
||||||
|
"ui.trash": "Move to trash",
|
||||||
|
"ui.cut": "Cut",
|
||||||
|
"ui.copy": "Copy",
|
||||||
|
"ui.paste": "Paste",
|
||||||
|
"ui.filter": "Filter current folder",
|
||||||
|
"ui.sort.foldersName": "Folders + name",
|
||||||
|
"ui.column.name": "Name",
|
||||||
|
"ui.column.type": "Type",
|
||||||
|
"ui.column.modified": "Modified",
|
||||||
|
"ui.column.size": "Size",
|
||||||
|
"ui.column.actions": "Actions",
|
||||||
|
"ui.create": "Create",
|
||||||
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.emptyFolder": "Empty folder",
|
||||||
|
"ui.noMatches": "No matches",
|
||||||
|
"ui.clearFilter": "Clear filter",
|
||||||
|
"ui.loading": "Loading...",
|
||||||
|
"ui.loadFailed": "Failed to load files"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Файлы",
|
||||||
|
"manifest.description": "Файловый менеджер рабочего пространства с созданием, переименованием, корзиной, фильтрацией и сортировкой.",
|
||||||
|
"contributions.workspaceItems.verstak.files.workspace.title": "Файлы",
|
||||||
|
"ui.back": "Назад",
|
||||||
|
"ui.forward": "Вперёд",
|
||||||
|
"ui.up": "На уровень выше",
|
||||||
|
"ui.refresh": "Обновить",
|
||||||
|
"ui.newFolder": "Новая папка",
|
||||||
|
"ui.newMarkdown": "Новый Markdown-файл",
|
||||||
|
"ui.newText": "Новый текстовый файл",
|
||||||
|
"ui.open": "Открыть",
|
||||||
|
"ui.rename": "Переименовать",
|
||||||
|
"ui.trash": "Переместить в корзину",
|
||||||
|
"ui.cut": "Вырезать",
|
||||||
|
"ui.copy": "Копировать",
|
||||||
|
"ui.paste": "Вставить",
|
||||||
|
"ui.filter": "Фильтр текущей папки",
|
||||||
|
"ui.sort.foldersName": "Сначала папки, затем по имени",
|
||||||
|
"ui.column.name": "Имя",
|
||||||
|
"ui.column.type": "Тип",
|
||||||
|
"ui.column.modified": "Изменён",
|
||||||
|
"ui.column.size": "Размер",
|
||||||
|
"ui.column.actions": "Действия",
|
||||||
|
"ui.create": "Создать",
|
||||||
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.emptyFolder": "Папка пуста",
|
||||||
|
"ui.noMatches": "Совпадений нет",
|
||||||
|
"ui.clearFilter": "Сбросить фильтр",
|
||||||
|
"ui.loading": "Загрузка...",
|
||||||
|
"ui.loadFailed": "Не удалось загрузить файлы"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
"description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "folder",
|
"icon": "folder",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -273,18 +273,22 @@
|
||||||
|
|
||||||
var scope = scopeFromProps(props || {});
|
var scope = scopeFromProps(props || {});
|
||||||
var entries = [];
|
var entries = [];
|
||||||
var statusText = 'Loading journal...';
|
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 statusText = tr('ui.loading', null, 'Loading journal...');
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' });
|
var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' });
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'journal-toolbar' });
|
var toolbar = el('div', { className: 'journal-toolbar' });
|
||||||
var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? 'Journal' : 'Journal · ' + scope.label });
|
var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label) });
|
||||||
var countEl = el('span', { className: 'journal-count' });
|
var countEl = el('span', { className: 'journal-count' });
|
||||||
var statusEl = el('span', { className: 'journal-status' });
|
var statusEl = el('span', { className: 'journal-status' });
|
||||||
var addBtn = el('button', {
|
var addBtn = el('button', {
|
||||||
className: 'journal-btn primary',
|
className: 'journal-btn primary',
|
||||||
'data-journal-action': 'add',
|
'data-journal-action': 'add',
|
||||||
innerHTML: iconSvg('add') + '<span>Add</span>',
|
innerHTML: iconSvg('add') + '<span>' + tr('ui.add', null, 'Add') + '</span>',
|
||||||
onClick: function () { showEntryModal(); }
|
onClick: function () { showEntryModal(); }
|
||||||
});
|
});
|
||||||
toolbar.appendChild(titleEl);
|
toolbar.appendChild(titleEl);
|
||||||
|
|
@ -302,7 +306,7 @@
|
||||||
if (scope.mode !== 'workspace') return Promise.resolve();
|
if (scope.mode !== 'workspace') return Promise.resolve();
|
||||||
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
||||||
return api.settings.write(scope.key, storageEntries(entries)).catch(function (err) {
|
return api.settings.write(scope.key, storageEntries(entries)).catch(function (err) {
|
||||||
statusText = 'Could not save journal: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save journal: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -316,19 +320,19 @@
|
||||||
all = all.concat(normalizeEntries((settings || {})[key], key));
|
all = all.concat(normalizeEntries((settings || {})[key], key));
|
||||||
});
|
});
|
||||||
entries = sortEntries(all);
|
entries = sortEntries(all);
|
||||||
statusText = 'Aggregating worklogs';
|
statusText = tr('ui.aggregating', null, 'Aggregating worklogs');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load journal: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return api.settings.read(scope.key).then(function (stored) {
|
return api.settings.read(scope.key).then(function (stored) {
|
||||||
entries = sortEntries(normalizeEntries(stored, scope.key));
|
entries = sortEntries(normalizeEntries(stored, scope.key));
|
||||||
statusText = 'Ready';
|
statusText = tr('ui.ready', null, 'Ready');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load journal: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -344,8 +348,8 @@
|
||||||
var reviewingCandidate = !editing && !!candidate;
|
var reviewingCandidate = !editing && !!candidate;
|
||||||
var reviewingTodo = !editing && !!completedTodo;
|
var reviewingTodo = !editing && !!completedTodo;
|
||||||
var dateInput = el('input', { className: 'journal-input', type: 'date', value: editing ? existingEntry.date : (reviewingCandidate ? candidateDate(candidate.startedAt) : (reviewingTodo ? candidateDate(completedTodo.completedAt) : today())), 'data-journal-input': 'date' });
|
var dateInput = el('input', { className: 'journal-input', type: 'date', value: editing ? existingEntry.date : (reviewingCandidate ? candidateDate(candidate.startedAt) : (reviewingTodo ? candidateDate(completedTodo.completedAt) : today())), 'data-journal-input': 'date' });
|
||||||
var titleInput = el('input', { className: 'journal-input', type: 'text', placeholder: 'Work item', value: editing ? existingEntry.title : (reviewingTodo ? completedTodo.title : ''), 'data-journal-input': 'title' });
|
var titleInput = el('input', { className: 'journal-input', type: 'text', placeholder: tr('ui.workItem', null, 'Work item'), value: editing ? existingEntry.title : (reviewingTodo ? completedTodo.title : ''), 'data-journal-input': 'title' });
|
||||||
var summaryInput = el('textarea', { className: 'journal-input textarea', placeholder: 'Body', 'data-journal-input': 'summary' });
|
var summaryInput = el('textarea', { className: 'journal-input textarea', placeholder: tr('ui.body', null, 'Body'), 'data-journal-input': 'summary' });
|
||||||
summaryInput.value = editing ? existingEntry.summary : (reviewingTodo ? completedTodo.description : '');
|
summaryInput.value = editing ? existingEntry.summary : (reviewingTodo ? completedTodo.description : '');
|
||||||
var minutesInput = el('input', { className: 'journal-input', type: 'number', min: '0', step: '1', value: editing ? existingEntry.minutes : (reviewingCandidate ? candidate.estimatedMinutes : (reviewingTodo ? '0' : '30')), 'data-journal-input': 'minutes' });
|
var minutesInput = el('input', { className: 'journal-input', type: 'number', min: '0', step: '1', value: editing ? existingEntry.minutes : (reviewingCandidate ? candidate.estimatedMinutes : (reviewingTodo ? '0' : '30')), 'data-journal-input': 'minutes' });
|
||||||
var billableInput = el('input', { type: 'checkbox', 'data-journal-input': 'billable' });
|
var billableInput = el('input', { type: 'checkbox', 'data-journal-input': 'billable' });
|
||||||
|
|
@ -397,20 +401,20 @@
|
||||||
if (event.target === event.currentTarget) closeEntryModal();
|
if (event.target === event.currentTarget) closeEntryModal();
|
||||||
} }, [
|
} }, [
|
||||||
el('div', { className: 'journal-modal' }, [
|
el('div', { className: 'journal-modal' }, [
|
||||||
el('div', { className: 'journal-modal-title', textContent: editing ? 'Edit journal entry' : (reviewingCandidate ? 'Review possible journal entry' : (reviewingTodo ? 'Create journal entry from completed todo' : 'Add journal entry')) }),
|
el('div', { className: 'journal-modal-title', textContent: editing ? tr('ui.editEntry', null, 'Edit journal entry') : (reviewingCandidate ? tr('ui.reviewCandidate', null, 'Review possible journal entry') : (reviewingTodo ? tr('ui.fromTodo', null, 'Create journal entry from completed todo') : tr('ui.addEntry', null, 'Add journal entry'))) }),
|
||||||
candidateContext,
|
candidateContext,
|
||||||
todoContext,
|
todoContext,
|
||||||
el('div', { className: 'journal-modal-grid' }, [
|
el('div', { className: 'journal-modal-grid' }, [
|
||||||
el('label', { className: 'journal-field' }, ['Date', dateInput]),
|
el('label', { className: 'journal-field' }, [tr('ui.date', null, 'Date'), dateInput]),
|
||||||
el('label', { className: 'journal-field' }, ['Minutes', minutesInput]),
|
el('label', { className: 'journal-field' }, [tr('ui.minutes', null, 'Minutes'), minutesInput]),
|
||||||
el('label', { className: 'journal-field wide' }, ['Title', titleInput]),
|
el('label', { className: 'journal-field wide' }, [tr('ui.fieldTitle', null, 'Title'), titleInput]),
|
||||||
el('label', { className: 'journal-field wide' }, ['Body', summaryInput]),
|
el('label', { className: 'journal-field wide' }, [tr('ui.body', null, 'Body'), summaryInput]),
|
||||||
el('label', { className: 'journal-billable' }, [billableInput, 'Billable'])
|
el('label', { className: 'journal-billable' }, [billableInput, tr('ui.billable', null, 'Billable')])
|
||||||
]),
|
]),
|
||||||
candidateActivities,
|
candidateActivities,
|
||||||
el('div', { className: 'journal-modal-actions' }, [
|
el('div', { className: 'journal-modal-actions' }, [
|
||||||
el('button', { className: 'journal-btn ghost', type: 'button', textContent: 'Cancel', onClick: closeEntryModal }),
|
el('button', { className: 'journal-btn ghost', type: 'button', textContent: tr('ui.cancel', null, 'Cancel'), onClick: closeEntryModal }),
|
||||||
el('button', { className: 'journal-btn primary', type: 'button', 'data-journal-action': 'save-entry', textContent: editing ? 'Save changes' : 'Add entry', onClick: saveEntry })
|
el('button', { className: 'journal-btn primary', type: 'button', 'data-journal-action': 'save-entry', textContent: editing ? tr('ui.saveChanges', null, 'Save changes') : tr('ui.addEntryShort', null, 'Add entry'), onClick: saveEntry })
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
|
|
@ -421,7 +425,7 @@
|
||||||
if (scope.mode !== 'workspace') return;
|
if (scope.mode !== 'workspace') return;
|
||||||
var title = text(formValue && formValue.title).trim();
|
var title = text(formValue && formValue.title).trim();
|
||||||
if (!title) {
|
if (!title) {
|
||||||
statusText = 'Title is required';
|
statusText = tr('ui.titleRequired', null, 'Title is required');
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
|
|
@ -469,7 +473,7 @@
|
||||||
function deleteEntry(entry) {
|
function deleteEntry(entry) {
|
||||||
if (scope.mode !== 'workspace' || !entry) return;
|
if (scope.mode !== 'workspace' || !entry) return;
|
||||||
entries = entries.filter(function (item) { return item.entryId !== entry.entryId; });
|
entries = entries.filter(function (item) { return item.entryId !== entry.entryId; });
|
||||||
statusText = 'Entry deleted';
|
statusText = tr('ui.deleted', null, 'Entry deleted');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
}
|
}
|
||||||
|
|
@ -477,7 +481,7 @@
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
listEl.appendChild(el('div', { className: 'journal-empty', textContent: scope.mode === 'global' ? 'No worklog entries yet.' : 'No worklog entries yet.' }));
|
listEl.appendChild(el('div', { className: 'journal-empty', textContent: tr('ui.empty', null, 'No worklog entries yet.') }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entries.forEach(function (entry) {
|
entries.forEach(function (entry) {
|
||||||
|
|
@ -493,8 +497,8 @@
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }),
|
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }),
|
||||||
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
||||||
el('button', { className: 'journal-icon-btn', type: 'button', title: 'Edit', 'aria-label': 'Edit', 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }),
|
el('button', { className: 'journal-icon-btn', type: 'button', title: tr('ui.edit', null, 'Edit'), 'aria-label': tr('ui.edit', null, 'Edit'), 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }),
|
||||||
el('button', { className: 'journal-icon-btn danger', type: 'button', title: 'Delete', 'aria-label': 'Delete', 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } })
|
el('button', { className: 'journal-icon-btn danger', type: 'button', title: tr('ui.delete', null, 'Delete'), 'aria-label': tr('ui.delete', null, 'Delete'), 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } })
|
||||||
]) : null
|
]) : null
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
@ -516,6 +520,13 @@
|
||||||
if (candidate) showEntryModal(null, candidate);
|
if (candidate) showEntryModal(null, candidate);
|
||||||
else if (completedTodo) showEntryModal(null, null, completedTodo);
|
else if (completedTodo) showEntryModal(null, null, completedTodo);
|
||||||
});
|
});
|
||||||
|
if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
|
api.i18n.onDidChangeLocale(function () {
|
||||||
|
titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label);
|
||||||
|
addBtn.innerHTML = iconSvg('add') + '<span>' + tr('ui.add', null, 'Add') + '</span>';
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
JournalView.unmount = function (containerEl) {
|
JournalView.unmount = function (containerEl) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Journal",
|
||||||
|
"manifest.description": "Workspace-scoped journal with user-authored entries and optional Activity links.",
|
||||||
|
"contributions.views.verstak.journal.view.title": "Journal",
|
||||||
|
"contributions.sidebarItems.verstak.journal.sidebar.title": "Journal",
|
||||||
|
"contributions.workspaceItems.verstak.journal.workspace.title": "Journal",
|
||||||
|
"ui.loading": "Loading journal...",
|
||||||
|
"ui.title": "Journal",
|
||||||
|
"ui.workspaceTitle": "Journal · {workspace}",
|
||||||
|
"ui.add": "Add",
|
||||||
|
"ui.saveError": "Could not save journal: {error}",
|
||||||
|
"ui.aggregating": "Aggregating worklogs",
|
||||||
|
"ui.loadError": "Could not load journal: {error}",
|
||||||
|
"ui.ready": "Ready",
|
||||||
|
"ui.workItem": "Work item",
|
||||||
|
"ui.body": "Body",
|
||||||
|
"ui.editEntry": "Edit journal entry",
|
||||||
|
"ui.reviewCandidate": "Review possible journal entry",
|
||||||
|
"ui.fromTodo": "Create journal entry from completed todo",
|
||||||
|
"ui.addEntry": "Add journal entry",
|
||||||
|
"ui.date": "Date",
|
||||||
|
"ui.minutes": "Minutes",
|
||||||
|
"ui.fieldTitle": "Title",
|
||||||
|
"ui.billable": "Billable",
|
||||||
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.saveChanges": "Save changes",
|
||||||
|
"ui.addEntryShort": "Add entry",
|
||||||
|
"ui.titleRequired": "Title is required",
|
||||||
|
"ui.deleted": "Entry deleted",
|
||||||
|
"ui.empty": "No worklog entries yet.",
|
||||||
|
"ui.edit": "Edit",
|
||||||
|
"ui.delete": "Delete"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Журнал",
|
||||||
|
"manifest.description": "Журнал рабочего пространства с ручными записями и необязательными связями с активностью.",
|
||||||
|
"contributions.views.verstak.journal.view.title": "Журнал",
|
||||||
|
"contributions.sidebarItems.verstak.journal.sidebar.title": "Журнал",
|
||||||
|
"contributions.workspaceItems.verstak.journal.workspace.title": "Журнал",
|
||||||
|
"ui.loading": "Загрузка журнала...",
|
||||||
|
"ui.title": "Журнал",
|
||||||
|
"ui.workspaceTitle": "Журнал · {workspace}",
|
||||||
|
"ui.add": "Добавить",
|
||||||
|
"ui.saveError": "Не удалось сохранить журнал: {error}",
|
||||||
|
"ui.aggregating": "Сбор записей о работе",
|
||||||
|
"ui.loadError": "Не удалось загрузить журнал: {error}",
|
||||||
|
"ui.ready": "Готово",
|
||||||
|
"ui.workItem": "Выполненная работа",
|
||||||
|
"ui.body": "Описание",
|
||||||
|
"ui.editEntry": "Изменить запись журнала",
|
||||||
|
"ui.reviewCandidate": "Проверить возможную запись журнала",
|
||||||
|
"ui.fromTodo": "Создать запись журнала из выполненной задачи",
|
||||||
|
"ui.addEntry": "Добавить запись журнала",
|
||||||
|
"ui.date": "Дата",
|
||||||
|
"ui.minutes": "Минуты",
|
||||||
|
"ui.fieldTitle": "Название",
|
||||||
|
"ui.billable": "Оплачиваемая работа",
|
||||||
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.saveChanges": "Сохранить изменения",
|
||||||
|
"ui.addEntryShort": "Добавить запись",
|
||||||
|
"ui.titleRequired": "Введите название",
|
||||||
|
"ui.deleted": "Запись удалена",
|
||||||
|
"ui.empty": "Записей о работе пока нет.",
|
||||||
|
"ui.edit": "Изменить",
|
||||||
|
"ui.delete": "Удалить"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped journal with user-authored entries and optional Activity links.",
|
"description": "Workspace-scoped journal with user-authored entries and optional Activity links.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "book-open",
|
"icon": "book-open",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,11 @@
|
||||||
var sortMode = 'title-asc';
|
var sortMode = 'title-asc';
|
||||||
var renameTarget = null;
|
var renameTarget = null;
|
||||||
|
|
||||||
|
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 notesParent() {
|
function notesParent() {
|
||||||
return workspaceRoot || '';
|
return workspaceRoot || '';
|
||||||
}
|
}
|
||||||
|
|
@ -262,8 +267,8 @@
|
||||||
// ─── UI Elements ────────────────────────────────────────
|
// ─── UI Elements ────────────────────────────────────────
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'notes-toolbar' });
|
var toolbar = el('div', { className: 'notes-toolbar' });
|
||||||
var createBtn = el('button', { className: 'notes-btn primary', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' });
|
var createBtn = el('button', { className: 'notes-btn primary', 'data-action': 'create', innerHTML: iconSvg('add') + ' ' + tr('ui.newNote', null, 'New Note') });
|
||||||
var filterInput = el('input', { className: 'notes-filter', 'data-notes-filter': '', placeholder: 'Filter notes' });
|
var filterInput = el('input', { className: 'notes-filter', 'data-notes-filter': '', placeholder: tr('ui.filter', null, 'Filter notes') });
|
||||||
var sortSelect = el('select', { className: 'notes-sort', 'data-notes-sort': '' }, [
|
var sortSelect = el('select', { className: 'notes-sort', 'data-notes-sort': '' }, [
|
||||||
el('option', { value: 'title-asc' }, ['A-Z']),
|
el('option', { value: 'title-asc' }, ['A-Z']),
|
||||||
el('option', { value: 'title-desc' }, ['Z-A'])
|
el('option', { value: 'title-desc' }, ['Z-A'])
|
||||||
|
|
@ -276,25 +281,25 @@
|
||||||
toolbar.appendChild(statusEl);
|
toolbar.appendChild(statusEl);
|
||||||
containerEl.appendChild(toolbar);
|
containerEl.appendChild(toolbar);
|
||||||
|
|
||||||
var titleBar = el('div', { className: 'notes-title-bar' }, ['Notes in ' + workspaceName]);
|
var titleBar = el('div', { className: 'notes-title-bar' }, [tr('ui.title', { workspace: workspaceName }, 'Notes in ' + workspaceName)]);
|
||||||
containerEl.appendChild(titleBar);
|
containerEl.appendChild(titleBar);
|
||||||
|
|
||||||
var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' });
|
var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' });
|
||||||
containerEl.appendChild(listContainer);
|
containerEl.appendChild(listContainer);
|
||||||
|
|
||||||
var createPanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
var createPanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
||||||
var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: 'Note title' });
|
var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: tr('ui.noteTitle', null, 'Note title') });
|
||||||
var createConfirm = el('button', { className: 'notes-btn', textContent: 'Create' });
|
var createConfirm = el('button', { className: 'notes-btn', textContent: tr('ui.create', null, 'Create') });
|
||||||
var createCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' });
|
var createCancel = el('button', { className: 'notes-btn', textContent: tr('ui.cancel', null, 'Cancel') });
|
||||||
createPanel.appendChild(createInput);
|
createPanel.appendChild(createInput);
|
||||||
createPanel.appendChild(createConfirm);
|
createPanel.appendChild(createConfirm);
|
||||||
createPanel.appendChild(createCancel);
|
createPanel.appendChild(createCancel);
|
||||||
containerEl.appendChild(createPanel);
|
containerEl.appendChild(createPanel);
|
||||||
|
|
||||||
var renamePanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
var renamePanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
||||||
var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: 'New title' });
|
var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: tr('ui.newTitle', null, 'New title') });
|
||||||
var renameConfirm = el('button', { className: 'notes-btn', textContent: 'Rename' });
|
var renameConfirm = el('button', { className: 'notes-btn', textContent: tr('ui.rename', null, 'Rename') });
|
||||||
var renameCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' });
|
var renameCancel = el('button', { className: 'notes-btn', textContent: tr('ui.cancel', null, 'Cancel') });
|
||||||
renamePanel.appendChild(renameInput);
|
renamePanel.appendChild(renameInput);
|
||||||
renamePanel.appendChild(renameConfirm);
|
renamePanel.appendChild(renameConfirm);
|
||||||
renamePanel.appendChild(renameCancel);
|
renamePanel.appendChild(renameCancel);
|
||||||
|
|
@ -321,7 +326,7 @@
|
||||||
|
|
||||||
function loadNotes() {
|
function loadNotes() {
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, ['Loading...']));
|
listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')]));
|
||||||
|
|
||||||
var parent = notesParent();
|
var parent = notesParent();
|
||||||
listNotes(parent).then(function (result) {
|
listNotes(parent).then(function (result) {
|
||||||
|
|
@ -392,36 +397,36 @@
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
if (!notes || notes.length === 0) {
|
if (!notes || notes.length === 0) {
|
||||||
renderEmpty('No notes yet');
|
renderEmpty(tr('ui.empty', null, 'No notes yet'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var shown = visibleNotes();
|
var shown = visibleNotes();
|
||||||
if (shown.length === 0) {
|
if (shown.length === 0) {
|
||||||
renderEmpty('No matching notes', 'Clear the filter to show all notes');
|
renderEmpty(tr('ui.noMatches', null, 'No matching notes'), tr('ui.clearFilterHint', null, 'Clear the filter to show all notes'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shown.forEach(function (note) {
|
shown.forEach(function (note) {
|
||||||
var actionButtons = [
|
var actionButtons = [
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'notes-item-btn',
|
className: 'notes-item-btn',
|
||||||
title: 'Open',
|
title: tr('ui.open', null, 'Open'),
|
||||||
'aria-label': 'Open',
|
'aria-label': tr('ui.open', null, 'Open'),
|
||||||
'data-note-action': 'open',
|
'data-note-action': 'open',
|
||||||
innerHTML: iconSvg('open'),
|
innerHTML: iconSvg('open'),
|
||||||
onClick: function (e) { e.stopPropagation(); openNote(note); }
|
onClick: function (e) { e.stopPropagation(); openNote(note); }
|
||||||
}),
|
}),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'notes-item-btn',
|
className: 'notes-item-btn',
|
||||||
title: 'Rename',
|
title: tr('ui.rename', null, 'Rename'),
|
||||||
'aria-label': 'Rename',
|
'aria-label': tr('ui.rename', null, 'Rename'),
|
||||||
'data-note-action': 'rename',
|
'data-note-action': 'rename',
|
||||||
innerHTML: iconSvg('rename'),
|
innerHTML: iconSvg('rename'),
|
||||||
onClick: function (e) { e.stopPropagation(); beginRename(note); }
|
onClick: function (e) { e.stopPropagation(); beginRename(note); }
|
||||||
}),
|
}),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'notes-item-btn',
|
className: 'notes-item-btn',
|
||||||
title: 'Move to Trash',
|
title: tr('ui.trash', null, 'Move to Trash'),
|
||||||
'aria-label': 'Move to Trash',
|
'aria-label': tr('ui.trash', null, 'Move to Trash'),
|
||||||
'data-note-action': 'trash',
|
'data-note-action': 'trash',
|
||||||
innerHTML: iconSvg('trash'),
|
innerHTML: iconSvg('trash'),
|
||||||
onClick: function (e) { e.stopPropagation(); confirmTrashNote(note); }
|
onClick: function (e) { e.stopPropagation(); confirmTrashNote(note); }
|
||||||
|
|
@ -457,7 +462,7 @@
|
||||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, [
|
listContainer.appendChild(el('div', { className: 'notes-empty' }, [
|
||||||
el('div', { innerHTML: iconSvg('note') }),
|
el('div', { innerHTML: iconSvg('note') }),
|
||||||
el('div', {}, [msg]),
|
el('div', {}, [msg]),
|
||||||
el('div', { className: 'notes-empty-hint' }, [hint || 'Click "New Note" to create one'])
|
el('div', { className: 'notes-empty-hint' }, [hint || tr('ui.emptyHint', null, 'Click "New Note" to create one')])
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -499,7 +504,7 @@
|
||||||
function confirmCreate() {
|
function confirmCreate() {
|
||||||
var title = createInput.value.trim();
|
var title = createInput.value.trim();
|
||||||
if (!title) return;
|
if (!title) return;
|
||||||
setStatus('Creating note...', 'loading');
|
setStatus(tr('ui.creating', null, 'Creating note...'), 'loading');
|
||||||
var parent = notesParent();
|
var parent = notesParent();
|
||||||
createNote(parent, title).then(function (data) {
|
createNote(parent, title).then(function (data) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
|
@ -509,7 +514,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideCreate();
|
hideCreate();
|
||||||
setStatus('Note created', 'success');
|
setStatus(tr('ui.created', null, 'Note created'), 'success');
|
||||||
loadNotes();
|
loadNotes();
|
||||||
// Open the newly created note
|
// Open the newly created note
|
||||||
if (data.path) {
|
if (data.path) {
|
||||||
|
|
@ -551,7 +556,7 @@
|
||||||
if (!renameTarget) return;
|
if (!renameTarget) return;
|
||||||
var newTitle = renameInput.value.trim();
|
var newTitle = renameInput.value.trim();
|
||||||
if (!newTitle) return;
|
if (!newTitle) return;
|
||||||
setStatus('Renaming...', 'loading');
|
setStatus(tr('ui.renaming', null, 'Renaming...'), 'loading');
|
||||||
renameNote(renameTarget.path, newTitle).then(function (data) {
|
renameNote(renameTarget.path, newTitle).then(function (data) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
data = data || {};
|
data = data || {};
|
||||||
|
|
@ -560,7 +565,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideRename();
|
hideRename();
|
||||||
setStatus('Note renamed', 'success');
|
setStatus(tr('ui.renamed', null, 'Note renamed'), 'success');
|
||||||
loadNotes();
|
loadNotes();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus('Error: ' + (err.message || err), 'error');
|
setStatus('Error: ' + (err.message || err), 'error');
|
||||||
|
|
@ -573,11 +578,11 @@
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
showTrashModal(note).then(function (confirmed) {
|
showTrashModal(note).then(function (confirmed) {
|
||||||
if (!confirmed || disposed) return;
|
if (!confirmed || disposed) return;
|
||||||
setStatus('Moving note to trash...', 'loading');
|
setStatus(tr('ui.trashing', null, 'Moving note to trash...'), 'loading');
|
||||||
api.files.trash(note.path).then(function () {
|
api.files.trash(note.path).then(function () {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
if (selectedPath === note.path) selectedPath = '';
|
if (selectedPath === note.path) selectedPath = '';
|
||||||
setStatus('Note moved to trash', 'success');
|
setStatus(tr('ui.trashed', null, 'Note moved to trash'), 'success');
|
||||||
loadNotes();
|
loadNotes();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus('Error: ' + (err.message || err), 'error');
|
setStatus('Error: ' + (err.message || err), 'error');
|
||||||
|
|
@ -590,11 +595,11 @@
|
||||||
function showConflictModal(title, existingPath, focusTarget) {
|
function showConflictModal(title, existingPath, focusTarget) {
|
||||||
var overlay = el('div', { className: 'notes-modal-overlay' });
|
var overlay = el('div', { className: 'notes-modal-overlay' });
|
||||||
var modal = el('div', { className: 'notes-modal' }, [
|
var modal = el('div', { className: 'notes-modal' }, [
|
||||||
el('div', { className: 'notes-modal-title' }, ['Name Conflict']),
|
el('div', { className: 'notes-modal-title' }, [tr('ui.conflictTitle', null, 'Name Conflict')]),
|
||||||
el('div', { className: 'notes-modal-msg' }, [
|
el('div', { className: 'notes-modal-msg' }, [
|
||||||
'A note with the title "' + title + '" already exists.',
|
tr('ui.conflictMessage', { title: title }, 'A note with the title "' + title + '" already exists.'),
|
||||||
existingPath ? ' Existing file: ' + existingPath + '.' : '',
|
existingPath ? tr('ui.existingFile', { path: existingPath }, ' Existing file: ' + existingPath + '.') : '',
|
||||||
' Please choose a different title.'
|
tr('ui.chooseDifferent', null, ' Please choose a different title.')
|
||||||
].join('')),
|
].join('')),
|
||||||
el('div', { className: 'notes-modal-actions' }, [
|
el('div', { className: 'notes-modal-actions' }, [
|
||||||
el('button', { className: 'notes-modal-btn confirm', textContent: 'OK', onClick: function () { overlay.remove(); (focusTarget || createInput).focus(); } })
|
el('button', { className: 'notes-modal-btn confirm', textContent: 'OK', onClick: function () { overlay.remove(); (focusTarget || createInput).focus(); } })
|
||||||
|
|
@ -612,13 +617,13 @@
|
||||||
resolve(value);
|
resolve(value);
|
||||||
}
|
}
|
||||||
var modal = el('div', { className: 'notes-modal' }, [
|
var modal = el('div', { className: 'notes-modal' }, [
|
||||||
el('div', { className: 'notes-modal-title' }, ['Move Note to Trash']),
|
el('div', { className: 'notes-modal-title' }, [tr('ui.trashTitle', null, 'Move Note to Trash')]),
|
||||||
el('div', { className: 'notes-modal-msg' }, [
|
el('div', { className: 'notes-modal-msg' }, [
|
||||||
'Move "' + (note.title || fileName(note.path)) + '" to trash?'
|
tr('ui.trashConfirm', { title: note.title || fileName(note.path) }, 'Move "' + (note.title || fileName(note.path)) + '" to trash?')
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'notes-modal-actions' }, [
|
el('div', { className: 'notes-modal-actions' }, [
|
||||||
el('button', { className: 'notes-modal-btn cancel', textContent: 'Cancel', onClick: function () { close(false); } }),
|
el('button', { className: 'notes-modal-btn cancel', textContent: tr('ui.cancel', null, 'Cancel'), onClick: function () { close(false); } }),
|
||||||
el('button', { className: 'notes-modal-btn danger', 'data-notes-confirm-trash': '', textContent: 'Move to Trash', onClick: function () { close(true); } })
|
el('button', { className: 'notes-modal-btn danger', 'data-notes-confirm-trash': '', textContent: tr('ui.trash', null, 'Move to Trash'), onClick: function () { close(true); } })
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
overlay.appendChild(modal);
|
overlay.appendChild(modal);
|
||||||
|
|
@ -655,6 +660,22 @@
|
||||||
loadContributionActions();
|
loadContributionActions();
|
||||||
loadNotes();
|
loadNotes();
|
||||||
|
|
||||||
|
var localeUnsubscribe = null;
|
||||||
|
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
|
localeUnsubscribe = api.i18n.onDidChangeLocale(function () {
|
||||||
|
createBtn.innerHTML = iconSvg('add') + ' ' + tr('ui.newNote', null, 'New Note');
|
||||||
|
filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter notes'));
|
||||||
|
titleBar.textContent = tr('ui.title', { workspace: workspaceName }, 'Notes in ' + workspaceName);
|
||||||
|
createInput.setAttribute('placeholder', tr('ui.noteTitle', null, 'Note title'));
|
||||||
|
renameInput.setAttribute('placeholder', tr('ui.newTitle', null, 'New title'));
|
||||||
|
createConfirm.textContent = tr('ui.create', null, 'Create');
|
||||||
|
createCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
||||||
|
renameConfirm.textContent = tr('ui.rename', null, 'Rename');
|
||||||
|
renameCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
||||||
|
renderList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var fileChangedUnsubscribe = null;
|
var fileChangedUnsubscribe = null;
|
||||||
if (api.events && typeof api.events.subscribe === 'function') {
|
if (api.events && typeof api.events.subscribe === 'function') {
|
||||||
api.events.subscribe('file.changed', function (event) {
|
api.events.subscribe('file.changed', function (event) {
|
||||||
|
|
@ -669,6 +690,7 @@
|
||||||
|
|
||||||
containerEl.__notesCleanup = function () {
|
containerEl.__notesCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Notes",
|
||||||
|
"manifest.description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
||||||
|
"contributions.workspaceItems.verstak.notes.workspace.title": "Notes",
|
||||||
|
"ui.newNote": "New Note",
|
||||||
|
"ui.filter": "Filter notes",
|
||||||
|
"ui.title": "Notes in {workspace}",
|
||||||
|
"ui.noteTitle": "Note title",
|
||||||
|
"ui.create": "Create",
|
||||||
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.newTitle": "New title",
|
||||||
|
"ui.rename": "Rename",
|
||||||
|
"ui.loading": "Loading...",
|
||||||
|
"ui.empty": "No notes yet",
|
||||||
|
"ui.noMatches": "No matching notes",
|
||||||
|
"ui.clearFilterHint": "Clear the filter to show all notes",
|
||||||
|
"ui.open": "Open",
|
||||||
|
"ui.trash": "Move to Trash",
|
||||||
|
"ui.emptyHint": "Click \"New Note\" to create one",
|
||||||
|
"ui.creating": "Creating note...",
|
||||||
|
"ui.created": "Note created",
|
||||||
|
"ui.renaming": "Renaming...",
|
||||||
|
"ui.renamed": "Note renamed",
|
||||||
|
"ui.trashing": "Moving note to trash...",
|
||||||
|
"ui.trashed": "Note moved to trash",
|
||||||
|
"ui.conflictTitle": "Name Conflict",
|
||||||
|
"ui.conflictMessage": "A note with the title \"{title}\" already exists.",
|
||||||
|
"ui.existingFile": " Existing file: {path}.",
|
||||||
|
"ui.chooseDifferent": " Please choose a different title.",
|
||||||
|
"ui.trashTitle": "Move Note to Trash",
|
||||||
|
"ui.trashConfirm": "Move \"{title}\" to trash?"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Заметки",
|
||||||
|
"manifest.description": "Менеджер Markdown-заметок рабочего пространства с созданием, переименованием и корзиной.",
|
||||||
|
"contributions.workspaceItems.verstak.notes.workspace.title": "Заметки",
|
||||||
|
"ui.newNote": "Новая заметка",
|
||||||
|
"ui.filter": "Фильтр заметок",
|
||||||
|
"ui.title": "Заметки · {workspace}",
|
||||||
|
"ui.noteTitle": "Название заметки",
|
||||||
|
"ui.create": "Создать",
|
||||||
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.newTitle": "Новое название",
|
||||||
|
"ui.rename": "Переименовать",
|
||||||
|
"ui.loading": "Загрузка...",
|
||||||
|
"ui.empty": "Заметок пока нет",
|
||||||
|
"ui.noMatches": "Подходящих заметок нет",
|
||||||
|
"ui.clearFilterHint": "Сбросьте фильтр, чтобы показать все заметки",
|
||||||
|
"ui.open": "Открыть",
|
||||||
|
"ui.trash": "Переместить в корзину",
|
||||||
|
"ui.emptyHint": "Нажмите «Новая заметка», чтобы создать первую",
|
||||||
|
"ui.creating": "Создание заметки...",
|
||||||
|
"ui.created": "Заметка создана",
|
||||||
|
"ui.renaming": "Переименование...",
|
||||||
|
"ui.renamed": "Заметка переименована",
|
||||||
|
"ui.trashing": "Перемещение заметки в корзину...",
|
||||||
|
"ui.trashed": "Заметка перемещена в корзину",
|
||||||
|
"ui.conflictTitle": "Конфликт названия",
|
||||||
|
"ui.conflictMessage": "Заметка с названием «{title}» уже существует.",
|
||||||
|
"ui.existingFile": " Существующий файл: {path}.",
|
||||||
|
"ui.chooseDifferent": " Выберите другое название.",
|
||||||
|
"ui.trashTitle": "Переместить заметку в корзину",
|
||||||
|
"ui.trashConfirm": "Переместить «{title}» в корзину?"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
"description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "edit",
|
"icon": "edit",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,11 @@
|
||||||
|
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
containerEl.className = 'pt-root';
|
containerEl.className = 'pt-root';
|
||||||
|
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
containerEl.__ptCleanup = [];
|
containerEl.__ptCleanup = [];
|
||||||
|
|
||||||
function trackCleanup(fn) {
|
function trackCleanup(fn) {
|
||||||
|
|
@ -79,7 +84,7 @@
|
||||||
var header = div('pt-header', [
|
var header = div('pt-header', [
|
||||||
span('pt-icon', '◉'),
|
span('pt-icon', '◉'),
|
||||||
div('pt-title-group', [
|
div('pt-title-group', [
|
||||||
el('h2', { className: 'pt-plugin-name' }, ['Platform Diagnostics']),
|
el('h2', { className: 'pt-plugin-name' }, [tr('ui.diagnostics', null, 'Platform Diagnostics')]),
|
||||||
el('p', { className: 'pt-plugin-id' }, [api.pluginId]),
|
el('p', { className: 'pt-plugin-id' }, [api.pluginId]),
|
||||||
]),
|
]),
|
||||||
span('pt-version', 'v' + (props && props.version ? props.version : '0.1.0')),
|
span('pt-version', 'v' + (props && props.version ? props.version : '0.1.0')),
|
||||||
|
|
@ -88,7 +93,7 @@
|
||||||
/* ── Status badge ──────────────────────────────────────────── */
|
/* ── Status badge ──────────────────────────────────────────── */
|
||||||
var badge = div('pt-badge pt-badge-success', [
|
var badge = div('pt-badge pt-badge-success', [
|
||||||
el('span', {}, ['✅']),
|
el('span', {}, ['✅']),
|
||||||
el('span', {}, ['Frontend Bundle Loaded']),
|
el('span', {}, [tr('ui.bundleLoaded', null, 'Frontend Bundle Loaded')]),
|
||||||
]);
|
]);
|
||||||
var badgeRow = div('', [badge]);
|
var badgeRow = div('', [badge]);
|
||||||
|
|
||||||
|
|
@ -274,7 +279,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
var bridgeCard = div('pt-card', [
|
var bridgeCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Real Plugin API Bridge']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.apiBridge', null, 'Real Plugin API Bridge')]),
|
||||||
el('ul', { className: 'pt-list' }, [
|
el('ul', { className: 'pt-list' }, [
|
||||||
el('li', { className: 'pt-list-item' }, [
|
el('li', { className: 'pt-list-item' }, [
|
||||||
span('pt-list-label', 'Persisted setting'),
|
span('pt-list-label', 'Persisted setting'),
|
||||||
|
|
@ -361,7 +366,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
var testsCard = div('pt-card', [
|
var testsCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Test Results']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.testResults', null, 'Test Results')]),
|
||||||
summaryRow,
|
summaryRow,
|
||||||
testsList,
|
testsList,
|
||||||
]);
|
]);
|
||||||
|
|
@ -398,7 +403,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
var capsCard = div('pt-card', [
|
var capsCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Registered Capabilities']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.capabilities', null, 'Registered Capabilities')]),
|
||||||
capList,
|
capList,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -421,7 +426,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
var infoCard = div('pt-card', [
|
var infoCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Plugin Info']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.pluginInfo', null, 'Plugin Info')]),
|
||||||
infoList,
|
infoList,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -456,7 +461,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
var apiCard = div('pt-card', [
|
var apiCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Host API Methods']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.hostMethods', null, 'Host API Methods')]),
|
||||||
apiStatusList,
|
apiStatusList,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -584,7 +589,7 @@
|
||||||
trackCleanup(function () { stopMouseCapture(); });
|
trackCleanup(function () { stopMouseCapture(); });
|
||||||
|
|
||||||
var mouseCard = div('pt-card', [
|
var mouseCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Mouse Event Inspector']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.mouseInspector', null, 'Mouse Event Inspector')]),
|
||||||
el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [
|
el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [
|
||||||
'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.',
|
'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.',
|
||||||
]),
|
]),
|
||||||
|
|
@ -667,6 +672,11 @@
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
containerEl.className = 'pt-root';
|
containerEl.className = 'pt-root';
|
||||||
|
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Counter state (local, not persisted) ──────────────────── */
|
/* ── Counter state (local, not persisted) ──────────────────── */
|
||||||
var counterState = { value: 0 };
|
var counterState = { value: 0 };
|
||||||
|
|
||||||
|
|
@ -674,7 +684,7 @@
|
||||||
var header = div('pt-header', [
|
var header = div('pt-header', [
|
||||||
span('pt-icon', '⚙️'),
|
span('pt-icon', '⚙️'),
|
||||||
div('pt-title-group', [
|
div('pt-title-group', [
|
||||||
el('h2', { className: 'pt-plugin-name' }, ['Platform Test Settings']),
|
el('h2', { className: 'pt-plugin-name' }, [tr('ui.settings', null, 'Platform Test Settings')]),
|
||||||
el('p', { className: 'pt-plugin-id' }, [api.pluginId]),
|
el('p', { className: 'pt-plugin-id' }, [api.pluginId]),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
@ -697,24 +707,24 @@
|
||||||
var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () {
|
var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () {
|
||||||
counterState.value += 1;
|
counterState.value += 1;
|
||||||
counterDisplay.firstChild.textContent = String(counterState.value);
|
counterDisplay.firstChild.textContent = String(counterState.value);
|
||||||
}}, ['+ Increment']);
|
}}, [tr('ui.increment', null, '+ Increment')]);
|
||||||
|
|
||||||
var decrementBtn = el('button', { className: 'btn btn-secondary', onClick: function () {
|
var decrementBtn = el('button', { className: 'btn btn-secondary', onClick: function () {
|
||||||
counterState.value = Math.max(0, counterState.value - 1);
|
counterState.value = Math.max(0, counterState.value - 1);
|
||||||
counterDisplay.firstChild.textContent = String(counterState.value);
|
counterDisplay.firstChild.textContent = String(counterState.value);
|
||||||
}}, ['− Decrement']);
|
}}, [tr('ui.decrement', null, '− Decrement')]);
|
||||||
|
|
||||||
var resetBtn = el('button', { className: 'btn btn-secondary', onClick: function () {
|
var resetBtn = el('button', { className: 'btn btn-secondary', onClick: function () {
|
||||||
counterState.value = 0;
|
counterState.value = 0;
|
||||||
counterDisplay.firstChild.textContent = '0';
|
counterDisplay.firstChild.textContent = '0';
|
||||||
}}, ['↺ Reset']);
|
}}, [tr('ui.reset', null, '↺ Reset')]);
|
||||||
|
|
||||||
var btnGroup = el('div', { style: { display: 'flex', gap: '0.5rem' } }, [
|
var btnGroup = el('div', { style: { display: 'flex', gap: '0.5rem' } }, [
|
||||||
incrementBtn, decrementBtn, resetBtn,
|
incrementBtn, decrementBtn, resetBtn,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var counterCard = div('pt-card', [
|
var counterCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Interactive Counter (Local State)']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.counter', null, 'Interactive Counter (Local State)')]),
|
||||||
counterDisplay,
|
counterDisplay,
|
||||||
btnGroup,
|
btnGroup,
|
||||||
el('p', { style: { marginTop: '0.75rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [
|
el('p', { style: { marginTop: '0.75rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [
|
||||||
|
|
@ -740,7 +750,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
var settingsCard = div('pt-card', [
|
var settingsCard = div('pt-card', [
|
||||||
el('h3', { className: 'pt-card-title' }, ['Plugin Settings (Demo)']),
|
el('h3', { className: 'pt-card-title' }, [tr('ui.demoSettings', null, 'Plugin Settings (Demo)')]),
|
||||||
settingsDemoList,
|
settingsDemoList,
|
||||||
el('p', { style: { marginTop: '0.5rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [
|
el('p', { style: { marginTop: '0.5rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [
|
||||||
'Use api.settings.read() / api.settings.write() for persisted settings.',
|
'Use api.settings.read() / api.settings.write() for persisted settings.',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Platform Test",
|
||||||
|
"manifest.description": "Runtime test plugin for verifying the Verstak platform: manifest loading, capability registration, contribution injection, event bus, and UI rendering.",
|
||||||
|
"contributions.views.verstak.platform-test.diagnostics.title": "Platform Diagnostics",
|
||||||
|
"contributions.commands.verstak.platform-test.run-tests.title": "Run Platform Tests",
|
||||||
|
"contributions.commands.verstak.platform-test.show-version.title": "Show Version Info",
|
||||||
|
"contributions.sidebarItems.verstak.platform-test.sidebar.title": "Platform Test",
|
||||||
|
"contributions.statusBarItems.verstak.platform-test.status.label": "[OK] All Tests Pass",
|
||||||
|
"contributions.settingsPanels.verstak.platform-test.settings.title": "Platform Test Settings",
|
||||||
|
"contributions.openProviders.verstak.platform-test.markdown-diagnostic.title": "Platform Test Markdown Diagnostic",
|
||||||
|
"ui.diagnostics": "Platform Diagnostics",
|
||||||
|
"ui.bundleLoaded": "Plugin Bundle Loaded",
|
||||||
|
"ui.apiBridge": "API Bridge",
|
||||||
|
"ui.testResults": "Test Results",
|
||||||
|
"ui.capabilities": "Granted Capabilities",
|
||||||
|
"ui.pluginInfo": "Plugin Info",
|
||||||
|
"ui.hostMethods": "Host Methods",
|
||||||
|
"ui.mouseInspector": "Mouse Event Inspector",
|
||||||
|
"ui.settings": "Platform Test Settings",
|
||||||
|
"ui.increment": "+ Increment",
|
||||||
|
"ui.decrement": "− Decrement",
|
||||||
|
"ui.reset": "Reset",
|
||||||
|
"ui.counter": "Interactive Counter",
|
||||||
|
"ui.demoSettings": "Demo Settings"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Тест платформы",
|
||||||
|
"manifest.description": "Диагностический плагин для проверки загрузки manifest, capabilities, contributions, шины событий и интерфейса Верстака.",
|
||||||
|
"contributions.views.verstak.platform-test.diagnostics.title": "Диагностика платформы",
|
||||||
|
"contributions.commands.verstak.platform-test.run-tests.title": "Запустить тесты платформы",
|
||||||
|
"contributions.commands.verstak.platform-test.show-version.title": "Показать сведения о версии",
|
||||||
|
"contributions.sidebarItems.verstak.platform-test.sidebar.title": "Тест платформы",
|
||||||
|
"contributions.statusBarItems.verstak.platform-test.status.label": "[OK] Все тесты пройдены",
|
||||||
|
"contributions.settingsPanels.verstak.platform-test.settings.title": "Настройки теста платформы",
|
||||||
|
"contributions.openProviders.verstak.platform-test.markdown-diagnostic.title": "Диагностика Markdown платформы",
|
||||||
|
"ui.diagnostics": "Диагностика платформы",
|
||||||
|
"ui.bundleLoaded": "Пакет плагина загружен",
|
||||||
|
"ui.apiBridge": "Мост API",
|
||||||
|
"ui.testResults": "Результаты тестов",
|
||||||
|
"ui.capabilities": "Предоставленные возможности",
|
||||||
|
"ui.pluginInfo": "Сведения о плагине",
|
||||||
|
"ui.hostMethods": "Методы хоста",
|
||||||
|
"ui.mouseInspector": "Инспектор событий мыши",
|
||||||
|
"ui.settings": "Настройки теста платформы",
|
||||||
|
"ui.increment": "+ Увеличить",
|
||||||
|
"ui.decrement": "− Уменьшить",
|
||||||
|
"ui.reset": "Сбросить",
|
||||||
|
"ui.counter": "Интерактивный счётчик",
|
||||||
|
"ui.demoSettings": "Демонстрационные настройки"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Runtime test plugin for verifying the Verstak platform: manifest loading, capability registration, contribution injection, event bus, and UI rendering.",
|
"description": "Runtime test plugin for verifying the Verstak platform: manifest loading, capability registration, contribution injection, event bus, and UI rendering.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "flask",
|
"icon": "flask",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,11 @@
|
||||||
var statusEl = null;
|
var statusEl = null;
|
||||||
var alertEl = null;
|
var alertEl = null;
|
||||||
var resultsEl = null;
|
var resultsEl = null;
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
|
state.status = tr('ui.minChars', null, 'Enter at least 2 characters.');
|
||||||
|
|
||||||
function ensureLayout() {
|
function ensureLayout() {
|
||||||
if (input) return;
|
if (input) return;
|
||||||
|
|
@ -325,7 +330,7 @@
|
||||||
input = el('input', {
|
input = el('input', {
|
||||||
className: 'search-input',
|
className: 'search-input',
|
||||||
type: 'search',
|
type: 'search',
|
||||||
placeholder: 'Search files, folders, text',
|
placeholder: tr('ui.placeholder', null, 'Search files, folders, text'),
|
||||||
value: state.query,
|
value: state.query,
|
||||||
'data-search-input': 'query',
|
'data-search-input': 'query',
|
||||||
onInput: function (event) {
|
onInput: function (event) {
|
||||||
|
|
@ -341,7 +346,7 @@
|
||||||
containerEl.appendChild(el('div', { className: 'search-toolbar' }, [
|
containerEl.appendChild(el('div', { className: 'search-toolbar' }, [
|
||||||
input,
|
input,
|
||||||
button,
|
button,
|
||||||
el('span', { className: 'search-scope', title: rootPath || 'Vault' }, [rootPath || 'Vault'])
|
el('span', { className: 'search-scope', title: rootPath || tr('ui.vault', null, 'Vault') }, [rootPath || tr('ui.vault', null, 'Vault')])
|
||||||
]));
|
]));
|
||||||
|
|
||||||
statusEl = el('div', { className: 'search-status' });
|
statusEl = el('div', { className: 'search-status' });
|
||||||
|
|
@ -357,7 +362,7 @@
|
||||||
if (document.activeElement !== input && input.value !== state.query) {
|
if (document.activeElement !== input && input.value !== state.query) {
|
||||||
input.value = state.query;
|
input.value = state.query;
|
||||||
}
|
}
|
||||||
button.textContent = state.searching ? 'Searching...' : 'Search';
|
button.textContent = state.searching ? tr('ui.searching', null, 'Searching...') : tr('ui.search', null, 'Search');
|
||||||
button.disabled = !!state.searching;
|
button.disabled = !!state.searching;
|
||||||
statusEl.className = 'search-status' + (state.error ? ' error' : '');
|
statusEl.className = 'search-status' + (state.error ? ' error' : '');
|
||||||
statusEl.textContent = state.error || state.status;
|
statusEl.textContent = state.error || state.status;
|
||||||
|
|
@ -365,7 +370,7 @@
|
||||||
if (state.providerErrors && state.providerErrors.length) {
|
if (state.providerErrors && state.providerErrors.length) {
|
||||||
if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden');
|
if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden');
|
||||||
alertEl.appendChild(el('details', {}, [
|
alertEl.appendChild(el('details', {}, [
|
||||||
el('summary', {}, ['Some plugin search providers did not respond']),
|
el('summary', {}, [tr('ui.providersFailed', null, 'Some plugin search providers did not respond')]),
|
||||||
el('div', {}, [state.providerErrors.join('; ')])
|
el('div', {}, [state.providerErrors.join('; ')])
|
||||||
]));
|
]));
|
||||||
} else if (typeof alertEl.setAttribute === 'function') {
|
} else if (typeof alertEl.setAttribute === 'function') {
|
||||||
|
|
@ -373,7 +378,7 @@
|
||||||
}
|
}
|
||||||
resultsEl.innerHTML = '';
|
resultsEl.innerHTML = '';
|
||||||
if (!state.results.length) {
|
if (!state.results.length) {
|
||||||
resultsEl.appendChild(el('div', { className: 'search-empty' }, [state.searching ? 'Searching...' : 'No results']));
|
resultsEl.appendChild(el('div', { className: 'search-empty' }, [state.searching ? tr('ui.searching', null, 'Searching...') : tr('ui.noResults', null, 'No results')]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.results.forEach(function (result) {
|
state.results.forEach(function (result) {
|
||||||
|
|
@ -391,7 +396,7 @@
|
||||||
]),
|
]),
|
||||||
result.openable ? el('button', {
|
result.openable ? el('button', {
|
||||||
className: 'search-btn search-open-btn',
|
className: 'search-btn search-open-btn',
|
||||||
textContent: 'Open',
|
textContent: tr('ui.open', null, 'Open'),
|
||||||
'data-search-open': result.path,
|
'data-search-open': result.path,
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
api.workbench.openResource({
|
api.workbench.openResource({
|
||||||
|
|
@ -415,7 +420,7 @@
|
||||||
if (query.length < 2) {
|
if (query.length < 2) {
|
||||||
state.searching = false;
|
state.searching = false;
|
||||||
state.results = [];
|
state.results = [];
|
||||||
state.status = 'Enter at least 2 characters.';
|
state.status = tr('ui.minChars', null, 'Enter at least 2 characters.');
|
||||||
state.error = '';
|
state.error = '';
|
||||||
state.providerErrors = [];
|
state.providerErrors = [];
|
||||||
render();
|
render();
|
||||||
|
|
@ -432,7 +437,7 @@
|
||||||
state.query = String(state.query || '').trim();
|
state.query = String(state.query || '').trim();
|
||||||
if (state.query.length < 2) {
|
if (state.query.length < 2) {
|
||||||
state.results = [];
|
state.results = [];
|
||||||
state.status = 'Enter at least 2 characters.';
|
state.status = tr('ui.minChars', null, 'Enter at least 2 characters.');
|
||||||
state.error = '';
|
state.error = '';
|
||||||
state.providerErrors = [];
|
state.providerErrors = [];
|
||||||
render();
|
render();
|
||||||
|
|
@ -441,7 +446,7 @@
|
||||||
state.searching = true;
|
state.searching = true;
|
||||||
state.error = '';
|
state.error = '';
|
||||||
state.providerErrors = [];
|
state.providerErrors = [];
|
||||||
state.status = 'Searching...';
|
state.status = tr('ui.searching', null, 'Searching...');
|
||||||
var seq = searchSeq + 1;
|
var seq = searchSeq + 1;
|
||||||
searchSeq = seq;
|
searchSeq = seq;
|
||||||
render();
|
render();
|
||||||
|
|
@ -451,7 +456,7 @@
|
||||||
var external = await runExternalProviders(api, rootPath, state.query, MAX_RESULTS - results.length);
|
var external = await runExternalProviders(api, rootPath, state.query, MAX_RESULTS - results.length);
|
||||||
if (seq !== searchSeq) return;
|
if (seq !== searchSeq) return;
|
||||||
state.results = results.concat(external.results);
|
state.results = results.concat(external.results);
|
||||||
state.status = state.results.length + ' result' + (state.results.length === 1 ? '' : 's');
|
state.status = tr('ui.count', { count: state.results.length }, state.results.length + ' result' + (state.results.length === 1 ? '' : 's'));
|
||||||
state.providerErrors = external.errors;
|
state.providerErrors = external.errors;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (seq !== searchSeq) return;
|
if (seq !== searchSeq) return;
|
||||||
|
|
@ -507,6 +512,12 @@
|
||||||
|
|
||||||
setupIntegrations();
|
setupIntegrations();
|
||||||
render();
|
render();
|
||||||
|
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
|
cleanupFns.push(api.i18n.onDidChangeLocale(function () {
|
||||||
|
if (input) input.setAttribute('placeholder', tr('ui.placeholder', null, 'Search files, folders, text'));
|
||||||
|
render();
|
||||||
|
}));
|
||||||
|
}
|
||||||
containerEl.__verstakSearchCleanup = function () {
|
containerEl.__verstakSearchCleanup = function () {
|
||||||
if (searchTimer) clearTimeout(searchTimer);
|
if (searchTimer) clearTimeout(searchTimer);
|
||||||
searchSeq += 1;
|
searchSeq += 1;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Search",
|
||||||
|
"manifest.description": "Workspace-scoped vault text search provider.",
|
||||||
|
"contributions.workspaceItems.verstak.search.workspace.title": "Search",
|
||||||
|
"contributions.commands.verstak.search.searchVaultText.title": "Search Vault Text",
|
||||||
|
"contributions.searchProviders.verstak.search.vault-text.label": "Vault Text Search",
|
||||||
|
"ui.minChars": "Enter at least 2 characters.",
|
||||||
|
"ui.placeholder": "Search files, folders, text",
|
||||||
|
"ui.vault": "Vault",
|
||||||
|
"ui.searching": "Searching...",
|
||||||
|
"ui.search": "Search",
|
||||||
|
"ui.providersFailed": "Some plugin search providers did not respond",
|
||||||
|
"ui.noResults": "No results",
|
||||||
|
"ui.open": "Open",
|
||||||
|
"ui.count": "{count} result(s)"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Поиск",
|
||||||
|
"manifest.description": "Поиск текста в хранилище в пределах рабочего пространства.",
|
||||||
|
"contributions.workspaceItems.verstak.search.workspace.title": "Поиск",
|
||||||
|
"contributions.commands.verstak.search.searchVaultText.title": "Искать текст в хранилище",
|
||||||
|
"contributions.searchProviders.verstak.search.vault-text.label": "Поиск по тексту хранилища",
|
||||||
|
"ui.minChars": "Введите не менее 2 символов.",
|
||||||
|
"ui.placeholder": "Поиск файлов, папок и текста",
|
||||||
|
"ui.vault": "Хранилище",
|
||||||
|
"ui.searching": "Поиск...",
|
||||||
|
"ui.search": "Искать",
|
||||||
|
"ui.providersFailed": "Некоторые поставщики поиска не ответили",
|
||||||
|
"ui.noResults": "Ничего не найдено",
|
||||||
|
"ui.open": "Открыть",
|
||||||
|
"ui.count": "Результатов: {count}"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped vault text search provider.",
|
"description": "Workspace-scoped vault text search provider.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "search",
|
"icon": "search",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,10 @@
|
||||||
var unlocked = false;
|
var unlocked = false;
|
||||||
var statusText = '';
|
var statusText = '';
|
||||||
var statusError = false;
|
var statusError = false;
|
||||||
|
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 setStatus(message, isError) {
|
function setStatus(message, isError) {
|
||||||
statusText = message || '';
|
statusText = message || '';
|
||||||
|
|
@ -162,13 +166,13 @@
|
||||||
className: 'secrets-input',
|
className: 'secrets-input',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
'data-secret-master-password': '',
|
'data-secret-master-password': '',
|
||||||
placeholder: 'Master password'
|
placeholder: tr('ui.masterPassword', null, 'Master password')
|
||||||
});
|
});
|
||||||
var confirmInput = initialized ? null : el('input', {
|
var confirmInput = initialized ? null : el('input', {
|
||||||
className: 'secrets-input',
|
className: 'secrets-input',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
'data-secret-master-password-confirm': '',
|
'data-secret-master-password-confirm': '',
|
||||||
placeholder: 'Repeat master password'
|
placeholder: tr('ui.repeatPassword', null, 'Repeat master password')
|
||||||
});
|
});
|
||||||
var unlockBtn = el('button', {
|
var unlockBtn = el('button', {
|
||||||
className: 'secrets-btn primary',
|
className: 'secrets-btn primary',
|
||||||
|
|
@ -176,7 +180,7 @@
|
||||||
'data-secret-unlock': '',
|
'data-secret-unlock': '',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
if (!initialized && passwordInput.value !== confirmInput.value) {
|
if (!initialized && passwordInput.value !== confirmInput.value) {
|
||||||
setStatus('Master passwords do not match', true);
|
setStatus(tr('ui.passwordMismatch', null, 'Master passwords do not match'), true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unlockBtn.disabled = true;
|
unlockBtn.disabled = true;
|
||||||
|
|
@ -189,17 +193,17 @@
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setStatus((err && err.message) ? err.message : String(err), true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, ['Unlock']);
|
}, [tr('ui.unlock', null, 'Unlock')]);
|
||||||
if (!initialized) unlockBtn.textContent = 'Create master password';
|
if (!initialized) unlockBtn.textContent = tr('ui.createMaster', null, 'Create master password');
|
||||||
var rows = [
|
var rows = [
|
||||||
el('div', { className: 'secrets-row' }, [
|
el('div', { className: 'secrets-row' }, [
|
||||||
el('label', { className: 'secrets-label' }, ['Password']),
|
el('label', { className: 'secrets-label' }, [tr('ui.password', null, 'Password')]),
|
||||||
passwordInput
|
passwordInput
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
if (confirmInput) {
|
if (confirmInput) {
|
||||||
rows.push(el('div', { className: 'secrets-row' }, [
|
rows.push(el('div', { className: 'secrets-row' }, [
|
||||||
el('label', { className: 'secrets-label' }, ['Repeat']),
|
el('label', { className: 'secrets-label' }, [tr('ui.repeat', null, 'Repeat')]),
|
||||||
confirmInput
|
confirmInput
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
@ -209,12 +213,12 @@
|
||||||
containerEl.appendChild(el('div', { className: 'secrets-root' }, [
|
containerEl.appendChild(el('div', { className: 'secrets-root' }, [
|
||||||
el('div', { className: 'secrets-panel' }, [
|
el('div', { className: 'secrets-panel' }, [
|
||||||
el('div', { className: 'secrets-toolbar' }, [
|
el('div', { className: 'secrets-toolbar' }, [
|
||||||
el('span', { className: 'secrets-title' }, ['Secrets'])
|
el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')])
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'secrets-main' }, [
|
el('div', { className: 'secrets-main' }, [
|
||||||
el('div', { className: 'secrets-card' }, [
|
el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [initialized ? 'Unlock secrets' : 'Create master password']),
|
el('h2', {}, [initialized ? tr('ui.unlockSecrets', null, 'Unlock secrets') : tr('ui.createMaster', null, 'Create master password')]),
|
||||||
el('div', { className: 'secrets-form' }, rows)
|
el('div', { className: 'secrets-form' }, rows)
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
|
@ -224,14 +228,14 @@
|
||||||
function renderList() {
|
function renderList() {
|
||||||
var children = [
|
var children = [
|
||||||
el('div', { className: 'secrets-toolbar' }, [
|
el('div', { className: 'secrets-toolbar' }, [
|
||||||
el('span', { className: 'secrets-title' }, ['Secrets']),
|
el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]),
|
||||||
el('span', { className: 'secrets-count' }, [String(records.length)]),
|
el('span', { className: 'secrets-count' }, [String(records.length)]),
|
||||||
el('span', { className: 'secrets-spacer' }),
|
el('span', { className: 'secrets-spacer' }),
|
||||||
el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, ['New'])
|
el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, [tr('ui.new', null, 'New')])
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
if (!records.length) {
|
if (!records.length) {
|
||||||
children.push(el('div', { className: 'secrets-empty' }, ['No secrets']));
|
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
groupRecords(records).forEach(function (group) {
|
groupRecords(records).forEach(function (group) {
|
||||||
|
|
@ -255,17 +259,17 @@
|
||||||
|
|
||||||
function renderSelected() {
|
function renderSelected() {
|
||||||
if (!selectedRecord) return el('div', { className: 'secrets-card' }, [
|
if (!selectedRecord) return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, ['Select a secret'])
|
el('h2', {}, [tr('ui.select', null, 'Select a secret')])
|
||||||
]);
|
]);
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
||||||
el('table', { className: 'secrets-table' }, [
|
el('table', { className: 'secrets-table' }, [
|
||||||
el('tbody', {}, [
|
el('tbody', {}, [
|
||||||
fieldRow('Group', scopeLabel(selectedRecord)),
|
fieldRow(tr('ui.group', null, 'Group'), scopeLabel(selectedRecord)),
|
||||||
fieldRow('ID', selectedRecord.id),
|
fieldRow('ID', selectedRecord.id),
|
||||||
fieldRow('Username', selectedRecord.username || ''),
|
fieldRow(tr('ui.username', null, 'Username'), selectedRecord.username || ''),
|
||||||
fieldRow('Password', selectedValue ? selectedValue : '••••••••••••', selectedValue ? '' : 'secrets-hidden-value'),
|
fieldRow(tr('ui.password', null, 'Password'), selectedValue ? selectedValue : '••••••••••••', selectedValue ? '' : 'secrets-hidden-value'),
|
||||||
fieldRow('Updated', selectedRecord.updatedAt || '')
|
fieldRow(tr('ui.updated', null, 'Updated'), selectedRecord.updatedAt || '')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'secrets-actions' }, [
|
el('div', { className: 'secrets-actions' }, [
|
||||||
|
|
@ -274,19 +278,19 @@
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-secret-copy-link': selectedRecord.id,
|
'data-secret-copy-link': selectedRecord.id,
|
||||||
onClick: function () { copySecretLink(selectedRecord.id); }
|
onClick: function () { copySecretLink(selectedRecord.id); }
|
||||||
}, ['Copy secret link']),
|
}, [tr('ui.copyLink', null, 'Copy secret link')]),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'secrets-btn',
|
className: 'secrets-btn',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-secret-edit': selectedRecord.id,
|
'data-secret-edit': selectedRecord.id,
|
||||||
onClick: function () { showEditSecret(); }
|
onClick: function () { showEditSecret(); }
|
||||||
}, ['Edit']),
|
}, [tr('ui.edit', null, 'Edit')]),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'secrets-btn danger',
|
className: 'secrets-btn danger',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-secret-delete': selectedRecord.id,
|
'data-secret-delete': selectedRecord.id,
|
||||||
onClick: function () { deleteSecret(selectedRecord.id); }
|
onClick: function () { deleteSecret(selectedRecord.id); }
|
||||||
}, ['Delete'])
|
}, [tr('ui.delete', null, 'Delete')])
|
||||||
]),
|
]),
|
||||||
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
||||||
]);
|
]);
|
||||||
|
|
@ -301,28 +305,28 @@
|
||||||
|
|
||||||
function renderSecretForm(existing) {
|
function renderSecretForm(existing) {
|
||||||
var isEdit = !!existing;
|
var isEdit = !!existing;
|
||||||
var title = el('input', { className: 'secrets-input', type: 'text', 'data-secret-title': '', placeholder: 'Title' });
|
var title = el('input', { className: 'secrets-input', type: 'text', 'data-secret-title': '', placeholder: tr('ui.fieldTitle', null, 'Title') });
|
||||||
title.value = existing ? text(existing.title) : '';
|
title.value = existing ? text(existing.title) : '';
|
||||||
var id = el('input', { className: 'secrets-input', type: 'text', placeholder: 'stable.id' });
|
var id = el('input', { className: 'secrets-input', type: 'text', placeholder: 'stable.id' });
|
||||||
id.value = existing ? text(existing.id) : '';
|
id.value = existing ? text(existing.id) : '';
|
||||||
id.disabled = isEdit;
|
id.disabled = isEdit;
|
||||||
var username = el('input', { className: 'secrets-input', type: 'text', placeholder: 'optional username' });
|
var username = el('input', { className: 'secrets-input', type: 'text', placeholder: tr('ui.optionalUsername', null, 'optional username') });
|
||||||
username.value = existing ? text(existing.username) : '';
|
username.value = existing ? text(existing.username) : '';
|
||||||
var value = el('textarea', { className: 'secrets-textarea', 'data-secret-value': '', placeholder: 'Secret value' });
|
var value = el('textarea', { className: 'secrets-textarea', 'data-secret-value': '', placeholder: tr('ui.secretValue', null, 'Secret value') });
|
||||||
value.value = isEdit ? selectedValue : '';
|
value.value = isEdit ? selectedValue : '';
|
||||||
var scope = el('select', { className: 'secrets-select' }, [
|
var scope = el('select', { className: 'secrets-select' }, [
|
||||||
el('option', { value: ScopeGlobal }, ['Global']),
|
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')]),
|
||||||
el('option', { value: ScopeWorkspace }, [workspaceRoot || 'Workspace'])
|
el('option', { value: ScopeWorkspace }, [workspaceRoot || tr('ui.workspace', null, 'Workspace')])
|
||||||
]);
|
]);
|
||||||
scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal);
|
scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal);
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [isEdit ? 'Edit secret' : 'New secret']),
|
el('h2', {}, [isEdit ? tr('ui.editSecret', null, 'Edit secret') : tr('ui.newSecret', null, 'New secret')]),
|
||||||
el('div', { className: 'secrets-form' }, [
|
el('div', { className: 'secrets-form' }, [
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Title']), title]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.fieldTitle', null, 'Title')]), title]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Username']), username]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.username', null, 'Username')]), username]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Scope']), scope]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.scope', null, 'Scope')]), scope]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Value']), value]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.value', null, 'Value')]), value]),
|
||||||
el('div', { className: 'secrets-actions' }, [
|
el('div', { className: 'secrets-actions' }, [
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'secrets-btn primary',
|
className: 'secrets-btn primary',
|
||||||
|
|
@ -345,8 +349,8 @@
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setStatus((err && err.message) ? err.message : String(err), true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, ['Save']),
|
}, [tr('ui.save', null, 'Save')]),
|
||||||
el('button', { className: 'secrets-btn', type: 'button', onClick: function () { mode = 'selected'; render(); } }, ['Cancel'])
|
el('button', { className: 'secrets-btn', type: 'button', onClick: function () { mode = 'selected'; render(); } }, [tr('ui.cancel', null, 'Cancel')])
|
||||||
]),
|
]),
|
||||||
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
||||||
])
|
])
|
||||||
|
|
@ -451,8 +455,13 @@
|
||||||
renderLocked();
|
renderLocked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function'
|
||||||
|
? api.i18n.onDidChangeLocale(render)
|
||||||
|
: null;
|
||||||
|
|
||||||
containerEl.__secretsCleanup = function () {
|
containerEl.__secretsCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Secrets",
|
||||||
|
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
||||||
|
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
||||||
|
"contributions.settingsPanels.verstak.secrets.settings.title": "Secrets",
|
||||||
|
"contributions.workspaceItems.verstak.secrets.workspace.title": "Secrets",
|
||||||
|
"ui.masterPassword": "Master password",
|
||||||
|
"ui.repeatPassword": "Repeat master password",
|
||||||
|
"ui.passwordMismatch": "Master passwords do not match",
|
||||||
|
"ui.unlock": "Unlock",
|
||||||
|
"ui.createMaster": "Create master password",
|
||||||
|
"ui.password": "Password",
|
||||||
|
"ui.repeat": "Repeat",
|
||||||
|
"ui.title": "Secrets",
|
||||||
|
"ui.unlockSecrets": "Unlock secrets",
|
||||||
|
"ui.new": "New",
|
||||||
|
"ui.empty": "No secrets",
|
||||||
|
"ui.select": "Select a secret",
|
||||||
|
"ui.group": "Group",
|
||||||
|
"ui.username": "Username",
|
||||||
|
"ui.updated": "Updated",
|
||||||
|
"ui.copyLink": "Copy secret link",
|
||||||
|
"ui.edit": "Edit",
|
||||||
|
"ui.delete": "Delete",
|
||||||
|
"ui.fieldTitle": "Title",
|
||||||
|
"ui.optionalUsername": "optional username",
|
||||||
|
"ui.secretValue": "Secret value",
|
||||||
|
"ui.global": "Global",
|
||||||
|
"ui.workspace": "Workspace",
|
||||||
|
"ui.editSecret": "Edit secret",
|
||||||
|
"ui.newSecret": "New secret",
|
||||||
|
"ui.scope": "Scope",
|
||||||
|
"ui.value": "Value",
|
||||||
|
"ui.save": "Save",
|
||||||
|
"ui.cancel": "Cancel"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Секреты",
|
||||||
|
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
||||||
|
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
||||||
|
"contributions.settingsPanels.verstak.secrets.settings.title": "Секреты",
|
||||||
|
"contributions.workspaceItems.verstak.secrets.workspace.title": "Секреты",
|
||||||
|
"ui.masterPassword": "Мастер-пароль",
|
||||||
|
"ui.repeatPassword": "Повторите мастер-пароль",
|
||||||
|
"ui.passwordMismatch": "Мастер-пароли не совпадают",
|
||||||
|
"ui.unlock": "Разблокировать",
|
||||||
|
"ui.createMaster": "Создать мастер-пароль",
|
||||||
|
"ui.password": "Пароль",
|
||||||
|
"ui.repeat": "Повтор",
|
||||||
|
"ui.title": "Секреты",
|
||||||
|
"ui.unlockSecrets": "Разблокировать секреты",
|
||||||
|
"ui.new": "Новый",
|
||||||
|
"ui.empty": "Секретов нет",
|
||||||
|
"ui.select": "Выберите секрет",
|
||||||
|
"ui.group": "Группа",
|
||||||
|
"ui.username": "Имя пользователя",
|
||||||
|
"ui.updated": "Обновлён",
|
||||||
|
"ui.copyLink": "Копировать ссылку на секрет",
|
||||||
|
"ui.edit": "Изменить",
|
||||||
|
"ui.delete": "Удалить",
|
||||||
|
"ui.fieldTitle": "Название",
|
||||||
|
"ui.optionalUsername": "необязательное имя пользователя",
|
||||||
|
"ui.secretValue": "Значение секрета",
|
||||||
|
"ui.global": "Общий",
|
||||||
|
"ui.workspace": "Рабочее пространство",
|
||||||
|
"ui.editSecret": "Изменить секрет",
|
||||||
|
"ui.newSecret": "Новый секрет",
|
||||||
|
"ui.scope": "Область",
|
||||||
|
"ui.value": "Значение",
|
||||||
|
"ui.save": "Сохранить",
|
||||||
|
"ui.cancel": "Отмена"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Encrypted global and workspace-scoped secret manager.",
|
"description": "Encrypted global and workspace-scoped secret manager.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "key-round",
|
"icon": "key-round",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
export let api = null
|
export let api = null
|
||||||
|
|
||||||
let settings = null
|
let settings = null
|
||||||
|
|
@ -15,19 +17,26 @@
|
||||||
let password = ''
|
let password = ''
|
||||||
let syncInterval = 5
|
let syncInterval = 5
|
||||||
let autoSync = false
|
let autoSync = false
|
||||||
|
let locale = api?.i18n?.getLocale?.() || 'en'
|
||||||
|
let unsubscribeLocale
|
||||||
|
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
locale
|
||||||
|
return api?.i18n?.t?.(key, params, fallback) || fallback || key
|
||||||
|
}
|
||||||
|
|
||||||
const INPUT_STYLE = 'width:100%;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;box-sizing:border-box;height:36px;'
|
const INPUT_STYLE = 'width:100%;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;box-sizing:border-box;height:36px;'
|
||||||
const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;'
|
const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;'
|
||||||
|
|
||||||
function sanitizeError(msg) {
|
function sanitizeError(msg) {
|
||||||
if (!msg) return 'Unknown error'
|
if (!msg) return tr('ui.unknownError', null, 'Unknown error')
|
||||||
let s = String(msg).replace(/<[^>]+>/g, '')
|
let s = String(msg).replace(/<[^>]+>/g, '')
|
||||||
if (s.length > 200) s = s.substring(0, 200) + '...'
|
if (s.length > 200) s = s.substring(0, 200) + '...'
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAPI() {
|
function syncAPI() {
|
||||||
if (!api?.sync) throw new Error('Plugin API sync namespace not available')
|
if (!api?.sync) throw new Error(tr('ui.apiUnavailable', null, 'Plugin API sync namespace not available'))
|
||||||
return api.sync
|
return api.sync
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +66,7 @@
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
if (syncInterval < 1 || syncInterval > 1440) {
|
if (syncInterval < 1 || syncInterval > 1440) {
|
||||||
errorMsg = 'Sync interval must be between 1 and 1440 minutes.'
|
errorMsg = tr('ui.intervalError', null, 'Sync interval must be between 1 and 1440 minutes.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading = true
|
loading = true
|
||||||
|
|
@ -68,7 +77,7 @@
|
||||||
await api.settings.writeAll({ serverUrl, username, autoSync, syncInterval })
|
await api.settings.writeAll({ serverUrl, username, autoSync, syncInterval })
|
||||||
}
|
}
|
||||||
await syncAPI().setInterval(autoSync ? syncInterval : 0)
|
await syncAPI().setInterval(autoSync ? syncInterval : 0)
|
||||||
resultMsg = 'Settings saved.'
|
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
|
||||||
resultKind = ''
|
resultKind = ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg = sanitizeError(e.message || e)
|
errorMsg = sanitizeError(e.message || e)
|
||||||
|
|
@ -77,7 +86,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
if (!serverUrl) { errorMsg = 'Server URL is required.'; return }
|
if (!serverUrl) { errorMsg = tr('ui.serverRequired', null, 'Server URL is required.'); return }
|
||||||
loading = true
|
loading = true
|
||||||
connectionResult = ''
|
connectionResult = ''
|
||||||
connectionOk = null
|
connectionOk = null
|
||||||
|
|
@ -85,22 +94,22 @@
|
||||||
try {
|
try {
|
||||||
await syncAPI().testConnection(serverUrl, username, password)
|
await syncAPI().testConnection(serverUrl, username, password)
|
||||||
connectionOk = true
|
connectionOk = true
|
||||||
connectionResult = 'Connection successful.'
|
connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
connectionOk = false
|
connectionOk = false
|
||||||
connectionResult = 'Connection failed: ' + sanitizeError(e.message || e)
|
connectionResult = tr('ui.connectionFailed', { error: sanitizeError(e.message || e) }, 'Connection failed: {error}')
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureSync() {
|
async function configureSync() {
|
||||||
if (!serverUrl) { errorMsg = 'Server URL is required.'; return }
|
if (!serverUrl) { errorMsg = tr('ui.serverRequired', null, 'Server URL is required.'); return }
|
||||||
loading = true
|
loading = true
|
||||||
errorMsg = ''
|
errorMsg = ''
|
||||||
connectionResult = ''
|
connectionResult = ''
|
||||||
try {
|
try {
|
||||||
await syncAPI().configure(serverUrl, username, password)
|
await syncAPI().configure(serverUrl, username, password)
|
||||||
connectionResult = 'Connected successfully.'
|
connectionResult = tr('ui.connectedSuccessfully', null, 'Connected successfully.')
|
||||||
connectionOk = true
|
connectionOk = true
|
||||||
username = ''
|
username = ''
|
||||||
password = ''
|
password = ''
|
||||||
|
|
@ -115,8 +124,8 @@
|
||||||
const conflicts = Array.isArray(result?.conflicts) ? result.conflicts : []
|
const conflicts = Array.isArray(result?.conflicts) ? result.conflicts : []
|
||||||
const applyErrors = Array.isArray(result?.applyErrors) ? result.applyErrors : []
|
const applyErrors = Array.isArray(result?.applyErrors) ? result.applyErrors : []
|
||||||
const parts = []
|
const parts = []
|
||||||
if (conflicts.length > 0) parts.push(conflicts.length + ' conflict(s)')
|
if (conflicts.length > 0) parts.push(tr('ui.conflictsCount', { count: conflicts.length }, '{count} conflict(s)'))
|
||||||
if (applyErrors.length > 0) parts.push(applyErrors.length + ' error(s)')
|
if (applyErrors.length > 0) parts.push(tr('ui.errorsCount', { count: applyErrors.length }, '{count} error(s)'))
|
||||||
return parts.join(' · ')
|
return parts.join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +155,7 @@
|
||||||
conflictDetails = []
|
conflictDetails = []
|
||||||
try {
|
try {
|
||||||
const r = await syncAPI().now()
|
const r = await syncAPI().now()
|
||||||
const summary = 'Pushed ' + (r?.pushed || 0) + ', pulled ' + (r?.pulled || 0)
|
const summary = tr('ui.syncSummary', { pushed: r?.pushed || 0, pulled: r?.pulled || 0 }, 'Pushed {pushed}, pulled {pulled}')
|
||||||
const warning = syncResultWarning(r)
|
const warning = syncResultWarning(r)
|
||||||
const conflicts = Array.isArray(r?.conflicts) ? r.conflicts : []
|
const conflicts = Array.isArray(r?.conflicts) ? r.conflicts : []
|
||||||
conflictDetails = conflicts.slice(0, 5).map(formatSyncConflict)
|
conflictDetails = conflicts.slice(0, 5).map(formatSyncConflict)
|
||||||
|
|
@ -167,7 +176,7 @@
|
||||||
|
|
||||||
function saveInterval() {
|
function saveInterval() {
|
||||||
if (syncInterval < 1 || syncInterval > 1440) {
|
if (syncInterval < 1 || syncInterval > 1440) {
|
||||||
errorMsg = 'Sync interval must be between 1 and 1440 minutes.'
|
errorMsg = tr('ui.intervalError', null, 'Sync interval must be between 1 and 1440 minutes.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
autoSync = syncInterval > 0
|
autoSync = syncInterval > 0
|
||||||
|
|
@ -180,7 +189,7 @@
|
||||||
resultMsg = ''
|
resultMsg = ''
|
||||||
try {
|
try {
|
||||||
await syncAPI().disconnect()
|
await syncAPI().disconnect()
|
||||||
resultMsg = 'Disconnected from server.'
|
resultMsg = tr('ui.disconnected', null, 'Disconnected from server.')
|
||||||
resultKind = ''
|
resultKind = ''
|
||||||
settings = null
|
settings = null
|
||||||
await load()
|
await load()
|
||||||
|
|
@ -196,7 +205,7 @@
|
||||||
resultMsg = ''
|
resultMsg = ''
|
||||||
try {
|
try {
|
||||||
await syncAPI().resetKey()
|
await syncAPI().resetKey()
|
||||||
resultMsg = 'Sync key reset. Connect again to pair this device.'
|
resultMsg = tr('ui.keyReset', null, 'Sync key reset. Connect again to pair this device.')
|
||||||
resultKind = ''
|
resultKind = ''
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -204,11 +213,19 @@
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
unsubscribeLocale = api?.i18n?.onDidChangeLocale?.((nextLocale) => {
|
||||||
|
locale = nextLocale
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => unsubscribeLocale?.())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="padding:1.5rem;max-width:500px;">
|
<div style="padding:1.5rem;max-width:500px;">
|
||||||
<h2 style="margin:0 0 0.25rem;color:#e0e0f0;font-size:1.2rem;">Sync</h2>
|
<h2 style="margin:0 0 0.25rem;color:#e0e0f0;font-size:1.2rem;">{tr('ui.title', null, 'Sync')}</h2>
|
||||||
<p style="color:#a0a0b8;font-size:0.85rem;margin-bottom:1.25rem;">Synchronize your vault across devices.</p>
|
<p style="color:#a0a0b8;font-size:0.85rem;margin-bottom:1.25rem;">{tr('ui.description', null, 'Synchronize your vault across devices.')}</p>
|
||||||
|
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">{errorMsg}</div>
|
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">{errorMsg}</div>
|
||||||
|
|
@ -218,7 +235,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if conflictDetails.length > 0 && !errorMsg}
|
{#if conflictDetails.length > 0 && !errorMsg}
|
||||||
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:6px;color:#f59e0b;font-size:0.85rem;">
|
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:6px;color:#f59e0b;font-size:0.85rem;">
|
||||||
<div style="font-weight:600;margin-bottom:0.35rem;">Sync conflicts</div>
|
<div style="font-weight:600;margin-bottom:0.35rem;">{tr('ui.syncConflicts', null, 'Sync conflicts')}</div>
|
||||||
{#each conflictDetails as detail}
|
{#each conflictDetails as detail}
|
||||||
<div>{detail}</div>
|
<div>{detail}</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -226,31 +243,31 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if settings && settings.lastError && !errorMsg}
|
{#if settings && settings.lastError && !errorMsg}
|
||||||
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">
|
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">
|
||||||
Last sync error: {sanitizeError(settings.lastError)}
|
{tr('ui.lastSyncError', { error: sanitizeError(settings.lastError) }, 'Last sync error: {error}')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
|
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
|
||||||
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">Server</h3>
|
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">{tr('ui.server', null, 'Server')}</h3>
|
||||||
|
|
||||||
<div style="margin-bottom:0.75rem;">
|
<div style="margin-bottom:0.75rem;">
|
||||||
<label for="sync-server-url" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Server URL</label>
|
<label for="sync-server-url" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.serverUrl', null, 'Server URL')}</label>
|
||||||
<input id="sync-server-url" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={serverUrl} placeholder="https://example.com" />
|
<input id="sync-server-url" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={serverUrl} placeholder="https://example.com" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom:0.75rem;">
|
<div style="margin-bottom:0.75rem;">
|
||||||
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Username</label>
|
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.username', null, 'Username')}</label>
|
||||||
<input id="sync-username" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={username} />
|
<input id="sync-username" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={username} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom:0.75rem;">
|
<div style="margin-bottom:0.75rem;">
|
||||||
<label for="sync-password" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Password</label>
|
<label for="sync-password" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.password', null, 'Password')}</label>
|
||||||
<input id="sync-password" type="password" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={password} />
|
<input id="sync-password" type="password" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={password} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
|
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
|
||||||
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={testConnection} disabled={loading || !serverUrl}>Test Connection</button>
|
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={testConnection} disabled={loading || !serverUrl}>{tr('ui.testConnection', null, 'Test Connection')}</button>
|
||||||
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={configureSync} disabled={loading || !serverUrl}>Connect</button>
|
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={configureSync} disabled={loading || !serverUrl}>{tr('ui.connect', null, 'Connect')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if connectionResult}
|
{#if connectionResult}
|
||||||
|
|
@ -259,34 +276,34 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
|
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
|
||||||
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">Sync Behavior</h3>
|
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">{tr('ui.behavior', null, 'Sync Behavior')}</h3>
|
||||||
|
|
||||||
<div style="margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem;">
|
<div style="margin-bottom:0.75rem;display:flex;align-items:center;gap:0.5rem;">
|
||||||
<input type="checkbox" id="auto-sync" bind:checked={autoSync} on:change={toggleAutoSync} style="width:16px;height:16px;accent-color:#4ecca3;" />
|
<input type="checkbox" id="auto-sync" bind:checked={autoSync} on:change={toggleAutoSync} style="width:16px;height:16px;accent-color:#4ecca3;" />
|
||||||
<label for="auto-sync" style="color:#e0e0f0;font-size:0.9rem;cursor:pointer;">Enable auto-sync</label>
|
<label for="auto-sync" style="color:#e0e0f0;font-size:0.9rem;cursor:pointer;">{tr('ui.enableAutoSync', null, 'Enable auto-sync')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom:0.75rem;">
|
<div style="margin-bottom:0.75rem;">
|
||||||
<label for="sync-interval" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Sync interval</label>
|
<label for="sync-interval" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.interval', null, 'Sync interval')}</label>
|
||||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||||
<input id="sync-interval" type="number" min="1" max="1440" bind:value={syncInterval} on:change={saveInterval} style="width:100px;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;height:36px;" />
|
<input id="sync-interval" type="number" min="1" max="1440" bind:value={syncInterval} on:change={saveInterval} style="width:100px;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;height:36px;" />
|
||||||
<span style="color:#a0a0b8;font-size:0.85rem;">minutes</span>
|
<span style="color:#a0a0b8;font-size:0.85rem;">{tr('ui.minutes', null, 'minutes')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if settings && settings.lastSyncAt}
|
{#if settings && settings.lastSyncAt}
|
||||||
<div style="color:#a0a0b8;font-size:0.85rem;">
|
<div style="color:#a0a0b8;font-size:0.85rem;">
|
||||||
Last sync: {settings.lastSyncAt}
|
{tr('ui.lastSync', { date: settings.lastSyncAt }, 'Last sync: {date}')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
|
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
|
||||||
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={saveSettings} disabled={loading}>Save</button>
|
<button style="background:#4ecca3;color:#1a1a2e;border:1px solid #4ecca3;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;font-weight:600;" on:click={saveSettings} disabled={loading}>{tr('ui.save', null, 'Save')}</button>
|
||||||
{#if settings && settings.configured}
|
{#if settings && settings.configured}
|
||||||
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={runSyncNow} disabled={loading}>Sync Now</button>
|
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={runSyncNow} disabled={loading}>{tr('ui.syncNow', null, 'Sync Now')}</button>
|
||||||
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={resetKey} disabled={loading}>Reset Key</button>
|
<button style="background:#1a1a2e;color:#e0e0f0;border:1px solid #1a3a5c;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={resetKey} disabled={loading}>{tr('ui.resetKey', null, 'Reset Key')}</button>
|
||||||
<button style="background:#e94560;color:#fff;border:1px solid #e94560;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={doDisconnect} disabled={loading}>Disconnect</button>
|
<button style="background:#e94560;color:#fff;border:1px solid #e94560;padding:0.4rem 0.75rem;border-radius:4px;cursor:pointer;font-size:0.85rem;" on:click={doDisconnect} disabled={loading}>{tr('ui.disconnect', null, 'Disconnect')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,13 @@
|
||||||
|
|
||||||
let status = 'disabled'
|
let status = 'disabled'
|
||||||
let interval
|
let interval
|
||||||
|
let locale = api?.i18n?.getLocale?.() || 'en'
|
||||||
|
let unsubscribeLocale
|
||||||
|
|
||||||
|
function tr(key, params, fallback) {
|
||||||
|
locale
|
||||||
|
return api?.i18n?.t?.(key, params, fallback) || fallback || key
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStatus() {
|
async function loadStatus() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -32,11 +39,11 @@
|
||||||
|
|
||||||
function statusText() {
|
function statusText() {
|
||||||
const labels = {
|
const labels = {
|
||||||
connected: 'Connected',
|
connected: tr('ui.status.connected', null, 'Connected'),
|
||||||
disconnected: 'Disconnected',
|
disconnected: tr('ui.status.disconnected', null, 'Disconnected'),
|
||||||
disabled: 'Not configured',
|
disabled: tr('ui.status.disabled', null, 'Not configured'),
|
||||||
error: 'Error',
|
error: tr('ui.status.error', null, 'Error'),
|
||||||
revoked: 'Revoked',
|
revoked: tr('ui.status.revoked', null, 'Revoked'),
|
||||||
}
|
}
|
||||||
return labels[status] || status
|
return labels[status] || status
|
||||||
}
|
}
|
||||||
|
|
@ -50,14 +57,18 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadStatus()
|
loadStatus()
|
||||||
interval = setInterval(loadStatus, 15000)
|
interval = setInterval(loadStatus, 15000)
|
||||||
|
unsubscribeLocale = api?.i18n?.onDidChangeLocale?.((nextLocale) => {
|
||||||
|
locale = nextLocale
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (interval) clearInterval(interval)
|
if (interval) clearInterval(interval)
|
||||||
|
unsubscribeLocale?.()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="sync-status-bar" on:click={openSettings} title="Sync: {statusText()}">
|
<button class="sync-status-bar" on:click={openSettings} title="{tr('ui.title', null, 'Sync')}: {statusText()}">
|
||||||
<span class="status-dot" style="background: {statusColor()}"></span>
|
<span class="status-dot" style="background: {statusColor()}"></span>
|
||||||
<span class="status-label">{statusText()}</span>
|
<span class="status-label">{statusText()}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Sync",
|
||||||
|
"contributions.settingsPanels.verstak.sync.settings.title": "Sync",
|
||||||
|
"contributions.statusBarItems.verstak.sync.status.label": "Sync",
|
||||||
|
"ui.title": "Sync",
|
||||||
|
"ui.description": "Synchronize your vault across devices.",
|
||||||
|
"ui.unknownError": "Unknown error",
|
||||||
|
"ui.apiUnavailable": "Plugin API sync namespace not available",
|
||||||
|
"ui.intervalError": "Sync interval must be between 1 and 1440 minutes.",
|
||||||
|
"ui.settingsSaved": "Settings saved.",
|
||||||
|
"ui.serverRequired": "Server URL is required.",
|
||||||
|
"ui.connectionSuccessful": "Connection successful.",
|
||||||
|
"ui.connectionFailed": "Connection failed: {error}",
|
||||||
|
"ui.connectedSuccessfully": "Connected successfully.",
|
||||||
|
"ui.conflictsCount": "{count} conflict(s)",
|
||||||
|
"ui.errorsCount": "{count} error(s)",
|
||||||
|
"ui.syncSummary": "Pushed {pushed}, pulled {pulled}",
|
||||||
|
"ui.disconnected": "Disconnected from server.",
|
||||||
|
"ui.keyReset": "Sync key reset. Connect again to pair this device.",
|
||||||
|
"ui.syncConflicts": "Sync conflicts",
|
||||||
|
"ui.lastSyncError": "Last sync error: {error}",
|
||||||
|
"ui.server": "Server",
|
||||||
|
"ui.serverUrl": "Server URL",
|
||||||
|
"ui.username": "Username",
|
||||||
|
"ui.password": "Password",
|
||||||
|
"ui.testConnection": "Test Connection",
|
||||||
|
"ui.connect": "Connect",
|
||||||
|
"ui.behavior": "Sync Behavior",
|
||||||
|
"ui.enableAutoSync": "Enable auto-sync",
|
||||||
|
"ui.interval": "Sync interval",
|
||||||
|
"ui.minutes": "minutes",
|
||||||
|
"ui.lastSync": "Last sync: {date}",
|
||||||
|
"ui.save": "Save",
|
||||||
|
"ui.syncNow": "Sync Now",
|
||||||
|
"ui.resetKey": "Reset Key",
|
||||||
|
"ui.disconnect": "Disconnect",
|
||||||
|
"ui.status.connected": "Connected",
|
||||||
|
"ui.status.disconnected": "Disconnected",
|
||||||
|
"ui.status.disabled": "Not configured",
|
||||||
|
"ui.status.error": "Error",
|
||||||
|
"ui.status.revoked": "Revoked"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Синхронизация",
|
||||||
|
"contributions.settingsPanels.verstak.sync.settings.title": "Синхронизация",
|
||||||
|
"contributions.statusBarItems.verstak.sync.status.label": "Синхронизация",
|
||||||
|
"ui.title": "Синхронизация",
|
||||||
|
"ui.description": "Синхронизируйте хранилище между устройствами.",
|
||||||
|
"ui.unknownError": "Неизвестная ошибка",
|
||||||
|
"ui.apiUnavailable": "API синхронизации плагина недоступен",
|
||||||
|
"ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.",
|
||||||
|
"ui.settingsSaved": "Настройки сохранены.",
|
||||||
|
"ui.serverRequired": "Укажите URL сервера.",
|
||||||
|
"ui.connectionSuccessful": "Подключение успешно.",
|
||||||
|
"ui.connectionFailed": "Не удалось подключиться: {error}",
|
||||||
|
"ui.connectedSuccessfully": "Подключение установлено.",
|
||||||
|
"ui.conflictsCount": "Конфликтов: {count}",
|
||||||
|
"ui.errorsCount": "Ошибок: {count}",
|
||||||
|
"ui.syncSummary": "Отправлено: {pushed}, получено: {pulled}",
|
||||||
|
"ui.disconnected": "Сервер отключён.",
|
||||||
|
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
|
||||||
|
"ui.syncConflicts": "Конфликты синхронизации",
|
||||||
|
"ui.lastSyncError": "Ошибка последней синхронизации: {error}",
|
||||||
|
"ui.server": "Сервер",
|
||||||
|
"ui.serverUrl": "URL сервера",
|
||||||
|
"ui.username": "Имя пользователя",
|
||||||
|
"ui.password": "Пароль",
|
||||||
|
"ui.testConnection": "Проверить подключение",
|
||||||
|
"ui.connect": "Подключить",
|
||||||
|
"ui.behavior": "Параметры синхронизации",
|
||||||
|
"ui.enableAutoSync": "Включить автосинхронизацию",
|
||||||
|
"ui.interval": "Интервал синхронизации",
|
||||||
|
"ui.minutes": "минут",
|
||||||
|
"ui.lastSync": "Последняя синхронизация: {date}",
|
||||||
|
"ui.save": "Сохранить",
|
||||||
|
"ui.syncNow": "Синхронизировать",
|
||||||
|
"ui.resetKey": "Сбросить ключ",
|
||||||
|
"ui.disconnect": "Отключить",
|
||||||
|
"ui.status.connected": "Подключено",
|
||||||
|
"ui.status.disconnected": "Отключено",
|
||||||
|
"ui.status.disabled": "Не настроено",
|
||||||
|
"ui.status.error": "Ошибка",
|
||||||
|
"ui.status.revoked": "Доступ отозван"
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"name": "Sync",
|
"name": "Sync",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"provides": ["verstak/sync/v1", "verstak/sync.status/v1"],
|
"provides": ["verstak/sync/v1", "verstak/sync.status/v1"],
|
||||||
"requires": ["verstak/core/files/v1"],
|
"requires": ["verstak/core/files/v1"],
|
||||||
|
|
|
||||||
|
|
@ -215,8 +215,13 @@
|
||||||
var statusText = '';
|
var statusText = '';
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
|
|
||||||
|
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 toolbar = el('div', { className: 'todo-toolbar' });
|
var toolbar = el('div', { className: 'todo-toolbar' });
|
||||||
var titleEl = el('span', { className: 'todo-title', textContent: scope.mode === 'global' ? 'Todos' : 'Todos · ' + scope.label });
|
var titleEl = el('span', { className: 'todo-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Todos') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Todos · ' + scope.label) });
|
||||||
var countEl = el('span', { className: 'todo-count' });
|
var countEl = el('span', { className: 'todo-count' });
|
||||||
var statusEl = el('span', { className: 'todo-status' });
|
var statusEl = el('span', { className: 'todo-status' });
|
||||||
var filtersEl = el('div', { className: 'todo-filters' });
|
var filtersEl = el('div', { className: 'todo-filters' });
|
||||||
|
|
@ -228,10 +233,10 @@
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
option('all', 'All statuses'),
|
option('all', tr('ui.allStatuses', null, 'All statuses')),
|
||||||
option('open', 'Open'),
|
option('open', tr('ui.status.open', null, 'Open')),
|
||||||
option('done', 'Done'),
|
option('done', tr('ui.status.done', null, 'Done')),
|
||||||
option('cancelled', 'Cancelled')
|
option('cancelled', tr('ui.status.cancelled', null, 'Cancelled'))
|
||||||
]);
|
]);
|
||||||
var workspaceFilterEl = el('select', {
|
var workspaceFilterEl = el('select', {
|
||||||
className: 'todo-select',
|
className: 'todo-select',
|
||||||
|
|
@ -249,14 +254,14 @@
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
option('due', 'Sort by due date'),
|
option('due', tr('ui.sort.due', null, 'Sort by due date')),
|
||||||
option('reminder', 'Sort by reminder'),
|
option('reminder', tr('ui.sort.reminder', null, 'Sort by reminder')),
|
||||||
option('updated', 'Sort by updated')
|
option('updated', tr('ui.sort.updated', null, 'Sort by updated'))
|
||||||
]);
|
]);
|
||||||
var searchInput = el('input', {
|
var searchInput = el('input', {
|
||||||
className: 'todo-input search',
|
className: 'todo-input search',
|
||||||
type: 'search',
|
type: 'search',
|
||||||
placeholder: 'Search todos',
|
placeholder: tr('ui.search', null, 'Search todos'),
|
||||||
'data-todo-filter': 'search',
|
'data-todo-filter': 'search',
|
||||||
onInput: function (event) {
|
onInput: function (event) {
|
||||||
searchQuery = text(event.target.value).trim().toLowerCase();
|
searchQuery = text(event.target.value).trim().toLowerCase();
|
||||||
|
|
@ -267,7 +272,7 @@
|
||||||
className: 'todo-btn primary',
|
className: 'todo-btn primary',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-todo-action': 'add',
|
'data-todo-action': 'add',
|
||||||
textContent: 'Add Todo',
|
textContent: tr('ui.add', null, 'Add Todo'),
|
||||||
onClick: function () { showTodoModal(null); }
|
onClick: function () { showTodoModal(null); }
|
||||||
});
|
});
|
||||||
var listEl = el('div', { className: 'todo-list' });
|
var listEl = el('div', { className: 'todo-list' });
|
||||||
|
|
@ -309,8 +314,8 @@
|
||||||
function renderWorkspaceFilterOptions() {
|
function renderWorkspaceFilterOptions() {
|
||||||
if (scope.mode !== 'global') return;
|
if (scope.mode !== 'global') return;
|
||||||
workspaceFilterEl.innerHTML = '';
|
workspaceFilterEl.innerHTML = '';
|
||||||
workspaceFilterEl.appendChild(option('', 'All workspaces'));
|
workspaceFilterEl.appendChild(option('', tr('ui.allWorkspaces', null, 'All workspaces')));
|
||||||
workspaceFilterEl.appendChild(option('__unassigned__', 'Unassigned'));
|
workspaceFilterEl.appendChild(option('__unassigned__', tr('ui.unassigned', null, 'Unassigned')));
|
||||||
workspaceRoots().forEach(function (workspace) {
|
workspaceRoots().forEach(function (workspace) {
|
||||||
workspaceFilterEl.appendChild(option(workspace, workspace));
|
workspaceFilterEl.appendChild(option(workspace, workspace));
|
||||||
});
|
});
|
||||||
|
|
@ -338,7 +343,7 @@
|
||||||
function persist() {
|
function persist() {
|
||||||
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
||||||
return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).catch(function (err) {
|
return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).catch(function (err) {
|
||||||
statusText = 'Could not save todos: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save todos: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +353,7 @@
|
||||||
return api.settings.read().then(function (settings) {
|
return api.settings.read().then(function (settings) {
|
||||||
todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY]));
|
todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY]));
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load todos: ' + (err && err.message ? err.message : String(err));
|
statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load todos: ' + (err && err.message ? err.message : String(err)));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -375,10 +380,10 @@
|
||||||
|
|
||||||
function showTodoModal(existingTodo) {
|
function showTodoModal(existingTodo) {
|
||||||
var editing = !!existingTodo;
|
var editing = !!existingTodo;
|
||||||
var titleInput = el('input', { className: 'todo-input', type: 'text', value: editing ? existingTodo.title : '', placeholder: 'Todo title', 'data-todo-input': 'title' });
|
var titleInput = el('input', { className: 'todo-input', type: 'text', value: editing ? existingTodo.title : '', placeholder: tr('ui.titlePlaceholder', null, 'Todo title'), 'data-todo-input': 'title' });
|
||||||
var descriptionInput = el('textarea', { className: 'todo-input textarea', placeholder: 'Optional description', 'data-todo-input': 'description' });
|
var descriptionInput = el('textarea', { className: 'todo-input textarea', placeholder: tr('ui.descriptionPlaceholder', null, 'Optional description'), 'data-todo-input': 'description' });
|
||||||
descriptionInput.value = editing ? existingTodo.description : '';
|
descriptionInput.value = editing ? existingTodo.description : '';
|
||||||
var priorityInput = el('select', { className: 'todo-select', 'data-todo-input': 'priority' }, [option('low', 'Low'), option('normal', 'Normal'), option('high', 'High')]);
|
var priorityInput = el('select', { className: 'todo-select', 'data-todo-input': 'priority' }, [option('low', tr('ui.priority.low', null, 'Low')), option('normal', tr('ui.priority.normal', null, 'Normal')), option('high', tr('ui.priority.high', null, 'High'))]);
|
||||||
priorityInput.value = editing ? existingTodo.priority : 'normal';
|
priorityInput.value = editing ? existingTodo.priority : 'normal';
|
||||||
var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' });
|
var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' });
|
||||||
var reminderInput = el('input', { className: 'todo-input', type: 'datetime-local', value: editing ? existingTodo.reminderAt : '', 'data-todo-input': 'reminderAt' });
|
var reminderInput = el('input', { className: 'todo-input', type: 'datetime-local', value: editing ? existingTodo.reminderAt : '', 'data-todo-input': 'reminderAt' });
|
||||||
|
|
@ -386,7 +391,7 @@
|
||||||
var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot;
|
var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot;
|
||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
workspaceInput = el('select', { className: 'todo-select', 'data-todo-input': 'workspaceRootPath' });
|
workspaceInput = el('select', { className: 'todo-select', 'data-todo-input': 'workspaceRootPath' });
|
||||||
workspaceInput.appendChild(option('', 'Unassigned'));
|
workspaceInput.appendChild(option('', tr('ui.unassigned', null, 'Unassigned')));
|
||||||
workspaceRoots().forEach(function (workspaceRoot) {
|
workspaceRoots().forEach(function (workspaceRoot) {
|
||||||
workspaceInput.appendChild(option(workspaceRoot, workspaceRoot));
|
workspaceInput.appendChild(option(workspaceRoot, workspaceRoot));
|
||||||
});
|
});
|
||||||
|
|
@ -396,7 +401,7 @@
|
||||||
function saveTodo() {
|
function saveTodo() {
|
||||||
var title = text(titleInput.value).trim();
|
var title = text(titleInput.value).trim();
|
||||||
if (!title) {
|
if (!title) {
|
||||||
statusText = 'Title is required';
|
statusText = tr('ui.titleRequired', null, 'Title is required');
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
|
|
@ -427,20 +432,20 @@
|
||||||
todos = sortTodos(todos);
|
todos = sortTodos(todos);
|
||||||
if (workspaceRoot && workspaceOptions.indexOf(workspaceRoot) === -1) workspaceOptions.push(workspaceRoot);
|
if (workspaceRoot && workspaceOptions.indexOf(workspaceRoot) === -1) workspaceOptions.push(workspaceRoot);
|
||||||
closeTodoModal();
|
closeTodoModal();
|
||||||
statusText = editing ? 'Todo updated' : 'Todo added';
|
statusText = editing ? tr('ui.updated', null, 'Todo updated') : tr('ui.added', null, 'Todo added');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = [
|
var fields = [
|
||||||
el('label', { className: 'todo-field wide' }, ['Title', titleInput]),
|
el('label', { className: 'todo-field wide' }, [tr('ui.field.title', null, 'Title'), titleInput]),
|
||||||
el('label', { className: 'todo-field wide' }, ['Description', descriptionInput]),
|
el('label', { className: 'todo-field wide' }, [tr('ui.field.description', null, 'Description'), descriptionInput]),
|
||||||
el('label', { className: 'todo-field' }, ['Priority', priorityInput]),
|
el('label', { className: 'todo-field' }, [tr('ui.field.priority', null, 'Priority'), priorityInput]),
|
||||||
el('label', { className: 'todo-field' }, ['Due date', dueInput]),
|
el('label', { className: 'todo-field' }, [tr('ui.field.due', null, 'Due date'), dueInput]),
|
||||||
el('label', { className: 'todo-field' }, ['Reminder', reminderInput])
|
el('label', { className: 'todo-field' }, [tr('ui.field.reminder', null, 'Reminder'), reminderInput])
|
||||||
];
|
];
|
||||||
if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, ['Workspace', workspaceInput]));
|
if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, [tr('ui.field.workspace', null, 'Workspace'), workspaceInput]));
|
||||||
else fields.push(el('div', { className: 'todo-field', textContent: 'Workspace: ' + scope.workspaceRoot }));
|
else fields.push(el('div', { className: 'todo-field', textContent: tr('ui.workspaceValue', { workspace: scope.workspaceRoot }, 'Workspace: ' + scope.workspaceRoot) }));
|
||||||
|
|
||||||
modalHost.innerHTML = '';
|
modalHost.innerHTML = '';
|
||||||
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
||||||
|
|
@ -448,11 +453,11 @@
|
||||||
if (event.target === event.currentTarget) closeTodoModal();
|
if (event.target === event.currentTarget) closeTodoModal();
|
||||||
} }, [
|
} }, [
|
||||||
el('div', { className: 'todo-modal' }, [
|
el('div', { className: 'todo-modal' }, [
|
||||||
el('div', { className: 'todo-modal-title', textContent: editing ? 'Edit Todo' : 'Add Todo' }),
|
el('div', { className: 'todo-modal-title', textContent: editing ? tr('ui.edit', null, 'Edit Todo') : tr('ui.add', null, 'Add Todo') }),
|
||||||
el('div', { className: 'todo-form-grid' }, fields),
|
el('div', { className: 'todo-form-grid' }, fields),
|
||||||
el('div', { className: 'todo-modal-actions' }, [
|
el('div', { className: 'todo-modal-actions' }, [
|
||||||
el('button', { className: 'todo-btn', type: 'button', textContent: 'Cancel', onClick: closeTodoModal }),
|
el('button', { className: 'todo-btn', type: 'button', textContent: tr('ui.cancel', null, 'Cancel'), onClick: closeTodoModal }),
|
||||||
el('button', { className: 'todo-btn primary', type: 'button', 'data-todo-action': 'save', textContent: editing ? 'Save changes' : 'Add Todo', onClick: saveTodo })
|
el('button', { className: 'todo-btn primary', type: 'button', 'data-todo-action': 'save', textContent: editing ? tr('ui.saveChanges', null, 'Save changes') : tr('ui.add', null, 'Add Todo'), onClick: saveTodo })
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
|
|
@ -469,14 +474,14 @@
|
||||||
updatedAt: timestamp
|
updatedAt: timestamp
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
statusText = nextStatus === 'done' ? 'Todo marked done' : (nextStatus === 'cancelled' ? 'Todo cancelled' : 'Todo reopened');
|
statusText = nextStatus === 'done' ? tr('ui.markedDone', null, 'Todo marked done') : (nextStatus === 'cancelled' ? tr('ui.cancelled', null, 'Todo cancelled') : tr('ui.reopened', null, 'Todo reopened'));
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTodo(todo) {
|
function deleteTodo(todo) {
|
||||||
todos = todos.filter(function (item) { return item.id !== todo.id; });
|
todos = todos.filter(function (item) { return item.id !== todo.id; });
|
||||||
statusText = 'Todo deleted';
|
statusText = tr('ui.deleted', null, 'Todo deleted');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
}
|
}
|
||||||
|
|
@ -513,11 +518,11 @@
|
||||||
var workspace = cleanWorkspace(todo.workspaceRootPath);
|
var workspace = cleanWorkspace(todo.workspaceRootPath);
|
||||||
var due = dueState(todo);
|
var due = dueState(todo);
|
||||||
var reminderDue = reminderIsDue(todo);
|
var reminderDue = reminderIsDue(todo);
|
||||||
if (scope.mode === 'global') meta.push(el('span', { className: 'todo-badge', textContent: workspace || 'Unassigned' }));
|
if (scope.mode === 'global') meta.push(el('span', { className: 'todo-badge', textContent: workspace || tr('ui.unassigned', null, 'Unassigned') }));
|
||||||
meta.push(el('span', { className: 'todo-badge ' + todo.priority, textContent: todo.priority + ' priority' }));
|
meta.push(el('span', { className: 'todo-badge ' + todo.priority, textContent: tr('ui.priorityValue', { priority: tr('ui.priority.' + todo.priority, null, todo.priority) }, todo.priority + ' priority') }));
|
||||||
meta.push(el('span', { className: 'todo-badge', textContent: todo.status }));
|
meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.status.' + todo.status, null, todo.status) }));
|
||||||
if (todo.dueAt) meta.push(el('span', { className: 'todo-badge ' + due, textContent: (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + formatDate(todo.dueAt) }));
|
if (todo.dueAt) meta.push(el('span', { className: 'todo-badge ' + due, textContent: tr('ui.dueValue', { prefix: due === 'overdue' ? tr('ui.overduePrefix', null, 'Overdue · ') : (due === 'due-soon' ? tr('ui.dueSoonPrefix', null, 'Due soon · ') : ''), date: formatDate(todo.dueAt) }, (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + formatDate(todo.dueAt)) }));
|
||||||
if (todo.reminderAt) meta.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: (reminderDue ? 'Reminder due ' : 'Reminder ') + formatDate(todo.reminderAt) }));
|
if (todo.reminderAt) meta.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: tr(reminderDue ? 'ui.reminderDueValue' : 'ui.reminderValue', { date: formatDate(todo.reminderAt) }, (reminderDue ? 'Reminder due ' : 'Reminder ') + formatDate(todo.reminderAt)) }));
|
||||||
return el('div', { className: 'todo-row-meta' }, meta);
|
return el('div', { className: 'todo-row-meta' }, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -527,30 +532,30 @@
|
||||||
if (!visible.length) {
|
if (!visible.length) {
|
||||||
listEl.appendChild(el('div', {
|
listEl.appendChild(el('div', {
|
||||||
className: 'todo-empty',
|
className: 'todo-empty',
|
||||||
textContent: todos.length ? 'No todos match the current filters.' : 'No todos yet.'
|
textContent: todos.length ? tr('ui.noMatches', null, 'No todos match the current filters.') : tr('ui.empty', null, 'No todos yet.')
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visible.forEach(function (todo) {
|
visible.forEach(function (todo) {
|
||||||
var actionButtons = [];
|
var actionButtons = [];
|
||||||
if (scope.mode === 'global' && todo.workspaceRootPath) {
|
if (scope.mode === 'global' && todo.workspaceRootPath) {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'open-workspace', textContent: 'Open workspace', onClick: function () { openWorkspace(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'open-workspace', textContent: tr('ui.openWorkspace', null, 'Open workspace'), onClick: function () { openWorkspace(todo); } }));
|
||||||
}
|
}
|
||||||
if (todo.status === 'open') {
|
if (todo.status === 'open') {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'mark-done', textContent: 'Done', onClick: function () { setTodoStatus(todo, 'done'); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'mark-done', textContent: tr('ui.status.done', null, 'Done'), onClick: function () { setTodoStatus(todo, 'done'); } }));
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'cancel', textContent: 'Cancel', onClick: function () { setTodoStatus(todo, 'cancelled'); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'cancel', textContent: tr('ui.cancel', null, 'Cancel'), onClick: function () { setTodoStatus(todo, 'cancelled'); } }));
|
||||||
} else {
|
} else {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'reopen', textContent: 'Reopen', onClick: function () { setTodoStatus(todo, 'open'); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'reopen', textContent: tr('ui.reopen', null, 'Reopen'), onClick: function () { setTodoStatus(todo, 'open'); } }));
|
||||||
}
|
}
|
||||||
if (scope.mode === 'workspace' && todo.status === 'done') {
|
if (scope.mode === 'workspace' && todo.status === 'done') {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'create-journal-entry', textContent: 'Create Journal Entry', onClick: function () { createJournalEntry(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'create-journal-entry', textContent: tr('ui.createJournal', null, 'Create Journal Entry'), onClick: function () { createJournalEntry(todo); } }));
|
||||||
}
|
}
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'edit', textContent: 'Edit', onClick: function () { showTodoModal(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'edit', textContent: tr('ui.editAction', null, 'Edit'), onClick: function () { showTodoModal(todo); } }));
|
||||||
actionButtons.push(el('button', { className: 'todo-btn danger', type: 'button', 'data-todo-action': 'delete', textContent: 'Delete', onClick: function () { deleteTodo(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn danger', type: 'button', 'data-todo-action': 'delete', textContent: tr('ui.delete', null, 'Delete'), onClick: function () { deleteTodo(todo); } }));
|
||||||
listEl.appendChild(el('div', { className: 'todo-row' + (todo.status === 'done' ? ' done' : ''), 'data-todo-id': todo.id }, [
|
listEl.appendChild(el('div', { className: 'todo-row' + (todo.status === 'done' ? ' done' : ''), 'data-todo-id': todo.id }, [
|
||||||
el('div', { className: 'todo-row-main' }, [
|
el('div', { className: 'todo-row-main' }, [
|
||||||
el('div', { className: 'todo-row-head' }, [
|
el('div', { className: 'todo-row-head' }, [
|
||||||
el('div', { className: 'todo-row-title', textContent: todo.title || 'Untitled todo' })
|
el('div', { className: 'todo-row-title', textContent: todo.title || tr('ui.untitled', null, 'Untitled todo') })
|
||||||
]),
|
]),
|
||||||
todo.description ? el('div', { className: 'todo-row-description', textContent: todo.description }) : null,
|
todo.description ? el('div', { className: 'todo-row-description', textContent: todo.description }) : null,
|
||||||
renderTodoMeta(todo)
|
renderTodoMeta(todo)
|
||||||
|
|
@ -563,8 +568,8 @@
|
||||||
function render() {
|
function render() {
|
||||||
var visible = visibleTodos();
|
var visible = visibleTodos();
|
||||||
countEl.textContent = visible.length === todos.length
|
countEl.textContent = visible.length === todos.length
|
||||||
? todos.length + ' todo' + (todos.length === 1 ? '' : 's')
|
? tr('ui.count', { count: todos.length }, todos.length + ' todo' + (todos.length === 1 ? '' : 's'))
|
||||||
: visible.length + ' of ' + todos.length + ' todos';
|
: tr('ui.filteredCount', { visible: visible.length, count: todos.length }, visible.length + ' of ' + todos.length + ' todos');
|
||||||
statusFilterEl.value = statusFilter;
|
statusFilterEl.value = statusFilter;
|
||||||
sortEl.value = sortMode;
|
sortEl.value = sortMode;
|
||||||
searchInput.value = searchQuery;
|
searchInput.value = searchQuery;
|
||||||
|
|
@ -578,6 +583,14 @@
|
||||||
Promise.all([loadStored(), loadWorkspaceOptions()]).then(function () {
|
Promise.all([loadStored(), loadWorkspaceOptions()]).then(function () {
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
|
api.i18n.onDidChangeLocale(function () {
|
||||||
|
titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Todos') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Todos · ' + scope.label);
|
||||||
|
searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search todos'));
|
||||||
|
addBtn.textContent = tr('ui.add', null, 'Add Todo');
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
TodoView.unmount = function (containerEl) {
|
TodoView.unmount = function (containerEl) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Todos",
|
||||||
|
"manifest.description": "Global and workspace todo tracking with due and reminder metadata.",
|
||||||
|
"contributions.views.verstak.todo.view.title": "Todos",
|
||||||
|
"contributions.sidebarItems.verstak.todo.sidebar.title": "Todos",
|
||||||
|
"contributions.workspaceItems.verstak.todo.workspace.title": "Todos",
|
||||||
|
"ui.title": "Todos",
|
||||||
|
"ui.workspaceTitle": "Todos · {workspace}",
|
||||||
|
"ui.allStatuses": "All statuses",
|
||||||
|
"ui.status.open": "Open",
|
||||||
|
"ui.status.done": "Done",
|
||||||
|
"ui.status.cancelled": "Cancelled",
|
||||||
|
"ui.sort.due": "Sort by due date",
|
||||||
|
"ui.sort.reminder": "Sort by reminder",
|
||||||
|
"ui.sort.updated": "Sort by updated",
|
||||||
|
"ui.search": "Search todos",
|
||||||
|
"ui.add": "Add Todo",
|
||||||
|
"ui.allWorkspaces": "All workspaces",
|
||||||
|
"ui.unassigned": "Unassigned",
|
||||||
|
"ui.saveError": "Could not save todos: {error}",
|
||||||
|
"ui.loadError": "Could not load todos: {error}",
|
||||||
|
"ui.titlePlaceholder": "Todo title",
|
||||||
|
"ui.descriptionPlaceholder": "Optional description",
|
||||||
|
"ui.priority.low": "Low",
|
||||||
|
"ui.priority.normal": "Normal",
|
||||||
|
"ui.priority.high": "High",
|
||||||
|
"ui.titleRequired": "Title is required",
|
||||||
|
"ui.updated": "Todo updated",
|
||||||
|
"ui.added": "Todo added",
|
||||||
|
"ui.field.title": "Title",
|
||||||
|
"ui.field.description": "Description",
|
||||||
|
"ui.field.priority": "Priority",
|
||||||
|
"ui.field.due": "Due date",
|
||||||
|
"ui.field.reminder": "Reminder",
|
||||||
|
"ui.field.workspace": "Workspace",
|
||||||
|
"ui.workspaceValue": "Workspace: {workspace}",
|
||||||
|
"ui.edit": "Edit Todo",
|
||||||
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.saveChanges": "Save changes",
|
||||||
|
"ui.markedDone": "Todo marked done",
|
||||||
|
"ui.cancelled": "Todo cancelled",
|
||||||
|
"ui.reopened": "Todo reopened",
|
||||||
|
"ui.deleted": "Todo deleted",
|
||||||
|
"ui.priorityValue": "{priority} priority",
|
||||||
|
"ui.overduePrefix": "Overdue · ",
|
||||||
|
"ui.dueSoonPrefix": "Due soon · ",
|
||||||
|
"ui.dueValue": "{prefix}Due {date}",
|
||||||
|
"ui.reminderDueValue": "Reminder due {date}",
|
||||||
|
"ui.reminderValue": "Reminder {date}",
|
||||||
|
"ui.noMatches": "No todos match the current filters.",
|
||||||
|
"ui.empty": "No todos yet.",
|
||||||
|
"ui.openWorkspace": "Open workspace",
|
||||||
|
"ui.reopen": "Reopen",
|
||||||
|
"ui.createJournal": "Create Journal Entry",
|
||||||
|
"ui.editAction": "Edit",
|
||||||
|
"ui.delete": "Delete",
|
||||||
|
"ui.untitled": "Untitled todo",
|
||||||
|
"ui.count": "{count} todo(s)",
|
||||||
|
"ui.filteredCount": "{visible} of {count} todos"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Задачи",
|
||||||
|
"manifest.description": "Общие задачи и задачи рабочих пространств со сроками и напоминаниями.",
|
||||||
|
"contributions.views.verstak.todo.view.title": "Задачи",
|
||||||
|
"contributions.sidebarItems.verstak.todo.sidebar.title": "Задачи",
|
||||||
|
"contributions.workspaceItems.verstak.todo.workspace.title": "Задачи",
|
||||||
|
"ui.title": "Задачи",
|
||||||
|
"ui.workspaceTitle": "Задачи · {workspace}",
|
||||||
|
"ui.allStatuses": "Все состояния",
|
||||||
|
"ui.status.open": "Открыта",
|
||||||
|
"ui.status.done": "Выполнена",
|
||||||
|
"ui.status.cancelled": "Отменена",
|
||||||
|
"ui.sort.due": "Сортировать по сроку",
|
||||||
|
"ui.sort.reminder": "Сортировать по напоминанию",
|
||||||
|
"ui.sort.updated": "Сортировать по обновлению",
|
||||||
|
"ui.search": "Поиск задач",
|
||||||
|
"ui.add": "Добавить задачу",
|
||||||
|
"ui.allWorkspaces": "Все рабочие пространства",
|
||||||
|
"ui.unassigned": "Не назначено",
|
||||||
|
"ui.saveError": "Не удалось сохранить задачи: {error}",
|
||||||
|
"ui.loadError": "Не удалось загрузить задачи: {error}",
|
||||||
|
"ui.titlePlaceholder": "Название задачи",
|
||||||
|
"ui.descriptionPlaceholder": "Необязательное описание",
|
||||||
|
"ui.priority.low": "Низкий",
|
||||||
|
"ui.priority.normal": "Обычный",
|
||||||
|
"ui.priority.high": "Высокий",
|
||||||
|
"ui.titleRequired": "Введите название",
|
||||||
|
"ui.updated": "Задача обновлена",
|
||||||
|
"ui.added": "Задача добавлена",
|
||||||
|
"ui.field.title": "Название",
|
||||||
|
"ui.field.description": "Описание",
|
||||||
|
"ui.field.priority": "Приоритет",
|
||||||
|
"ui.field.due": "Срок",
|
||||||
|
"ui.field.reminder": "Напоминание",
|
||||||
|
"ui.field.workspace": "Рабочее пространство",
|
||||||
|
"ui.workspaceValue": "Рабочее пространство: {workspace}",
|
||||||
|
"ui.edit": "Изменить задачу",
|
||||||
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.saveChanges": "Сохранить изменения",
|
||||||
|
"ui.markedDone": "Задача выполнена",
|
||||||
|
"ui.cancelled": "Задача отменена",
|
||||||
|
"ui.reopened": "Задача открыта снова",
|
||||||
|
"ui.deleted": "Задача удалена",
|
||||||
|
"ui.priorityValue": "Приоритет: {priority}",
|
||||||
|
"ui.overduePrefix": "Просрочено · ",
|
||||||
|
"ui.dueSoonPrefix": "Скоро срок · ",
|
||||||
|
"ui.dueValue": "{prefix}Срок: {date}",
|
||||||
|
"ui.reminderDueValue": "Пора напомнить: {date}",
|
||||||
|
"ui.reminderValue": "Напоминание: {date}",
|
||||||
|
"ui.noMatches": "Нет задач, соответствующих фильтрам.",
|
||||||
|
"ui.empty": "Задач пока нет.",
|
||||||
|
"ui.openWorkspace": "Открыть рабочее пространство",
|
||||||
|
"ui.reopen": "Открыть снова",
|
||||||
|
"ui.createJournal": "Создать запись журнала",
|
||||||
|
"ui.editAction": "Изменить",
|
||||||
|
"ui.delete": "Удалить",
|
||||||
|
"ui.untitled": "Задача без названия",
|
||||||
|
"ui.count": "Задач: {count}",
|
||||||
|
"ui.filteredCount": "Показано {visible} из {count}"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Global and workspace todo tracking with due and reminder metadata.",
|
"description": "Global and workspace todo tracking with due and reminder metadata.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "list-todo",
|
"icon": "list-todo",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,10 @@
|
||||||
statusError: false,
|
statusError: false,
|
||||||
disposed: false
|
disposed: false
|
||||||
};
|
};
|
||||||
|
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 workspaceOptions() {
|
function workspaceOptions() {
|
||||||
var values = {};
|
var values = {};
|
||||||
|
|
@ -170,18 +174,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusText(entries) {
|
function statusText(entries) {
|
||||||
if (state.loading) return 'Loading deleted items...';
|
if (state.loading) return tr('ui.loading', null, 'Loading deleted items...');
|
||||||
if (state.status) return state.status;
|
if (state.status) return state.status;
|
||||||
return entries.length === 1 ? '1 deleted item' : entries.length + ' deleted items';
|
return tr('ui.count', { count: entries.length }, entries.length === 1 ? '1 deleted item' : entries.length + ' deleted items');
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderConfirmation() {
|
function renderConfirmation() {
|
||||||
var entry = state.entries.find(function (item) { return item.trashId === state.confirmingId; });
|
var entry = state.entries.find(function (item) { return item.trashId === state.confirmingId; });
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
return el('div', { className: 'trash-confirm-backdrop', 'data-trash-confirm': entry.trashId }, [
|
return el('div', { className: 'trash-confirm-backdrop', 'data-trash-confirm': entry.trashId }, [
|
||||||
el('section', { className: 'trash-confirm', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Delete permanently' }, [
|
el('section', { className: 'trash-confirm', role: 'dialog', 'aria-modal': 'true', 'aria-label': tr('ui.deletePermanently', null, 'Delete permanently') }, [
|
||||||
el('h3', {}, ['Delete permanently?']),
|
el('h3', {}, [tr('ui.deleteQuestion', null, 'Delete permanently?')]),
|
||||||
el('p', {}, ['This cannot be undone.']),
|
el('p', {}, [tr('ui.cannotUndo', null, 'This cannot be undone.')]),
|
||||||
el('p', { className: 'trash-confirm-path' }, [entry.originalPath || nameFor(entry)]),
|
el('p', { className: 'trash-confirm-path' }, [entry.originalPath || nameFor(entry)]),
|
||||||
el('div', { className: 'trash-confirm-actions' }, [
|
el('div', { className: 'trash-confirm-actions' }, [
|
||||||
el('button', {
|
el('button', {
|
||||||
|
|
@ -189,14 +193,14 @@
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'data-trash-confirm-cancel': entry.trashId,
|
'data-trash-confirm-cancel': entry.trashId,
|
||||||
onClick: function () { state.confirmingId = ''; render(); }
|
onClick: function () { state.confirmingId = ''; render(); }
|
||||||
}, ['Cancel']),
|
}, [tr('ui.cancel', null, 'Cancel')]),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'trash-btn trash-btn-danger',
|
className: 'trash-btn trash-btn-danger',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
disabled: state.busyId === entry.trashId,
|
disabled: state.busyId === entry.trashId,
|
||||||
'data-trash-confirm-delete': entry.trashId,
|
'data-trash-confirm-delete': entry.trashId,
|
||||||
onClick: function () { deletePermanently(entry); }
|
onClick: function () { deletePermanently(entry); }
|
||||||
}, [state.busyId === entry.trashId ? 'Deleting...' : 'Delete permanently'])
|
}, [state.busyId === entry.trashId ? tr('ui.deleting', null, 'Deleting...') : tr('ui.deletePermanently', null, 'Delete permanently')])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
@ -213,7 +217,7 @@
|
||||||
value: state.workspace,
|
value: state.workspace,
|
||||||
'data-trash-filter-workspace': '',
|
'data-trash-filter-workspace': '',
|
||||||
onChange: function (event) { state.workspace = event.target.value; render(); }
|
onChange: function (event) { state.workspace = event.target.value; render(); }
|
||||||
}, [el('option', { value: '' }, ['All workspaces'])]);
|
}, [el('option', { value: '' }, [tr('ui.allWorkspaces', null, 'All workspaces')])]);
|
||||||
workspaceOptions().forEach(function (workspace) {
|
workspaceOptions().forEach(function (workspace) {
|
||||||
workspaceSelect.appendChild(el('option', { value: workspace }, [workspace]));
|
workspaceSelect.appendChild(el('option', { value: workspace }, [workspace]));
|
||||||
});
|
});
|
||||||
|
|
@ -224,9 +228,9 @@
|
||||||
'data-trash-filter-type': '',
|
'data-trash-filter-type': '',
|
||||||
onChange: function (event) { state.type = event.target.value; render(); }
|
onChange: function (event) { state.type = event.target.value; render(); }
|
||||||
}, [
|
}, [
|
||||||
el('option', { value: '' }, ['All types']),
|
el('option', { value: '' }, [tr('ui.allTypes', null, 'All types')]),
|
||||||
el('option', { value: 'file' }, ['Files']),
|
el('option', { value: 'file' }, [tr('ui.files', null, 'Files')]),
|
||||||
el('option', { value: 'folder' }, ['Folders'])
|
el('option', { value: 'folder' }, [tr('ui.folders', null, 'Folders')])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var sortSelect = el('select', {
|
var sortSelect = el('select', {
|
||||||
|
|
@ -235,28 +239,28 @@
|
||||||
'data-trash-sort': '',
|
'data-trash-sort': '',
|
||||||
onChange: function (event) { state.sort = event.target.value; render(); }
|
onChange: function (event) { state.sort = event.target.value; render(); }
|
||||||
}, [
|
}, [
|
||||||
el('option', { value: 'date-desc' }, ['Deleted: newest']),
|
el('option', { value: 'date-desc' }, [tr('ui.newest', null, 'Deleted: newest')]),
|
||||||
el('option', { value: 'date-asc' }, ['Deleted: oldest']),
|
el('option', { value: 'date-asc' }, [tr('ui.oldest', null, 'Deleted: oldest')]),
|
||||||
el('option', { value: 'name-asc' }, ['Name']),
|
el('option', { value: 'name-asc' }, [tr('ui.name', null, 'Name')]),
|
||||||
el('option', { value: 'type-asc' }, ['Type'])
|
el('option', { value: 'type-asc' }, [tr('ui.type', null, 'Type')])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
containerEl.appendChild(el('div', { className: 'trash-toolbar' }, [
|
containerEl.appendChild(el('div', { className: 'trash-toolbar' }, [
|
||||||
el('span', { className: 'trash-title' }, ['Trash']),
|
el('span', { className: 'trash-title' }, [tr('ui.title', null, 'Trash')]),
|
||||||
el('span', { className: 'trash-count' }, [state.entries.length + ' total']),
|
el('span', { className: 'trash-count' }, [tr('ui.total', { count: state.entries.length }, state.entries.length + ' total')]),
|
||||||
el('span', { className: 'trash-spacer' }),
|
el('span', { className: 'trash-spacer' }),
|
||||||
el('input', {
|
el('input', {
|
||||||
className: 'trash-control trash-search',
|
className: 'trash-control trash-search',
|
||||||
type: 'search',
|
type: 'search',
|
||||||
value: state.query,
|
value: state.query,
|
||||||
placeholder: 'Filter name or path',
|
placeholder: tr('ui.filter', null, 'Filter name or path'),
|
||||||
'data-trash-filter-search': '',
|
'data-trash-filter-search': '',
|
||||||
onInput: function (event) { state.query = event.target.value; render(); }
|
onInput: function (event) { state.query = event.target.value; render(); }
|
||||||
}),
|
}),
|
||||||
workspaceSelect,
|
workspaceSelect,
|
||||||
typeSelect,
|
typeSelect,
|
||||||
sortSelect,
|
sortSelect,
|
||||||
el('button', { className: 'trash-btn', type: 'button', onClick: loadEntries }, ['Refresh'])
|
el('button', { className: 'trash-btn', type: 'button', onClick: loadEntries }, [tr('ui.refresh', null, 'Refresh')])
|
||||||
]));
|
]));
|
||||||
containerEl.appendChild(el('div', {
|
containerEl.appendChild(el('div', {
|
||||||
className: 'trash-status' + (state.statusError ? ' error' : ''),
|
className: 'trash-status' + (state.statusError ? ' error' : ''),
|
||||||
|
|
@ -265,17 +269,17 @@
|
||||||
|
|
||||||
var list = el('div', { className: 'trash-list', 'data-trash-list': '' });
|
var list = el('div', { className: 'trash-list', 'data-trash-list': '' });
|
||||||
list.appendChild(el('div', { className: 'trash-header' }, [
|
list.appendChild(el('div', { className: 'trash-header' }, [
|
||||||
el('span', {}, ['Name']),
|
el('span', {}, [tr('ui.name', null, 'Name')]),
|
||||||
el('span', {}, ['Workspace']),
|
el('span', {}, [tr('ui.workspace', null, 'Workspace')]),
|
||||||
el('span', {}, ['Original path']),
|
el('span', {}, [tr('ui.originalPath', null, 'Original path')]),
|
||||||
el('span', {}, ['Deleted']),
|
el('span', {}, [tr('ui.deleted', null, 'Deleted')]),
|
||||||
el('span', {}, ['Type / size']),
|
el('span', {}, [tr('ui.typeSize', null, 'Type / size')]),
|
||||||
el('span', {}, ['Actions'])
|
el('span', {}, [tr('ui.actions', null, 'Actions')])
|
||||||
]));
|
]));
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
list.appendChild(el('div', { className: 'trash-empty' }, ['Loading deleted items...']));
|
list.appendChild(el('div', { className: 'trash-empty' }, [tr('ui.loading', null, 'Loading deleted items...')]));
|
||||||
} else if (!rows.length) {
|
} else if (!rows.length) {
|
||||||
list.appendChild(el('div', { className: 'trash-empty' }, [state.entries.length ? 'No deleted items match the current filters.' : 'Trash is empty.']));
|
list.appendChild(el('div', { className: 'trash-empty' }, [state.entries.length ? tr('ui.noMatches', null, 'No deleted items match the current filters.') : tr('ui.empty', null, 'Trash is empty.')]));
|
||||||
} else {
|
} else {
|
||||||
rows.forEach(function (entry) {
|
rows.forEach(function (entry) {
|
||||||
var size = formatSize(entry.size);
|
var size = formatSize(entry.size);
|
||||||
|
|
@ -296,14 +300,14 @@
|
||||||
disabled: state.busyId === entry.trashId,
|
disabled: state.busyId === entry.trashId,
|
||||||
'data-trash-restore': entry.trashId,
|
'data-trash-restore': entry.trashId,
|
||||||
onClick: function () { restoreEntry(entry); }
|
onClick: function () { restoreEntry(entry); }
|
||||||
}, [state.busyId === entry.trashId ? 'Restoring...' : 'Restore']),
|
}, [state.busyId === entry.trashId ? tr('ui.restoring', null, 'Restoring...') : tr('ui.restore', null, 'Restore')]),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'trash-btn trash-btn-danger',
|
className: 'trash-btn trash-btn-danger',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
disabled: state.busyId === entry.trashId,
|
disabled: state.busyId === entry.trashId,
|
||||||
'data-trash-delete': entry.trashId,
|
'data-trash-delete': entry.trashId,
|
||||||
onClick: function () { state.confirmingId = entry.trashId; render(); }
|
onClick: function () { state.confirmingId = entry.trashId; render(); }
|
||||||
}, ['Delete permanently'])
|
}, [tr('ui.deletePermanently', null, 'Delete permanently')])
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
@ -386,8 +390,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEntries();
|
loadEntries();
|
||||||
|
var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function'
|
||||||
|
? api.i18n.onDidChangeLocale(render)
|
||||||
|
: null;
|
||||||
containerEl.__trashCleanup = function () {
|
containerEl.__trashCleanup = function () {
|
||||||
state.disposed = true;
|
state.disposed = true;
|
||||||
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Trash",
|
||||||
|
"manifest.description": "Global vault trash manager for restoring or permanently deleting deleted files and folders.",
|
||||||
|
"contributions.views.verstak.trash.view.title": "Trash",
|
||||||
|
"contributions.sidebarItems.verstak.trash.sidebar.title": "Trash",
|
||||||
|
"ui.loading": "Loading deleted items...",
|
||||||
|
"ui.count": "{count} deleted item(s)",
|
||||||
|
"ui.deletePermanently": "Delete permanently",
|
||||||
|
"ui.deleteQuestion": "Delete permanently?",
|
||||||
|
"ui.cannotUndo": "This cannot be undone.",
|
||||||
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.deleting": "Deleting...",
|
||||||
|
"ui.allWorkspaces": "All workspaces",
|
||||||
|
"ui.allTypes": "All types",
|
||||||
|
"ui.files": "Files",
|
||||||
|
"ui.folders": "Folders",
|
||||||
|
"ui.newest": "Deleted: newest",
|
||||||
|
"ui.oldest": "Deleted: oldest",
|
||||||
|
"ui.name": "Name",
|
||||||
|
"ui.type": "Type",
|
||||||
|
"ui.title": "Trash",
|
||||||
|
"ui.total": "{count} total",
|
||||||
|
"ui.filter": "Filter name or path",
|
||||||
|
"ui.refresh": "Refresh",
|
||||||
|
"ui.workspace": "Workspace",
|
||||||
|
"ui.originalPath": "Original path",
|
||||||
|
"ui.deleted": "Deleted",
|
||||||
|
"ui.typeSize": "Type / size",
|
||||||
|
"ui.actions": "Actions",
|
||||||
|
"ui.noMatches": "No deleted items match the current filters.",
|
||||||
|
"ui.empty": "Trash is empty.",
|
||||||
|
"ui.restoring": "Restoring...",
|
||||||
|
"ui.restore": "Restore"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"manifest.name": "Корзина",
|
||||||
|
"manifest.description": "Общая корзина хранилища для восстановления или окончательного удаления файлов и папок.",
|
||||||
|
"contributions.views.verstak.trash.view.title": "Корзина",
|
||||||
|
"contributions.sidebarItems.verstak.trash.sidebar.title": "Корзина",
|
||||||
|
"ui.loading": "Загрузка удалённых элементов...",
|
||||||
|
"ui.count": "Удалённых элементов: {count}",
|
||||||
|
"ui.deletePermanently": "Удалить навсегда",
|
||||||
|
"ui.deleteQuestion": "Удалить навсегда?",
|
||||||
|
"ui.cannotUndo": "Это действие нельзя отменить.",
|
||||||
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.deleting": "Удаление...",
|
||||||
|
"ui.allWorkspaces": "Все рабочие пространства",
|
||||||
|
"ui.allTypes": "Все типы",
|
||||||
|
"ui.files": "Файлы",
|
||||||
|
"ui.folders": "Папки",
|
||||||
|
"ui.newest": "Сначала недавно удалённые",
|
||||||
|
"ui.oldest": "Сначала давно удалённые",
|
||||||
|
"ui.name": "Имя",
|
||||||
|
"ui.type": "Тип",
|
||||||
|
"ui.title": "Корзина",
|
||||||
|
"ui.total": "Всего: {count}",
|
||||||
|
"ui.filter": "Фильтр по имени или пути",
|
||||||
|
"ui.refresh": "Обновить",
|
||||||
|
"ui.workspace": "Рабочее пространство",
|
||||||
|
"ui.originalPath": "Исходный путь",
|
||||||
|
"ui.deleted": "Удалён",
|
||||||
|
"ui.typeSize": "Тип / размер",
|
||||||
|
"ui.actions": "Действия",
|
||||||
|
"ui.noMatches": "Нет удалённых элементов, соответствующих фильтрам.",
|
||||||
|
"ui.empty": "Корзина пуста.",
|
||||||
|
"ui.restoring": "Восстановление...",
|
||||||
|
"ui.restore": "Восстановить"
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Global vault trash manager for restoring or permanently deleting deleted files and folders.",
|
"description": "Global vault trash manager for restoring or permanently deleting deleted files and folders.",
|
||||||
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "trash",
|
"icon": "trash",
|
||||||
"provides": [
|
"provides": [
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,14 @@ with open(path, 'w', encoding='utf-8') as f:
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. backend binary
|
# 3. locale catalogs declared by plugin.json
|
||||||
|
if [ -d "$plugin_dir/locales" ]; then
|
||||||
|
mkdir -p "$dist_dir/locales"
|
||||||
|
cp -r "$plugin_dir/locales/." "$dist_dir/locales/"
|
||||||
|
echo " └─ locales ($(find "$dist_dir/locales" -type f | wc -l) file(s))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. backend binary
|
||||||
if [ -d "$plugin_dir/backend" ]; then
|
if [ -d "$plugin_dir/backend" ]; then
|
||||||
# Find the compiled binary (same name as plugin directory)
|
# Find the compiled binary (same name as plugin directory)
|
||||||
local bin_name="$plugin_name"
|
local bin_name="$plugin_name"
|
||||||
|
|
@ -91,7 +98,7 @@ with open(path, 'w', encoding='utf-8') as f:
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Verify dist package has at least plugin.json
|
# 5. Verify dist package has at least plugin.json
|
||||||
if [ ! -f "$dist_dir/plugin.json" ]; then
|
if [ ! -f "$dist_dir/plugin.json" ]; then
|
||||||
echo " ❌ dist package missing plugin.json"
|
echo " ❌ dist package missing plugin.json"
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const pluginsRoot = path.join(root, 'plugins');
|
||||||
|
const contributionFields = {
|
||||||
|
views: 'title',
|
||||||
|
commands: 'title',
|
||||||
|
settingsPanels: 'title',
|
||||||
|
sidebarItems: 'title',
|
||||||
|
fileActions: 'label',
|
||||||
|
noteActions: 'label',
|
||||||
|
contextMenuEntries: 'label',
|
||||||
|
searchProviders: 'label',
|
||||||
|
statusBarItems: 'label',
|
||||||
|
openProviders: 'title',
|
||||||
|
workspaceItems: 'title',
|
||||||
|
};
|
||||||
|
const problems = [];
|
||||||
|
|
||||||
|
for (const entry of fs.readdirSync(pluginsRoot, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const pluginRoot = path.join(pluginsRoot, entry.name);
|
||||||
|
const manifestPath = path.join(pluginRoot, 'plugin.json');
|
||||||
|
if (!fs.existsSync(manifestPath)) continue;
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
const localization = manifest.localization;
|
||||||
|
if (!localization || localization.defaultLocale !== 'en') {
|
||||||
|
problems.push(`${manifest.id}: localization.defaultLocale must be en`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!localization.locales || !localization.locales.en || !localization.locales.ru) {
|
||||||
|
problems.push(`${manifest.id}: en and ru catalogs must be declared`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogs = {};
|
||||||
|
for (const locale of ['en', 'ru']) {
|
||||||
|
const relative = localization.locales[locale];
|
||||||
|
const catalogPath = path.resolve(pluginRoot, relative);
|
||||||
|
if (!catalogPath.startsWith(pluginRoot + path.sep) || !fs.existsSync(catalogPath)) {
|
||||||
|
problems.push(`${manifest.id}: missing safe ${locale} catalog at ${relative}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
catalogs[locale] = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
problems.push(`${manifest.id}: invalid ${locale} catalog: ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const [key, value] of Object.entries(catalogs[locale])) {
|
||||||
|
if (typeof value !== 'string') problems.push(`${manifest.id}: ${locale}.${key} must be a string`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!catalogs.en || !catalogs.ru) continue;
|
||||||
|
|
||||||
|
const enKeys = Object.keys(catalogs.en).sort();
|
||||||
|
const ruKeys = Object.keys(catalogs.ru).sort();
|
||||||
|
if (JSON.stringify(enKeys) !== JSON.stringify(ruKeys)) {
|
||||||
|
problems.push(`${manifest.id}: en/ru catalog keys differ`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const required = ['manifest.name'];
|
||||||
|
if (manifest.description) required.push('manifest.description');
|
||||||
|
for (const [point, field] of Object.entries(contributionFields)) {
|
||||||
|
for (const item of manifest.contributes?.[point] || []) {
|
||||||
|
required.push(`contributions.${point}.${item.id}.${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of required) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(catalogs.en, key)) problems.push(`${manifest.id}: en catalog missing ${key}`);
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(catalogs.ru, key)) problems.push(`${manifest.id}: ru catalog missing ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (problems.length > 0) {
|
||||||
|
problems.forEach((problem) => console.error(` FAIL ${problem}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(' OK official plugin locale catalogs are complete and aligned');
|
||||||
|
|
@ -76,6 +76,19 @@ else
|
||||||
echo " ℹ️ python3 not available — skipping manifest validation"
|
echo " ℹ️ python3 not available — skipping manifest validation"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
# Verify plugin-owned English and Russian catalogs.
|
||||||
|
echo "[localization catalogs]"
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
set +e
|
||||||
|
node "$ROOT/scripts/check-locales.mjs"
|
||||||
|
STATUS=$?
|
||||||
|
set -e
|
||||||
|
report "localization catalogs" "$STATUS"
|
||||||
|
else
|
||||||
|
echo " ⚠️ node not available — skipping localization catalog validation"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
# Guard official plugins against bypassing the v2 plugin API for note features.
|
# Guard official plugins against bypassing the v2 plugin API for note features.
|
||||||
echo "[frontend API boundary]"
|
echo "[frontend API boundary]"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ const sourcePath = path.join(root, 'plugins', 'todo', 'frontend', 'src', 'index.
|
||||||
const manifestPath = path.join(root, 'plugins', 'todo', 'plugin.json');
|
const manifestPath = path.join(root, 'plugins', 'todo', 'plugin.json');
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
const localeCatalogs = {
|
||||||
|
en: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'todo', 'locales', 'en.json'), 'utf8')),
|
||||||
|
ru: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'todo', 'locales', 'ru.json'), 'utf8')),
|
||||||
|
};
|
||||||
|
|
||||||
class FakeNode {
|
class FakeNode {
|
||||||
constructor(tagName) {
|
constructor(tagName) {
|
||||||
|
|
@ -140,8 +144,10 @@ function loadComponent(document, emittedEvents) {
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeApi(initialSettings = {}) {
|
function makeApi(initialSettings = {}, initialLocale = 'en') {
|
||||||
const settings = { ...initialSettings };
|
const settings = { ...initialSettings };
|
||||||
|
let locale = initialLocale;
|
||||||
|
const localeListeners = [];
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
settingsApi: {
|
settingsApi: {
|
||||||
|
|
@ -157,8 +163,31 @@ function makeApi(initialSettings = {}) {
|
||||||
{ name: 'Project', relativePath: 'Project', type: 'folder' },
|
{ name: 'Project', relativePath: 'Project', type: 'folder' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
setLocale(nextLocale) {
|
||||||
|
locale = nextLocale;
|
||||||
|
localeListeners.slice().forEach((listener) => listener(locale));
|
||||||
|
},
|
||||||
get api() {
|
get api() {
|
||||||
return { settings: this.settingsApi, files: this.files };
|
return {
|
||||||
|
settings: this.settingsApi,
|
||||||
|
files: this.files,
|
||||||
|
i18n: {
|
||||||
|
getLocale: () => locale,
|
||||||
|
t: (key, params, fallback) => {
|
||||||
|
const message = localeCatalogs[locale]?.[key] || localeCatalogs.en[key] || fallback || key;
|
||||||
|
return message.replace(/\{([^}]+)\}/g, (placeholder, name) => (
|
||||||
|
Object.prototype.hasOwnProperty.call(params || {}, name) ? String(params[name]) : placeholder
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onDidChangeLocale: (listener) => {
|
||||||
|
localeListeners.push(listener);
|
||||||
|
return () => {
|
||||||
|
const index = localeListeners.indexOf(listener);
|
||||||
|
if (index !== -1) localeListeners.splice(index, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +234,13 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
||||||
if (createdTodo.dueAt !== '2000-01-01' || createdTodo.reminderAt !== '2000-01-01T09:00') throw new Error('Todo due/reminder metadata was not stored');
|
if (createdTodo.dueAt !== '2000-01-01' || createdTodo.reminderAt !== '2000-01-01T09:00') throw new Error('Todo due/reminder metadata was not stored');
|
||||||
if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered');
|
if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered');
|
||||||
|
|
||||||
|
apiState.setLocale('ru');
|
||||||
|
if (!container.textContent.includes('Задачи · Project') || !container.textContent.includes('Просрочено')) {
|
||||||
|
throw new Error('Todo view did not update to Russian without remounting');
|
||||||
|
}
|
||||||
|
if ((apiState.settings['todos:global'] || []).length !== 1) throw new Error('locale change lost Todo state');
|
||||||
|
apiState.setLocale('en');
|
||||||
|
|
||||||
byData(container, 'data-todo-action', 'edit').click();
|
byData(container, 'data-todo-action', 'edit').click();
|
||||||
byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated';
|
byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated';
|
||||||
byData(container, 'data-todo-action', 'save').click();
|
byData(container, 'data-todo-action', 'save').click();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue