137 lines
5.3 KiB
TypeScript
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)
|