Files
peoplelib/src/ui/views/library.js
T
lofyerandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> b8c8d24107 feat: 完善本地书库与发布更新流程
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-28 21:55:16 +08:00

137 lines
5.4 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.
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 = '<div class="empty">书库为空,去「检索」页添加文献 / 图书吧</div>';
return;
}
grid.innerHTML = items.map((it) => {
// exists 由主进程按实际磁盘状态给出:文件被手动删掉时要如实反映
const openable = (it.files || []).some((f) => f.exists);
const badge = openable
? '<span class="card-badge">已下载</span>'
: ((it.files || []).length
? '<span class="card-badge miss">文件缺失</span>'
: '<span class="card-badge miss">未下载</span>');
return `
<div class="card" data-id="${escapeHtml(it.id)}">
<div class="card-cover" style="${coverStyle(it.cover)}">${it.cover ? '' : `<div class="ph">${escapeHtml(it.title)}</div>`}</div>
<div class="card-title">${escapeHtml(it.title)}</div>
${(it.authors && it.authors.length) ? `<div class="card-sub">${escapeHtml(it.authors.slice(0, 2).join(', '))}</div>` : ''}
${badge}
<div class="lib-card-actions">
<button class="open-btn" data-act="open" ${openable ? '' : 'disabled'}>打开</button>
${openable ? '<button data-act="reveal">定位</button>' : ''}
${it.url ? '<button data-act="page">页面</button>' : ''}
<button data-act="remove">移除</button>
</div>
</div>`;
}).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('移除条目', `
<p>确定移除「${escapeHtml(it.title)}」吗?</p>
${hasFile ? '<p style="margin-top:8px"><label><input type="checkbox" id="delFiles" /> 同时删除已下载的文件</label></p>' : ''}
`, () => ({ 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('添加本地文件', `
<p>文件:${escapeHtml(p)}</p>
<input type="text" id="localTitle" placeholder="标题" value="${escapeHtml(name)}" />
<input type="text" id="localAuthor" placeholder="作者(可选)" />
`, () => ({
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;