$('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);
});
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 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);
refreshLibraryDir();
refreshAskSave();
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();
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');