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} 本书吗?

      ${preview}
    ${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;