2026-05-26 01:31:28 +08:00
|
|
|
|
export const createSidebarWorkspaceRuntime = (dependencies = {}) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
cssEscape,
|
|
|
|
|
|
currentDocumentId,
|
|
|
|
|
|
escapeHtml,
|
|
|
|
|
|
parseJsonScript,
|
|
|
|
|
|
setCommandPending,
|
|
|
|
|
|
sidebarShellRuntimeFunction,
|
|
|
|
|
|
} = dependencies;
|
|
|
|
|
|
const MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
|
|
|
|
|
|
const MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX = 'mnote.localFolder.recentRoots:';
|
|
|
|
|
|
const MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeSidebarTreeMode(value) {
|
|
|
|
|
|
var mode = String(value || '').trim();
|
|
|
|
|
|
return mode === 'filetree' ? 'filetree' : 'page';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readStoredSidebarTreeMode() {
|
|
|
|
|
|
var runtimeFn = sidebarShellRuntimeFunction('readStoredSidebarTreeMode');
|
|
|
|
|
|
if (runtimeFn) return runtimeFn();
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree'));
|
|
|
|
|
|
if (params.has('treeView') || params.has('sidebarTree')) return fromUrl;
|
|
|
|
|
|
try {
|
|
|
|
|
|
var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : '';
|
|
|
|
|
|
return normalizeSidebarTreeMode(stored);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return 'page';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function persistSidebarTreeMode(mode) {
|
|
|
|
|
|
var runtimeFn = sidebarShellRuntimeFunction('persistSidebarTreeMode');
|
|
|
|
|
|
if (runtimeFn) return runtimeFn(mode);
|
|
|
|
|
|
var normalized = normalizeSidebarTreeMode(mode);
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized);
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized);
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
return normalized;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function activeSidebarTreeMode() {
|
|
|
|
|
|
var runtimeFn = sidebarShellRuntimeFunction('activeSidebarTreeMode');
|
|
|
|
|
|
if (runtimeFn) return runtimeFn();
|
|
|
|
|
|
var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
|
|
|
|
|
|
if (active instanceof HTMLElement) {
|
|
|
|
|
|
return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab'));
|
|
|
|
|
|
}
|
|
|
|
|
|
return readStoredSidebarTreeMode();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveWorkspaceId(trigger) {
|
|
|
|
|
|
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (direct) return direct;
|
|
|
|
|
|
var root = trigger.closest('[data-workspace-id]');
|
|
|
|
|
|
if (root) {
|
|
|
|
|
|
var value = (root.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (value) return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
var documentId = currentDocumentId();
|
|
|
|
|
|
if (documentId) {
|
|
|
|
|
|
var shell = document.querySelector('.document-shell[data-document-id="' + cssEscape(documentId) + '"][data-workspace-id]');
|
|
|
|
|
|
if (shell instanceof HTMLElement) {
|
|
|
|
|
|
var shellWorkspaceId = (shell.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (shellWorkspaceId) return shellWorkspaceId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var activePane = document.querySelector('[data-pane-visible="true"][data-pane-workspace-id]');
|
|
|
|
|
|
if (activePane instanceof HTMLElement) {
|
|
|
|
|
|
var paneWorkspaceId = (activePane.getAttribute('data-pane-workspace-id') || '').trim();
|
|
|
|
|
|
if (paneWorkspaceId) return paneWorkspaceId;
|
|
|
|
|
|
}
|
|
|
|
|
|
var anyDocumentShell = document.querySelector('.document-shell[data-workspace-id]');
|
|
|
|
|
|
if (anyDocumentShell instanceof HTMLElement) {
|
|
|
|
|
|
var documentWorkspaceId = (anyDocumentShell.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (documentWorkspaceId) return documentWorkspaceId;
|
|
|
|
|
|
}
|
|
|
|
|
|
var fromDom = document.querySelector('[data-workspace-id]');
|
|
|
|
|
|
if (fromDom instanceof HTMLElement) {
|
|
|
|
|
|
var domWorkspaceId = (fromDom.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (domWorkspaceId) return domWorkspaceId;
|
|
|
|
|
|
}
|
|
|
|
|
|
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentWorkspaceId() {
|
|
|
|
|
|
return resolveWorkspaceId(document.body);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentSourceKind() {
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var fromUrl = (params.get('sourceKind') || '').trim();
|
|
|
|
|
|
if (fromUrl) return fromUrl;
|
|
|
|
|
|
var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-source-kind') || '').trim() : '';
|
|
|
|
|
|
if (fromBody) return fromBody;
|
|
|
|
|
|
if (
|
|
|
|
|
|
window.location.pathname === '/' &&
|
|
|
|
|
|
!(params.get('workspaceId') || '').trim() &&
|
|
|
|
|
|
!(params.get('rootUri') || '').trim() &&
|
|
|
|
|
|
!(params.get('pageId') || '').trim()
|
|
|
|
|
|
) {
|
|
|
|
|
|
return 'local_folder';
|
|
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
return 'local_folder';
|
2026-05-26 01:31:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentRootUri() {
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var fromUrl = (params.get('rootUri') || '').trim();
|
|
|
|
|
|
if (fromUrl) return fromUrl;
|
|
|
|
|
|
return document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').trim() : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rememberCloudWorkspaceId(workspaceId) {
|
|
|
|
|
|
var normalized = String(workspaceId || '').trim();
|
|
|
|
|
|
if (!normalized || normalized === 'local-folder' || normalized === 'default' || normalized.indexOf('local-ws:') === 0) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (window.localStorage) window.localStorage.setItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY, normalized);
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readCurrentCloudWorkspaceIdFromPage() {
|
|
|
|
|
|
if (currentSourceKind() === 'local_folder') return '';
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var fromUrl = (params.get('workspaceId') || '').trim();
|
|
|
|
|
|
if (fromUrl && fromUrl !== 'default' && fromUrl.indexOf('local-ws:') !== 0) return fromUrl;
|
|
|
|
|
|
var fromDom = document.querySelector('[data-workspace-id]');
|
|
|
|
|
|
if (fromDom instanceof HTMLElement) {
|
|
|
|
|
|
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (value && value !== 'local-folder' && value !== 'default' && value.indexOf('local-ws:') !== 0) return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rememberCurrentCloudWorkspaceId() {
|
|
|
|
|
|
rememberCloudWorkspaceId(readCurrentCloudWorkspaceIdFromPage());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readLastCloudWorkspaceId() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var stored = window.localStorage ? window.localStorage.getItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY) : '';
|
|
|
|
|
|
if (stored && stored.trim() && stored.trim() !== 'default' && stored.trim().indexOf('local-ws:') !== 0) return stored.trim();
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var fromUrl = (params.get('workspaceId') || '').trim();
|
|
|
|
|
|
if (fromUrl && currentSourceKind() !== 'local_folder' && fromUrl.indexOf('local-ws:') !== 0) return fromUrl;
|
|
|
|
|
|
var fromDom = document.querySelector('[data-workspace-id]');
|
|
|
|
|
|
if (fromDom instanceof HTMLElement) {
|
|
|
|
|
|
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
|
|
|
|
|
|
if (value && value !== 'local-folder' && value.indexOf('local-ws:') !== 0) return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function copyWorkspaceSourceParams(targetUrl) {
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
2026-05-27 11:31:12 +08:00
|
|
|
|
['sourceKind', 'rootUri', 'fileTreeScope', 'secondaryDocumentId', 'secondarySourceKind', 'secondaryRootUri'].forEach(function(name) {
|
2026-05-26 01:31:28 +08:00
|
|
|
|
var value = (params.get(name) || '').trim();
|
|
|
|
|
|
if (value) targetUrl.searchParams.set(name, value);
|
|
|
|
|
|
});
|
|
|
|
|
|
var sourceKind = currentSourceKind();
|
|
|
|
|
|
var rootUri = currentRootUri();
|
|
|
|
|
|
if (sourceKind && !targetUrl.searchParams.get('sourceKind')) targetUrl.searchParams.set('sourceKind', sourceKind);
|
|
|
|
|
|
if (rootUri && !targetUrl.searchParams.get('rootUri')) targetUrl.searchParams.set('rootUri', rootUri);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentWorkspaceSourcePayload() {
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
var payload = {};
|
|
|
|
|
|
['sourceKind', 'rootUri'].forEach(function(name) {
|
|
|
|
|
|
var value = (params.get(name) || '').trim();
|
|
|
|
|
|
if (value) payload[name] = value;
|
|
|
|
|
|
});
|
|
|
|
|
|
var sourceKind = currentSourceKind();
|
|
|
|
|
|
var rootUri = currentRootUri();
|
|
|
|
|
|
if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind;
|
|
|
|
|
|
if (rootUri && !payload.rootUri) payload.rootUri = rootUri;
|
|
|
|
|
|
return payload;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentActorStorageId() {
|
|
|
|
|
|
var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-actor-id') || '').trim() : '';
|
|
|
|
|
|
if (fromBody && fromBody !== 'anonymous') return fromBody;
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function recentLocalRootsStorageKey() {
|
|
|
|
|
|
var actorId = currentActorStorageId();
|
|
|
|
|
|
return actorId ? MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX + encodeURIComponent(actorId) : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readRecentLocalRoots() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var storageKey = recentLocalRootsStorageKey();
|
|
|
|
|
|
if (!storageKey) return [];
|
|
|
|
|
|
var raw = window.localStorage ? window.localStorage.getItem(storageKey) : '';
|
|
|
|
|
|
var parsed = raw ? JSON.parse(raw) : [];
|
|
|
|
|
|
return Array.isArray(parsed) ? parsed.filter(function(value) {
|
|
|
|
|
|
return typeof value === 'string' && value.trim();
|
|
|
|
|
|
}).slice(0, 10) : [];
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function rememberLocalRoot(rootUri) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!window.localStorage) return;
|
|
|
|
|
|
var storageKey = recentLocalRootsStorageKey();
|
|
|
|
|
|
if (!storageKey) return;
|
|
|
|
|
|
var roots = readRecentLocalRoots().filter(function(value) { return value !== rootUri; });
|
|
|
|
|
|
roots.unshift(rootUri);
|
|
|
|
|
|
window.localStorage.setItem(storageKey, JSON.stringify(roots.slice(0, 10)));
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pathToFileRootUri(value) {
|
|
|
|
|
|
var trimmed = String(value || '').trim();
|
|
|
|
|
|
if (!trimmed) return '';
|
|
|
|
|
|
if (/^file:\/\//i.test(trimmed)) return trimmed;
|
|
|
|
|
|
if (trimmed.charAt(0) !== '/') return '';
|
|
|
|
|
|
return 'file://' + trimmed.split('/').map(function(part, index) {
|
|
|
|
|
|
return index === 0 ? '' : encodeURIComponent(part);
|
|
|
|
|
|
}).join('/');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fileRootUriToPathInput(rootUri) {
|
|
|
|
|
|
var value = String(rootUri || '').replace(/^file:\/\//, '');
|
|
|
|
|
|
try {
|
|
|
|
|
|
return decodeURIComponent(value);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function recentLocalRootLabel(rootUri) {
|
|
|
|
|
|
var label = fileRootUriToPathInput(rootUri);
|
|
|
|
|
|
return label || '本地文件夹';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeGrantedLocalFolderRootUri(grant) {
|
|
|
|
|
|
var rootUri = String(grant && grant.rootUri || '').trim();
|
|
|
|
|
|
if (rootUri && /^file:\/\//i.test(rootUri)) return rootUri;
|
|
|
|
|
|
var rootPath = String(grant && grant.rootPath || '').trim();
|
|
|
|
|
|
if (rootPath) return pathToFileRootUri(rootPath);
|
|
|
|
|
|
if (rootUri) return pathToFileRootUri(rootUri);
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isDefaultWorkspaceAutoGrant(grant) {
|
|
|
|
|
|
var source = String(grant && grant.source || '').trim();
|
|
|
|
|
|
var workspaceId = String(grant && grant.workspaceId || '').trim();
|
|
|
|
|
|
var rootUri = String(grant && grant.rootUri || '').trim();
|
|
|
|
|
|
var permission = String(grant && grant.permission || '').trim();
|
|
|
|
|
|
var createdBy = String(grant && (grant.createdBy || grant.ownerUserId) || '').trim();
|
|
|
|
|
|
var targetUser = String(grant && (grant.userId || grant.targetUserId) || '').trim();
|
|
|
|
|
|
return source === 'auto'
|
|
|
|
|
|
&& workspaceId
|
|
|
|
|
|
&& permission === 'write'
|
|
|
|
|
|
&& createdBy === targetUser
|
|
|
|
|
|
&& /^local:\/\/users\/.+\/workspaces\/my-space$/.test(rootUri);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openLocalFolderRoot(rootUri) {
|
|
|
|
|
|
if (currentSourceKind() !== 'local_folder') {
|
|
|
|
|
|
rememberCurrentCloudWorkspaceId();
|
|
|
|
|
|
}
|
|
|
|
|
|
rememberLocalRoot(rootUri);
|
|
|
|
|
|
var targetUrl = new URL('/', window.location.origin);
|
|
|
|
|
|
targetUrl.searchParams.set('treeView', 'filetree');
|
|
|
|
|
|
targetUrl.searchParams.set('sourceKind', 'local_folder');
|
|
|
|
|
|
targetUrl.searchParams.set('rootUri', rootUri);
|
|
|
|
|
|
window.location.href = targetUrl.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchToCloudWorkspace() {
|
|
|
|
|
|
var targetUrl = new URL('/', window.location.origin);
|
|
|
|
|
|
targetUrl.searchParams.set('treeView', activeSidebarTreeMode() || 'filetree');
|
|
|
|
|
|
targetUrl.searchParams.set('mnoteHome', '1');
|
|
|
|
|
|
window.location.href = targetUrl.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeTrashModal() {
|
|
|
|
|
|
var modal = document.querySelector('[data-testid="mnote-trash-modal"]');
|
|
|
|
|
|
if (!modal) return;
|
|
|
|
|
|
var workbench = modal.querySelector('[data-testid="mnote-trash-workbench"]');
|
|
|
|
|
|
if (workbench && workbench.__mnoteTrashEventSource && typeof workbench.__mnoteTrashEventSource.close === 'function') {
|
|
|
|
|
|
workbench.__mnoteTrashEventSource.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
var fileScrollTop = Number(modal.getAttribute('data-file-scroll-top') || '0');
|
|
|
|
|
|
var fileRoot = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
|
if (fileRoot instanceof HTMLElement && Number.isFinite(fileScrollTop)) fileRoot.scrollTop = fileScrollTop;
|
|
|
|
|
|
if (modal.parentElement) modal.parentElement.removeChild(modal);
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-trash-modal-open');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function executeTrashWorkbenchScripts(parsed) {
|
|
|
|
|
|
parsed.querySelectorAll('script').forEach(function(script) {
|
|
|
|
|
|
var nextScript = document.createElement('script');
|
|
|
|
|
|
Array.from(script.attributes || []).forEach(function(attr) {
|
|
|
|
|
|
nextScript.setAttribute(attr.name, attr.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
nextScript.textContent = script.textContent || '';
|
|
|
|
|
|
document.body.appendChild(nextScript);
|
|
|
|
|
|
if (nextScript.parentElement) nextScript.parentElement.removeChild(nextScript);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderLocalFolderTrashPlaceholder(content, rootUri) {
|
|
|
|
|
|
content.innerHTML = [
|
|
|
|
|
|
'<section class="mnote-trash-workbench" data-testid="mnote-trash-workbench" data-trash-source-kind="local_folder">',
|
|
|
|
|
|
'<header class="mnote-trash-header"><h1>本地文件夹垃圾箱</h1>',
|
|
|
|
|
|
'<p>本地删除项保存在当前目录的 <code>.mnote/trash</code> 与 <code>.mnote/trash-index.json</code> 中。</p></header>',
|
|
|
|
|
|
'<section class="mnote-trash-section"><div class="mnote-trash-empty">当前弹窗已保持在本地文件夹上下文:' + escapeHtml(rootUri || '未选择本地目录') + '</div></section>',
|
|
|
|
|
|
'</section>'
|
|
|
|
|
|
].join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openTrashModal(trigger) {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="mnote-trash-modal"]');
|
|
|
|
|
|
if (existing) return;
|
|
|
|
|
|
var fileRoot = document.getElementById('sidebar-file-tree-root');
|
|
|
|
|
|
var activeRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-selected="true"], #sidebar-tree-root .tree-row[data-active="true"]');
|
|
|
|
|
|
var overlay = document.createElement('div');
|
|
|
|
|
|
overlay.className = 'mnote-trash-modal';
|
|
|
|
|
|
overlay.setAttribute('data-testid', 'mnote-trash-modal');
|
|
|
|
|
|
overlay.setAttribute('role', 'dialog');
|
|
|
|
|
|
overlay.setAttribute('aria-modal', 'true');
|
|
|
|
|
|
overlay.setAttribute('aria-label', '垃圾箱');
|
|
|
|
|
|
overlay.setAttribute('data-file-scroll-top', fileRoot instanceof HTMLElement ? String(fileRoot.scrollTop) : '0');
|
|
|
|
|
|
if (activeRow instanceof HTMLElement) {
|
|
|
|
|
|
overlay.setAttribute('data-active-row-id', activeRow.getAttribute('data-row-id') || activeRow.getAttribute('data-node-id') || '');
|
|
|
|
|
|
}
|
|
|
|
|
|
overlay.innerHTML = '<div class="mnote-trash-modal__backdrop" data-mnote-trash-modal-close="true"></div>' +
|
|
|
|
|
|
'<section class="mnote-trash-modal__panel">' +
|
|
|
|
|
|
'<button type="button" class="mnote-trash-modal__close" data-testid="mnote-trash-modal-close" data-mnote-trash-modal-close="true" aria-label="关闭垃圾箱">×</button>' +
|
|
|
|
|
|
'<div class="mnote-trash-modal__content" data-testid="mnote-trash-modal-content"><div class="mnote-trash-empty">正在加载垃圾箱...</div></div>' +
|
|
|
|
|
|
'</section>';
|
|
|
|
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-trash-modal-open', 'true');
|
|
|
|
|
|
var content = overlay.querySelector('[data-testid="mnote-trash-modal-content"]');
|
|
|
|
|
|
var sourceKind = currentSourceKind();
|
|
|
|
|
|
var workspaceId = resolveWorkspaceId(trigger || document.body);
|
|
|
|
|
|
var url = new URL('/trash', window.location.origin);
|
|
|
|
|
|
if (sourceKind === 'local_folder') {
|
|
|
|
|
|
var rootUri = currentRootUri();
|
|
|
|
|
|
if (!rootUri) {
|
|
|
|
|
|
renderLocalFolderTrashPlaceholder(content, '');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
url.searchParams.set('sourceKind', 'local_folder');
|
|
|
|
|
|
url.searchParams.set('rootUri', rootUri);
|
|
|
|
|
|
} else if (workspaceId) {
|
|
|
|
|
|
url.searchParams.set('workspaceId', workspaceId);
|
|
|
|
|
|
}
|
|
|
|
|
|
fetch(url.toString(), { headers: { 'x-mnote-trash-modal': '1' } }).then(function(response) {
|
|
|
|
|
|
return response.text().then(function(html) {
|
|
|
|
|
|
if (!response.ok) throw new Error('trash_modal_load_failed_' + response.status);
|
|
|
|
|
|
var parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
|
|
|
|
var workbench = parsed.querySelector('[data-testid="mnote-trash-workbench"]');
|
|
|
|
|
|
if (!workbench) throw new Error('trash_modal_missing_workbench');
|
|
|
|
|
|
content.innerHTML = '';
|
|
|
|
|
|
content.appendChild(workbench);
|
|
|
|
|
|
executeTrashWorkbenchScripts(parsed);
|
|
|
|
|
|
});
|
|
|
|
|
|
}).catch(function(error) {
|
|
|
|
|
|
content.innerHTML = '<div class="mnote-trash-empty" data-testid="mnote-trash-modal-error">' + escapeHtml(error && error.message ? error.message : String(error)) + '</div>';
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeLocalFolderDialog() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="mnote-local-folder-dialog"]');
|
|
|
|
|
|
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readPathFromDirectoryFiles(files) {
|
|
|
|
|
|
var first = files && files.length ? files[0] : null;
|
|
|
|
|
|
if (!first) return '';
|
|
|
|
|
|
var rawPath = typeof first.path === 'string' ? first.path : '';
|
|
|
|
|
|
var relative = typeof first.webkitRelativePath === 'string' ? first.webkitRelativePath : '';
|
|
|
|
|
|
if (rawPath && relative) {
|
|
|
|
|
|
var suffix = relative.split('/').filter(Boolean).join('/');
|
|
|
|
|
|
if (suffix && rawPath.endsWith(suffix)) {
|
|
|
|
|
|
return rawPath.slice(0, rawPath.length - suffix.length).replace(/[\/\\]$/, '');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (rawPath) return rawPath;
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function requestBrowserFolderChoice(statusNode) {
|
|
|
|
|
|
return new Promise(function(resolve) {
|
|
|
|
|
|
var input = document.createElement('input');
|
|
|
|
|
|
input.type = 'file';
|
|
|
|
|
|
input.multiple = true;
|
|
|
|
|
|
input.setAttribute('webkitdirectory', '');
|
|
|
|
|
|
input.setAttribute('directory', '');
|
|
|
|
|
|
input.style.position = 'fixed';
|
|
|
|
|
|
input.style.left = '-9999px';
|
|
|
|
|
|
input.addEventListener('change', function() {
|
|
|
|
|
|
var selectedPath = readPathFromDirectoryFiles(input.files || []);
|
|
|
|
|
|
if (input.parentElement) input.parentElement.removeChild(input);
|
|
|
|
|
|
if (!selectedPath && statusNode instanceof HTMLElement) {
|
|
|
|
|
|
statusNode.textContent = '当前浏览器没有暴露本机绝对路径,请在下方输入路径。';
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve(selectedPath);
|
|
|
|
|
|
}, { once: true });
|
|
|
|
|
|
document.body.appendChild(input);
|
|
|
|
|
|
input.click();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function requestNativeFolderChoice(statusNode) {
|
|
|
|
|
|
var desktopPicker = window.__mnoteDesktop && typeof window.__mnoteDesktop.selectLocalFolder === 'function'
|
|
|
|
|
|
? window.__mnoteDesktop.selectLocalFolder
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (desktopPicker) {
|
|
|
|
|
|
var selected = await desktopPicker();
|
|
|
|
|
|
return typeof selected === 'string' ? selected : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof window.showDirectoryPicker === 'function') {
|
|
|
|
|
|
var handle = await window.showDirectoryPicker({ mode: 'read' });
|
|
|
|
|
|
var handlePath = handle && (handle.path || handle.mnotePath || handle.nativePath);
|
|
|
|
|
|
if (typeof handlePath === 'string' && handlePath.trim()) return handlePath;
|
|
|
|
|
|
if (statusNode instanceof HTMLElement) {
|
|
|
|
|
|
statusNode.textContent = '已选择“' + (handle && handle.name ? handle.name : '文件夹') + '”,但浏览器没有暴露本机绝对路径,请在下方确认路径。';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
return requestBrowserFolderChoice(statusNode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openLocalFolderDialog(initialMessage) {
|
|
|
|
|
|
closeLocalFolderDialog();
|
|
|
|
|
|
var recent = readRecentLocalRoots();
|
|
|
|
|
|
var dialog = document.createElement('div');
|
|
|
|
|
|
dialog.className = 'mnote-local-folder-dialog';
|
|
|
|
|
|
dialog.setAttribute('data-testid', 'mnote-local-folder-dialog');
|
|
|
|
|
|
dialog.setAttribute('role', 'dialog');
|
|
|
|
|
|
dialog.setAttribute('aria-modal', 'true');
|
|
|
|
|
|
dialog.style.position = 'fixed';
|
|
|
|
|
|
dialog.style.inset = '0';
|
|
|
|
|
|
dialog.style.zIndex = '2147483646';
|
|
|
|
|
|
dialog.style.background = 'rgba(15, 23, 42, 0.28)';
|
|
|
|
|
|
dialog.style.display = 'flex';
|
|
|
|
|
|
dialog.style.alignItems = 'center';
|
|
|
|
|
|
dialog.style.justifyContent = 'center';
|
|
|
|
|
|
var card = document.createElement('div');
|
|
|
|
|
|
card.className = 'mnote-local-folder-dialog__card';
|
|
|
|
|
|
card.style.width = 'min(640px, calc(100vw - 32px))';
|
|
|
|
|
|
card.style.background = '#fff';
|
|
|
|
|
|
card.style.border = '1px solid rgba(27, 28, 28, 0.12)';
|
|
|
|
|
|
card.style.borderRadius = '8px';
|
|
|
|
|
|
card.style.padding = '16px';
|
|
|
|
|
|
card.style.boxShadow = '0 18px 48px rgba(15, 23, 42, 0.22)';
|
|
|
|
|
|
card.style.display = 'grid';
|
|
|
|
|
|
card.style.gap = '12px';
|
|
|
|
|
|
var title = document.createElement('h2');
|
|
|
|
|
|
title.textContent = '打开本地文件夹';
|
|
|
|
|
|
title.style.margin = '0';
|
|
|
|
|
|
title.style.fontSize = '18px';
|
|
|
|
|
|
title.style.lineHeight = '1.4';
|
|
|
|
|
|
var status = document.createElement('p');
|
|
|
|
|
|
status.className = 'mnote-local-folder-dialog__status';
|
|
|
|
|
|
status.setAttribute('data-testid', 'mnote-local-folder-status');
|
|
|
|
|
|
status.textContent = initialMessage || '选择一个本机文件夹,或输入绝对路径。';
|
|
|
|
|
|
status.style.margin = '0';
|
|
|
|
|
|
status.style.color = '#4b5563';
|
|
|
|
|
|
status.style.fontSize = '13px';
|
|
|
|
|
|
var input = document.createElement('input');
|
|
|
|
|
|
input.type = 'text';
|
|
|
|
|
|
input.autocomplete = 'off';
|
|
|
|
|
|
input.spellcheck = false;
|
|
|
|
|
|
input.placeholder = '/mnt/Data1T/mnote/design/04-tree-domain/done';
|
|
|
|
|
|
input.setAttribute('data-testid', 'mnote-local-folder-path-input');
|
|
|
|
|
|
input.value = recent.length ? fileRootUriToPathInput(recent[0]) : '';
|
|
|
|
|
|
input.style.width = '100%';
|
|
|
|
|
|
input.style.boxSizing = 'border-box';
|
|
|
|
|
|
input.style.border = '1px solid rgba(27, 28, 28, 0.18)';
|
|
|
|
|
|
input.style.borderRadius = '6px';
|
|
|
|
|
|
input.style.padding = '10px 12px';
|
|
|
|
|
|
input.style.fontSize = '14px';
|
|
|
|
|
|
var actions = document.createElement('div');
|
|
|
|
|
|
actions.className = 'mnote-local-folder-dialog__actions';
|
|
|
|
|
|
actions.style.display = 'flex';
|
|
|
|
|
|
actions.style.gap = '8px';
|
|
|
|
|
|
actions.style.justifyContent = 'flex-end';
|
|
|
|
|
|
var choose = document.createElement('button');
|
|
|
|
|
|
choose.type = 'button';
|
|
|
|
|
|
choose.textContent = '选择文件夹';
|
|
|
|
|
|
choose.setAttribute('data-testid', 'mnote-local-folder-native-picker');
|
|
|
|
|
|
var cancel = document.createElement('button');
|
|
|
|
|
|
cancel.type = 'button';
|
|
|
|
|
|
cancel.textContent = '取消';
|
|
|
|
|
|
var confirm = document.createElement('button');
|
|
|
|
|
|
confirm.type = 'button';
|
|
|
|
|
|
confirm.textContent = '打开';
|
|
|
|
|
|
confirm.setAttribute('data-testid', 'mnote-local-folder-open-confirm');
|
|
|
|
|
|
[choose, cancel, confirm].forEach(function(button) {
|
|
|
|
|
|
button.style.border = '1px solid rgba(27, 28, 28, 0.14)';
|
|
|
|
|
|
button.style.borderRadius = '6px';
|
|
|
|
|
|
button.style.padding = '8px 12px';
|
|
|
|
|
|
button.style.background = '#fff';
|
|
|
|
|
|
button.style.cursor = 'pointer';
|
|
|
|
|
|
button.style.fontSize = '14px';
|
|
|
|
|
|
});
|
|
|
|
|
|
function submit() {
|
|
|
|
|
|
var rootUri = pathToFileRootUri(input.value);
|
|
|
|
|
|
if (!rootUri) {
|
|
|
|
|
|
status.textContent = '请输入以 / 开头的绝对路径,或 file:// URI。';
|
|
|
|
|
|
input.focus();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
closeLocalFolderDialog();
|
|
|
|
|
|
openLocalFolderRoot(rootUri);
|
|
|
|
|
|
}
|
|
|
|
|
|
choose.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
requestNativeFolderChoice(status).then(function(selectedPath) {
|
|
|
|
|
|
if (selectedPath) {
|
|
|
|
|
|
input.value = selectedPath;
|
|
|
|
|
|
submit();
|
|
|
|
|
|
}
|
|
|
|
|
|
}).catch(function(error) {
|
|
|
|
|
|
status.textContent = error && error.name === 'AbortError'
|
|
|
|
|
|
? '已取消选择。'
|
|
|
|
|
|
: '无法打开系统文件夹选择器,请输入路径。';
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
cancel.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeLocalFolderDialog();
|
|
|
|
|
|
});
|
|
|
|
|
|
confirm.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
submit();
|
|
|
|
|
|
});
|
|
|
|
|
|
input.addEventListener('keydown', function(event) {
|
|
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
submit();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
actions.appendChild(choose);
|
|
|
|
|
|
actions.appendChild(cancel);
|
|
|
|
|
|
actions.appendChild(confirm);
|
|
|
|
|
|
card.appendChild(title);
|
|
|
|
|
|
card.appendChild(status);
|
|
|
|
|
|
card.appendChild(input);
|
|
|
|
|
|
var authorizedSection = document.createElement('div');
|
|
|
|
|
|
authorizedSection.className = 'mnote-local-folder-dialog__authorized';
|
|
|
|
|
|
authorizedSection.setAttribute('data-testid', 'mnote-local-folder-authorized-roots');
|
|
|
|
|
|
authorizedSection.hidden = true;
|
|
|
|
|
|
card.appendChild(authorizedSection);
|
|
|
|
|
|
fetch('/api/user/access-policy', {
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
headers: { 'accept': 'application/json' },
|
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
|
}).then(function(response) {
|
|
|
|
|
|
return response.ok ? response.json() : null;
|
|
|
|
|
|
}).then(function(payload) {
|
|
|
|
|
|
var grants = payload && Array.isArray(payload.grants) ? payload.grants : [];
|
|
|
|
|
|
var activeGrants = grants.filter(function(grant) {
|
|
|
|
|
|
return grant && grant.active !== false && grant.status !== 'revoked';
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!activeGrants.length) return;
|
|
|
|
|
|
authorizedSection.hidden = false;
|
|
|
|
|
|
var authorizedTitle = document.createElement('div');
|
|
|
|
|
|
authorizedTitle.style.fontSize = '12px';
|
|
|
|
|
|
authorizedTitle.style.color = '#6b7280';
|
|
|
|
|
|
authorizedTitle.textContent = '已授权文件夹';
|
|
|
|
|
|
var authorizedList = document.createElement('div');
|
|
|
|
|
|
authorizedList.className = 'mnote-local-folder-dialog__recent';
|
|
|
|
|
|
activeGrants.slice(0, 8).forEach(function(grant) {
|
|
|
|
|
|
if (isDefaultWorkspaceAutoGrant(grant)) return;
|
|
|
|
|
|
var rootUri = normalizeGrantedLocalFolderRootUri(grant);
|
|
|
|
|
|
if (!rootUri) return;
|
|
|
|
|
|
var button = document.createElement('button');
|
|
|
|
|
|
button.type = 'button';
|
|
|
|
|
|
button.setAttribute('data-testid', 'mnote-local-folder-authorized-root');
|
|
|
|
|
|
button.textContent = fileRootUriToPathInput(rootUri);
|
|
|
|
|
|
button.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeLocalFolderDialog();
|
|
|
|
|
|
openLocalFolderRoot(rootUri);
|
|
|
|
|
|
});
|
|
|
|
|
|
authorizedList.appendChild(button);
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!authorizedList.childElementCount) return;
|
|
|
|
|
|
authorizedSection.appendChild(authorizedTitle);
|
|
|
|
|
|
authorizedSection.appendChild(authorizedList);
|
|
|
|
|
|
}).catch(function() {});
|
|
|
|
|
|
if (recent.length) {
|
|
|
|
|
|
var recentTitle = document.createElement('div');
|
|
|
|
|
|
recentTitle.style.fontSize = '12px';
|
|
|
|
|
|
recentTitle.style.color = '#6b7280';
|
|
|
|
|
|
recentTitle.textContent = '最近使用';
|
|
|
|
|
|
card.appendChild(recentTitle);
|
|
|
|
|
|
var recentList = document.createElement('div');
|
|
|
|
|
|
recentList.className = 'mnote-local-folder-dialog__recent';
|
|
|
|
|
|
recent.slice(0, 5).forEach(function(rootUri) {
|
|
|
|
|
|
var button = document.createElement('button');
|
|
|
|
|
|
button.type = 'button';
|
|
|
|
|
|
button.textContent = recentLocalRootLabel(rootUri);
|
|
|
|
|
|
button.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeLocalFolderDialog();
|
|
|
|
|
|
openLocalFolderRoot(rootUri);
|
|
|
|
|
|
});
|
|
|
|
|
|
recentList.appendChild(button);
|
|
|
|
|
|
});
|
|
|
|
|
|
card.appendChild(recentList);
|
|
|
|
|
|
}
|
|
|
|
|
|
card.appendChild(actions);
|
|
|
|
|
|
dialog.appendChild(card);
|
|
|
|
|
|
dialog.addEventListener('click', function(event) {
|
|
|
|
|
|
if (event.target === dialog) closeLocalFolderDialog();
|
|
|
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(dialog);
|
|
|
|
|
|
input.focus();
|
|
|
|
|
|
input.select();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function requestOpenLocalFolder() {
|
|
|
|
|
|
openLocalFolderDialog('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createDefaultLocalWorkspace(trigger) {
|
|
|
|
|
|
setCommandPending(trigger, true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/local-folder/workspaces/default', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
body: '{}'
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json().catch(function() { return null; });
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error((payload && payload.message) || ('local_workspace_create_failed_' + response.status));
|
|
|
|
|
|
}
|
|
|
|
|
|
var workspace = payload && payload.workspace || {};
|
|
|
|
|
|
var rootUri = String(workspace.rootUri || '').trim();
|
|
|
|
|
|
if (!rootUri) throw new Error('local_workspace_create_missing_root_uri');
|
|
|
|
|
|
openLocalFolderRoot(rootUri);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
var status = document.querySelector('[data-testid="mnote-local-folder-status"]');
|
|
|
|
|
|
if (status instanceof HTMLElement) {
|
|
|
|
|
|
status.textContent = error && error.message ? error.message : String(error);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
openLocalFolderDialog(error && error.message ? error.message : String(error));
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setCommandPending(trigger, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function autoOpenRecentLocalRootOnHome() {
|
|
|
|
|
|
if (window.location.pathname !== '/') return false;
|
|
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
if ((params.get('sourceKind') || '').trim()) return false;
|
|
|
|
|
|
if ((params.get('rootUri') || '').trim()) return false;
|
|
|
|
|
|
if ((params.get('workspaceId') || '').trim()) return false;
|
|
|
|
|
|
if ((params.get('pageId') || '').trim()) return false;
|
|
|
|
|
|
if ((params.get('mnoteHome') || '').trim() === '1') return false;
|
|
|
|
|
|
var recent = readRecentLocalRoots();
|
|
|
|
|
|
if (!recent.length) return false;
|
|
|
|
|
|
openLocalFolderRoot(recent[0]);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeWorkspaceSourceMenu() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
|
|
|
|
|
|
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
|
|
|
|
|
document.querySelectorAll('[data-testid="mnote-workspace-source-trigger"]').forEach(function(trigger) {
|
|
|
|
|
|
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function workspaceSourceLabel(rootUri) {
|
|
|
|
|
|
var label = String(rootUri || '').replace(/^file:\/\//, '');
|
|
|
|
|
|
try {
|
|
|
|
|
|
label = decodeURIComponent(label);
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
return label || '本地文件夹';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openWorkspaceSourceMenu(trigger) {
|
|
|
|
|
|
closeWorkspaceSourceMenu();
|
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|
|
|
|
|
var menu = document.createElement('div');
|
|
|
|
|
|
menu.className = 'mnote-workspace-source-menu';
|
|
|
|
|
|
menu.setAttribute('data-testid', 'mnote-workspace-source-menu');
|
|
|
|
|
|
menu.setAttribute('role', 'menu');
|
|
|
|
|
|
var cloudButton = document.createElement('button');
|
|
|
|
|
|
cloudButton.type = 'button';
|
|
|
|
|
|
cloudButton.className = 'mnote-workspace-source-menu__item';
|
|
|
|
|
|
cloudButton.setAttribute('data-testid', 'mnote-switch-cloud-workspace');
|
|
|
|
|
|
cloudButton.setAttribute('role', 'menuitem');
|
|
|
|
|
|
cloudButton.textContent = '我的空间';
|
|
|
|
|
|
cloudButton.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeWorkspaceSourceMenu();
|
|
|
|
|
|
switchToCloudWorkspace();
|
|
|
|
|
|
});
|
|
|
|
|
|
menu.appendChild(cloudButton);
|
|
|
|
|
|
var recent = readRecentLocalRoots();
|
|
|
|
|
|
if (recent.length) {
|
|
|
|
|
|
var label = document.createElement('div');
|
|
|
|
|
|
label.className = 'mnote-workspace-source-menu__label';
|
|
|
|
|
|
label.textContent = '最近本地文件夹';
|
|
|
|
|
|
menu.appendChild(label);
|
|
|
|
|
|
recent.slice(0, 8).forEach(function(rootUri) {
|
|
|
|
|
|
var button = document.createElement('button');
|
|
|
|
|
|
button.type = 'button';
|
|
|
|
|
|
button.className = 'mnote-workspace-source-menu__item';
|
|
|
|
|
|
button.setAttribute('data-testid', 'mnote-recent-local-root');
|
|
|
|
|
|
button.setAttribute('data-root-uri', rootUri);
|
|
|
|
|
|
button.setAttribute('role', 'menuitem');
|
|
|
|
|
|
button.textContent = workspaceSourceLabel(rootUri);
|
|
|
|
|
|
button.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeWorkspaceSourceMenu();
|
|
|
|
|
|
openLocalFolderRoot(rootUri);
|
|
|
|
|
|
});
|
|
|
|
|
|
menu.appendChild(button);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
var openOther = document.createElement('button');
|
|
|
|
|
|
openOther.type = 'button';
|
|
|
|
|
|
openOther.className = 'mnote-workspace-source-menu__item mnote-workspace-source-menu__item--primary';
|
|
|
|
|
|
openOther.setAttribute('data-testid', 'mnote-open-other-local-folder');
|
|
|
|
|
|
openOther.setAttribute('role', 'menuitem');
|
|
|
|
|
|
openOther.textContent = '打开其他本地文件夹';
|
|
|
|
|
|
openOther.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeWorkspaceSourceMenu();
|
|
|
|
|
|
requestOpenLocalFolder();
|
|
|
|
|
|
});
|
|
|
|
|
|
menu.appendChild(openOther);
|
|
|
|
|
|
trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAccountMenu() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="mnote-account-menu"]');
|
|
|
|
|
|
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
|
|
|
|
|
document.querySelectorAll('[data-testid="mnote-account-menu-trigger"]').forEach(function(trigger) {
|
|
|
|
|
|
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeProfileDialog() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="mnote-profile-dialog"]');
|
|
|
|
|
|
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeAdminAccessPolicyDialog() {
|
|
|
|
|
|
var existing = document.querySelector('[data-testid="mnote-admin-access-policy-modal"]');
|
|
|
|
|
|
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-admin-access-policy-modal-open');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function accountMenuFallbackSession() {
|
|
|
|
|
|
var body = document.body instanceof HTMLElement ? document.body : null;
|
|
|
|
|
|
var actorId = body ? String(body.getAttribute('data-mnote-actor-id') || '').trim() : '';
|
|
|
|
|
|
return {
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
name: actorId || '当前用户',
|
|
|
|
|
|
email: '',
|
|
|
|
|
|
userId: actorId || 'anonymous',
|
|
|
|
|
|
actorType: actorId && actorId !== 'anonymous' ? 'user' : 'anonymous',
|
|
|
|
|
|
authMode: 'browser'
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAccountInfo(menu, session) {
|
|
|
|
|
|
var nameNode = menu.querySelector('[data-account-info="name"]');
|
|
|
|
|
|
var emailNode = menu.querySelector('[data-account-info="email"]');
|
|
|
|
|
|
var idNode = menu.querySelector('[data-account-info="user-id"]');
|
|
|
|
|
|
var typeNode = menu.querySelector('[data-account-info="actor-type"]');
|
|
|
|
|
|
if (nameNode) nameNode.textContent = session.name || session.userId || '当前用户';
|
|
|
|
|
|
if (emailNode) emailNode.textContent = session.email || '未提供邮箱';
|
|
|
|
|
|
if (idNode) {
|
|
|
|
|
|
idNode.textContent = session.userId || 'anonymous';
|
|
|
|
|
|
idNode.setAttribute('title', session.userId || 'anonymous');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeNode) typeNode.textContent = session.actorType || session.authMode || 'unknown';
|
|
|
|
|
|
var copyButton = menu.querySelector('[data-account-copy-user-id]');
|
|
|
|
|
|
if (copyButton instanceof HTMLElement) {
|
|
|
|
|
|
copyButton.dataset.copyValue = session.userId || 'anonymous';
|
|
|
|
|
|
copyButton.setAttribute('title', '复制用户 ID');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sessionIsAdmin(session) {
|
|
|
|
|
|
return String(session && session.actorType || '').trim() === 'admin';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAccountInfo(menu) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/auth/session', {
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
headers: { 'accept': 'application/json' },
|
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json().catch(function() { return null; });
|
|
|
|
|
|
if (!response.ok || !payload) throw new Error('session_failed_' + response.status);
|
|
|
|
|
|
renderAccountInfo(menu, payload);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
renderAccountInfo(menu, accountMenuFallbackSession());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchAccountSession() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/auth/session', {
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
headers: { 'accept': 'application/json' },
|
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json().catch(function() { return null; });
|
|
|
|
|
|
if (!response.ok || !payload) throw new Error('session_failed_' + response.status);
|
|
|
|
|
|
return payload;
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return accountMenuFallbackSession();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openProfileDialog(session) {
|
|
|
|
|
|
closeProfileDialog();
|
|
|
|
|
|
closeAccountMenu();
|
|
|
|
|
|
var dialog = document.createElement('div');
|
|
|
|
|
|
dialog.className = 'mnote-profile-dialog';
|
|
|
|
|
|
dialog.setAttribute('data-testid', 'mnote-profile-dialog');
|
|
|
|
|
|
dialog.setAttribute('role', 'dialog');
|
|
|
|
|
|
dialog.setAttribute('aria-modal', 'true');
|
|
|
|
|
|
dialog.setAttribute('aria-labelledby', 'mnote-profile-dialog-title');
|
|
|
|
|
|
var userId = escapeHtml(session.userId || 'anonymous');
|
|
|
|
|
|
dialog.innerHTML =
|
|
|
|
|
|
'<div class="mnote-profile-dialog__backdrop" data-profile-dialog-close></div>' +
|
|
|
|
|
|
'<section class="mnote-profile-dialog__panel">' +
|
|
|
|
|
|
'<header class="mnote-profile-dialog__header">' +
|
|
|
|
|
|
'<div>' +
|
|
|
|
|
|
'<div class="mnote-profile-dialog__eyebrow">个人信息</div>' +
|
|
|
|
|
|
'<h2 id="mnote-profile-dialog-title">个人信息</h2>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<button type="button" class="mnote-profile-dialog__close" data-profile-dialog-close aria-label="关闭"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
|
|
|
|
|
|
'</header>' +
|
|
|
|
|
|
'<div class="mnote-profile-dialog__identity">' +
|
|
|
|
|
|
'<div class="mnote-profile-dialog__avatar">' + escapeHtml(String(session.name || session.email || session.userId || '用').slice(0, 1).toUpperCase()) + '</div>' +
|
|
|
|
|
|
'<div><strong>' + escapeHtml(session.name || session.userId || '当前用户') + '</strong><span>' + escapeHtml(session.email || '未提供邮箱') + '</span></div>' +
|
|
|
|
|
|
'</div>' +
|
|
|
|
|
|
'<dl class="mnote-profile-dialog__list">' +
|
|
|
|
|
|
'<div><dt>用户名</dt><dd>' + escapeHtml(session.name || '未设置') + '</dd></div>' +
|
|
|
|
|
|
'<div><dt>邮箱</dt><dd>' + escapeHtml(session.email || '未提供邮箱') + '</dd></div>' +
|
|
|
|
|
|
'<div><dt>用户 ID</dt><dd><code title="' + userId + '">' + userId + '</code><button type="button" data-profile-copy-user-id data-copy-value="' + userId + '"><span class="material-symbols-outlined" data-icon="content_copy" aria-hidden="true"></span></button></dd></div>' +
|
|
|
|
|
|
'<div><dt>身份</dt><dd>' + escapeHtml(session.actorType || session.authMode || 'unknown') + '</dd></div>' +
|
|
|
|
|
|
'</dl>' +
|
|
|
|
|
|
'</section>';
|
|
|
|
|
|
dialog.querySelectorAll('[data-profile-dialog-close]').forEach(function(button) {
|
|
|
|
|
|
button.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeProfileDialog();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
var copyButton = dialog.querySelector('[data-profile-copy-user-id]');
|
|
|
|
|
|
if (copyButton) {
|
|
|
|
|
|
copyButton.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
void copyTreeContextValue(copyButton.getAttribute('data-copy-value') || '', 'profile-user-id');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
document.body.appendChild(dialog);
|
|
|
|
|
|
var closeButton = dialog.querySelector('.mnote-profile-dialog__close');
|
|
|
|
|
|
if (closeButton instanceof HTMLElement) closeButton.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openAdminAccessPolicyDialog(trigger) {
|
|
|
|
|
|
closeAdminAccessPolicyDialog();
|
|
|
|
|
|
closeAccountMenu();
|
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
|
var role = trigger.getAttribute('data-access-policy-role') === 'admin' ? 'admin' : 'user';
|
|
|
|
|
|
var template = document.querySelector('[data-testid="mnote-admin-access-policy-template-' + role + '"]');
|
|
|
|
|
|
var dialog = document.createElement('div');
|
|
|
|
|
|
dialog.className = 'mnote-admin-policy-modal';
|
|
|
|
|
|
dialog.setAttribute('data-testid', 'mnote-admin-access-policy-modal');
|
|
|
|
|
|
dialog.setAttribute('role', 'dialog');
|
|
|
|
|
|
dialog.setAttribute('aria-modal', 'true');
|
|
|
|
|
|
dialog.setAttribute('aria-labelledby', 'mnote-admin-policy-dialog-title');
|
|
|
|
|
|
dialog.innerHTML =
|
|
|
|
|
|
'<div class="mnote-admin-policy-modal__backdrop" data-admin-access-policy-close></div>' +
|
|
|
|
|
|
'<section class="mnote-admin-policy-modal__panel">' +
|
|
|
|
|
|
'<button type="button" class="mnote-admin-policy-modal__close" data-admin-access-policy-close aria-label="关闭授权管理"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
|
|
|
|
|
|
'<div class="mnote-admin-policy-modal__content" data-testid="mnote-admin-access-policy-modal-content"></div>' +
|
|
|
|
|
|
'</section>';
|
|
|
|
|
|
var content = dialog.querySelector('[data-testid="mnote-admin-access-policy-modal-content"]');
|
|
|
|
|
|
if (content) {
|
|
|
|
|
|
content.innerHTML = template ? template.innerHTML : '<div class="mnote-admin-policy-empty">授权管理面板加载失败</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof window.MNOTEInitAccessPolicyPanel === 'function') {
|
|
|
|
|
|
window.MNOTEInitAccessPolicyPanel(dialog);
|
|
|
|
|
|
}
|
|
|
|
|
|
dialog.querySelectorAll('[data-admin-access-policy-close]').forEach(function(button) {
|
|
|
|
|
|
button.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
closeAdminAccessPolicyDialog();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(dialog);
|
|
|
|
|
|
document.documentElement.setAttribute('data-mnote-admin-access-policy-modal-open', 'true');
|
|
|
|
|
|
var closeButton = dialog.querySelector('.mnote-admin-policy-modal__close');
|
|
|
|
|
|
if (closeButton instanceof HTMLElement) closeButton.focus();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function signOutAccount(trigger) {
|
|
|
|
|
|
setCommandPending(trigger, true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
var response = await fetch('/api/auth', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
|
body: JSON.stringify({ action: 'auth:signOut', args: {} })
|
|
|
|
|
|
});
|
|
|
|
|
|
var payload = await response.json().catch(function() { return {}; });
|
|
|
|
|
|
payload = payload || {};
|
|
|
|
|
|
if (!response.ok || payload.error) {
|
|
|
|
|
|
throw new Error(payload.error || '退出登录失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
window.location.assign('/auth');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
var menu = document.querySelector('[data-testid="mnote-account-menu"]');
|
|
|
|
|
|
var errorNode = menu ? menu.querySelector('[data-account-error]') : null;
|
|
|
|
|
|
if (errorNode instanceof HTMLElement) {
|
|
|
|
|
|
errorNode.textContent = error && error.message ? error.message : '退出登录失败';
|
|
|
|
|
|
errorNode.hidden = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
setCommandPending(trigger, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openAccountMenu(trigger) {
|
|
|
|
|
|
closeAccountMenu();
|
|
|
|
|
|
if (!(trigger instanceof HTMLElement)) return;
|
|
|
|
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|
|
|
|
|
var menu = document.createElement('div');
|
|
|
|
|
|
menu.className = 'mnote-workspace-source-menu mnote-account-menu';
|
|
|
|
|
|
menu.setAttribute('data-testid', 'mnote-account-menu');
|
|
|
|
|
|
menu.setAttribute('role', 'menu');
|
|
|
|
|
|
menu.innerHTML =
|
|
|
|
|
|
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-profile" role="menuitem"><span class="material-symbols-outlined" data-icon="account_circle" aria-hidden="true"></span><span>个人信息</span></button>' +
|
|
|
|
|
|
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-access-policy" role="menuitem" hidden><span class="material-symbols-outlined" data-icon="admin_panel_settings" aria-hidden="true"></span><span>授权管理</span></button>' +
|
|
|
|
|
|
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__logout" data-testid="mnote-account-sign-out" role="menuitem">退出登录</button>' +
|
|
|
|
|
|
'<div class="mnote-account-menu__error" data-account-error hidden></div>';
|
|
|
|
|
|
var sessionPromise = fetchAccountSession();
|
|
|
|
|
|
var profileButton = menu.querySelector('[data-testid="mnote-account-profile"]');
|
|
|
|
|
|
if (profileButton) {
|
|
|
|
|
|
profileButton.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
sessionPromise.then(openProfileDialog);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
var accessPolicyLink = menu.querySelector('[data-testid="mnote-account-access-policy"]');
|
|
|
|
|
|
sessionPromise.then(function(session) {
|
|
|
|
|
|
if (!(accessPolicyLink instanceof HTMLElement)) return;
|
|
|
|
|
|
accessPolicyLink.hidden = false;
|
|
|
|
|
|
accessPolicyLink.setAttribute('data-access-policy-role', sessionIsAdmin(session) ? 'admin' : 'user');
|
|
|
|
|
|
});
|
|
|
|
|
|
if (accessPolicyLink) {
|
|
|
|
|
|
accessPolicyLink.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
openAdminAccessPolicyDialog(accessPolicyLink);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
var signOutButton = menu.querySelector('[data-testid="mnote-account-sign-out"]');
|
|
|
|
|
|
if (signOutButton) {
|
|
|
|
|
|
signOutButton.addEventListener('click', function(event) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
void signOutAccount(signOutButton);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
var host = trigger.closest('[data-testid="wolai-sidebar-quick-actions"]') || trigger.parentElement;
|
|
|
|
|
|
if (host) host.appendChild(menu);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
activeSidebarTreeMode,
|
|
|
|
|
|
autoOpenRecentLocalRootOnHome,
|
|
|
|
|
|
closeAccountMenu,
|
|
|
|
|
|
closeLocalFolderDialog,
|
|
|
|
|
|
closeTrashModal,
|
|
|
|
|
|
closeWorkspaceSourceMenu,
|
|
|
|
|
|
copyWorkspaceSourceParams,
|
|
|
|
|
|
createDefaultLocalWorkspace,
|
|
|
|
|
|
currentRootUri,
|
|
|
|
|
|
currentSourceKind,
|
|
|
|
|
|
currentWorkspaceId,
|
|
|
|
|
|
currentWorkspaceSourcePayload,
|
|
|
|
|
|
normalizeSidebarTreeMode,
|
|
|
|
|
|
openAccountMenu,
|
|
|
|
|
|
openTrashModal,
|
|
|
|
|
|
openWorkspaceSourceMenu,
|
|
|
|
|
|
persistSidebarTreeMode,
|
|
|
|
|
|
readStoredSidebarTreeMode,
|
|
|
|
|
|
requestOpenLocalFolder,
|
|
|
|
|
|
resolveWorkspaceId,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|