From f0cbbe6d02a9b98bce4b8c97f743f8c42edecdb5 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 2 Mar 2026 15:15:26 +0800 Subject: [PATCH] Allow to insert original chapters by prev/next chapter id --- package.json | 4 +- src/db/indexedDb.ts | 4 ++ src/db/pocketBase.ts | 6 ++- src/types.ts | 4 ++ src/utils/qd.test.ts | 86 +++++++++++++++++++++++++++++++++++++++ src/utils/qd.ts | 97 ++++++++++++++++++++++++++------------------ 6 files changed, 159 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 7e85885..c5beee9 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "builddbg": "node build.js --dev --debug", "buildrel": "node build.js", "lint": "eslint src", - "test": "node build_test.js && node --enable-source-maps test.cjs" + "build_test": "node build_test.js", + "test": "node build_test.js && node --enable-source-maps test.cjs", + "run_test": "node --enable-source-maps test.cjs" }, "type": "module", "types": "./data.d.ts", diff --git a/src/db/indexedDb.ts b/src/db/indexedDb.ts index a3b75aa..f6edde1 100644 --- a/src/db/indexedDb.ts +++ b/src/db/indexedDb.ts @@ -345,6 +345,8 @@ export class IndexedDb implements Db { name: data.chapterInfo.chapterName, bookId: data.bookId, time: data.time, + prev: data.chapterInfo.prev, + next: data.chapterInfo.next, }; } else if (!oldValue) { currents.set(data.id, value); @@ -354,6 +356,8 @@ export class IndexedDb implements Db { name: data.chapterInfo.chapterName, bookId: data.bookId, time: data.time, + prev: data.chapterInfo.prev, + next: data.chapterInfo.next, }); } }, bookId, 'bookId'); diff --git a/src/db/pocketBase.ts b/src/db/pocketBase.ts index b7c9954..2dabdd3 100644 --- a/src/db/pocketBase.ts +++ b/src/db/pocketBase.ts @@ -244,7 +244,7 @@ export class PocketBaseDb implements Db { const currents: Map = new Map(); const list = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getFullList({ filter: `bookId = ${bookId}`, - fields: 'id,chapterId,bookId,time,data.chapterInfo.chapterName', + fields: 'id,chapterId,bookId,time,data.chapterInfo.chapterName,data.chapterInfo.prev,data.chapterInfo.next', }); const re: QdChapterSimpleInfo[] = []; for (const item of list) { @@ -261,6 +261,8 @@ export class PocketBaseDb implements Db { name: data.chapterInfo.chapterName, bookId: item.bookId, time: item.time, + prev: data.chapterInfo.prev, + next: data.chapterInfo.next, }; } else if (!oldValue) { currents.set(key, value); @@ -270,6 +272,8 @@ export class PocketBaseDb implements Db { name: data.chapterInfo.chapterName, bookId: item.bookId, time: item.time, + prev: data.chapterInfo.prev, + next: data.chapterInfo.next, }); } } diff --git a/src/types.ts b/src/types.ts index 4638324..d7f69b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,6 +28,10 @@ export type QdChapterSimpleInfo = { name: string; bookId: number; time: number; + /// Previous chapter ID + prev?: number; + /// Next chapter ID + next?: number; } export type QdChapterHistoryInfo = { diff --git a/src/utils/qd.test.ts b/src/utils/qd.test.ts index 7ab179c..080b58f 100644 --- a/src/utils/qd.test.ts +++ b/src/utils/qd.test.ts @@ -83,4 +83,90 @@ export default function t() { }, ]); }); + test(filename, 'merge_volumes_2', () => { + const sourceList: Volume[] = [ + { + id: 'test', + name: 'Test Volume', + isVip: false, + chapters: [ + { id: 3, name: 'Chapter 3' }, + ] + } + ]; + const targetList: QdChapterSimpleInfo[] = [ + { + primaryKey: 0, + id: 1, + name: 'Chapter 1', + bookId: 0, + time: 10, + next: 3, + }, + { + primaryKey: 0, + id: 4, + name: 'Chapter 4', + bookId: 0, + time: 11, + prev: 3, + }, + { + primaryKey: 0, + id: 33, + name: 'Other', + bookId: 0, + time: 12, + }, + ]; + expect(get_new_volumes(targetList, sourceList, ChapterShowMode.All)).toEqual([ + { + id: 'vol_new', + name: '其他已保存章节', + isVip: false, + chapters: [ + { id: 33, name: 'Other', isSaved: true }, + ], + }, + { + id: 'test', + name: 'Test Volume', + isVip: false, + chapters: [ + { id: 1, name: 'Chapter 1', isSaved: true }, + { id: 3, name: 'Chapter 3' }, + { id: 4, name: 'Chapter 4', isSaved: true }, + ] + } + ]); + expect(get_new_volumes(targetList, sourceList, ChapterShowMode.SavedOnly)).toEqual([ + { + id: 'vol_new', + name: '其他已保存章节', + isVip: false, + chapters: [ + { id: 33, name: 'Other' }, + ], + }, + { + id: 'test', + name: 'Test Volume', + isVip: false, + chapters: [ + { id: 1, name: 'Chapter 1' }, + { id: 4, name: 'Chapter 4' }, + ], + }, + ]); + expect(get_new_volumes(targetList, sourceList, ChapterShowMode.UnsavedOnly)).toEqual([ + { + id: 'test', + name: 'Test Volume', + isVip: false, + chapters: [ + { id: 3, name: 'Chapter 3' }, + ], + }, + ]); + }); } diff --git a/src/utils/qd.ts b/src/utils/qd.ts index 4e4d177..c65d776 100644 --- a/src/utils/qd.ts +++ b/src/utils/qd.ts @@ -38,28 +38,72 @@ export function hash_qdchapter_info(info: QdChapterInfo): string { export function get_new_volumes(chapterLists: QdChapterSimpleInfo[], volumes: Volume[], keepMode: ChapterShowMode): Volume[] { const vols: Volume[] = []; - if (keepMode == ChapterShowMode.All) { - const volMap: Map = new Map(); + if (keepMode == ChapterShowMode.All || keepMode == ChapterShowMode.SavedOnly) { + const chMap: Map = new Map(); + const volMap: Map = new Map(); for (const vo of volumes) { const vol = structuredClone(vo); for (const ch of vol.chapters) { - volMap.set(ch.id, ch); + chMap.set(ch.id, ch); + volMap.set(ch.id, vol); } vols.push(vol); } const volCh: Chapter[] = []; + let needed: QdChapterSimpleInfo[] = []; for (const ch of chapterLists) { - const chInfo = volMap.get(ch.id); + const chInfo = chMap.get(ch.id); if (!chInfo) { - volCh.push({ - id: ch.id, - name: ch.name, - isSaved: true, - }); + needed.push(ch); } else { chInfo.isSaved = true; } } + let changed = false; + do { + const current_len = needed.length; + const not_found: QdChapterSimpleInfo[] = []; + for (const ch of needed) { + if (ch.prev) { + const prevVol = volMap.get(ch.prev); + if (prevVol) { + const chIndex = prevVol.chapters.findIndex(c => c.id === ch.prev); + if (chIndex !== -1) { + prevVol.chapters.splice(chIndex + 1, 0, { + id: ch.id, + name: ch.name, + isSaved: true, + }); + continue; + } + } + } + if (ch.next) { + const nextVol = volMap.get(ch.next); + if (nextVol) { + const chIndex = nextVol.chapters.findIndex(c => c.id === ch.next); + if (chIndex !== -1) { + nextVol.chapters.splice(chIndex, 0, { + id: ch.id, + name: ch.name, + isSaved: true, + }); + continue; + } + } + } + not_found.push(ch); + } + needed = not_found; + changed = current_len !== needed.length; + } while (changed && needed.length > 0); + for (const ch of needed) { + volCh.push({ + id: ch.id, + name: ch.name, + isSaved: true, + }); + } if (volCh.length > 0) { vols.unshift({ id: 'vol_new', @@ -68,39 +112,12 @@ export function get_new_volumes(chapterLists: QdChapterSimpleInfo[], volumes: Vo isVip: false, }); } - } else if (keepMode == ChapterShowMode.SavedOnly) { - const chIds = new Set(chapterLists.map(ch => ch.id)); - for (const vol of volumes) { - const newChs = vol.chapters.filter(ch => chIds.has(ch.id)); - if (newChs.length > 0) { - vols.push({ - ...vol, - chapters: newChs, - }); + if (keepMode == ChapterShowMode.SavedOnly) { + for (const vol of vols) { + vol.chapters = vol.chapters.filter(ch => ch.isSaved); + vol.chapters.forEach(ch => delete ch.isSaved); } } - for (const vol of vols) { - for (const ch of vol.chapters) { - chIds.delete(ch.id); - } - } - if (chIds.size > 0) { - const volCh: Chapter[] = []; - for (const ch of chapterLists) { - if (chIds.has(ch.id)) { - volCh.push({ - id: ch.id, - name: ch.name, - }); - } - } - vols.unshift({ - id: 'vol_new', - name: '其他已保存章节', - chapters: volCh, - isVip: false, - }); - } } else if (keepMode == ChapterShowMode.UnsavedOnly) { const chIds = new Set(chapterLists.map(ch => ch.id)); for (const vol of volumes) {