$('minBtn').onclick = () => window.api.minimize(); $('maxBtn').onclick = () => window.api.maximize(); $('closeBtn').onclick = () => window.api.close(); let uiTheme = 'dark'; function applyUiTheme(value) { uiTheme = value === 'light' ? 'light' : 'dark'; document.documentElement.dataset.uiTheme = uiTheme; const label = uiTheme === 'light' ? '切换到暗色主题' : '切换到明亮主题'; $('uiThemeBtn').title = label; $('uiThemeBtn').setAttribute('aria-label', label); } $('uiThemeBtn').onclick = async () => { const next = uiTheme === 'dark' ? 'light' : 'dark'; applyUiTheme(next); const result = await window.api.ui.setTheme(next); if (!result || !result.ok) applyUiTheme(uiTheme === 'dark' ? 'light' : 'dark'); }; async function initUiTheme() { const result = await window.api.ui.getTheme(); applyUiTheme(result && result.ok ? result.data : 'dark'); const unsubscribe = window.api.ui.onThemeChanged(applyUiTheme); if (typeof unsubscribe === 'function') { window.addEventListener('beforeunload', unsubscribe, { once: true }); } } initUiTheme(); let currentTab = 'library'; function switchTab(tab) { currentTab = tab; document.querySelectorAll('.tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === tab)); $('libraryTab').classList.toggle('hidden', tab !== 'library'); $('notesTab').classList.toggle('hidden', tab !== 'notes'); $('browseTab').classList.toggle('hidden', tab !== 'browse'); $('settingsTab').classList.toggle('hidden', tab !== 'settings'); if (tab === 'library') Library.refresh(true); if (tab === 'notes') Notes.refresh(); } document.querySelectorAll('.tab').forEach((t) => { t.onclick = () => switchTab(t.dataset.tab); }); DownloadCenter.init(); Browse.init(); Library.init(); Notes.init(); if (window.api.reader && window.api.reader.onNotesChanged) { const unsubscribeNotes = window.api.reader.onNotesChanged(() => { Notes.markDirty(); if (currentTab === 'notes') Notes.refresh(true); }); if (typeof unsubscribeNotes === 'function') { window.addEventListener('beforeunload', unsubscribeNotes, { once: true }); } } const sortSelect = $('sortSelect'); sortSelect.value = Library.getSortMode(); sortSelect.onchange = () => Library.setSortMode(sortSelect.value); async function initSourceManager() { const listEl = $('sourceList'); const res = await window.api.sources.list(); const all = res.ok ? res.data : []; const enabled = getEnabledSources(); listEl.innerHTML = all.map((s) => { const checked = enabled ? enabled.includes(s.id) : true; return ` `; }).join(''); listEl.querySelectorAll('input[type="checkbox"]').forEach((cb) => { cb.onchange = () => { const ids = Array.from(listEl.querySelectorAll('input[type="checkbox"]:checked')) .map((el) => el.dataset.id); setEnabledSources(ids); Browse.reloadSources(); }; }); } initSourceManager(); async function refreshZlibStatus() { const r = await window.api.zlib.hasCreds(); const logged = r.ok && r.data; $('zlibStatus').textContent = logged ? '已配置(凭据保存在本地)' : '未登录'; $('zlibLoginBtn').textContent = logged ? '重新登录' : '登录'; $('zlibLogoutBtn').classList.toggle('hidden', !logged); } $('zlibLoginBtn').onclick = async () => { const r = await openModal('Z-Library 登录', `
使用 Z-Library 账号登录(保存在本地 userData 目录)
一行一个地址,必须是 HTTPS。自定义镜像会排在内置列表前面优先尝试;留空则只用内置列表。
新目录:
检查到:
这些笔记目前仍可在「我的笔记」中查看,删除后无法恢复。
` : ''} `, () => ({ annotations: !!(document.getElementById('orphanDelAnnotations') || {}).checked, notes: !!(document.getElementById('orphanDelNotes') || {}).checked })); if (!choice) return; if (!choice.annotations && !choice.notes) return; const r = await window.api.reader.purgeOrphans(choice); if (!r || !r.ok) { await confirmModal('清理失败', (r && r.error) || '未知错误'); return; } const done = r.data || {}; $('orphanStatus').textContent = `已清理 ${done.annotationsRemoved || 0} 份批注、${done.notesRemoved || 0} 本阅读资料`; $('orphanPurgeBtn').classList.add('hidden'); orphanFound = null; }; const AI_PROTOCOL_INFO = { anthropic: { label: 'Anthropic', baseUrl: 'https://api.anthropic.com/v1', model: 'claude-sonnet-4-5', hint: 'Anthropic 原生 Messages API,图像使用 base64 source 格式。' }, 'openai-responses': { label: 'OpenAI Responses', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1-mini', hint: 'OpenAI 原生 Responses API,使用 /v1/responses。' }, 'chat-completions': { label: 'OpenAI 兼容', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o-mini', hint: 'Chat Completions API,适用于 DeepSeek、Kimi、硅基流动、Ollama 等兼容服务。' } }; function syncAiProtocolUi() { const info = AI_PROTOCOL_INFO[$('aiProtocol').value] || AI_PROTOCOL_INFO['chat-completions']; $('aiBaseUrl').placeholder = info.baseUrl; $('aiModel').placeholder = info.model; $('aiProtocolHint').textContent = info.hint; } $('aiProtocol').onchange = syncAiProtocolUi; async function refreshAiStatus() { const r = await window.api.ai.status(); const s = r.ok && r.data ? r.data : null; if (!s) { $('aiStatus').textContent = '状态读取失败'; return; } $('aiProtocol').value = s.protocol || 'chat-completions'; $('aiBaseUrl').value = s.baseUrl || ''; $('aiModel').value = s.model || ''; $('aiVision').checked = !!s.vision; syncAiProtocolUi(); const ready = s.ready === undefined ? (s.hasKey || s.isLocal) : !!s.ready; const protocol = AI_PROTOCOL_INFO[s.protocol] || AI_PROTOCOL_INFO['chat-completions']; if (ready) { $('aiStatus').textContent = `已就绪 · ${protocol.label} · ${s.model}${s.vision ? ' · 支持图像' : ''}${s.hasKey ? (s.persistent ? '(Key 已加密存储)' : '(Key 仅本次运行有效)') : '(本地模型,无需 Key)'}`; } else if (s.modelConfigured) { $('aiStatus').textContent = s.keyState === 'unreadable' ? `模型已配置 · ${protocol.label} · ${s.model} · 已保存的 API Key 无法读取,请重新输入` : `模型已配置 · ${protocol.label} · ${s.model} · 尚缺 API Key`; } else { $('aiStatus').textContent = '尚未保存模型配置:请填写接口地址与模型名称'; } $('aiKey').placeholder = s.hasKey ? '已保存,留空表示不修改' : (s.keyState === 'unreadable' ? '原 Key 无法读取,请重新输入' : (s.isLocal ? '本地模型可留空' : '必填(仅本地模型可留空)')); $('aiClearBtn').classList.toggle('hidden', !s.hasKey); } $('aiSaveBtn').onclick = async () => { const btn = $('aiSaveBtn'); const keyInput = $('aiKey'); const cfg = { protocol: $('aiProtocol').value, baseUrl: $('aiBaseUrl').value.trim(), model: $('aiModel').value.trim(), vision: $('aiVision').checked }; // 留空表示不改动已存的 Key,避免用户只改模型名就把 Key 清掉 if (keyInput.value.trim()) cfg.apiKey = keyInput.value.trim(); const r = await window.api.ai.save(cfg); keyInput.value = ''; btn.textContent = r.ok ? '已保存 ✓' : '保存失败'; btn.title = r.ok ? '' : (r.error || ''); if (!r.ok) await confirmModal('保存失败', r.error || '请检查接口地址与模型名称'); await refreshAiStatus(); setTimeout(() => { btn.textContent = '保存'; }, 1500); }; $('aiClearBtn').onclick = async () => { const ok = await confirmModal('清除 AI 配置', '确定清除接口地址、模型与 API Key 吗?'); if (!ok) return; await window.api.ai.clear(); $('aiKey').value = ''; await refreshAiStatus(); }; refreshAiStatus(); async function runUpdateCheck(silent) { const statusEl = $('updateStatus'); const btn = $('checkUpdateBtn'); if (!silent) { btn.disabled = true; statusEl.textContent = '正在检查...'; } const res = await window.api.checkUpdate(); if (!silent) btn.disabled = false; if (!res.ok) { if (!silent) statusEl.textContent = '检查失败:' + res.error; return; } const { latest, hasUpdate, url } = res.data; if (hasUpdate) { statusEl.textContent = `发现新版本 ${latest}`; const ok = await openModal( '发现新版本', `检测到新版本 ${escapeHtml(latest)},是否前往 GitHub Releases 下载?
` ); if (ok) window.api.openExternal(url); } else if (!silent) { statusEl.textContent = '已是最新版本'; } } window.api.getVersion().then((r) => { if (r && r.ok) $('appVersion').textContent = 'v' + r.data; }); const autoCheckEl = $('autoCheckUpdate'); window.api.settings.get('autoCheckUpdate', false).then((r) => { autoCheckEl.checked = !!(r.ok && r.data); if (autoCheckEl.checked) runUpdateCheck(true); }); autoCheckEl.onchange = () => window.api.settings.set('autoCheckUpdate', autoCheckEl.checked); $('checkUpdateBtn').onclick = () => runUpdateCheck(false); switchTab('library');