feat(reader): stop detecting 卷 in txt splitting (volume lines stay body text, volume-only books fall back to pseudo-sections); replace chapter dropdown with a 目录 sidebar overlay (active row highlighted + centered, click-away/pick to close)

This commit is contained in:
2026-09-07 22:42:10 +08:00
parent 7fb01ce335
commit 7e63c21902
5 changed files with 54 additions and 66 deletions
+6 -7
View File
@@ -39,20 +39,19 @@ it("短文无标记 → 单节,chapterText 即全文", () => {
expect(chapterText(t, chs, 0)).toBe(t);
});
it("卷不是章节边界:只按章切,卷题作分组标签并并入所属章开头", () => {
it("卷不感知:只按章切,卷题行留在正文里", () => {
const t =
"第一卷 天云世界\n第一章 铜镜\n" + pad(500) + "\n第二章 修炼\n" + pad(500) +
"\n\n\n第二卷 仙界\n第三章 飞升\n" + pad(300);
const chs = splitChapters(t);
expect(chs.map((c) => c.title)).toEqual(["第一章 铜镜", "第二章 修炼", "第三章 飞升"]);
expect(chs.map((c) => c.vol)).toEqual(["第一卷 天云世界", "第一卷 天云世界", "第二卷 仙界"]);
expect(chs[2].start).toBe(t.indexOf("第二卷 仙界")); // 卷题行从本章开头渲染,而非上一章结尾
expect(chapterText(t, chs, 2)).toMatch(/^第二卷 仙界/);
expect(chs[0].start).toBe(t.indexOf("第一章"));
expect(chs[2].start).toBe(t.indexOf("第三章"));
expect(chapterText(t, chs, 1)).toContain("第二卷 仙界"); // 卷题在第二章结尾,不进第三章开头
});
it("只有卷没有章的书:卷即章节", () => {
it("只有卷没有章的书:按伪章节切", () => {
const t = "第一卷 起\n" + pad(600) + "\n第二卷 承\n" + pad(600) + "\n第三卷 转\n" + pad(600);
const chs = splitChapters(t);
expect(chs.map((c) => c.title)).toEqual(["第一卷 起", "第二卷 承", "第三卷 转"]);
expect(chs.every((c) => c.vol === undefined)).toBe(true);
expect(chs).toEqual([{ title: "第 1 节", start: 0 }]);
});