feat: 缺失文件扫描支持清理并发布 v2.1.1
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
This commit is contained in:
@@ -74,7 +74,7 @@ DRM 保护的 MOBI/AZW/AZW3、KFX、Topaz 以及损坏或不兼容的文件不
|
|||||||
2. 双击目录中的 `PeopleLib.exe`。
|
2. 双击目录中的 `PeopleLib.exe`。
|
||||||
3. 保留整个程序目录,不要只移动 exe。用户数据默认保存在程序同级的 `data/`。
|
3. 保留整个程序目录,不要只移动 exe。用户数据默认保存在程序同级的 `data/`。
|
||||||
|
|
||||||
当前版本为 **2.1.0**。可在「设置」中手动检查更新,也可启用启动时自动检查。检测到新版本后,应用会打开对应的 GitHub Release 下载页,更新前请退出旧版本并覆盖程序文件,`data/` 目录无需替换。
|
当前版本为 **2.1.1**。可在「设置」中手动检查更新,也可启用启动时自动检查。检测到新版本后,应用会打开对应的 GitHub Release 下载页,更新前请退出旧版本并覆盖程序文件,`data/` 目录无需替换。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|||||||
@@ -506,6 +506,13 @@ ipcMain.handle('library:scan', () => wrap(() => {
|
|||||||
for (const job of coverGenerator.ensureAll()) job.catch(() => {});
|
for (const job of coverGenerator.ensureAll()) job.catch(() => {});
|
||||||
return r;
|
return r;
|
||||||
}));
|
}));
|
||||||
|
ipcMain.handle('library:removeMissing', () => wrap(async () => {
|
||||||
|
const ids = library.list()
|
||||||
|
.filter((item) => item.missing)
|
||||||
|
.map((item) => String(item.id));
|
||||||
|
for (const id of ids) await requestReaderPurge(id);
|
||||||
|
return library.removeMissing();
|
||||||
|
}));
|
||||||
|
|
||||||
// 本地书库
|
// 本地书库
|
||||||
ipcMain.handle('library:list', () => wrap(() => library.list().map((item) => ({
|
ipcMain.handle('library:list', () => wrap(() => library.list().map((item) => ({
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foliate-js": "1.0.1",
|
"foliate-js": "1.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"description": "多源开放文献、电子书与本地书库客户端",
|
"description": "多源开放文献、电子书与本地书库客户端",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "peoplelib",
|
"author": "peoplelib",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ipcRenderer.invoke('library:importLocal', selectionId, options)
|
ipcRenderer.invoke('library:importLocal', selectionId, options)
|
||||||
),
|
),
|
||||||
scan: () => ipcRenderer.invoke('library:scan'),
|
scan: () => ipcRenderer.invoke('library:scan'),
|
||||||
|
removeMissing: () => ipcRenderer.invoke('library:removeMissing'),
|
||||||
onChanged: (cb) => {
|
onChanged: (cb) => {
|
||||||
const h = () => cb();
|
const h = () => cb();
|
||||||
ipcRenderer.on('library:changed', h);
|
ipcRenderer.on('library:changed', h);
|
||||||
|
|||||||
@@ -357,6 +357,66 @@ app.whenReady().then(async () => {
|
|||||||
.every((title) => allTitles.includes(title))
|
.every((title) => allTitles.includes(title))
|
||||||
&& (await js("document.getElementById('libStatus').textContent")) === '共 3 条'
|
&& (await js("document.getElementById('libStatus').textContent")) === '共 3 条'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rescanMissingFile = path.join(TMP, 'rescan-missing.txt');
|
||||||
|
fs.writeFileSync(rescanMissingFile, 'rescan missing fixture');
|
||||||
|
const rescanMissingShelf = library.addShelf({ name: 'Rescan Missing Shelf' });
|
||||||
|
const rescanMissingBook = library.add({
|
||||||
|
title: 'Rescan Missing Book',
|
||||||
|
shelfId: rescanMissingShelf.id,
|
||||||
|
tags: ['Rescan Missing Tag'],
|
||||||
|
files: [{ path: rescanMissingFile, name: 'rescan-missing.txt', format: 'TXT' }]
|
||||||
|
});
|
||||||
|
await pollJs(
|
||||||
|
'缺失扫描夹具显示在书库',
|
||||||
|
`!!document.querySelector('#libGrid .card[data-id="${rescanMissingBook.id}"]')`
|
||||||
|
);
|
||||||
|
fs.unlinkSync(rescanMissingFile);
|
||||||
|
await js("document.getElementById('rescanBtn').click()");
|
||||||
|
await waitForModal('发现文件缺失');
|
||||||
|
check(
|
||||||
|
'重新扫描报告缺失数量并说明保留阅读资料',
|
||||||
|
await js(`(() => {
|
||||||
|
const text = document.getElementById('modalBody').textContent;
|
||||||
|
return text.includes('1 条书库内容的文件均已缺失')
|
||||||
|
&& text.includes('对应分类和标签也会一并清理')
|
||||||
|
&& text.includes('笔记、书签、进度和标注会保留');
|
||||||
|
})()`)
|
||||||
|
);
|
||||||
|
await js("document.getElementById('modalCancel').click()");
|
||||||
|
await pollJs(
|
||||||
|
'取消缺失清理后恢复扫描按钮',
|
||||||
|
"!document.getElementById('rescanBtn').disabled",
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'取消清理保留缺失书库条目',
|
||||||
|
!!library.get(rescanMissingBook.id)
|
||||||
|
&& library.get(rescanMissingBook.id).missing
|
||||||
|
&& library.listShelves().some((shelf) => shelf.id === rescanMissingShelf.id)
|
||||||
|
&& library.listTags().some((tag) => tag.name === 'Rescan Missing Tag')
|
||||||
|
);
|
||||||
|
|
||||||
|
await js("document.getElementById('rescanBtn').click()");
|
||||||
|
await waitForModal('发现文件缺失');
|
||||||
|
await submitModal();
|
||||||
|
await poll(
|
||||||
|
'确认后清理缺失书库条目',
|
||||||
|
() => Promise.resolve(!library.get(rescanMissingBook.id))
|
||||||
|
);
|
||||||
|
await pollJs(
|
||||||
|
'缺失条目清理后书库恢复',
|
||||||
|
`!document.querySelector('#libGrid .card[data-id="${rescanMissingBook.id}"]')
|
||||||
|
&& document.querySelectorAll('#libGrid .card').length === 3`
|
||||||
|
);
|
||||||
|
check(
|
||||||
|
'缺失清理移除对应空分类和标签且不影响其余内容',
|
||||||
|
library.list().length === 3
|
||||||
|
&& library.list().every((item) => item.id !== rescanMissingBook.id)
|
||||||
|
&& !library.listShelves().some((shelf) => shelf.id === rescanMissingShelf.id)
|
||||||
|
&& !library.listTags().some((tag) => tag.name === 'Rescan Missing Tag')
|
||||||
|
);
|
||||||
|
|
||||||
await js(`(() => {
|
await js(`(() => {
|
||||||
document.getElementById('librarySearchInput').value = 'Rsrch';
|
document.getElementById('librarySearchInput').value = 'Rsrch';
|
||||||
document.getElementById('librarySearchBtn').click();
|
document.getElementById('librarySearchBtn').click();
|
||||||
|
|||||||
@@ -335,6 +335,61 @@ test('批量移除只写一次索引,可选删除库内文件', () => {
|
|||||||
assert.deepStrictEqual(store.removeMany(['missing'], false), { removed: 0 });
|
assert.deepStrictEqual(store.removeMany(['missing'], false), { removed: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('清理缺失条目时一并清理对应的空书架和标签', () => {
|
||||||
|
const root = freshRoot('remove-missing');
|
||||||
|
const emptyShelf = store.addShelf({ name: '仅缺失书籍分类' });
|
||||||
|
const sharedShelf = store.addShelf({ name: '仍在使用分类' });
|
||||||
|
const unrelatedShelf = store.addShelf({ name: '无关空分类' });
|
||||||
|
store.addTag({ name: '仅缺失书籍标签' });
|
||||||
|
store.addTag({ name: '仍在使用标签' });
|
||||||
|
store.addTag({ name: '无关空标签' });
|
||||||
|
|
||||||
|
const missingA = store.add({
|
||||||
|
title: '缺失 A',
|
||||||
|
shelfId: emptyShelf.id,
|
||||||
|
tags: ['仅缺失书籍标签', '仍在使用标签'],
|
||||||
|
files: [{ path: path.join(root, 'missing-a.pdf') }]
|
||||||
|
});
|
||||||
|
const missingB = store.add({
|
||||||
|
title: '缺失 B',
|
||||||
|
shelfId: sharedShelf.id,
|
||||||
|
tags: ['仍在使用标签'],
|
||||||
|
files: [{ path: path.join(root, 'missing-b.pdf') }]
|
||||||
|
});
|
||||||
|
const existingPath = path.join(root, 'existing.pdf');
|
||||||
|
fs.writeFileSync(existingPath, '%PDF-1.4\n');
|
||||||
|
const existing = store.add({
|
||||||
|
title: '仍存在',
|
||||||
|
shelfId: sharedShelf.id,
|
||||||
|
tags: ['仍在使用标签'],
|
||||||
|
files: [{ path: existingPath }]
|
||||||
|
});
|
||||||
|
const noFiles = store.add({ title: '无文件元数据', files: [] });
|
||||||
|
|
||||||
|
assert.deepStrictEqual(store.removeMissing(), {
|
||||||
|
removed: 2,
|
||||||
|
shelvesRemoved: 1,
|
||||||
|
tagsRemoved: 1
|
||||||
|
});
|
||||||
|
assert.strictEqual(store.get(missingA.id), null);
|
||||||
|
assert.strictEqual(store.get(missingB.id), null);
|
||||||
|
assert.ok(store.get(existing.id));
|
||||||
|
assert.ok(store.get(noFiles.id), '没有文件的元数据条目不应按文件缺失清理');
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
store.listShelves().map((shelf) => shelf.name).sort(),
|
||||||
|
[sharedShelf.name, unrelatedShelf.name].sort()
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
store.listTags().map((tag) => tag.name).sort(),
|
||||||
|
['仍在使用标签', '无关空标签'].sort()
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(store.removeMissing(), {
|
||||||
|
removed: 0,
|
||||||
|
shelvesRemoved: 0,
|
||||||
|
tagsRemoved: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('explicit tag creation enforces the existing catalog limit', () => {
|
test('explicit tag creation enforces the existing catalog limit', () => {
|
||||||
const root = freshRoot('limit');
|
const root = freshRoot('limit');
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|||||||
+13
-1
@@ -73,7 +73,7 @@ test('下载校验协议,拒绝 file:// 等非 http(s)', () => {
|
|||||||
test('下载请求使用可识别且带项目地址的 User-Agent', () => {
|
test('下载请求使用可识别且带项目地址的 User-Agent', () => {
|
||||||
assert.match(mainSrc, /UA:\s*DL_UA[\s\S]+require\('\.\/src\/sources\/http'\)/);
|
assert.match(mainSrc, /UA:\s*DL_UA[\s\S]+require\('\.\/src\/sources\/http'\)/);
|
||||||
const httpSrc = fs.readFileSync(path.join(__dirname, '..', 'sources', 'http.js'), 'utf8');
|
const httpSrc = fs.readFileSync(path.join(__dirname, '..', 'sources', 'http.js'), 'utf8');
|
||||||
assert.match(httpSrc, /PeopleLib\/2\.1\.0 \(\+https:\/\/github\.com\/lofyer\/peoplelib\)/);
|
assert.match(httpSrc, /PeopleLib\/2\.1\.1 \(\+https:\/\/github\.com\/lofyer\/peoplelib\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('移除书籍默认保留阅读资料,仅显式勾选时清理', () => {
|
test('移除书籍默认保留阅读资料,仅显式勾选时清理', () => {
|
||||||
@@ -108,6 +108,18 @@ test('批量整理与移除走单次索引提交,不按条目循环 IPC', () =
|
|||||||
'渲染层不应再逐条发 IPC');
|
'渲染层不应再逐条发 IPC');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('重新扫描清理缺失条目前在主进程重新核对磁盘状态', () => {
|
||||||
|
const start = mainSrc.indexOf("ipcMain.handle('library:removeMissing'");
|
||||||
|
assert.ok(start > 0, '缺少文件缺失清理通道');
|
||||||
|
const end = mainSrc.indexOf('ipcMain.handle(', start + 20);
|
||||||
|
const segment = mainSrc.slice(start, end < 0 ? undefined : end);
|
||||||
|
assert.match(segment, /wrap\(async \(\) =>/);
|
||||||
|
assert.match(segment, /library\.list\(\)[\s\S]*\.filter\(\(item\) => item\.missing\)/);
|
||||||
|
assert.match(segment, /await requestReaderPurge\(id\)/);
|
||||||
|
assert.match(segment, /library\.removeMissing\(\)/);
|
||||||
|
assert.doesNotMatch(segment, /readerStore\.forget|annotations\.forget|aiSessions\.forget/);
|
||||||
|
});
|
||||||
|
|
||||||
test('孤立阅读资料只报告不自动删除,清理需分别勾选', () => {
|
test('孤立阅读资料只报告不自动删除,清理需分别勾选', () => {
|
||||||
const start = mainSrc.indexOf("ipcMain.handle('reader:orphanReport'");
|
const start = mainSrc.indexOf("ipcMain.handle('reader:orphanReport'");
|
||||||
assert.ok(start > 0, '缺少孤立资料对账通道');
|
assert.ok(start > 0, '缺少孤立资料对账通道');
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ test('wikisource: 搜索使用整数偏移并携带可识别 User-Agent', async
|
|||||||
assert.strictEqual(result.items[0].subtitle, '中文');
|
assert.strictEqual(result.items[0].subtitle, '中文');
|
||||||
assert.strictEqual(result.maxPage, 500, 'MediaWiki 搜索最多允许偏移到 10000 条');
|
assert.strictEqual(result.maxPage, 500, 'MediaWiki 搜索最多允许偏移到 10000 条');
|
||||||
assert.ok(request.url.includes('sroffset=20'), request.url);
|
assert.ok(request.url.includes('sroffset=20'), request.url);
|
||||||
assert.match(request.options.headers['User-Agent'], /PeopleLib\/2\.1\.0/);
|
assert.match(request.options.headers['User-Agent'], /PeopleLib\/2\.1\.1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('wikisource: 浏览按 continuation 令牌翻页', async () => {
|
test('wikisource: 浏览按 continuation 令牌翻页', async () => {
|
||||||
|
|||||||
@@ -645,6 +645,17 @@ test('书库多选提供全选、批量整理与批量移除', () => {
|
|||||||
assert.match(library, /window\.api\.library\.updateMany\(patches\)/);
|
assert.match(library, /window\.api\.library\.updateMany\(patches\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('重新扫描发现文件缺失后提示清理书库条目', () => {
|
||||||
|
const library = fs.readFileSync(libFile, 'utf8');
|
||||||
|
const preload = fs.readFileSync(path.join(__dirname, '..', '..', 'preload.js'), 'utf8');
|
||||||
|
assert.match(library, /const missing = Math\.max\(0, Number\(\(r\.data \|\| \{\}\)\.missing\)/);
|
||||||
|
assert.match(library, /confirmModal\(\s*'发现文件缺失'/);
|
||||||
|
assert.match(library, /对应分类和标签也会一并清理/);
|
||||||
|
assert.match(library, /笔记、书签、进度和标注会保留/);
|
||||||
|
assert.match(library, /window\.api\.library\.removeMissing\(\)/);
|
||||||
|
assert.match(preload, /removeMissing: \(\) => ipcRenderer\.invoke\('library:removeMissing'\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test('卡片定位上下文不随多选状态消失', () => {
|
test('卡片定位上下文不随多选状态消失', () => {
|
||||||
const css = fs.readFileSync(path.join(__dirname, '..', 'ui', 'style.css'), 'utf8');
|
const css = fs.readFileSync(path.join(__dirname, '..', 'ui', 'style.css'), 'utf8');
|
||||||
// 退出多选是同步移除 class,重绘要等 IPC;若定位上下文只在 select-mode 下建立,
|
// 退出多选是同步移除 class,重绘要等 IPC;若定位上下文只在 select-mode 下建立,
|
||||||
|
|||||||
+33
-1
@@ -978,6 +978,38 @@ function removeMany(ids, deleteFiles) {
|
|||||||
return { removed: doomed.length };
|
return { removed: doomed.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeMissing() {
|
||||||
|
load();
|
||||||
|
const doomed = items.filter((item) => {
|
||||||
|
const files = item.files || [];
|
||||||
|
return files.length > 0 && files.every((file) => !fs.existsSync(toAbsolute(file.path)));
|
||||||
|
});
|
||||||
|
if (!doomed.length) return { removed: 0, shelvesRemoved: 0, tagsRemoved: 0 };
|
||||||
|
|
||||||
|
const ids = new Set(doomed.map((item) => item.id));
|
||||||
|
const candidateShelves = new Set(doomed.map((item) => item.shelfId).filter(Boolean));
|
||||||
|
const candidateTags = new Set(doomed.flatMap((item) => normalizeTags(item.tags).map(tagKey)));
|
||||||
|
const nextItems = items.filter((item) => !ids.has(item.id));
|
||||||
|
const usedShelves = new Set(nextItems.map((item) => item.shelfId).filter(Boolean));
|
||||||
|
const usedTags = new Set(nextItems.flatMap((item) => normalizeTags(item.tags).map(tagKey)));
|
||||||
|
const nextShelves = shelves.filter((shelf) => (
|
||||||
|
!candidateShelves.has(shelf.id) || usedShelves.has(shelf.id)
|
||||||
|
));
|
||||||
|
const nextTags = tags.filter((tag) => (
|
||||||
|
!candidateTags.has(tagKey(tag.name)) || usedTags.has(tagKey(tag.name))
|
||||||
|
));
|
||||||
|
const shelvesRemoved = shelves.length - nextShelves.length;
|
||||||
|
const tagsRemoved = tags.length - nextTags.length;
|
||||||
|
|
||||||
|
commit(nextItems, true, nextShelves, nextTags);
|
||||||
|
for (const item of doomed) removeCoverFile(item.id);
|
||||||
|
return {
|
||||||
|
removed: doomed.length,
|
||||||
|
shelvesRemoved,
|
||||||
|
tagsRemoved
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function remove(id, deleteFiles) {
|
function remove(id, deleteFiles) {
|
||||||
load();
|
load();
|
||||||
const it = items.find((x) => x.id === id);
|
const it = items.find((x) => x.id === id);
|
||||||
@@ -1311,7 +1343,7 @@ function importLegacy(legacyDir) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init, getRoot, filesDir, allocFilePath, sanitize,
|
init, getRoot, filesDir, allocFilePath, sanitize,
|
||||||
list, get, findBySource, listShelves, listTags,
|
list, get, findBySource, listShelves, listTags,
|
||||||
add, importLocal, update, updateMany, remove, removeMany, attachFile,
|
add, importLocal, update, updateMany, remove, removeMany, removeMissing, attachFile,
|
||||||
addShelf, updateShelf, removeShelf,
|
addShelf, updateShelf, removeShelf,
|
||||||
addTag, updateTag, removeTag,
|
addTag, updateTag, removeTag,
|
||||||
scan, migrateTo, finalizeMigration, rollbackMigration, importLegacy,
|
scan, migrateTo, finalizeMigration, rollbackMigration, importLegacy,
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 PeopleLib/2.1.0 (+https://github.com/lofyer/peoplelib)';
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 PeopleLib/2.1.1 (+https://github.com/lofyer/peoplelib)';
|
||||||
const { fetch: undiciFetch, ProxyAgent } = require('undici');
|
const { fetch: undiciFetch, ProxyAgent } = require('undici');
|
||||||
|
|
||||||
// 代理配置:默认直连(空字符串)。一旦设置,所有 HTTP 请求统一走该代理。
|
// 代理配置:默认直连(空字符串)。一旦设置,所有 HTTP 请求统一走该代理。
|
||||||
|
|||||||
+31
-1
@@ -84,9 +84,39 @@ const Library = (() => {
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '扫描中...';
|
btn.textContent = '扫描中...';
|
||||||
const r = await window.api.library.scan();
|
const r = await window.api.library.scan();
|
||||||
btn.textContent = r.ok && r.data && r.data.added ? `新增 ${r.data.added} 本 ✓` : '已是最新 ✓';
|
if (!r || !r.ok) {
|
||||||
|
btn.textContent = '扫描失败';
|
||||||
|
await confirmModal('扫描失败', (r && r.error) || '未知错误');
|
||||||
|
setTimeout(() => { btn.textContent = '重新扫描'; btn.disabled = false; }, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btn.textContent = r.data && r.data.added ? `新增 ${r.data.added} 本 ✓` : '已是最新 ✓';
|
||||||
dirty = true;
|
dirty = true;
|
||||||
await refresh(true);
|
await refresh(true);
|
||||||
|
const missing = Math.max(0, Number((r.data || {}).missing) || 0);
|
||||||
|
if (missing) {
|
||||||
|
const clean = await confirmModal(
|
||||||
|
'发现文件缺失',
|
||||||
|
`扫描发现 ${missing} 条书库内容的文件均已缺失,是否从书库中清理这些条目?不再被其他书籍使用的对应分类和标签也会一并清理;笔记、书签、进度和标注会保留。`
|
||||||
|
);
|
||||||
|
if (clean) {
|
||||||
|
const removed = await window.api.library.removeMissing();
|
||||||
|
if (!removed || !removed.ok) {
|
||||||
|
await confirmModal('清理失败', (removed && removed.error) || '未知错误');
|
||||||
|
} else {
|
||||||
|
const count = Math.max(0, Number((removed.data || {}).removed) || 0);
|
||||||
|
const shelvesRemoved = Math.max(0, Number((removed.data || {}).shelvesRemoved) || 0);
|
||||||
|
const tagsRemoved = Math.max(0, Number((removed.data || {}).tagsRemoved) || 0);
|
||||||
|
const extras = [
|
||||||
|
shelvesRemoved ? `${shelvesRemoved} 个分类` : '',
|
||||||
|
tagsRemoved ? `${tagsRemoved} 个标签` : ''
|
||||||
|
].filter(Boolean);
|
||||||
|
btn.textContent = `已清理 ${count} 条${extras.length ? `、${extras.join('、')}` : ''} ✓`;
|
||||||
|
dirty = true;
|
||||||
|
await refresh(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
setTimeout(() => { btn.textContent = '重新扫描'; btn.disabled = false; }, 2000);
|
setTimeout(() => { btn.textContent = '重新扫描'; btn.disabled = false; }, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user