const Library = (() => {
let dirty = true;
let sortMode = localStorage.getItem('libSortMode') || 'added';
let shelves = [];
let libraryTags = [];
let selectedShelf = '';
let selectedTag = '';
let searchQuery = '';
let refreshSeq = 0;
let grid, statusEl;
let selectMode = false;
const selectedIds = new Set();
let visibleIds = [];
// 必须与 main.js 的 READABLE_EXT 保持一致。漏掉格式不会报错,
// 只是「阅读」按钮和封面点击静默消失,看上去像阅读器打不开这类文件
const READABLE_RE = /\.(pdf|epub|mobi|azw|azw3|txt|md)$/i;
const isReadableFile = (file) => !!file && file.exists
&& READABLE_RE.test(file.path || file.name || '');
const SORTERS = {
recent: (a, b) => (
(b.lastReadAt || 0) - (a.lastReadAt || 0)
|| (b.addedAt || 0) - (a.addedAt || 0)
),
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')
};
const CARD_ICONS = {
read: '',
open: '',
reveal: '',
page: '',
organize: '',
remove: ''
};
function cardAction(action, label, primary = false, disabled = false) {
return ``;
}
function init() {
grid = $('libGrid');
statusEl = $('libStatus');
$('addLocalBtn').onclick = addLocal;
$('rescanBtn').onclick = rescan;
$('addShelfBtn').onclick = addShelf;
$('addTagBtn').onclick = addTag;
$('librarySearchBtn').onclick = applySearch;
$('libraryClearSearchBtn').onclick = clearSearch;
$('librarySearchInput').onkeydown = (event) => {
if (event.key === 'Enter') applySearch();
};
$('librarySearchInput').onsearch = () => {
if (!$('librarySearchInput').value) clearSearch();
};
document.querySelectorAll('#libraryTab .library-filter[data-shelf]').forEach((button) => {
button.onclick = () => selectShelf(button.dataset.shelf || '');
});
$('librarySelectModeBtn').onclick = () => setSelectMode(!selectMode);
$('librarySelectExitBtn').onclick = () => setSelectMode(false);
$('librarySelectAll').onchange = (event) => {
if (event.target.checked) visibleIds.forEach((id) => selectedIds.add(id));
else selectedIds.clear();
syncSelectionUi();
};
$('libraryBulkOrganizeBtn').onclick = bulkOrganize;
$('libraryBulkRemoveBtn').onclick = bulkRemove;
window.api.library.onChanged(() => { dirty = true; refresh(true); });
if (window.api.reader && window.api.reader.onNotesChanged) {
window.api.reader.onNotesChanged(() => {
dirty = true;
if (!$('libraryTab').classList.contains('hidden')) 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);
}
function noteCountsOf(result) {
const counts = new Map();
if (!result || !result.ok || !result.data) return counts;
let data = result.data.counts || result.data;
if (Array.isArray(data)) {
data.forEach((item) => {
if (Array.isArray(item)) {
counts.set(String(item[0]), Math.max(0, Math.floor(Number(item[1]) || 0)));
} else if (item && (item.entryId != null || item.id != null)) {
const entryId = item.entryId == null ? item.id : item.entryId;
const count = item.count == null ? item.noteCount : item.count;
counts.set(String(entryId), Math.max(0, Math.floor(Number(count) || 0)));
}
});
return counts;
}
if (data && typeof data === 'object') {
Object.entries(data).forEach(([entryId, count]) => {
if (count && typeof count === 'object') count = count.count == null ? count.noteCount : count.count;
counts.set(String(entryId), Math.max(0, Math.floor(Number(count) || 0)));
});
}
return counts;
}
function normalizedSearch(value) {
return String(value || '')
.normalize('NFKC')
.toLocaleLowerCase()
.replace(/\s+/g, ' ')
.trim();
}
function isSubsequence(needle, haystack) {
if (!needle || !haystack) return false;
let offset = 0;
for (const character of needle) {
offset = haystack.indexOf(character, offset);
if (offset < 0) return false;
offset++;
}
return true;
}
function matchesSearch(item, query) {
const normalized = normalizedSearch(query);
if (!normalized) return true;
const title = normalizedSearch(item.title);
const authors = normalizedSearch((item.authors || []).join(' '));
const combined = `${title} ${authors}`.trim();
return normalized.split(' ').every((token) => (
title.includes(token)
|| authors.includes(token)
|| (token.length > 1 && isSubsequence(token, combined))
));
}
function applySearch() {
searchQuery = $('librarySearchInput').value.trim();
$('libraryClearSearchBtn').classList.toggle('hidden', !searchQuery);
dirty = true;
refresh(true);
}
function clearSearch() {
searchQuery = '';
$('librarySearchInput').value = '';
$('libraryClearSearchBtn').classList.add('hidden');
dirty = true;
refresh(true);
}
function setSelectMode(on) {
selectMode = !!on;
if (!selectMode) selectedIds.clear();
$('libraryTab').classList.toggle('select-mode', selectMode);
$('librarySelectionBar').classList.toggle('hidden', !selectMode);
const toggle = $('librarySelectModeBtn');
toggle.classList.toggle('active', selectMode);
toggle.setAttribute('aria-pressed', String(selectMode));
dirty = true;
refresh(true);
}
function syncSelectionUi() {
if (!selectMode) return;
const total = visibleIds.length;
const picked = visibleIds.filter((id) => selectedIds.has(id)).length;
const all = $('librarySelectAll');
all.checked = total > 0 && picked === total;
all.indeterminate = picked > 0 && picked < total;
all.disabled = total === 0;
$('librarySelectAllLabel').textContent = all.checked ? '取消全选' : '全选';
$('librarySelectionCount').textContent = picked
? `已选择 ${picked} 项${picked === total ? '(当前全部)' : ''}`
: '未选择';
$('libraryBulkOrganizeBtn').disabled = !picked;
$('libraryBulkRemoveBtn').disabled = !picked;
grid.querySelectorAll(':scope > .card').forEach((card) => {
const on = selectedIds.has(card.dataset.id);
card.classList.toggle('selected', on);
const box = card.querySelector('.card-select input');
if (box) box.checked = on;
});
}
function toggleSelection(id, on) {
if (on === undefined) on = !selectedIds.has(id);
if (on) selectedIds.add(id);
else selectedIds.delete(id);
syncSelectionUi();
}
function selectedEntries(items) {
return items.filter((item) => selectedIds.has(String(item.id)));
}
function cardHtml(it, noteCounts, annotationCounts) {
const openable = (it.files || []).some((file) => file.exists);
const readable = (it.files || []).some(isReadableFile);
const badge = openable
? '已下载'
: ((it.files || []).length
? '文件缺失'
: '未下载');
const noteCount = noteCounts.get(String(it.id)) || 0;
const noteBadge = noteCount > 0
? `笔记 ${noteCount}`
: '';
const annotationCount = annotationCounts.get(String(it.id)) || 0;
const annotationBadge = annotationCount > 0
? `批注 ${annotationCount}`
: '';
const tagBadges = (it.tags || []).slice(0, 3)
.map((tag) => `${escapeHtml(tag)}`)
.join('');
const selectBox = selectMode
? ``
: '';
// 多选时封面不再触发阅读,否则勾选途中容易误开阅读器
const coverActs = readable && !selectMode;
return `
${selectBox}
${it.cover ? '' : `
${escapeHtml(it.title)}
`}
${tagBadges ? `
${tagBadges}
` : ''}
${badge}
${annotationBadge}${noteBadge}
${escapeHtml(it.title)}
${(it.authors && it.authors.length) ? `
${escapeHtml(it.authors.slice(0, 2).join(', '))}
` : ''}
${selectMode ? '' : `
${readable ? cardAction('read', '阅读', true) : ''}
${cardAction('open', readable ? '外部打开' : '打开', !readable, !openable)}
${openable ? cardAction('reveal', '在文件夹中显示') : ''}
${it.url ? cardAction('page', '打开来源页面') : ''}
${cardAction('organize', '整理书架和标签')}
${cardAction('remove', '移除书籍')}
`}
`;
}
function bindCard(card) {
const id = card.dataset.id;
card.querySelectorAll('button').forEach((button) => {
button.onclick = (event) => {
event.stopPropagation();
onAction(id, button.dataset.act);
};
});
if (selectMode) {
const box = card.querySelector('.card-select input');
box.onclick = (event) => event.stopPropagation();
box.onchange = () => toggleSelection(id, box.checked);
card.onclick = () => toggleSelection(id);
return;
}
const cover = card.querySelector('.card-cover[data-act="read"]');
if (!cover) return;
cover.onclick = (event) => {
event.stopPropagation();
onAction(id, 'read');
};
cover.onkeydown = (event) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
onAction(id, 'read');
};
}
function reconcileCards(items, noteCounts, annotationCounts) {
const existing = new Map(
Array.from(grid.querySelectorAll(':scope > .card')).map((card) => [card.dataset.id, card])
);
const keep = new Set();
items.forEach((item, index) => {
const id = String(item.id);
const markup = cardHtml(item, noteCounts, annotationCounts).trim();
let card = existing.get(id);
if (!card || card.__peoplelibMarkup !== markup) {
const template = document.createElement('template');
template.innerHTML = markup;
const replacement = template.content.firstElementChild;
replacement.__peoplelibMarkup = markup;
bindCard(replacement);
if (card) card.replaceWith(replacement);
card = replacement;
}
keep.add(id);
const expected = grid.children[index] || null;
if (expected !== card) grid.insertBefore(card, expected);
});
Array.from(grid.querySelectorAll(':scope > .card')).forEach((card) => {
if (!keep.has(card.dataset.id)) card.remove();
});
Array.from(grid.children).forEach((child) => {
if (!child.classList.contains('card')) child.remove();
});
}
async function refresh(force) {
if (!force && !dirty) return;
const currentRefresh = ++refreshSeq;
const [res, noteCountResult, annotationCountResult, shelfResult, tagResult] = await Promise.all([
window.api.library.list(),
window.api.reader.getNoteCounts().catch(() => null),
window.api.reader.getAnnotationCounts().catch(() => null),
window.api.library.listShelves(),
window.api.library.listTags()
]);
if (currentRefresh !== refreshSeq) return;
dirty = false;
if (!res.ok) { statusEl.textContent = '加载失败:' + res.error; return; }
shelves = shelfResult && shelfResult.ok && Array.isArray(shelfResult.data)
? shelfResult.data
: [];
libraryTags = tagResult && tagResult.ok && Array.isArray(tagResult.data)
? tagResult.data
: [];
if (selectedShelf && selectedShelf !== '__uncategorized__'
&& !shelves.some((shelf) => shelf.id === selectedShelf)) selectedShelf = '';
if (selectedTag && !libraryTags.some((tag) => tag.name === selectedTag)) selectedTag = '';
renderOrganizationSidebar();
const noteCounts = noteCountsOf(noteCountResult);
const annotationCounts = noteCountsOf(annotationCountResult);
const allItems = res.data.slice();
const scopedItems = allItems.filter((item) => {
if (selectedShelf === '__uncategorized__' && item.shelfId) return false;
if (selectedShelf && selectedShelf !== '__uncategorized__' && item.shelfId !== selectedShelf) return false;
if (selectedTag && !(item.tags || []).some((tag) => (
String(tag).toLocaleLowerCase() === selectedTag.toLocaleLowerCase()
))) return false;
return true;
});
const items = scopedItems
.filter((item) => matchesSearch(item, searchQuery))
.sort(SORTERS[sortMode] || SORTERS.added);
const missingCount = items.filter((it) => it.missing).length;
statusEl.textContent = searchQuery
? `搜索“${searchQuery}”显示 ${items.length} 条,当前分类 ${scopedItems.length} 条`
+ `${missingCount ? `,其中 ${missingCount} 条文件缺失` : ''}`
: (items.length === allItems.length
? `共 ${allItems.length} 条${missingCount ? `,${missingCount} 条文件缺失` : ''}`
: `显示 ${items.length} 条,共 ${allItems.length} 条${missingCount ? `,当前 ${missingCount} 条文件缺失` : ''}`);
// 全选只覆盖当前筛选结果;条目被筛掉或删除后,其选中状态一并作废,
// 避免对看不见的书执行批量操作
visibleIds = items.map((item) => String(item.id));
const visible = new Set(visibleIds);
Array.from(selectedIds).forEach((id) => { if (!visible.has(id)) selectedIds.delete(id); });
if (!items.length) {
grid.innerHTML = `${allItems.length
? (searchQuery ? '没有匹配标题或作者的书籍' : '当前分类中没有书籍')
: '书库为空,去「检索」页添加文献 / 图书吧'}
`;
syncSelectionUi();
return;
}
reconcileCards(items, noteCounts, annotationCounts);
syncSelectionUi();
}
function selectShelf(id) {
selectedShelf = String(id || '');
selectedTag = '';
dirty = true;
refresh(true);
}
function selectTag(name) {
selectedTag = selectedTag === name ? '' : name;
selectedShelf = '';
dirty = true;
refresh(true);
}
function renderOrganizationSidebar() {
document.querySelectorAll('#libraryTab .library-filter[data-shelf]').forEach((button) => {
button.classList.toggle(
'active',
!selectedTag && (button.dataset.shelf || '') === selectedShelf
);
});
const shelfList = $('libraryShelfList');
shelfList.textContent = '';
shelves.forEach((shelf) => {
const row = document.createElement('div');
row.className = 'library-shelf-row';
const filter = document.createElement('button');
filter.type = 'button';
filter.className = 'library-filter';
filter.classList.toggle('active', !selectedTag && selectedShelf === shelf.id);
filter.textContent = shelf.name;
filter.title = shelf.name;
filter.onclick = () => selectShelf(shelf.id);
const actions = document.createElement('div');
actions.className = 'library-shelf-actions';
const rename = document.createElement('button');
rename.type = 'button';
rename.className = 'notes-icon-btn';
rename.textContent = '✎';
rename.title = '重命名';
rename.setAttribute('aria-label', `重命名${shelf.name}`);
rename.onclick = () => renameShelf(shelf);
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'notes-icon-btn danger';
remove.textContent = '×';
remove.title = '删除';
remove.setAttribute('aria-label', `删除${shelf.name}`);
remove.onclick = () => deleteShelf(shelf);
actions.append(rename, remove);
row.append(filter, actions);
shelfList.appendChild(row);
});
const tagList = $('libraryTagList');
tagList.textContent = '';
if (!libraryTags.length) {
const empty = document.createElement('div');
empty.className = 'library-filter';
empty.textContent = '暂无标签';
tagList.appendChild(empty);
return;
}
libraryTags.forEach((tag) => {
const row = document.createElement('div');
row.className = 'library-tag-row';
const button = document.createElement('button');
button.type = 'button';
button.className = 'library-filter';
button.classList.toggle('active', selectedTag === tag.name);
const count = document.createElement('span');
count.className = 'library-filter-count';
count.textContent = String(tag.count || 0);
const name = document.createElement('span');
name.textContent = `# ${tag.name}`;
button.append(name, count);
button.onclick = () => selectTag(tag.name);
const actions = document.createElement('div');
actions.className = 'library-tag-actions';
const rename = document.createElement('button');
rename.type = 'button';
rename.className = 'notes-icon-btn';
rename.textContent = '✎';
rename.title = '重命名';
rename.setAttribute('aria-label', `重命名${tag.name}`);
rename.onclick = () => renameTag(tag);
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'notes-icon-btn danger';
remove.textContent = '×';
remove.title = '删除';
remove.setAttribute('aria-label', `删除${tag.name}`);
remove.onclick = () => deleteTag(tag);
actions.append(rename, remove);
row.append(button, actions);
tagList.appendChild(row);
});
}
async function addShelf() {
const result = await openModal('新建书架', `
书架是单层分类,一本书只能放在一个书架中。
`, async () => {
const name = $('libraryShelfName').value.trim();
if (!name) { $('libraryShelfError').textContent = '请输入书架名称'; return false; }
const response = await window.api.library.addShelf({ name });
if (!response || !response.ok) {
$('libraryShelfError').textContent = (response && response.error) || '新建失败';
return false;
}
return response.data;
});
if (!result) return;
selectedShelf = result.id;
selectedTag = '';
dirty = true;
await refresh(true);
}
async function renameShelf(shelf) {
const result = await openModal('重命名书架', `
`, async () => {
const name = $('libraryShelfName').value.trim();
if (!name) { $('libraryShelfError').textContent = '请输入书架名称'; return false; }
const response = await window.api.library.updateShelf(shelf.id, { name });
if (!response || !response.ok) {
$('libraryShelfError').textContent = (response && response.error) || '重命名失败';
return false;
}
return true;
});
if (!result) return;
dirty = true;
await refresh(true);
}
async function deleteShelf(shelf) {
const ok = await confirmModal(
'删除书架',
`确定删除「${shelf.name}」吗?书籍不会被删除,将移至「未分类」。`
);
if (!ok) return;
const result = await window.api.library.removeShelf(shelf.id);
if (!result || !result.ok) {
await confirmModal('删除失败', (result && result.error) || '未知错误');
return;
}
if (selectedShelf === shelf.id) selectedShelf = '__uncategorized__';
dirty = true;
await refresh(true);
}
async function addTag() {
const result = await openModal('新建标签', `
标签可以同时分配给多本书。
`, async () => {
const name = $('libraryTagName').value.trim();
if (!name) { $('libraryTagError').textContent = '请输入标签名称'; return false; }
const response = await window.api.library.addTag({ name });
if (!response || !response.ok) {
$('libraryTagError').textContent = (response && response.error) || '新建失败';
return false;
}
return response.data;
});
if (!result) return;
selectedTag = result.name;
selectedShelf = '';
dirty = true;
await refresh(true);
}
async function renameTag(tag) {
const result = await openModal('重命名标签', `
`, async () => {
const name = $('libraryTagName').value.trim();
if (!name) { $('libraryTagError').textContent = '请输入标签名称'; return false; }
const response = await window.api.library.updateTag(tag.id, { name });
if (!response || !response.ok) {
$('libraryTagError').textContent = (response && response.error) || '重命名失败';
return false;
}
return response.data;
});
if (!result) return;
if (selectedTag === tag.name) selectedTag = result.name;
dirty = true;
await refresh(true);
}
async function deleteTag(tag) {
const ok = await confirmModal(
'删除标签',
`确定删除「${tag.name}」吗?该标签会从所有书籍中移除,书籍不会被删除。`
);
if (!ok) return;
const result = await window.api.library.removeTag(tag.id);
if (!result || !result.ok) {
await confirmModal('删除失败', (result && result.error) || '未知错误');
return;
}
if (selectedTag === tag.name) selectedTag = '';
dirty = true;
await refresh(true);
}
// 批量整理:标签用三态复选框。indeterminate 表示"部分书有该标签",
// 保持这个状态就不改动这些书原有的标签;只有用户明确勾上或取消才统一应用。
async function bulkOrganize() {
const listed = await window.api.library.list();
if (!listed || !listed.ok) {
await confirmModal('整理失败', (listed && listed.error) || '无法读取书库');
return;
}
const targets = selectedEntries(listed.data || []);
if (!targets.length) return;
const shelfIds = new Set(targets.map((item) => item.shelfId || ''));
const sharedShelf = shelfIds.size === 1 ? [...shelfIds][0] : null;
const options = shelves.map((shelf) => (
``
)).join('');
const counts = new Map();
targets.forEach((item) => (item.tags || []).forEach((tag) => {
const key = String(tag).toLocaleLowerCase();
counts.set(key, (counts.get(key) || 0) + 1);
}));
const tagOptions = libraryTags.map((tag) => {
const held = counts.get(tag.name.toLocaleLowerCase()) || 0;
const state = held === 0 ? 'none' : (held === targets.length ? 'all' : 'some');
return ``;
}).join('');
// indeterminate 只能用 DOM 属性设置,HTML 里写不出来;
// 点击后按 部分 -> 全选 -> 全不选 -> 部分 循环,保证"保持原样"始终可回到
const primeTagBoxes = () => {
document.querySelectorAll('#libraryBulkTags input[type="checkbox"]').forEach((box) => {
if (box.dataset.state !== 'some') return;
box.indeterminate = true;
box.onclick = () => {
const phase = box.dataset.phase || 'some';
const next = phase === 'some' ? 'all' : (phase === 'all' ? 'none' : 'some');
box.dataset.phase = next;
box.indeterminate = next === 'some';
box.checked = next === 'all';
};
});
};
const pending = openModal(`批量整理 ${targets.length} 本`, `
`, async () => {
const boxes = Array.from(document.querySelectorAll('#libraryBulkTags input[type="checkbox"]'));
const add = [];
const strip = [];
boxes.forEach((box) => {
if (box.indeterminate) return;
if (box.checked) add.push(box.value);
else if (box.dataset.state !== 'none') strip.push(box.value.toLocaleLowerCase());
});
const shelfValue = $('libraryBulkShelf').value;
const errorEl = $('libraryBulkError');
// 一次提交:逐条 update 会把整个书库索引重写 N 遍
const patches = targets.map((item) => {
const patch = {};
if (shelfValue !== '__keep__') patch.shelfId = shelfValue || null;
const kept = (item.tags || []).filter((tag) => !strip.includes(String(tag).toLocaleLowerCase()));
const merged = kept.slice();
add.forEach((tag) => {
if (!merged.some((existing) => String(existing).toLocaleLowerCase() === tag.toLocaleLowerCase())) {
merged.push(tag);
}
});
patch.tags = merged;
return { id: item.id, patch };
});
const response = await window.api.library.updateMany(patches);
if (!response || !response.ok) {
errorEl.textContent = (response && response.error) || '保存失败';
return false;
}
return true;
});
primeTagBoxes();
if (!await pending) return;
setSelectMode(false);
}
async function bulkRemove() {
const listed = await window.api.library.list();
if (!listed || !listed.ok) {
await confirmModal('移除失败', (listed && listed.error) || '无法读取书库');
return;
}
const targets = selectedEntries(listed.data || []);
if (!targets.length) return;
const withFiles = targets.filter((item) => (item.files || []).some((file) => file.path)).length;
const preview = targets.slice(0, 5).map((item) => `${escapeHtml(item.title)}`).join('');
const choice = await openModal('批量移除', `
确定移除以下 ${targets.length} 本书吗?
${targets.length > 5 ? `另有 ${targets.length - 5} 本未列出
` : ''}
${withFiles ? `` : ''}
默认保留阅读资料,移除后仍可在「我的笔记」中查看。
`, () => ({
deleteFiles: !!(document.getElementById('bulkDelFiles') || {}).checked,
deleteReadingData: !!document.getElementById('bulkDelReadingData').checked
}));
if (!choice) return;
const removed = await window.api.library.removeMany(targets.map((item) => item.id), choice);
setSelectMode(false);
if (!removed || !removed.ok) {
await confirmModal('移除失败', (removed && removed.error) || '未知错误');
return;
}
statusEl.textContent = `已移除 ${(removed.data && removed.data.removed) || targets.length} 本`;
}
async function organizeBook(item) {
const options = shelves.map((shelf) => (
``
)).join('');
const selectedTags = new Set((item.tags || []).map((tag) => String(tag).toLocaleLowerCase()));
const tagOptions = libraryTags.map((tag) => (
``
)).join('');
const result = await openModal('整理书籍', `
`, async () => {
const tags = Array.from(
document.querySelectorAll('#libraryBookTags input[type="checkbox"]:checked')
).map((input) => input.value);
const response = await window.api.library.update(item.id, {
shelfId: $('libraryBookShelf').value || null,
tags
});
if (!response || !response.ok) {
$('libraryOrganizeError').textContent = (response && response.error) || '保存失败';
return false;
}
return true;
});
if (!result) return;
dirty = true;
await refresh(true);
}
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 === 'read') {
const files = it.files || [];
const idx = files.findIndex(isReadableFile);
const r = await window.api.reader.open(id, idx >= 0 ? idx : undefined);
if (!r.ok) await confirmModal('无法阅读', r.error || '打开阅读器失败');
} else 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 === 'organize') {
await organizeBook(it);
} else if (act === 'remove') {
const hasFile = (it.files || []).some((f) => f.path);
const r = await openModal('移除条目', `
确定移除「${escapeHtml(it.title)}」吗?
${hasFile ? '' : ''}
默认保留阅读资料,移除后仍可在「我的笔记」中查看。
`, () => ({
deleteFiles: !!(document.getElementById('delFiles') || {}).checked,
deleteReadingData: !!document.getElementById('delReadingData').checked
}));
if (!r) return;
const removed = await window.api.library.remove(id, r);
if (!removed || !removed.ok) {
await confirmModal('移除失败', (removed && removed.error) || '未知错误');
return;
}
dirty = true;
refresh(true);
}
}
async function addLocal() {
const source = await openModal('添加本地内容', `
可以选择一个或多个文件,也可以递归导入整个文件夹。
`, () => document.querySelector('input[name="localImportSource"]:checked').value);
if (!source) return;
const picked = await window.api.library.pickLocal(source);
if (!picked || !picked.ok) {
await confirmModal('无法导入', (picked && picked.error) || '选择本地内容失败');
return;
}
if (!picked.data) return;
const selection = picked.data;
const sample = Array.isArray(selection.sample) ? selection.sample : [];
const single = selection.count === 1 && sample[0];
const displayPaths = (selection.paths || []).slice(0, 3)
.map((value) => `${escapeHtml(value)}
`).join('');
const options = await openModal('导入本地图书', `
发现 ${selection.count} 个支持的图书文件。
${displayPaths}
${single ? `
` : ''}
`, () => ({
organization: document.querySelector(
'input[name="localImportOrganization"]:checked'
).value,
title: single ? $('localTitle').value.trim() : '',
author: single ? $('localAuthor').value.trim() : ''
}));
if (!options) return;
const result = await window.api.library.importLocal(selection.selectionId, options);
if (!result || !result.ok) {
await confirmModal('导入失败', (result && result.error) || '未知错误');
return;
}
dirty = true;
await refresh(true);
const data = result.data || {};
statusEl.textContent = `已导入 ${data.added || 0} 本`
+ (data.skippedDuplicates ? `,跳过 ${data.skippedDuplicates} 个重复文件` : '')
+ ((data.skipped || 0) > (data.skippedDuplicates || 0)
? `,另跳过 ${(data.skipped || 0) - (data.skippedDuplicates || 0)} 个无效项`
: '');
}
function markDirty() { dirty = true; }
return { init, refresh, markDirty, getSortMode, setSortMode };
})();
window.Library = Library;