/** * useApps — 应用注册表 + stdMenus 标准菜单模板 * 从 js/apps.js 原始逻辑转写,保留完整功能。 */ import { el } from '../utils' import { wm } from './useWM' import { ui } from './useUI' import { fs, setAppsRef } from './useFS' export const Apps = { registry: {} as Record, register(def: any) { def.w = def.w || 720; def.h = def.h || 480 def.minW = def.minW || 320; def.minH = def.minH || 240 def.about = def.about || `${def.name} — macOS 网页版内置应用` this.registry[def.id] = def }, get(id: string) { return this.registry[id] }, open(id: string, args?: any) { const app = this.get(id) if (!app) { console.warn('[apps] 未注册:', id); return null } if (app.singleton !== false) { const ex = wm.windowsForApp(id)[0] if (ex) { if (args && app.onArgs) { try { app.onArgs(args, ex) } catch (e) { console.error(e) } } wm.focus(ex); return ex } } const win = wm.openWindow({ app, title: app.name, icon: app.icon, w: app.w, h: app.h, minW: app.minW, minH: app.minH, data: args }) try { app.render(win, args || {}) } catch (e) { console.error('[apps] 渲染失败:', id, e) if (win.body) win.body.append(el('div', { class: 'empty-state' }, el('div', { class: 'es-icon', text: '⚠️' }), el('div', { text: `${app.name} 打开失败` }))) } return win }, quit(id: string) { wm.windowsForApp(id).slice().forEach((w: any) => wm.close(w)) }, openPath(path: string) { const n = fs.node(path) if (!n) { ui.alert('找不到项目', `"${fs.baseName(path)}"不存在。`, '/assets/icons/finder.png'); return } if (n.t === 'd') return this.open('finder', { path }) if (n.t === 'a') return this.open(n.app!) const ext = (path.split('.').pop() || '').toLowerCase() if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext) || ext === 'pdf') return this.open('preview', { path }) return this.open('textedit', { path }) }, } /** 标准菜单构造模板 */ export function stdMenus(app: any, { file = [], edit = [], view = [], format = [], store = [] }: any = {}) { const menus: any[] = [] menus.push({ label: '文件', items: () => [ ...file, file.length ? { sep: true } : null, { label: '关闭窗口', key: '⌘W', disabled: !wm.activeWin.value, action: () => wm.activeWin.value && wm.close(wm.activeWin.value) }, ].filter(Boolean) }) const editBase = () => { const a = document.activeElement as any const isText = a && (a.tagName === 'INPUT' || a.tagName === 'TEXTAREA' || a.isContentEditable) const doCmd = (cmd: string) => { if (!isText) return if (cmd === 'selectAll') { a.select ? a.select() : document.execCommand('selectAll'); return } if (cmd === 'cut' || cmd === 'copy') { const sel = a.value?.slice(a.selectionStart, a.selectionEnd) ?? '' if (cmd === 'copy' && sel) navigator.clipboard?.writeText(sel) if (cmd === 'cut' && sel) { navigator.clipboard?.writeText(sel); a.setRangeText('', a.selectionStart, a.selectionEnd, 'end'); a.dispatchEvent(new Event('input', { bubbles: true })) } } if (cmd === 'paste') navigator.clipboard?.readText().then((t: string) => { a.setRangeText(t, a.selectionStart, a.selectionEnd, 'end'); a.dispatchEvent(new Event('input', { bubbles: true })) }).catch(() => {}) } return [ ...edit, edit.length ? { sep: true } : null, { label: '剪切', key: '⌘X', disabled: !isText, action: () => doCmd('cut') }, { label: '拷贝', key: '⌘C', disabled: !isText, action: () => doCmd('copy') }, { label: '粘贴', key: '⌘V', disabled: !isText, action: () => doCmd('paste') }, { label: '全选', key: '⌘A', disabled: !isText, action: () => doCmd('selectAll') }, ].filter(Boolean) } menus.push({ label: '编辑', items: editBase }) menus.push({ label: '显示', items: () => [ ...view, view.length ? { sep: true } : null, { label: wm.activeWin.value?.state === 'fullscreen' ? '退出全屏' : '进入全屏', key: '⌃⌘F', disabled: !wm.activeWin.value, action: () => wm.activeWin.value && wm.toggleFullscreen(wm.activeWin.value) }, ].filter(Boolean) }) if (format.length) menus.push({ label: '格式', items: () => format }) if (store.length) menus.push({ label: '商店', items: () => store }) menus.push({ label: '窗口', items: () => { const wins = wm.windows return [ { label: '最小化', key: '⌘M', disabled: !wm.activeWin.value, action: () => wm.activeWin.value && wm.minimize(wm.activeWin.value) }, { label: '缩放', disabled: !wm.activeWin.value, action: () => wm.activeWin.value && wm.toggleZoom(wm.activeWin.value) }, { sep: true }, ...wins.map((w: any) => ({ label: w.title, checked: w === wm.activeWin.value, action: () => wm.focus(w) })), wins.length ? { sep: true } : null, { label: '全部前置', disabled: !wins.length, action: () => wins.forEach((w: any) => wm.restore(w)) }, ].filter(Boolean) } }) menus.push({ label: '帮助', items: () => [ { label: `${app.name}帮助`, action: () => ui.dialog({ icon: app.icon, title: `${app.name}帮助`, msg: app.about, buttons: ['好'] }) }, ] }) return menus } // Wire up FS reference to Apps setAppsRef(Apps, null)