$('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 目录)

`, async () => { const email = $('zlibEmail').value.trim(); const password = $('zlibPassword').value; if (!email || !password) { $('zlibErr').textContent = '请输入邮箱和密码'; return false; } $('zlibErr').textContent = '登录中...'; const res = await window.api.zlib.login(email, password); if (!res.ok) { $('zlibErr').textContent = res.error || '登录失败'; return false; } if (res.data && res.data.ok === false) { $('zlibErr').textContent = res.data.error || '登录失败'; return false; } return true; }); if (r) refreshZlibStatus(); }; $('zlibLogoutBtn').onclick = async () => { const ok = await confirmModal('退出 Z-Library', '确定要清除本地保存的 Z-Library 凭据吗?'); if (ok) { await window.api.zlib.logout(); refreshZlibStatus(); } }; refreshZlibStatus(); async function refreshZlibMirrors() { const r = await window.api.zlib.getMirrors(); const custom = (r.ok && r.data && r.data.custom) || []; $('zlibMirrorStatus').textContent = custom.length ? `已自定义 ${custom.length} 个镜像,优先于内置列表` : '使用内置镜像列表'; } $('zlibMirrorBtn').onclick = async () => { const current = await window.api.zlib.getMirrors(); if (!current.ok) return; const custom = (current.data && current.data.custom) || []; const defaults = (current.data && current.data.defaults) || []; const r = await openModal('Z-Library 镜像站点', `

一行一个地址,必须是 HTTPS。自定义镜像会排在内置列表前面优先尝试;留空则只用内置列表。

内置镜像:${escapeHtml(defaults.join('、'))}
`, async () => { const lines = $('zlibMirrorList').value.split('\n').map((s) => s.trim()).filter(Boolean); const res = await window.api.zlib.setMirrors(lines); if (!res.ok) { $('zlibMirrorErr').textContent = res.error || '保存失败'; return false; } return true; }); if (r) { refreshZlibMirrors(); refreshZlibStatus(); } }; refreshZlibMirrors(); async function refreshLibraryDir() { const r = await window.api.library.getDir(); if (r.ok && r.data) $('libDirPath').textContent = r.data.dir + (r.data.isDefault ? '(默认)' : ''); } $('libDirPickBtn').onclick = async () => { const pick = await window.api.library.pickDir(); if (!pick.ok || !pick.data) return; const dest = pick.data; const cur = await window.api.library.getDir(); if (cur.ok && cur.data && cur.data.dir === dest) return; // 让用户决定旧目录里已有的书怎么处理 let migrate = false; const choice = await openModal('切换书库目录', `

新目录:

${escapeHtml(dest)}
`, () => { const sel = document.querySelector('input[name="migMode"]:checked'); return { migrate: sel && sel.value === 'migrate' }; }); if (!choice) return; migrate = choice.migrate; const res = await window.api.library.setDir(dest, migrate); if (!res.ok) { await confirmModal('切换失败', res.error || '未知错误'); return; } await refreshLibraryDir(); if (window.Library) window.Library.markDirty(); }; $('libDirOpenBtn').onclick = async () => { const r = await window.api.library.getDir(); if (r.ok && r.data) window.api.openPath(r.data.dir); }; async function refreshAskSave() { const r = await window.api.settings.get('askSavePath', false); $('askSaveChk').checked = !!(r.ok && r.data); } $('askSaveChk').onchange = () => window.api.settings.set('askSavePath', $('askSaveChk').checked); async function refreshMangadexQuality() { const r = await window.api.settings.get('mangadex.imageQuality', 'dataSaver'); $('mangadexQualitySelect').value = (r.ok && r.data === 'data') ? 'data' : 'dataSaver'; } $('mangadexQualitySelect').onchange = () => ( window.api.settings.set('mangadex.imageQuality', $('mangadexQualitySelect').value) ); async function refreshMangadexLanguage() { const r = await window.api.settings.get('mangadex.chapterLanguage', 'zh'); $('mangadexLanguageSelect').value = r.ok && ['zh', 'zh-hk', 'all'].includes(r.data) ? r.data : 'zh'; } $('mangadexLanguageSelect').onchange = () => ( window.api.settings.set('mangadex.chapterLanguage', $('mangadexLanguageSelect').value) ); refreshLibraryDir(); refreshAskSave(); refreshMangadexQuality(); refreshMangadexLanguage(); async function refreshProxy() { const r = await window.api.proxy.get(); if (r.ok) $('proxyInput').value = r.data || ''; } $('proxySaveBtn').onclick = async () => { const r = await window.api.proxy.set($('proxyInput').value.trim()); $('proxySaveBtn').textContent = r.ok ? '已保存 ✓' : '保存失败'; $('proxySaveBtn').title = r.ok ? '' : (r.error || '代理地址无效'); setTimeout(() => { $('proxySaveBtn').textContent = '保存'; }, 1500); }; refreshProxy(); async function refreshSemanticKeyStatus() { const r = await window.api.semanticScholar.keyStatus(); const status = r.ok && r.data ? r.data : { configured: false, persistent: false }; $('semanticKeyStatus').textContent = status.configured ? (status.persistent ? '已配置(由系统安全存储加密)' : '已配置(仅本次运行)') : '未配置(匿名请求容易被限流)'; $('semanticKeyClearBtn').classList.toggle('hidden', !status.configured); } $('semanticKeySaveBtn').onclick = async () => { const key = $('semanticKeyInput').value.trim(); if (!key) { $('semanticKeySaveBtn').textContent = '请输入 Key'; setTimeout(() => { $('semanticKeySaveBtn').textContent = '保存'; }, 1500); return; } const r = await window.api.semanticScholar.setKey(key); $('semanticKeyInput').value = ''; $('semanticKeySaveBtn').textContent = r.ok ? '已保存 ✓' : '保存失败'; $('semanticKeySaveBtn').title = r.ok ? '' : (r.error || '保存失败'); await refreshSemanticKeyStatus(); setTimeout(() => { $('semanticKeySaveBtn').textContent = '保存'; }, 1500); }; $('semanticKeyClearBtn').onclick = async () => { const ok = await confirmModal('清除 API Key', '确定清除 Semantic Scholar API Key 吗?'); if (!ok) return; await window.api.semanticScholar.clearKey(); await refreshSemanticKeyStatus(); }; refreshSemanticKeyStatus(); let orphanFound = null; function describeOrphans(data) { const notes = data.notes || []; const annotations = data.annotations || []; if (!notes.length && !annotations.length) return '没有残留数据'; const parts = []; if (notes.length) { const noteTotal = notes.reduce((sum, item) => sum + item.notes, 0); parts.push(`${notes.length} 本已移除书籍留有阅读资料(含 ${noteTotal} 条笔记)`); } if (annotations.length) { const mb = (data.totalBytes || 0) / 1024 / 1024; const size = mb >= 0.1 ? `${mb.toFixed(1)} MB` : `${Math.round((data.totalBytes || 0) / 1024)} KB`; parts.push(`${annotations.length} 份孤立批注(约 ${size})`); } return parts.join(';'); } $('orphanScanBtn').onclick = async () => { const btn = $('orphanScanBtn'); btn.disabled = true; btn.textContent = '检查中...'; const r = await window.api.reader.orphanReport(); btn.disabled = false; btn.textContent = '检查'; if (!r || !r.ok) { $('orphanStatus').textContent = `检查失败:${(r && r.error) || '未知错误'}`; return; } orphanFound = r.data; $('orphanStatus').textContent = describeOrphans(r.data); const hasAny = (r.data.notes || []).length > 0 || (r.data.annotations || []).length > 0; $('orphanPurgeBtn').classList.toggle('hidden', !hasAny); }; $('orphanPurgeBtn').onclick = async () => { if (!orphanFound) return; const notes = orphanFound.notes || []; const annotations = orphanFound.annotations || []; // 笔记是用户创作,删除不可撤销,必须让用户单独确认这一项 const lines = []; if (annotations.length) lines.push(`
  • ${annotations.length} 份孤立批注
  • `); if (notes.length) { const noteTotal = notes.reduce((sum, item) => sum + item.notes, 0); lines.push(`
  • ${notes.length} 本已移除书籍的阅读资料,含 ${noteTotal} 条笔记
  • `); } const choice = await openModal('清理残留阅读资料', `

    检查到:

    ${notes.length ? `

    这些笔记目前仍可在「我的笔记」中查看,删除后无法恢复。

    ` : ''} `, () => ({ 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');