Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
137 lines
5.4 KiB
JavaScript
137 lines
5.4 KiB
JavaScript
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;
|