'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 = `