Files
Clipboard/renderer/settings.js
2026-09-12 13:59:12 +08:00

228 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 喂给 <img>。与主窗口共用同一个 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 = `
<div class="theme-preview" style="background:${t.bg};color:${t.text}">
<span class="theme-preview-text">Aa</span>
<span class="theme-preview-accent" style="background:${t.accent}"></span>
</div>
<div class="theme-info">
<div class="theme-name">${t.name}</div>
<div class="theme-desc">${t.desc}</div>
</div>
`;
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不写 localStoragepayload 里没有 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();