/* ============ 系统层:设置 / 电源 / 菜单栏 / Dock / 控制中心 / 通知 / Spotlight / 屏保 ============ */ 'use strict'; const WALLPAPERS = [ { id: 'monterey', name: 'Monterey 抽象', src: 'assets/wallpapers/monterey.jpg' }, { id: 'sonoma', name: 'Sonoma 落日', src: 'assets/wallpapers/sonoma.jpg' }, { id: 'ventura', name: 'Ventura 霞光', src: 'assets/wallpapers/ventura.jpg' }, { id: 'bigsur', name: 'Big Sur 海岸', src: 'assets/wallpapers/big-sur-day.jpg', dark: 'assets/wallpapers/big-sur-night.jpg' }, { id: 'sequoia', name: 'Sequoia 山谷', src: 'assets/wallpapers/sequoia.jpg' }, { id: 'galaxy', name: '银河', src: 'assets/wallpapers/galaxy.jpg' }, ]; const DEFAULT_SETTINGS = { appearance: 'light', wallpaper: 'monterey', accent: '#0a84ff', reduceTransparency: false, reduceMotion: false, increaseContrast: false, dockSize: 54, dockMagnify: true, dockMagnifyLevel: 1.6, dockPosition: 'bottom', dockAutohide: false, brightness: 1, nightShift: false, nightShiftStrength: 0.4, volume: 0.6, muted: false, wifi: true, bluetooth: true, airdrop: false, focus: false, vpn: false, userName: '客人用户', avatar: 'assets/icons/avatar.svg', passwordEnabled: false, password: '', screensaverType: 'off', screensaverDelay: 5, h24: false, language: 'zh-Hans', region: '中国', firstDayMonday: true, searchEngine: 'bing', computerName: 'MacBook Pro', siriApps: true, siriFiles: true, siriSettings: true, loginItems: {}, notifAllow: {}, notificationsEnabled: true, // 通知总开关;notifAllow[appId]===false 时该 App 完全静默 timezone: 'local', // 'local' 或 IANA 时区名(如 Asia/Shanghai) wifiNetwork: '家庭网络 5G', // 已知网络的连接状态(模拟,但持久化) btDevices: {}, // 蓝牙设备连接状态(模拟,但持久化) kbRepeat: 7, kbDelay: 4, mouseSpeed: 5, // 键盘/鼠标参数(模拟,但持久化) finderView: 'icon', finderSort: 'name', }; const Sys = { settings: null, unlocked: false, fullscreenWin: null, activeApp: 'finder', mediaEls: new Set(), _dockHideT: null, _saverT: null, _clockT: null, init() { this.settings = Object.assign(structuredClone(DEFAULT_SETTINGS), Store.get('settings', {})); this.applyAll(); this.buildMenubar(); this.buildDock(); this.initIdleWatch(); addEventListener('resize', debounce(() => { WM.clampAll(); this.layoutDock(); }, 120)); matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this.applyAppearance()); document.addEventListener('keydown', e => this.globalKeys(e)); // 全屏时鼠标触及顶部唤出菜单栏 document.addEventListener('pointermove', e => { if (this.fullscreenWin) { $('#menubar').classList.toggle('autohide', e.clientY > 2 && !e.target.closest('#menubar')); } }); }, save() { Store.set('settings', this.settings); }, set(k, v) { this.settings[k] = v; this.save(); this.applyAll(); }, applyAll() { this.applyAppearance(); this.applyWallpaper(); this.applyBrightness(); this.applyNightShift(); this.applyVolume(); document.documentElement.style.setProperty('--accent', this.settings.accent); }, isDark() { const a = this.settings.appearance; if (a === 'dark') return true; if (a === 'auto') return matchMedia('(prefers-color-scheme: dark)').matches; return false; }, applyAppearance() { const b = document.body; b.classList.toggle('dark', this.isDark()); b.classList.toggle('reduce-transparency', !!this.settings.reduceTransparency); b.classList.toggle('reduce-motion', !!this.settings.reduceMotion); b.classList.toggle('increase-contrast', !!this.settings.increaseContrast); this.applyWallpaper(); }, wallpaperSrc() { const w = WALLPAPERS.find(w => w.id === this.settings.wallpaper) || WALLPAPERS[0]; return (this.isDark() && w.dark) ? w.dark : w.src; }, applyWallpaper() { const src = this.wallpaperSrc(); const desk = $('#desktop'), lock = $('#lockscreen-bg'); desk.style.backgroundImage = `url("${src}")`; lock.style.backgroundImage = `url("${src}")`; // 壁纸缺失时回退到 Big Sur 日落渐变 const probe = new Image(); probe.onerror = () => { const grad = 'linear-gradient(160deg,#4a6fa5 0%,#c86b85 45%,#f0a35e 100%)'; desk.style.backgroundImage = grad; lock.style.backgroundImage = grad; }; probe.src = src; }, applyBrightness() { $('#overlay-brightness').style.opacity = String((1 - this.settings.brightness) * 0.55); }, applyNightShift() { $('#overlay-nightshift').style.opacity = this.settings.nightShift ? String(this.settings.nightShiftStrength * 0.32) : '0'; }, applyVolume() { const v = this.settings.muted ? 0 : this.settings.volume; this.mediaEls.forEach(m => { try { m.volume = clamp(v, 0, 1); } catch (e) {} }); Bus.emit('volume:changed', v); // App 内音量滑块监听此事件与全局保持一致 }, /* 媒体注册:切换 src 后元素仍保留在集合中;仅在窗口关闭时由应用注销 */ registerMedia(m) { this.mediaEls.add(m); try { m.volume = this.settings.muted ? 0 : this.settings.volume; } catch (e) {} return m; }, unregisterMedia(m) { this.mediaEls.delete(m); }, /* ================= 电源流程 ================= */ boot() { $('#poweroff').classList.add('hidden'); $('#lockscreen').classList.add('hidden'); $('#desktop').classList.add('hidden'); $('#menubar').classList.add('hidden'); $('#dock').classList.add('hidden'); const boot = $('#bootscreen'), fill = $('#boot-progress-fill'); boot.classList.remove('hidden'); fill.style.transition = 'none'; fill.style.width = '0%'; requestAnimationFrame(() => requestAnimationFrame(() => { fill.style.transition = 'width 2.3s cubic-bezier(.25,.6,.35,1)'; fill.style.width = '100%'; })); setTimeout(() => { boot.classList.add('hidden'); this.showLock(); }, 3100); }, /* 关闭菜单、控制中心、通知中心、Spotlight、Launchpad 等临时浮层 */ closeOverlays() { UI.closeAllMenus(); $('#control-center').classList.add('hidden'); Notify.hideCenter(); Spotlight.close(); if (globalThis.Launchpad) Launchpad.hide(); if (!$('#screensaver').classList.contains('hidden')) this.hideScreensaver(); }, /* 依次请求关闭所有窗口并等待脏文档确认;任一取消则整体中止。 * 确认对话框(z-index 7000)在此过程中始终位于锁屏(9500)/关机层(10000)之下显示之前,故不会被遮住。 */ async closeAllWindowsGracefully() { for (const w of WM.windows.slice()) { const r = await WM.close(w); if (r !== 'closed') return false; // cancelled → 中止会话结束流程 } return true; }, showLock() { this.closeOverlays(); this.unlocked = false; // 锁屏不再关闭任何窗口:窗口、层级、最小化与全屏状态全部保留,解锁后原样恢复 const ls = $('#lockscreen'); ls.classList.remove('hidden', 'unlocking', 'shake'); $('#lock-password-row').classList.add('hidden'); $('#lock-error').classList.add('hidden'); $('#lock-hint').textContent = '点按或按 Enter 键解锁'; $('#lock-username').textContent = this.settings.userName; $('#lock-avatar').src = this.settings.avatar; this.applyWallpaper(); this.tickLockClock(); clearInterval(this._lockT); this._lockT = setInterval(() => this.tickLockClock(), 1000); ls.onclick = () => this.lockAdvance(); $('#lock-password').onkeydown = e => { e.stopPropagation(); if (e.key === 'Enter') this.tryUnlock(); }; }, tickLockClock() { const d = new Date(); $('#lock-date').textContent = fmtDateCN(d, this.settings.timezone); $('#lock-time').textContent = fmtTime(d, this.settings.h24, this.settings.timezone); }, lockAdvance() { if (this.unlocked) return; if (this.settings.passwordEnabled && $('#lock-password-row').classList.contains('hidden')) { $('#lock-password-row').classList.remove('hidden'); $('#lock-hint').textContent = '按 Enter 键解锁'; $('#lock-password').value = ''; setTimeout(() => $('#lock-password').focus(), 50); return; } if (this.settings.passwordEnabled) { this.tryUnlock(); return; } this.unlock(); }, tryUnlock() { const v = $('#lock-password').value; if (v === this.settings.password) this.unlock(); else { const ls = $('#lockscreen'); $('#lock-error').classList.remove('hidden'); ls.classList.remove('shake'); void ls.offsetWidth; ls.classList.add('shake'); $('#lock-password').value = ''; } }, unlock() { this.unlocked = true; clearInterval(this._lockT); const ls = $('#lockscreen'); ls.classList.add('unlocking'); setTimeout(() => { ls.classList.add('hidden'); ls.classList.remove('unlocking'); $('#desktop').classList.remove('hidden'); $('#menubar').classList.remove('hidden'); this.layoutDock(); $('#dock').classList.remove('hidden'); this.renderDesktopIcons(); this.setActiveApp('finder'); // 登录项 for (const [appId, on] of Object.entries(this.settings.loginItems)) { if (on && Apps.get(appId)) setTimeout(() => Apps.open(appId), 400); } this.resetIdle(); }, 420); }, /* 会话结束统一流程:系统确认 → 等待全部脏文档确认(可中止)→ 执行 */ async _sessionEnd(confirmTitle, confirmMsg, okLabel, danger, act) { if (this._sessionBusy) return; this._sessionBusy = true; try { if (!await UI.confirm(confirmTitle, confirmMsg, { ok: okLabel, danger, icon: 'assets/icons/settings.png' })) return; this.closeOverlays(); if (!await this.closeAllWindowsGracefully()) return; // 用户取消脏文档确认 → 中止 this.unlocked = false; act(); } finally { this._sessionBusy = false; } }, powerOff() { return this._sessionEnd('确定要关机吗?', '关机前将请你确认所有未保存的更改。', '关机', true, () => { ['#desktop', '#menubar', '#dock', '#lockscreen', '#notification-center'].forEach(s => $(s).classList.add('hidden')); $('#poweroff').classList.remove('hidden'); $('#poweroff').onclick = () => { $('#poweroff').classList.add('hidden'); this.boot(); }; }); }, restart() { return this._sessionEnd('确定要重新启动吗?', '重新启动前将请你确认所有未保存的更改。', '重新启动', false, () => this.boot()); }, logout() { return this._sessionEnd(`确定要注销“${this.settings.userName}”吗?`, '注销前将请你确认所有未保存的更改。', '注销', false, () => this.showLock()); }, sleep() { this.closeOverlays(); this.unlocked = false; // 睡眠不关闭窗口:仅熄灭显示,唤醒后经锁屏恢复原状 ['#desktop', '#menubar', '#dock'].forEach(s => $(s).classList.add('hidden')); const black = el('div', { style: { position: 'fixed', inset: '0', background: '#000', zIndex: '9999' } }); document.body.append(black); const wake = () => { black.remove(); document.removeEventListener('keydown', wake); this.showLock(); }; setTimeout(() => { black.addEventListener('pointerdown', wake); document.addEventListener('keydown', wake); }, 600); }, globalKeys(e) { // Spotlight if ((e.ctrlKey || e.metaKey) && e.code === 'Space') { e.preventDefault(); if (this.unlocked) Spotlight.toggle(); return; } if (!this.unlocked) { if (e.key === 'Enter' && !$('#lockscreen').classList.contains('hidden') && document.activeElement !== $('#lock-password')) this.lockAdvance(); return; } // Ctrl+Cmd+F 全屏 if (e.ctrlKey && e.metaKey && (e.key === 'f' || e.key === 'F')) { e.preventDefault(); if (WM.activeWin) WM.toggleFullscreen(WM.activeWin); return; } // ⌘W 关闭窗口 / ⌘M 最小化(meta 在多数浏览器难捕获,兼容 ctrl+alt) if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === 'w' && WM.activeWin) { if (e.metaKey) { e.preventDefault(); WM.close(WM.activeWin); } } if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'm' && WM.activeWin && e.metaKey) { e.preventDefault(); WM.minimize(WM.activeWin); } // ⌘Q 退出前台应用 if (e.metaKey && e.key.toLowerCase() === 'q') { e.preventDefault(); if (this.activeApp && this.activeApp !== 'finder') Apps.quit(this.activeApp); } }, /* ================= 屏保 ================= */ initIdleWatch() { const reset = () => this.resetIdle(); ['pointermove', 'pointerdown', 'keydown', 'wheel'].forEach(ev => document.addEventListener(ev, reset, { passive: true, capture: true })); }, resetIdle() { if (!$('#screensaver').classList.contains('hidden')) this.hideScreensaver(); clearTimeout(this._saverT); if (!this.unlocked || this.settings.screensaverType === 'off') return; this._saverT = setTimeout(() => this.showScreensaver(), this.settings.screensaverDelay * 60000); }, showScreensaver() { const ss = $('#screensaver'); ss.innerHTML = ''; ss.classList.remove('hidden'); if (this.settings.screensaverType === 'clock') { const t = el('div', { class: 't' }), d = el('div', { class: 'd' }); ss.append(el('div', { class: 'ss-clock' }, t, d)); const tick = () => { const n = new Date(); t.textContent = fmtTime(n, this.settings.h24); d.textContent = fmtDateCN(n); }; tick(); ss._timer = setInterval(tick, 1000); } else { const srcs = WALLPAPERS.map(w => w.src); let i = 0; const slides = srcs.map(s => el('div', { class: 'ss-slide', style: { backgroundImage: `url("${s}")` } })); slides.forEach(s => ss.append(s)); const cycle = () => { slides.forEach((s, j) => s.style.opacity = j === i ? '1' : '0'); i = (i + 1) % slides.length; }; cycle(); ss._timer = setInterval(cycle, 5000); } }, hideScreensaver() { const ss = $('#screensaver'); clearInterval(ss._timer); ss.classList.add('hidden'); ss.innerHTML = ''; }, /* ================= 菜单栏 ================= */ buildMenubar() { const left = $('#menubar-left'), right = $('#menubar-right'); left.innerHTML = ''; right.innerHTML = ''; // Apple 菜单 const apple = el('div', { class: 'mb-item mb-apple', role: 'button', 'aria-label': 'Apple 菜单' }, el('svg', { viewBox: '0 0 384 512', html: '' })); apple.addEventListener('click', e => { e.stopPropagation(); UI.closeAllMenus(); this.openAppleMenu(apple); }); left.append(apple); this._appMenuItems = []; // 右侧状态项 const mk = (cls, html, title) => { const d = el('div', { class: 'mb-item ' + cls, title: title || '' }); if (html != null) d.innerHTML = html; return d; }; this._mbBattery = mk('mb-battery', `100%`, '电池电量:100%(电源适配器)'); this._mbWifi = mk('mb-wifi', '', 'Wi-Fi'); this._mbWifi.append(this.wifiSvg()); this._mbSpot = mk('mb-spot', '', '聚焦搜索 (Ctrl+空格)'); this._mbCC = mk('mb-cc', '', '控制中心'); this._mbClock = mk('', '', ''); this._mbClock.id = 'mb-clock'; right.append(this._mbBattery, this._mbWifi, this._mbSpot, this._mbCC, this._mbClock); this._mbSpot.addEventListener('click', () => Spotlight.toggle()); this._mbCC.addEventListener('click', e => { e.stopPropagation(); this.toggleControlCenter(); }); this._mbWifi.addEventListener('click', e => { e.stopPropagation(); this.toggleControlCenter(); }); this._mbClock.addEventListener('click', e => { e.stopPropagation(); this.toggleNotificationCenter(); }); this.tickClock(); clearInterval(this._clockT); this._clockT = setInterval(() => this.tickClock(), 1000); this.initDockMag(); }, wifiSvg() { const on = this.settings.wifi; const color = 'currentColor'; const svg = el('span', { style: { display: 'inline-flex', opacity: on ? '1' : '.45' } }); svg.innerHTML = `${on ? '' : ''}`; return svg; }, tickClock() { if (this._mbClock) this._mbClock.textContent = fmtMenuClock(new Date(), this.settings.h24, this.settings.timezone); // 闹钟检查 globalThis.ClockApp && ClockApp.tick(); }, openAppleMenu(anchor) { const r = anchor.getBoundingClientRect(); UI.menu([ { label: '关于本机', action: () => Apps.open('settings', { pane: 'about' }) }, { sep: true }, { label: '系统设置…', action: () => Apps.open('settings') }, { label: 'App Store…', action: () => Apps.open('appstore') }, { sep: true }, { label: '强制退出…', key: '⌥⌘⎋', action: () => this.forceQuitDialog() }, { sep: true }, { label: '睡眠', action: () => this.sleep() }, { label: '重新启动…', action: () => this.restart() }, { label: '关机…', action: () => this.powerOff() }, { sep: true }, { label: '锁定屏幕', key: '⌃⌘Q', action: () => this.showLock() }, { label: `注销“${this.settings.userName}”…`, action: () => this.logout() }, ], r.left, r.bottom + 4); }, forceQuitDialog() { const running = [...new Set(WM.windows.map(w => w.appId))].map(id => Apps.get(id)).filter(Boolean); const mask = el('div', { class: 'modal-mask' }); const list = el('div', { style: { margin: '10px 0', maxHeight: '180px', overflowY: 'auto', textAlign: 'left' } }); let sel = null; const render = () => { list.innerHTML = ''; if (!running.length) list.append(el('div', { class: 'empty-state', style: { height: '80px' }, text: '没有正在运行的应用' })); running.forEach(app => { const row = el('div', { class: 'menu-item enabled', style: { borderRadius: '6px' }, onclick: () => { sel = app; $$('.menu-item', list).forEach(n => n.style.background = ''); row.style.background = 'var(--selection)'; } }, iconImg(app.icon, '', ''), el('span', { text: app.name })); row.querySelector('img').style.width = '20px'; row.querySelector('img').style.height = '20px'; list.append(row); }); }; render(); const dlg = el('div', { class: 'dialog', style: { width: '300px' } }, el('div', { class: 'dlg-title', text: '强制退出应用程序' }), el('div', { class: 'dlg-msg', text: '选择要强制退出的应用,未保存的更改将会丢失。' }), list ); const btns = el('div', { class: 'dlg-btns row' }, el('button', { class: 'btn', text: '取消', onclick: () => mask.remove() }), el('button', { class: 'btn danger', text: '强制退出', onclick: () => { if (sel) { WM.windowsForApp(sel.id).slice().forEach(w => WM.close(w)); running.splice(running.indexOf(sel), 1); sel = null; render(); } } }) ); dlg.append(btns); mask.append(dlg); document.body.append(mask); mask.addEventListener('pointerdown', e => { if (e.target === mask) mask.remove(); }); }, setActiveApp(appId) { this.activeApp = appId || 'finder'; const app = Apps.get(this.activeApp); // 重建应用菜单区 $$('.mb-appmenu', $('#menubar-left')).forEach(n => n.remove()); const left = $('#menubar-left'); const menus = app ? app.menus(WM.activeWin && WM.activeWin.appId === app.id ? WM.activeWin : null) : []; const nameItem = el('div', { class: 'mb-item mb-appmenu app-name', text: app ? app.name : '访达' }); nameItem.addEventListener('click', e => { e.stopPropagation(); UI.closeAllMenus(); const r = nameItem.getBoundingClientRect(); const nm = app ? app.name : '访达'; UI.menu([ { label: `关于${nm}`, action: () => UI.dialog({ icon: app?.icon, title: nm, msg: app?.about || 'macOS 网页版内置应用', buttons: ['好'] }) }, { sep: true }, { label: '设置…', action: () => Apps.open('settings') }, { sep: true }, { label: `退出${nm}`, key: '⌘Q', disabled: this.activeApp === 'finder', action: () => Apps.quit(this.activeApp) }, ], r.left, r.bottom + 4); }); left.append(nameItem); for (const m of menus) { const item = el('div', { class: 'mb-item mb-appmenu', text: m.label }); item.addEventListener('click', e => { e.stopPropagation(); if (item.classList.contains('open')) { UI.closeAllMenus(); item.classList.remove('open'); return; } UI.closeAllMenus(); $$('.mb-item.open').forEach(n => n.classList.remove('open')); item.classList.add('open'); const r = item.getBoundingClientRect(); // 展开瞬间按当前 activeWin.appState 重新生成菜单,确保 disabled/checked/action 为最新状态 const curApp = Apps.get(this.activeApp); const curWin = curApp && WM.activeWin && WM.activeWin.appId === curApp.id ? WM.activeWin : null; const freshMenus = curApp ? curApp.menus(curWin) : []; const fm = freshMenus.find(x => x.label === m.label) || m; const pop = UI.menu(typeof fm.items === 'function' ? fm.items() : fm.items, r.left, r.bottom + 4); const obs = new MutationObserver(() => { if (!document.body.contains(pop)) item.classList.remove('open'); obs.disconnect(); }); obs.observe(document.body, { childList: true }); }); item.addEventListener('pointerenter', () => { if ($$('.menu-pop').length && !item.classList.contains('open') && $$('.mb-item.open').length) item.click(); }); left.append(item); } }, /* ================= Dock ================= */ dockPinned() { return ['finder', 'launchpad', 'safari', 'mail', 'messages', 'facetime', 'maps', 'notes', 'textedit', 'calendar', 'photos', 'music', 'calculator', 'terminal', 'activitymonitor', 'appstore', 'settings']; }, buildDock() { Bus.on('wm:changed', () => this.updateDockDots()); Bus.on('trash:changed', () => this.renderDock()); }, dockItems() { const pinned = this.dockPinned(); const running = [...new Set(WM.windows.map(w => w.appId))].filter(id => !pinned.includes(id)); return [...pinned, ...running]; }, renderDock() { const dock = $('#dock'); dock.innerHTML = ''; const s = this.settings; for (const id of this.dockItems()) { const app = Apps.get(id); if (!app) continue; const ic = el('div', { class: 'dock-icon', dataset: { app: id }, title: '' }, iconImg(app.icon, '', app.name), el('span', { class: 'dock-tip', text: app.name }), el('span', { class: 'run-dot hidden' }) ); const badge = Notify.badgeCount(id); if (badge > 0) ic.append(el('span', { class: 'badge dock-badge', text: String(badge) })); ic.style.width = ic.style.height = this.effDockSize() + 'px'; ic.addEventListener('click', () => this.dockClick(id, ic)); ic.addEventListener('contextmenu', e => this.dockIconMenu(e, id)); dock.append(ic); } const sep = el('div', { class: 'dock-sep' }); dock.append(sep); // 废纸篓 const trashEmpty = FS.list(FS.TRASH).length === 0; const trash = el('div', { class: 'dock-icon', dataset: { app: '__trash' }, title: '废纸篓' }, iconImg(trashEmpty ? 'assets/icons/trash.svg' : 'assets/icons/trash-full.svg', '', '废纸篓'), el('span', { class: 'dock-tip', text: '废纸篓' }) ); trash.style.width = trash.style.height = this.effDockSize() + 'px'; trash.addEventListener('click', () => Apps.open('finder', { path: FS.TRASH })); trash.addEventListener('contextmenu', e => { e.preventDefault(); e.stopPropagation(); UI.contextMenu([ { label: '打开', action: () => Apps.open('finder', { path: FS.TRASH }) }, { label: '清空废纸篓', disabled: trashEmpty, action: async () => { if (await UI.confirm('确定要清空废纸篓吗?', '此操作无法撤销。', { ok: '清空', danger: true })) FS.emptyTrash(); } }, ], e); }); dock.append(trash); dock.addEventListener('contextmenu', e => { if (e.target.closest('.dock-icon')) return; e.preventDefault(); this.dockBgMenu(e); }); this.layoutDock(); this.updateDockDots(); }, /* 窄屏自动缩小 Dock 图标 */ effDockSize() { const s = this.settings.dockSize; if (innerWidth <= 640) return Math.min(s, 40); if (innerWidth <= 1100) return Math.min(s, 48); return s; }, layoutDock() { const dock = $('#dock'), hz = $('#dock-hotzone'), s = this.settings; dock.className = dock.className.replace(/pos-\w+|hidden-\w+/g, '').trim(); dock.classList.add('pos-' + s.dockPosition); hz.className = s.dockPosition; // 尺寸 $$('.dock-icon', dock).forEach(ic => { ic.style.width = ic.style.height = this.effDockSize() + 'px'; }); // 自动隐藏 const hidden = s.dockAutohide && !this._dockPeeking; dock.classList.toggle('hidden-' + s.dockPosition, !!hidden && this.unlocked); if (this.fullscreenWin) dock.classList.add('hidden-' + s.dockPosition); // 绑定热区 hz.onpointerenter = () => { if (s.dockAutohide) this.dockPeek(true); }; dock.onpointerenter = () => { clearTimeout(this._dockHideT); if (s.dockMagnify) dock.addEventListener('pointermove', this._dockMag); }; dock.onpointerleave = () => { dock.removeEventListener('pointermove', this._dockMag); this.dockResetMag(); if (s.dockAutohide) { clearTimeout(this._dockHideT); this._dockHideT = setTimeout(() => this.dockPeek(false), 450); } }; }, dockPeek(show) { this._dockPeeking = show; const dock = $('#dock'), s = this.settings; clearTimeout(this._dockHideT); dock.classList.toggle('hidden-' + s.dockPosition, !show); if (!show) this._dockPeeking = false; }, dockHide(force) { const dock = $('#dock'), s = this.settings; dock.classList.toggle('hidden-' + s.dockPosition, force || (s.dockAutohide && !this._dockPeeking)); }, _dockMag: null, dockResetMag() { const s = this.settings; $$('.dock-icon', $('#dock')).forEach(ic => { ic.style.width = ic.style.height = this.effDockSize() + 'px'; ic.style.margin = '0'; }); }, initDockMag() { this._dockMag = e => { const s = this.settings; if (!s.dockMagnify) return; const base = this.effDockSize(), max = base * s.dockMagnifyLevel; const vertical = s.dockPosition !== 'bottom'; for (const ic of $$('.dock-icon', $('#dock'))) { const r = ic.getBoundingClientRect(); const c = vertical ? r.top + r.height / 2 : r.left + r.width / 2; const m = vertical ? e.clientY : e.clientX; const d = Math.abs(m - c); const range = base * 2.4; const scale = d < range ? base + (max - base) * Math.cos((d / range) * Math.PI / 2) : base; ic.style.width = ic.style.height = scale + 'px'; } }; }, updateDockDots() { for (const ic of $$('.dock-icon', $('#dock'))) { const id = ic.dataset.app; if (!id || id === '__trash') continue; const running = WM.windows.some(w => w.appId === id); ic.querySelector('.run-dot')?.classList.toggle('hidden', !running); // 同步 dock 项(未固定应用退出后移除) } const want = this.dockItems(); const have = $$('.dock-icon', $('#dock')).map(n => n.dataset.app).filter(a => a && a !== '__trash'); if (want.join() !== have.join()) this.renderDock(); }, dockClick(id, ic) { const wins = WM.windowsForApp(id); if (!wins.length) { ic.classList.add('bouncing'); setTimeout(() => ic.classList.remove('bouncing'), 1700); Apps.open(id); return; } const min = wins.filter(w => w.state === 'minimized'); if (min.length) { min.forEach(w => WM.restore(w)); return; } if (WM.activeWin && WM.activeWin.appId === id && wins.length > 1) { // 轮换窗口 const i = wins.indexOf(WM.activeWin); WM.focus(wins[(i + 1) % wins.length]); } else WM.focus(wins[wins.length - 1]); }, dockIconMenu(e, id) { e.preventDefault(); e.stopPropagation(); const app = Apps.get(id); const running = WM.windowsForApp(id).length > 0; UI.contextMenu([ { label: running ? '显示所有窗口' : '打开', action: () => { running ? WM.windowsForApp(id).forEach(w => WM.restore(w)) : Apps.open(id); } }, { sep: true }, { label: '选项', submenu: [ { label: '登录时打开', checked: !!this.settings.loginItems[id], action: () => { this.settings.loginItems[id] = !this.settings.loginItems[id]; this.save(); } }, ] }, { label: running ? '退出' : '在访达中显示', action: () => { running ? Apps.quit(id) : Apps.open('finder', { path: FS.HOME + '/Applications' }); } }, ], e); }, dockBgMenu(e) { const s = this.settings; UI.contextMenu([ { label: '自动隐藏', checked: s.dockAutohide, action: () => { s.dockAutohide = !s.dockAutohide; this.save(); this.layoutDock(); } }, { label: '位置', submenu: ['bottom', 'left', 'right'].map(p => ({ label: { bottom: '置于底部', left: '置于左边', right: '置于右边' }[p], checked: s.dockPosition === p, action: () => { s.dockPosition = p; this.save(); this.layoutDock(); } })) }, { sep: true }, { label: '程序坞设置…', action: () => Apps.open('settings', { pane: 'dock' }) }, ], e); }, /* ================= 控制中心 ================= */ toggleControlCenter() { const cc = $('#control-center'); if (!cc.classList.contains('hidden')) { cc.classList.add('hidden'); return; } Notify.hideCenter(); this.renderControlCenter(); cc.classList.remove('hidden'); const close = e => { if (!e.target.closest('#control-center') && !e.target.closest('.mb-cc') && !e.target.closest('.mb-wifi')) { cc.classList.add('hidden'); document.removeEventListener('pointerdown', close); } }; setTimeout(() => document.addEventListener('pointerdown', close), 0); }, ccToggle(iconSvg, label, sub, key, onClick) { const s = this.settings; const ico = el('div', { class: 'cc-ico' + (s[key] ? ' on' : '') }); ico.innerHTML = iconSvg; const row = el('div', { class: 'cc-row', style: { cursor: 'pointer' } }, ico, el('div', { class: 'cc-lab' }, label, el('small', { text: s[key] ? '打开' : '关闭' }))); row.addEventListener('click', () => { if (onClick) onClick(!s[key]); else { s[key] = !s[key]; this.save(); } this.applyAll(); this.renderControlCenter(); if (key === 'wifi') { this._mbWifi.innerHTML = ''; this._mbWifi.append(this.wifiSvg()); } }); return row; }, renderControlCenter() { const cc = $('#control-center'), s = this.settings; cc.innerHTML = ''; const svgWifi = ''; const svgBt = ''; const svgDrop = ''; const svgMoon = ''; const svgSun = ''; const svgVol = ''; cc.append( el('div', { class: 'cc-card' }, this.ccToggle(svgWifi, 'Wi-Fi', '', 'wifi'), this.ccToggle(svgBt, '蓝牙', '', 'bluetooth'), this.ccToggle(svgDrop, '隔空投送', '', 'airdrop')), el('div', { class: 'cc-card' }, this.ccToggle(svgMoon, '专注模式', '', 'focus'), this.ccToggle('', 'VPN', '', 'vpn')), el('div', { class: 'cc-card span2' }, el('div', { class: 'cc-title', text: '显示器' }), el('div', { class: 'cc-slider-row' }, (() => { const i = el('span'); i.innerHTML = svgSun; return i; })(), (() => { const r = el('input', { type: 'range', class: 'slider', min: '20', max: '100', value: String(Math.round(s.brightness * 100)) }); r.style.setProperty('--fill', (s.brightness * 100) + '%'); r.addEventListener('input', () => { s.brightness = r.value / 100; r.style.setProperty('--fill', r.value + '%'); Sys.applyBrightness(); }); r.addEventListener('change', () => Sys.save()); return r; })())), el('div', { class: 'cc-card span2' }, el('div', { class: 'cc-title', text: '声音' }), el('div', { class: 'cc-slider-row' }, (() => { const i = el('span'); i.innerHTML = svgVol; return i; })(), (() => { const r = el('input', { type: 'range', class: 'slider', min: '0', max: '100', value: String(Math.round(s.volume * 100)) }); r.style.setProperty('--fill', (s.volume * 100) + '%'); r.addEventListener('input', () => { s.volume = r.value / 100; s.muted = r.value == 0; r.style.setProperty('--fill', r.value + '%'); Sys.applyVolume(); }); r.addEventListener('change', () => Sys.save()); return r; })())), el('div', { class: 'cc-card span2' }, this.ccToggle('', '夜览', '', 'nightShift')) ); }, /* ================= 通知中心 ================= */ toggleNotificationCenter() { const nc = $('#notification-center'); const open = nc.classList.contains('nc-hidden') || nc.classList.contains('hidden'); if (open) { $('#control-center').classList.add('hidden'); Notify.showCenter(); } else Notify.hideCenter(); }, /* 还原所有设置 */ async resetAll() { const ok = await UI.confirm('还原所有设置?', '将清除本应用保存在此浏览器中的全部数据(设置、文件、备忘录等)并恢复默认。', { ok: '还原', danger: true, icon: 'assets/icons/settings.png' }); if (!ok) return false; Store.clearAll(); location.reload(); return true; } }; /* ============ 通知 API ============ */ const Notify = { list: [], init() { this.list = Store.get('notifs', []); }, save() { Store.set('notifs', this.list.slice(-60)); }, badgeCount(appId) { return this.list.filter(n => n.appId === appId && !n.read).length; }, /* 判断某 App 是否允许发通知(总开关 + 每 App 开关,默认允许) */ allowed(appId) { const s = Sys.settings; if (!s.notificationsEnabled) return false; return s.notifAllow[appId] !== false; }, /* breakthrough: 允许突破专注模式(闹钟等关键提醒使用) */ send({ appId, title, body, silent = false, breakthrough = false }) { if (!this.allowed(appId)) return null; // 开关关闭:无横幅、无记录、无徽标 const app = Apps.get(appId) || { name: appId, icon: 'assets/icons/settings.png' }; const n = { id: uid(), appId, title, body, ts: Date.now(), read: false, icon: app.icon }; this.list.push(n); this.save(); // 专注模式:仍保存记录与未读,仅不弹横幅(breakthrough 除外) if ((!Sys.settings.focus || breakthrough) && !silent) this.banner(n, app); if (!$('#notification-center').classList.contains('hidden') && !$('#notification-center').classList.contains('nc-hidden')) this.renderCenter(); this.updateBadges(); return n; }, banner(n, app) { const b = el('div', { class: 'banner' }, iconImg(n.icon, '', ''), el('div', null, el('div', { class: 'n-title', text: n.title }), el('div', { class: 'n-text', text: n.body }))); // 只有点击横幅才标记已读;横幅自然消失不改变未读状态 b.addEventListener('click', () => { this.markRead(n.id); b.remove(); if (Apps.get(n.appId)) Apps.open(n.appId); }); $('#banner-container').append(b); setTimeout(() => { b.classList.add('out'); setTimeout(() => b.remove(), 320); }, 5200); }, markRead(id) { const n = this.list.find(x => x.id === id); if (n && !n.read) { n.read = true; this.save(); this.updateBadges(); } }, remove(id) { this.list = this.list.filter(x => x.id !== id); this.save(); this.renderCenter(); this.updateBadges(); }, clearAll() { this.list = []; this.save(); this.renderCenter(); this.updateBadges(); }, /* 局部更新 Dock 徽标,不重建整个 Dock */ updateBadges() { $$('#dock .dock-icon').forEach(ic => { const id = ic.dataset.app; const cnt = id === 'trash' ? 0 : this.badgeCount(id); let badge = ic.querySelector('.dock-badge'); if (cnt > 0) { if (!badge) { badge = el('span', { class: 'badge dock-badge' }); ic.append(badge); } badge.textContent = String(cnt); } else if (badge) badge.remove(); }); }, showCenter() { const nc = $('#notification-center'); nc.classList.remove('hidden', 'nc-hidden'); this.renderCenter(); }, hideCenter() { $('#notification-center').classList.add('nc-hidden'); }, renderCenter() { const nc = $('#notification-center'); if (nc.classList.contains('hidden')) return; nc.innerHTML = ''; const d = new Date(); nc.append(el('div', { class: 'nc-date' }, el('div', { class: 'd1', text: WEEK_CN[d.getDay()] }), el('div', { class: 'd2', text: `${d.getMonth() + 1}月${d.getDate()}日` }))); // 天气小组件 const w = globalThis.WeatherApp ? WeatherApp.widgetData() : null; if (w) nc.append(el('div', { class: 'nc-widget' }, el('div', { class: 'w-city', text: w.city }), el('div', { class: 'w-temp', text: w.temp + '°' }), el('div', { class: 'w-desc', text: `${w.desc} 最高${w.hi}° 最低${w.lo}°` }))); const list = this.list.slice().reverse(); if (!list.length) nc.append(el('div', { class: 'nc-empty', text: '没有新通知' })); for (const n of list.slice(0, 30)) { const item = el('div', { class: 'nc-notif' }, el('button', { class: 'n-close', text: '✕', 'aria-label': '清除通知', onclick: e => { e.stopPropagation(); this.remove(n.id); } }), iconImg(n.icon, '', ''), el('div', { class: 'n-body' }, el('div', { class: 'n-title' }, el('span', { text: n.title }), el('time', { text: relTime(n.ts) })), el('div', { class: 'n-text', text: n.body }))); item.addEventListener('click', () => { this.markRead(n.id); if (Apps.get(n.appId)) Apps.open(n.appId); }); nc.append(item); } if (list.length) nc.append(el('button', { class: 'nc-clear-all', text: '清除所有通知', onclick: () => this.clearAll() })); } }; /* ============ Spotlight ============ */ const Spotlight = { sel: 0, items: [], toggle() { $('#spotlight').classList.contains('hidden') ? this.open() : this.close(); }, open() { const sp = $('#spotlight'); sp.classList.remove('hidden'); const input = $('#spotlight-input'); input.value = ''; this.render(''); setTimeout(() => input.focus(), 30); input.oninput = () => { this.sel = 0; this.render(input.value.trim()); }; input.onkeydown = e => { if (e.key === 'Escape') this.close(); else if (e.key === 'ArrowDown') { e.preventDefault(); this.moveSel(1); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.moveSel(-1); } else if (e.key === 'Enter') { const it = this.items[this.sel]; if (it) { this.close(); it.action(); } } }; sp.onclick = e => { if (e.target === sp) this.close(); }; }, close() { $('#spotlight').classList.add('hidden'); }, moveSel(d) { if (!this.items.length) return; this.sel = (this.sel + d + this.items.length) % this.items.length; $$('.sp-item').forEach((n, i) => n.classList.toggle('sel', i === this.sel)); $$('.sp-item')[this.sel]?.scrollIntoView({ block: 'nearest' }); }, render(q) { const box = $('#spotlight-results'); box.innerHTML = ''; this.items = []; if (!q) return; const s = Sys.settings; const addSection = name => box.append(el('div', { class: 'sp-section', text: name })); const addItem = (section, icon, label, sub, action) => { this.items.push({ action }); const item = el('div', { class: 'sp-item' + (this.items.length - 1 === this.sel ? ' sel' : '') }, typeof icon === 'string' ? iconImg(icon, '', '') : icon, el('span', { text: label }), sub ? el('span', { class: 'sp-sub', text: sub }) : null); item.addEventListener('click', () => { this.close(); action(); }); item.addEventListener('pointerenter', () => { this.sel = this.items.length - 1; $$('.sp-item').forEach((n, i) => n.classList.toggle('sel', i === this.sel)); }); if (section) { const secs = $$('.sp-section', box).map(n => n.textContent); if (!secs.includes(section)) addSection(section); } box.append(item); }; // 计算器 if (/^[\d\s+\-*/().%×÷^]+$/.test(q) && /\d/.test(q) && /[+\-*/×÷^%]/.test(q)) { try { const expr = q.replace(/×/g, '*').replace(/÷/g, '/').replace(/\^/g, '**').replace(/%/g, '/100'); const val = Function('"use strict";return (' + expr + ')')(); if (isFinite(val)) { const c = el('div', { class: 'sp-calc', text: `${q} = ${+val.toFixed(8)}` }); box.append(c); this.items.push({ action: () => navigator.clipboard?.writeText(String(+val.toFixed(8))) }); } } catch (e) {} } const ql = q.toLowerCase(); if (s.siriApps) { const apps = Object.values(Apps.registry) .filter(a => (!a.storeApp || (globalThis.AppStoreApp && AppStoreApp.isInstalled(a.id)))) .filter(a => a.name.toLowerCase().includes(ql) || a.id.includes(ql)).slice(0, 5); apps.forEach(a => addItem('应用程序', a.icon, a.name, '应用程序', () => Apps.open(a.id))); } if (s.siriFiles) { const files = FS.search(q).slice(0, 6); files.forEach(f => addItem('文件与文件夹', FS.iconFor(f.path), FS.baseName(f.path), FS.dirName(f.path).replace(FS.HOME, '~'), () => Apps.openPath(f.path))); } if (s.siriSettings && window.SettingsApp) { const panes = SettingsApp.panes.filter(p => p.name.includes(q)).slice(0, 4); panes.forEach(p => addItem('系统设置', 'assets/icons/settings.png', p.name, '系统设置', () => Apps.open('settings', { pane: p.id }))); } if (!this.items.length && !box.querySelector('.sp-calc')) box.append(el('div', { class: 'sp-item disabled', text: `未找到“${q}”相关结果` })); } }; window.Sys = Sys; window.Notify = Notify; window.Spotlight = Spotlight; window.WALLPAPERS = WALLPAPERS; window.DEFAULT_SETTINGS = DEFAULT_SETTINGS;