const Library = (() => { let dirty = true; let sortMode = localStorage.getItem('libSortMode') || 'added'; let grid, statusEl; const SORTERS = { added: (a, b) => (b.addedAt || 0) - (a.addedAt || 0), title: (a, b) => String(a.title).localeCompare(String(b.title), 'zh'), author: (a, b) => String((a.authors || [])[0] || '').localeCompare(String((b.authors || [])[0] || ''), 'zh') }; function init() { grid = $('libGrid'); statusEl = $('libStatus'); $('addLocalBtn').onclick = addLocal; $('rescanBtn').onclick = rescan; window.api.library.onChanged(() => { dirty = true; refresh(true); }); } async function rescan() { const btn = $('rescanBtn'); btn.disabled = true; btn.textContent = '扫描中...'; const r = await window.api.library.scan(); btn.textContent = r.ok && r.data && r.data.added ? `新增 ${r.data.added} 本 ✓` : '已是最新 ✓'; dirty = true; await refresh(true); setTimeout(() => { btn.textContent = '重新扫描'; btn.disabled = false; }, 2000); } function getSortMode() { return sortMode; } function setSortMode(m) { sortMode = m; localStorage.setItem('libSortMode', m); dirty = true; refresh(true); } async function refresh(force) { if (!force && !dirty) return; const res = await window.api.library.list(); dirty = false; if (!res.ok) { statusEl.textContent = '加载失败:' + res.error; return; } const items = res.data.slice().sort(SORTERS[sortMode] || SORTERS.added); const missingCount = items.filter((it) => it.missing).length; statusEl.textContent = `共 ${items.length} 条` + (missingCount ? `,${missingCount} 条文件缺失` : ''); if (!items.length) { grid.innerHTML = '
书库为空,去「检索」页添加文献 / 图书吧
'; return; } grid.innerHTML = items.map((it) => { // exists 由主进程按实际磁盘状态给出:文件被手动删掉时要如实反映 const openable = (it.files || []).some((f) => f.exists); const badge = openable ? '已下载' : ((it.files || []).length ? '文件缺失' : '未下载'); return `
${it.cover ? '' : `
${escapeHtml(it.title)}
`}
${escapeHtml(it.title)}
${(it.authors && it.authors.length) ? `
${escapeHtml(it.authors.slice(0, 2).join(', '))}
` : ''} ${badge}
${openable ? '' : ''} ${it.url ? '' : ''}
`; }).join(''); grid.querySelectorAll('.card').forEach((el) => { const id = el.dataset.id; el.querySelectorAll('button').forEach((btn) => { btn.onclick = (e) => { e.stopPropagation(); onAction(id, btn.dataset.act); }; }); }); } async function onAction(id, act) { const res = await window.api.library.get(id); if (!res.ok || !res.data) return; const it = res.data; if (act === 'open') { const f = (it.files || []).find((x) => x.exists) || (it.files || [])[0]; if (!f) return; const r = await window.api.openPath(f.path); if (!r.ok) await confirmModal('打开失败', r.error || '无法打开该文件'); } else if (act === 'reveal') { const f = (it.files || []).find((x) => x.exists); if (f) window.api.showItem(f.path); } else if (act === 'page') { if (it.url) window.api.openExternal(it.url); } else if (act === 'remove') { const hasFile = (it.files || []).some((f) => f.path); const r = await openModal('移除条目', `

确定移除「${escapeHtml(it.title)}」吗?

${hasFile ? '

' : ''} `, () => ({ del: !!(document.getElementById('delFiles') || {}).checked })); if (!r) return; await window.api.library.remove(id, r.del); dirty = true; refresh(true); } } async function addLocal() { const r = await window.api.pickFile(); if (!r.ok || !r.data) return; const { path: p, name } = r.data; const res = await openModal('添加本地文件', `

文件:${escapeHtml(p)}

`, () => ({ title: (document.getElementById('localTitle').value || name).trim(), author: (document.getElementById('localAuthor').value || '').trim() })); if (!res) return; await window.api.library.add({ title: res.title, authors: res.author ? [res.author] : [], files: [{ path: p, name: p.split(/[\\/]/).pop(), format: (p.split('.').pop() || '').toUpperCase() }] }); dirty = true; refresh(true); } function markDirty() { dirty = true; } return { init, refresh, markDirty, getSortMode, setSortMode }; })(); window.Library = Library;