Align official plugins with v2 runtime
This commit is contained in:
@@ -84,6 +84,29 @@
|
||||
return escapeHtml(s).replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function cleanPath(path) {
|
||||
return String(path || '').split('/').filter(Boolean).join('/');
|
||||
}
|
||||
|
||||
function normalizeNoteFilename(title) {
|
||||
var value = String(title == null ? '' : title).trim();
|
||||
if (/\.markdown$/i.test(value) && value.length > 9) value = value.slice(0, -9);
|
||||
else if (/\.md$/i.test(value) && value.length > 3) value = value.slice(0, -3);
|
||||
if (!value) throw new Error('note title must not be empty');
|
||||
value = value.replace(/\s+/g, '_');
|
||||
value = value.replace(/[\u2012\u2013\u2014\u2015\u2212]/g, '-');
|
||||
value = value.replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g, '');
|
||||
var out = '';
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
var ch = value.charAt(i);
|
||||
if (/[A-Za-z0-9._-]/.test(ch) || /[\p{L}\p{N}]/u.test(ch)) out += ch;
|
||||
else if (/\S/.test(ch)) out += '_';
|
||||
}
|
||||
out = out.replace(/[_.-]+/g, '_').replace(/^[._\-\s]+|[._\-\s]+$/g, '');
|
||||
if (!out) throw new Error('note title normalizes to an empty filename');
|
||||
return out + '.md';
|
||||
}
|
||||
|
||||
function renderInline(text, isNotesContext) {
|
||||
var html = escapeHtml(text);
|
||||
// Internal wiki links [[Title]] — only render in notes context
|
||||
@@ -408,14 +431,7 @@
|
||||
if (!dirty || disposed) return Promise.resolve();
|
||||
saveState = 'saving';
|
||||
updateStatus();
|
||||
var savePromise;
|
||||
if (editorMode === 'notes-markdown') {
|
||||
savePromise = api.backend.call('SaveNote', resourcePath, currentContent).then(function (errStr) {
|
||||
if (errStr) throw new Error(errStr);
|
||||
});
|
||||
} else {
|
||||
savePromise = api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true });
|
||||
}
|
||||
var savePromise = api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true });
|
||||
return savePromise.then(function () {
|
||||
if (disposed) return;
|
||||
savedContent = currentContent;
|
||||
@@ -442,15 +458,7 @@
|
||||
if (dirty && !window.confirm('Discard unsaved changes and reload from disk?')) return;
|
||||
editorWrap.innerHTML = '';
|
||||
editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...']));
|
||||
var readPromise;
|
||||
if (editorMode === 'notes-markdown') {
|
||||
readPromise = api.backend.call('ReadNote', resourcePath).then(function (result) {
|
||||
var content = Array.isArray(result) ? result[0] : result;
|
||||
return content == null ? '' : content;
|
||||
});
|
||||
} else {
|
||||
readPromise = api.files.readText(resourcePath);
|
||||
}
|
||||
var readPromise = api.files.readText(resourcePath);
|
||||
readPromise.then(function (content) {
|
||||
if (disposed) return;
|
||||
currentContent = String(content == null ? '' : content);
|
||||
@@ -512,15 +520,23 @@
|
||||
event.preventDefault();
|
||||
var noteTitle = link.getAttribute('data-note-link');
|
||||
if (!noteTitle) return;
|
||||
var parentPath = resourcePath;
|
||||
var idx = parentPath.indexOf('/Notes/');
|
||||
if (idx !== -1) {
|
||||
parentPath = parentPath.substring(0, idx);
|
||||
}
|
||||
api.request.open({
|
||||
path: parentPath + '/Notes/' + noteTitle + '.md',
|
||||
var currentPath = cleanPath(resourcePath);
|
||||
var notesIdx = currentPath.indexOf('/Notes/');
|
||||
var notesRoot = notesIdx === -1 ? 'Notes' : currentPath.slice(0, notesIdx) + '/Notes';
|
||||
var targetPath = cleanPath(notesRoot + '/' + normalizeNoteFilename(noteTitle));
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: targetPath,
|
||||
mode: 'view',
|
||||
context: { notesMode: true }
|
||||
extension: '.md',
|
||||
context: {
|
||||
sourcePluginId: 'verstak.default-editor',
|
||||
sourceView: 'editor',
|
||||
isInsideNotesFolder: true,
|
||||
notesMode: true
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error('[default-editor] open internal link:', err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"workbench.open"
|
||||
],
|
||||
"frontend": {
|
||||
"entry": "frontend/dist/index.js"
|
||||
"entry": "frontend/src/index.js"
|
||||
},
|
||||
"contributes": {
|
||||
"openProviders": [
|
||||
|
||||
@@ -145,6 +145,35 @@
|
||||
return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
|
||||
}
|
||||
|
||||
function normalizeNoteFilename(title) {
|
||||
var value = String(title == null ? '' : title).trim();
|
||||
if (/\.markdown$/i.test(value) && value.length > 9) value = value.slice(0, -9);
|
||||
else if (/\.md$/i.test(value) && value.length > 3) value = value.slice(0, -3);
|
||||
if (!value) throw new Error('note title must not be empty');
|
||||
value = value.replace(/\s+/g, '_');
|
||||
value = value.replace(/[\u2012\u2013\u2014\u2015\u2212]/g, '-');
|
||||
value = value.replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g, '');
|
||||
var out = '';
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
var ch = value.charAt(i);
|
||||
if (/[A-Za-z0-9._-]/.test(ch) || /[\p{L}\p{N}]/u.test(ch)) out += ch;
|
||||
else if (/\S/.test(ch)) out += '_';
|
||||
}
|
||||
out = out.replace(/[_.-]+/g, '_').replace(/^[._\-\s]+|[._\-\s]+$/g, '');
|
||||
if (!out) throw new Error('note title normalizes to an empty filename');
|
||||
return out + '.md';
|
||||
}
|
||||
|
||||
function isConflictError(err) {
|
||||
var msg = (err && err.message) ? err.message : String(err || '');
|
||||
return /conflict|already exists|exists/i.test(msg);
|
||||
}
|
||||
|
||||
function isNotFoundError(err) {
|
||||
var msg = (err && err.message) ? err.message : String(err || '');
|
||||
return /not.?found|does not exist|no such/i.test(msg);
|
||||
}
|
||||
|
||||
var FILE_ICONS = {
|
||||
folder: 'M3 5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v1H3V5Zm0 6h18v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7Z',
|
||||
markdown: 'M5 3h10l4 4v14H5V3Zm9 1.5V8h3.5L14 4.5ZM8 11h8v2H8v-2Zm0 4h8v2H8v-2Z',
|
||||
@@ -285,6 +314,56 @@
|
||||
return full.indexOf(workspaceRoot + '/') === 0 ? full.slice(workspaceRoot.length + 1) : full;
|
||||
}
|
||||
|
||||
function ensureFolder(path) {
|
||||
return api.files.createFolder(path).catch(function (err) {
|
||||
if (!isConflictError(err)) throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function createNoteInFolder(notesFolderPath, title) {
|
||||
var trimmedTitle = String(title || '').trim();
|
||||
if (!trimmedTitle) return Promise.reject(new Error('note title must not be empty'));
|
||||
var notePath = cleanPath(notesFolderPath) + '/' + normalizeNoteFilename(trimmedTitle);
|
||||
return ensureFolder(notesFolderPath).then(function () {
|
||||
return api.files.metadata(notePath).then(function () {
|
||||
return { path: notePath, conflict: true };
|
||||
}).catch(function (err) {
|
||||
if (!isNotFoundError(err)) throw err;
|
||||
return api.files.writeText(notePath, '# ' + trimmedTitle + '\n', {
|
||||
createIfMissing: true,
|
||||
overwrite: false
|
||||
}).then(function () {
|
||||
return { path: notePath };
|
||||
}).catch(function (writeErr) {
|
||||
if (isConflictError(writeErr)) return { path: notePath, conflict: true };
|
||||
throw writeErr;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ensureOverviewInFolder(notesFolderPath) {
|
||||
var folderPath = cleanPath(notesFolderPath);
|
||||
var ovPath = folderPath + '/Overview.md';
|
||||
return api.files.metadata(ovPath).then(function () {
|
||||
return { path: ovPath };
|
||||
}).catch(function (err) {
|
||||
if (!isNotFoundError(err)) throw err;
|
||||
return ensureFolder(folderPath).then(function () {
|
||||
var local = localPath(folderPath);
|
||||
var parentName = baseName(parentPath(local)) || baseName(folderPath) || 'Overview';
|
||||
return api.files.writeText(ovPath, '# ' + parentName + '\n', {
|
||||
createIfMissing: true,
|
||||
overwrite: false
|
||||
}).catch(function (writeErr) {
|
||||
if (!isConflictError(writeErr)) throw writeErr;
|
||||
}).then(function () {
|
||||
return { path: ovPath };
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var toolbar = el('div', { className: 'files-toolbar' });
|
||||
var breadcrumb = el('div', { className: 'files-breadcrumb' });
|
||||
var backBtn = iconButton('back', 'Back', 'back', goBack);
|
||||
@@ -771,35 +850,31 @@
|
||||
ctxMenu.appendChild(ctxItem('Create Note', '', function () {
|
||||
var title = prompt('Note title:');
|
||||
if (!title) return;
|
||||
api.backend.call('NormalizeNoteTitle', title).then(function (result) {
|
||||
var safeTitle = Array.isArray(result) ? result[0] : (typeof result === 'string' ? result : title);
|
||||
if (!safeTitle) safeTitle = title;
|
||||
return api.backend.call('CreateNote', scopedPath(entryLocalPath), safeTitle).then(function (cr) {
|
||||
var crErr = Array.isArray(cr) ? cr[1] : '';
|
||||
if (crErr) { window.alert(crErr); return; }
|
||||
loadEntries();
|
||||
var crValue = Array.isArray(cr) ? cr[0] : cr;
|
||||
var notePath = (crValue && crValue.path) ? crValue.path : '';
|
||||
if (notePath) {
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: localPath(notePath),
|
||||
mode: 'edit',
|
||||
extension: '.md',
|
||||
context: { notesMode: true, sourcePluginId: 'verstak.files' }
|
||||
}).catch(function () {});
|
||||
}
|
||||
});
|
||||
createNoteInFolder(entry.relativePath, title).then(function (created) {
|
||||
if (created && created.conflict) {
|
||||
window.alert('A note with this title already exists.');
|
||||
return;
|
||||
}
|
||||
loadEntries();
|
||||
var notePath = (created && created.path) ? created.path : '';
|
||||
if (notePath) {
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: notePath,
|
||||
mode: 'edit',
|
||||
extension: '.md',
|
||||
context: { notesMode: true, sourcePluginId: 'verstak.files' }
|
||||
}).catch(function () {});
|
||||
}
|
||||
}).catch(function (err) { window.alert('Failed to create note: ' + (err.message || String(err))); });
|
||||
}, 'create-note', 'markdownAdd'));
|
||||
ctxMenu.appendChild(ctxItem('Open Overview', '', function () {
|
||||
api.backend.call('EnsureOverview', scopedPath(entryLocalPath)).then(function (result) {
|
||||
var ovValue = Array.isArray(result) ? result[0] : result;
|
||||
var overviewPath = (ovValue && ovValue.path) ? ovValue.path : '';
|
||||
ensureOverviewInFolder(entry.relativePath).then(function (result) {
|
||||
var overviewPath = (result && result.path) ? result.path : '';
|
||||
if (overviewPath) {
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: localPath(overviewPath),
|
||||
path: overviewPath,
|
||||
mode: 'view',
|
||||
extension: '.md',
|
||||
context: { notesMode: true, sourcePluginId: 'verstak.files' }
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"ui.register"
|
||||
],
|
||||
"frontend": {
|
||||
"entry": "frontend/dist/index.js"
|
||||
"entry": "frontend/src/index.js"
|
||||
},
|
||||
"contributes": {
|
||||
"workspaceItems": [
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
'.notes-list{flex:1;overflow:auto;min-height:0}',
|
||||
'.notes-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55);cursor:pointer;font-size:.85rem}',
|
||||
'.notes-item:hover{background:#17172d}',
|
||||
'.notes-item.overview{background:#111126;border-left:2px solid #4ecca3}',
|
||||
'.notes-item.selected{background:#1a2a3a}',
|
||||
'.notes-item-icon{width:1.25rem;height:1.25rem;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;color:#8b8ba8}',
|
||||
'.notes-item-icon svg{width:16px;height:16px;display:block;fill:currentColor}',
|
||||
@@ -101,6 +100,58 @@
|
||||
return parts[parts.length - 1] || '';
|
||||
}
|
||||
|
||||
function cleanPath(path) {
|
||||
return String(path || '').split('/').filter(Boolean).join('/');
|
||||
}
|
||||
|
||||
function parentPath(path) {
|
||||
path = cleanPath(path);
|
||||
var idx = path.lastIndexOf('/');
|
||||
return idx === -1 ? '' : path.slice(0, idx);
|
||||
}
|
||||
|
||||
function notesFolderPath(parent) {
|
||||
parent = cleanPath(parent);
|
||||
return parent ? parent + '/Notes' : 'Notes';
|
||||
}
|
||||
|
||||
function titleFromFilename(filename) {
|
||||
filename = String(filename || '').trim();
|
||||
if (/\.markdown$/i.test(filename)) filename = filename.slice(0, -9);
|
||||
else if (/\.md$/i.test(filename)) filename = filename.slice(0, -3);
|
||||
return filename.replace(/_/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalizeNoteFilename(title) {
|
||||
var original = String(title == null ? '' : title);
|
||||
var value = original.trim();
|
||||
if (/\.markdown$/i.test(value) && value.length > 9) value = value.slice(0, -9);
|
||||
else if (/\.md$/i.test(value) && value.length > 3) value = value.slice(0, -3);
|
||||
if (!value) throw new Error('note title must not be empty');
|
||||
value = value.replace(/\s+/g, '_');
|
||||
value = value.replace(/[\u2012\u2013\u2014\u2015\u2212]/g, '-');
|
||||
value = value.replace(/[<>:"/\\|?*\x00-\x1f\x7f]/g, '');
|
||||
var out = '';
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
var ch = value.charAt(i);
|
||||
if (/[A-Za-z0-9._-]/.test(ch) || /[\p{L}\p{N}]/u.test(ch)) out += ch;
|
||||
else if (/\S/.test(ch)) out += '_';
|
||||
}
|
||||
out = out.replace(/[_.-]+/g, '_').replace(/^[._\-\s]+|[._\-\s]+$/g, '');
|
||||
if (!out) throw new Error('note title normalizes to an empty filename');
|
||||
return out + '.md';
|
||||
}
|
||||
|
||||
function isConflictError(err) {
|
||||
var msg = (err && err.message) ? err.message : String(err || '');
|
||||
return /conflict|already exists|exists/i.test(msg);
|
||||
}
|
||||
|
||||
function isNotFoundError(err) {
|
||||
var msg = (err && err.message) ? err.message : String(err || '');
|
||||
return /not.?found|does not exist|no such/i.test(msg);
|
||||
}
|
||||
|
||||
var NotesView = {
|
||||
mount: function (containerEl, props, api) {
|
||||
injectStyles();
|
||||
@@ -122,17 +173,80 @@
|
||||
return workspaceRoot || '';
|
||||
}
|
||||
|
||||
// ─── Backend bridge ──────────────────────────────────────
|
||||
// Uses api.backend.call() from VerstakPluginAPI — the official bridge.
|
||||
// Direct window.go.api.App access is NOT allowed per platform rules.
|
||||
function noteFromEntry(parent, entry) {
|
||||
return {
|
||||
title: titleFromFilename(entry.name),
|
||||
filename: entry.name,
|
||||
path: entry.relativePath,
|
||||
parentPath: cleanPath(parent)
|
||||
};
|
||||
}
|
||||
|
||||
function sortNotes(list) {
|
||||
return list.sort(function (a, b) {
|
||||
return String(a.title || '').toLowerCase().localeCompare(String(b.title || '').toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
function listNotes(parent) {
|
||||
return api.files.list(notesFolderPath(parent)).then(function (entries) {
|
||||
return sortNotes((entries || []).filter(function (entry) {
|
||||
return entry.type === 'file' && /\.(md|markdown)$/i.test(entry.name || '');
|
||||
}).map(function (entry) {
|
||||
return noteFromEntry(parent, entry);
|
||||
}));
|
||||
}).catch(function (err) {
|
||||
if (isNotFoundError(err)) return [];
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureNotesFolder(parent) {
|
||||
return api.files.createFolder(notesFolderPath(parent)).catch(function (err) {
|
||||
if (!isConflictError(err)) throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function createNote(parent, title) {
|
||||
var trimmedTitle = String(title || '').trim();
|
||||
if (!trimmedTitle) return Promise.reject(new Error('note title must not be empty'));
|
||||
var path = notesFolderPath(parent) + '/' + normalizeNoteFilename(trimmedTitle);
|
||||
return ensureNotesFolder(parent).then(function () {
|
||||
return api.files.writeText(path, '# ' + trimmedTitle + '\n', {
|
||||
createIfMissing: true,
|
||||
overwrite: false
|
||||
}).then(function () {
|
||||
return { path: path };
|
||||
}).catch(function (writeErr) {
|
||||
if (isConflictError(writeErr)) return { path: path, conflict: true };
|
||||
throw writeErr;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renameNote(notePath, newTitle) {
|
||||
var trimmedTitle = String(newTitle || '').trim();
|
||||
if (!trimmedTitle) return Promise.reject(new Error('note title must not be empty'));
|
||||
var newPath = parentPath(notePath) + '/' + normalizeNoteFilename(trimmedTitle);
|
||||
if (newPath === notePath) return Promise.resolve({ path: notePath });
|
||||
return api.files.metadata(newPath).then(function () {
|
||||
return { path: newPath, conflict: true };
|
||||
}).catch(function (err) {
|
||||
if (!isNotFoundError(err)) throw err;
|
||||
return api.files.move(notePath, newPath, { overwrite: false }).then(function () {
|
||||
return { path: newPath };
|
||||
}).catch(function (moveErr) {
|
||||
if (isConflictError(moveErr)) return { path: newPath, conflict: true };
|
||||
throw moveErr;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── UI Elements ────────────────────────────────────────
|
||||
|
||||
var toolbar = el('div', { className: 'notes-toolbar' });
|
||||
var overviewBtn = el('button', { className: 'notes-btn primary', 'data-action': 'overview', innerHTML: iconSvg('overview') + ' Overview' });
|
||||
var createBtn = el('button', { className: 'notes-btn', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' });
|
||||
var statusEl = el('span', { className: 'notes-status' });
|
||||
toolbar.appendChild(overviewBtn);
|
||||
toolbar.appendChild(createBtn);
|
||||
toolbar.appendChild(el('span', { style: { flex: '1' } }));
|
||||
toolbar.appendChild(statusEl);
|
||||
@@ -186,14 +300,9 @@
|
||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, ['Loading...']));
|
||||
|
||||
var parent = notesParent();
|
||||
api.backend.call('ListNotes', parent).then(function (result) {
|
||||
listNotes(parent).then(function (result) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
renderEmpty('Could not load notes: ' + unpack.error);
|
||||
return;
|
||||
}
|
||||
notes = unpack.value || [];
|
||||
notes = result || [];
|
||||
renderList();
|
||||
}).catch(function (err) {
|
||||
if (disposed) return;
|
||||
@@ -209,13 +318,13 @@
|
||||
}
|
||||
notes.forEach(function (note) {
|
||||
var row = el('div', {
|
||||
className: 'notes-item' + (note.isOverview ? ' overview' : '') + (note.path === selectedPath ? ' selected' : ''),
|
||||
className: 'notes-item' + (note.path === selectedPath ? ' selected' : ''),
|
||||
'data-note-path': note.path,
|
||||
'data-note-title': note.title,
|
||||
onClick: function () { selectNote(note); },
|
||||
onDblclick: function () { openNote(note); }
|
||||
}, [
|
||||
el('span', { className: 'notes-item-icon', innerHTML: note.isOverview ? iconSvg('overview') : iconSvg('note') }),
|
||||
el('span', { className: 'notes-item-icon', innerHTML: iconSvg('note') }),
|
||||
el('span', { className: 'notes-item-name', textContent: note.title || fileName(note.path), title: note.title || note.path }),
|
||||
el('span', { className: 'notes-item-actions' }, [
|
||||
el('button', {
|
||||
@@ -224,12 +333,12 @@
|
||||
innerHTML: iconSvg('open'),
|
||||
onClick: function (e) { e.stopPropagation(); openNote(note); }
|
||||
}),
|
||||
!note.isOverview ? el('button', {
|
||||
el('button', {
|
||||
className: 'notes-item-btn',
|
||||
title: 'Rename',
|
||||
innerHTML: iconSvg('rename'),
|
||||
onClick: function (e) { e.stopPropagation(); beginRename(note); }
|
||||
}) : null
|
||||
})
|
||||
])
|
||||
]);
|
||||
listContainer.appendChild(row);
|
||||
@@ -239,7 +348,7 @@
|
||||
function renderEmpty(msg) {
|
||||
listContainer.innerHTML = '';
|
||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, [
|
||||
el('div', {}, [iconSvg('note')]),
|
||||
el('div', { innerHTML: iconSvg('note') }),
|
||||
el('div', {}, [msg]),
|
||||
el('div', { className: 'notes-empty-hint' }, ['Click "New Note" to create one'])
|
||||
]));
|
||||
@@ -268,40 +377,6 @@
|
||||
}).catch(function (err) { console.error('[notes] openResource:', err); });
|
||||
}
|
||||
|
||||
function openOverview() {
|
||||
setStatus('Opening overview...', 'loading');
|
||||
var parent = notesParent();
|
||||
api.backend.call('EnsureOverview', parent).then(function (result) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
setStatus('Error: ' + unpack.error, 'error');
|
||||
return;
|
||||
}
|
||||
var overviewPath = (unpack.value && unpack.value.path) || '';
|
||||
if (!overviewPath) {
|
||||
setStatus('Failed to get overview path', 'error');
|
||||
return;
|
||||
}
|
||||
loadNotes();
|
||||
api.workbench.openResource({
|
||||
kind: 'vault-file',
|
||||
path: overviewPath,
|
||||
mode: 'view',
|
||||
extension: '.md',
|
||||
context: {
|
||||
sourcePluginId: 'verstak.notes',
|
||||
sourceView: 'notes',
|
||||
isInsideNotesFolder: true,
|
||||
notesMode: true,
|
||||
notesScopePath: notesParent()
|
||||
}
|
||||
}).catch(function (err) { setStatus('Error opening overview: ' + (err.message || err), 'error'); });
|
||||
}).catch(function (err) {
|
||||
setStatus('Error: ' + (err.message || err), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Create ────────────────────────────────────────────
|
||||
|
||||
function showCreate() {
|
||||
@@ -319,14 +394,9 @@
|
||||
if (!title) return;
|
||||
setStatus('Creating note...', 'loading');
|
||||
var parent = notesParent();
|
||||
api.backend.call('CreateNote', parent, title).then(function (result) {
|
||||
createNote(parent, title).then(function (data) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
setStatus('Error: ' + unpack.error, 'error');
|
||||
return;
|
||||
}
|
||||
var data = unpack.value || {};
|
||||
data = data || {};
|
||||
if (data.conflict) {
|
||||
showConflictModal(title, data.path);
|
||||
return;
|
||||
@@ -375,14 +445,9 @@
|
||||
var newTitle = renameInput.value.trim();
|
||||
if (!newTitle) return;
|
||||
setStatus('Renaming...', 'loading');
|
||||
api.backend.call('RenameNote', renameTarget.path, newTitle).then(function (result) {
|
||||
renameNote(renameTarget.path, newTitle).then(function (data) {
|
||||
if (disposed) return;
|
||||
var unpack = unpackResult(result);
|
||||
if (unpack.error) {
|
||||
setStatus('Error: ' + unpack.error, 'error');
|
||||
return;
|
||||
}
|
||||
var data = unpack.value || {};
|
||||
data = data || {};
|
||||
if (data.conflict) {
|
||||
showConflictModal(newTitle, data.path);
|
||||
return;
|
||||
@@ -415,7 +480,6 @@
|
||||
|
||||
// ─── Event Wiring ───────────────────────────────────────
|
||||
|
||||
overviewBtn.addEventListener('click', openOverview);
|
||||
createBtn.addEventListener('click', showCreate);
|
||||
createConfirm.addEventListener('click', confirmCreate);
|
||||
createCancel.addEventListener('click', hideCreate);
|
||||
@@ -448,16 +512,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
function unpackResult(result) {
|
||||
if (Array.isArray(result) && result.length === 2) {
|
||||
return { value: result[0], error: result[1] || '' };
|
||||
}
|
||||
if (result && result.value !== undefined && result.error !== undefined) {
|
||||
return result;
|
||||
}
|
||||
return { value: result, error: '' };
|
||||
}
|
||||
|
||||
window.VerstakPluginRegister('verstak.notes', {
|
||||
components: { NotesView: NotesView }
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"apiVersion": "0.1.0",
|
||||
"description": "Workspace-scoped notes manager with create, rename, overview, and workbench integration.",
|
||||
"source": "official",
|
||||
"icon": "file-text",
|
||||
"icon": "edit",
|
||||
"provides": [
|
||||
"verstak/notes/v1"
|
||||
],
|
||||
@@ -14,9 +14,6 @@
|
||||
"verstak/core/files/v1",
|
||||
"verstak/core/workbench/v1"
|
||||
],
|
||||
"optionalRequires": [
|
||||
"verstak/core/notes/v1"
|
||||
],
|
||||
"permissions": [
|
||||
"files.read",
|
||||
"files.write",
|
||||
@@ -31,7 +28,7 @@
|
||||
{
|
||||
"id": "verstak.notes.workspace",
|
||||
"title": "Notes",
|
||||
"icon": "file-text",
|
||||
"icon": "edit",
|
||||
"component": "NotesView"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"workbench.open"
|
||||
],
|
||||
"frontend": {
|
||||
"entry": "frontend/dist/index.js"
|
||||
"entry": "frontend/src/index.js"
|
||||
},
|
||||
"contributes": {
|
||||
"views": [
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
return s
|
||||
}
|
||||
|
||||
async function backendCall(method, ...args) {
|
||||
if (api?.backend && typeof api.backend.call === 'function') {
|
||||
return await api.backend.call(method, ...args)
|
||||
}
|
||||
throw new Error('Plugin API backend.call not available')
|
||||
function syncAPI() {
|
||||
if (!api?.sync) throw new Error('Plugin API sync namespace not available')
|
||||
return api.sync
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -45,7 +43,7 @@
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
settings = await backendCall('SyncStatus')
|
||||
settings = await syncAPI().status()
|
||||
if (settings) {
|
||||
if (settings.serverUrl) serverUrl = settings.serverUrl
|
||||
if (settings.syncInterval != null) syncInterval = settings.syncInterval
|
||||
@@ -65,9 +63,10 @@
|
||||
errorMsg = ''
|
||||
resultMsg = ''
|
||||
try {
|
||||
if (api?.settings?.write) {
|
||||
await api.settings.write({ serverUrl, username, autoSync, syncInterval })
|
||||
if (api?.settings?.writeAll) {
|
||||
await api.settings.writeAll({ serverUrl, username, autoSync, syncInterval })
|
||||
}
|
||||
await syncAPI().setInterval(autoSync ? syncInterval : 0)
|
||||
resultMsg = 'Settings saved.'
|
||||
resultKind = ''
|
||||
} catch (e) {
|
||||
@@ -83,7 +82,7 @@
|
||||
connectionOk = null
|
||||
errorMsg = ''
|
||||
try {
|
||||
await backendCall('SyncTestConnection', serverUrl, username, password)
|
||||
await syncAPI().testConnection(serverUrl, username, password)
|
||||
connectionOk = true
|
||||
connectionResult = 'Connection successful.'
|
||||
} catch (e) {
|
||||
@@ -99,7 +98,7 @@
|
||||
errorMsg = ''
|
||||
connectionResult = ''
|
||||
try {
|
||||
await backendCall('SyncConfigure', serverUrl, username, password)
|
||||
await syncAPI().configure(serverUrl, username, password)
|
||||
connectionResult = 'Connected successfully.'
|
||||
connectionOk = true
|
||||
username = ''
|
||||
@@ -125,7 +124,7 @@
|
||||
errorMsg = ''
|
||||
resultMsg = ''
|
||||
try {
|
||||
const r = await backendCall('SyncNow')
|
||||
const r = await syncAPI().now()
|
||||
const summary = 'Pushed ' + (r?.pushed || 0) + ', pulled ' + (r?.pulled || 0)
|
||||
const warning = syncResultWarning(r)
|
||||
resultMsg = warning ? summary + ' · ' + warning : summary
|
||||
@@ -157,7 +156,7 @@
|
||||
errorMsg = ''
|
||||
resultMsg = ''
|
||||
try {
|
||||
await backendCall('SyncDisconnect')
|
||||
await syncAPI().disconnect()
|
||||
resultMsg = 'Disconnected from server.'
|
||||
resultKind = ''
|
||||
settings = null
|
||||
@@ -184,18 +183,18 @@
|
||||
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">Server</h3>
|
||||
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<label style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Server URL</label>
|
||||
<input 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" />
|
||||
<label for="sync-server-url" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">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" />
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<label style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Username</label>
|
||||
<input 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} />
|
||||
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">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} />
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<label style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">Password</label>
|
||||
<input 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} />
|
||||
<label for="sync-password" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">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} />
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
|
||||
@@ -217,9 +216,9 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:0.75rem;">
|
||||
<label 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;">Sync interval</label>
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;">
|
||||
<input 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,6 @@
|
||||
let status = 'disabled'
|
||||
let interval
|
||||
|
||||
function wailsCall(method, ...args) {
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
const fn = window['go']['main']['App'][method]
|
||||
if (typeof fn === 'function') return fn(...args)
|
||||
}
|
||||
} catch (e) { console.error('Wails error:', method, e) }
|
||||
return Promise.reject(new Error('Wails not connected: ' + method))
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const s = await api?.settings?.read?.()
|
||||
@@ -25,7 +15,7 @@
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
const s = await wailsCall('SyncStatus')
|
||||
const s = await api?.sync?.status?.()
|
||||
if (s?.statusLabel) {
|
||||
status = s.statusLabel
|
||||
} else if (s?.configured === false) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": "official",
|
||||
"provides": ["verstak/sync/v1", "verstak/sync.status/v1"],
|
||||
"requires": ["verstak/core/files/v1"],
|
||||
"permissions": ["files.read", "files.write", "network.remote", "settings.read", "settings.write", "ui.register"],
|
||||
"permissions": ["files.read", "files.write", "network.remote", "storage.namespace", "sync.participate", "ui.register"],
|
||||
"frontend": { "entry": "frontend/dist/index.js" },
|
||||
"contributes": {
|
||||
"settingsPanels": [{ "id": "verstak.sync.settings", "title": "Sync", "component": "SyncSettings" }],
|
||||
|
||||
Reference in New Issue
Block a user