2026-07-20 14:30:44 +08:00

137 lines
5.3 KiB
TypeScript

/**
* 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<string, any>,
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)