'use strict'; // ---------- 标签页 ---------- // 放在 IPC 解构之前:即使 preload 没注入,标签页也要能切换。 (() => { const bar = document.getElementById('tab-bar'); if (!bar) return; const tabs = Array.from(bar.querySelectorAll('[role="tab"]')); if (!tabs.length) return; function select(tab) { for (const t of tabs) { const on = t === tab; t.setAttribute('aria-selected', on ? 'true' : 'false'); t.tabIndex = on ? 0 : -1; const panel = document.getElementById(t.getAttribute('aria-controls')); if (panel) panel.hidden = !on; } } for (const t of tabs) { t.addEventListener('click', () => { select(t); t.focus(); }); } bar.addEventListener('keydown', (e) => { if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return; e.preventDefault(); // 按「当前选中」而不是 document.activeElement 找位置: // 窗口未获焦点时 activeElement 不可靠,测试里也无法聚焦隐藏窗口。 const i = tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true'); const step = e.key === 'ArrowRight' ? 1 : tabs.length - 1; const next = tabs[((i < 0 ? 0 : i) + step) % tabs.length]; select(next); next.focus(); }); select(tabs[0]); })(); const { get, set, clearHistory, openDir, chooseDir, openExternal } = window.settingsApi; const $auto=document.getElementById('opt-autolaunch'),$hotkey=document.getElementById('opt-hotkey'),$max=document.getElementById('opt-maxitems'),$display=document.getElementById('opt-displaylimit'),$poll=document.getElementById('opt-pollms'),$dir=document.getElementById('opt-datadir'),$change=document.getElementById('btn-change-dir'),$open=document.getElementById('btn-open-dir'),$clear=document.getElementById('btn-clear'),$close=document.getElementById('close-btn'),$status=document.getElementById('status-left'); function flash(msg){$status.textContent=msg;setTimeout(()=>$status.textContent='就绪',2000)} // settings:set 在主进程先改运行时状态、后写盘;写盘失败(磁盘满/只读/杀软锁文件) // 会带 saved:false 回来 —— 本次会话已生效但重启还原,必须明确提示, // 不能让用户以为设置已经保存。 function flashIfNotSaved(res){ if(res&&res.saved===false){flash('⚠ 设置未保存到磁盘,重启后还原');return true} return false } async function refresh(){const c=await get();$auto.checked=!!c.autoLaunch;$hotkey.value=c.hotkey||'';$max.value=String(c.maxItems);$display.value=String(c.displayLimit);$poll.value=String(c.pollMs);$dir.textContent=c.dataDir} $auto.addEventListener('change',async()=>{const res=await set('autoLaunch',$auto.checked);if(!flashIfNotSaved(res))flash('已'+($auto.checked?'开启':'关闭')+'开机自启动')}); $max.addEventListener('change',async()=>{const res=await set('maxItems',parseInt($max.value,10));if(!flashIfNotSaved(res))flash('最大条数已设为 '+$max.value)}); $display.addEventListener('change',async()=>{const res=await set('displayLimit',parseInt($display.value,10));if(!flashIfNotSaved(res))flash('界面显示条数已设为 '+$display.value)}); $poll.addEventListener('change',async()=>{const res=await set('pollMs',parseInt($poll.value,10));if(!flashIfNotSaved(res))flash('轮询间隔已设为 '+$poll.value+' ms')}); $hotkey.addEventListener('change',async()=>{ const res=await set('hotkey',$hotkey.value); if(flashIfNotSaved(res))return; if($hotkey.value&&res&&res.hotkeyRegistered===false){ // 主进程 register 返回 false:组合键被别的程序占用。配置照存, // 但必须告诉用户,否则他会以为快捷键坏了。 flash('快捷键可能被其他程序占用,未生效'); }else{ flash($hotkey.value?'唤起快捷键已设为 '+$hotkey.value:'已禁用唤起快捷键'); } }); $open.addEventListener('click',()=>openDir()); $clear.addEventListener('click',async()=>{if(!confirm('确定清空所有非置顶历史记录?此操作不可撤销。'))return;flash('已清除 '+await clearHistory()+' 条')}); $change.addEventListener('click',async()=>{if(typeof chooseDir!=='function')return;const res=await chooseDir();if(res&&res.ok===false&&res.message)flash(res.message)}); $close.addEventListener('click',()=>window.close());document.addEventListener('keydown',e=>{if(e.key==='Escape')window.close()});refresh(); // ---------- 应用图标(标题栏左上角)---------- // 一次性从主进程拿 PNG data URL 喂给 。与主窗口共用同一个 IPC, // 主进程缓存命中后零成本;图标缺失静默失败,不影响设置面板功能。 window.api.getAppIcon().then((dataUrl) => { if (!dataUrl) return; const el = document.getElementById('app-icon'); if (el) el.src = dataUrl; }).catch(() => { /* 图标缺失不影响功能 */ }); // ---------- 外链(关于页)---------- // 窗口不能直接导航到外部 URL(会把设置页整个顶掉),点击统一拦下来 // 走主进程 shell.openExternal 交给系统浏览器;scheme 白名单在主进程卡。 for (const a of document.querySelectorAll('a.ext-link')) { a.addEventListener('click', (e) => { e.preventDefault(); if (typeof openExternal === 'function') openExternal(a.href); }); } // ---------- 主题 ---------- // 不要用 `const themeApi`:contextBridge 暴露的 window.themeApi 是不可配置属性, // 顶层同名 const 会让整个脚本在实例化阶段抛 SyntaxError(一行都不执行)。 const themeIpc = window.themeApi; const { set: themeSet, setAccent, setFollowSystem, onSystemChanged } = themeIpc; const THEMES = [ { id: 'linear', name: 'Linear Dark', desc: '极简暗色专业', bg: '#0E0E12', text: '#E6E6E9', accent: '#5E6AD2' }, { id: 'midnight', name: 'Midnight', desc: '极致暗色', bg: '#08090B', text: '#E8E8EA', accent: '#7C3AED' }, { id: 'carbon', name: 'Carbon', desc: 'VS Code 灰', bg: '#1A1D23', text: '#D4D4D4', accent: '#3B82F6' }, { id: 'solarized', name: 'Solarized', desc: '经典 Sola', bg: '#002B36', text: '#93A1A1', accent: '#268BD2' }, { id: 'github', name: 'GitHub Dark', desc: 'GitHub 风', bg: '#0D1117', text: '#C9D1D9', accent: '#8957E5' }, { id: 'light', name: 'Light', desc: '白天模式', bg: '#FAFAFA', text: '#111827', accent: '#5E6AD2' }, ]; const ACCENTS = [ { id: 'purple', color: '#5E6AD2' }, { id: 'blue', color: '#3B82F6' }, { id: 'green', color: '#10B981' }, { id: 'orange', color: '#F59E0B' }, { id: 'red', color: '#EF4444' }, ]; function renderThemes(currentTheme) { const grid = document.getElementById('theme-grid'); if (!grid) return; grid.innerHTML = ''; for (const t of THEMES) { const card = document.createElement('div'); card.className = 'theme-card' + (t.id === currentTheme ? ' active' : ''); card.dataset.theme = t.id; card.innerHTML = `
Aa
${t.name}
${t.desc}
`; card.addEventListener('click', async () => { const res = await themeSet(t.id); if (res) { const eff = res.effectiveTheme || res.theme; document.documentElement.setAttribute('data-theme', eff); document.documentElement.setAttribute('data-accent', res.accent); try { // rawTheme 在 themeSet 路径上 = eff(顺手把 followSystem 关了), // 但旧主进程没发 rawTheme 字段,这里用 eff 兜底。 localStorage.setItem('clipboard-theme', JSON.stringify({ theme: res.rawTheme || eff, effectiveTheme: eff, accent: res.accent, followSystem: res.followSystem, })); } catch (_) {} document.getElementById('opt-follow-system').checked = false; renderThemes(eff); flashIfNotSaved(res); } }); grid.appendChild(card); } } function renderAccents(currentAccent) { const grid = document.getElementById('accent-grid'); if (!grid) return; grid.innerHTML = ''; for (const a of ACCENTS) { const sw = document.createElement('button'); sw.className = 'accent-swatch' + (a.id === currentAccent ? ' active' : ''); sw.dataset.accent = a.id; sw.style.background = a.color; sw.title = a.id; sw.addEventListener('click', async () => { const res = await setAccent(a.id); if (res) { document.documentElement.setAttribute('data-accent', res.accent); try { localStorage.setItem('clipboard-theme', JSON.stringify({ theme: res.rawTheme || res.theme, effectiveTheme: res.effectiveTheme || res.theme, accent: res.accent, followSystem: res.followSystem, })); } catch (_) {} renderAccents(res.accent); flashIfNotSaved(res); } }); grid.appendChild(sw); } } const $follow = document.getElementById('opt-follow-system'); if ($follow) { $follow.addEventListener('change', async () => { const res = await setFollowSystem($follow.checked); if (res) { try { localStorage.setItem('clipboard-theme', JSON.stringify({ theme: res.rawTheme || res.theme, effectiveTheme: res.effectiveTheme || res.theme, accent: res.accent, followSystem: res.followSystem, })); } catch (_) {} if (!flashIfNotSaved(res)) flash($follow.checked ? '已开启跟随系统' : '已关闭跟随系统'); } }); } onSystemChanged((payload) => { // 同 renderer:只更新 DOM,不写 localStorage(payload 里没有 rawTheme)。 const eff = payload.effectiveTheme || payload.theme; document.documentElement.setAttribute('data-theme', eff); document.documentElement.setAttribute('data-accent', payload.accent); renderThemes(eff); }); async function initTheme() { try { // 先订阅再拉当前值,否则跟随系统改主题时这个窗口收不到广播 try { await themeIpc.subscribe(); } catch (e) { console.error('[theme] subscribe failed:', e.message); } const cfg = await themeIpc.get(); const eff = cfg.effectiveTheme || cfg.theme; document.documentElement.setAttribute('data-theme', eff); document.documentElement.setAttribute('data-accent', cfg.accent); if ($follow) $follow.checked = cfg.followSystem; renderThemes(eff); renderAccents(cfg.accent); } catch (e) { console.error('[theme] initTheme failed:', e.message); } } initTheme();