@gheop/tojiru 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/convert.js +1 -0
- package/dist/convert.js.map +1 -1
- package/dist/manifest.js +2 -0
- package/dist/manifest.js.map +1 -1
- package/dist/pages.js +14 -5
- package/dist/pages.js.map +1 -1
- package/package.json +1 -1
- package/reader/index.html +1 -0
- package/reader/reader.css +23 -3
- package/reader/reader.js +57 -6
package/README.md
CHANGED
|
@@ -87,14 +87,16 @@ Generation shows per-page progress on stderr (e.g. `Converting 12/30`).
|
|
|
87
87
|
| `--quality <n>` | WebP quality 1-100 for lossy raster pages (default: 80) |
|
|
88
88
|
| `--spread` | Lay pages out two-up (double-page spread) |
|
|
89
89
|
| `--rtl` | Right-to-left reading order (manga); pairs with `--spread` |
|
|
90
|
+
| `--paged` | Default to the paged view (one page per screen); the reader can switch back |
|
|
90
91
|
|
|
91
92
|
### Reading
|
|
92
93
|
|
|
93
94
|
The generated reader works the same whatever the source format:
|
|
94
95
|
|
|
95
96
|
- **Search** — Ctrl+F (or `/`) opens a text search for PDFs that shipped a text layer. Matches list the page with a highlighted excerpt; click one to jump there.
|
|
96
|
-
- **Table of contents** — when a PDF has an outline, it shows at the top of the thumbnail column; click an entry to jump to its page.
|
|
97
|
+
- **Table of contents** — when a PDF has an outline, it shows at the top of the thumbnail column; click an entry to jump to its page, and the current section is highlighted as you read.
|
|
97
98
|
- **Dark mode** — follows the system by default; the ◐ button toggles it and remembers your choice.
|
|
99
|
+
- **Paged view** — the ≣ / ▭ button switches between continuous scroll and one page per screen (horizontal, arrow-key or swipe navigation). `--paged` makes the paged view the default; the choice is remembered.
|
|
98
100
|
- **Double-page / manga** — `--spread` shows two pages per row; add `--rtl` for right-to-left order (page 1 on the right, left arrow advances).
|
|
99
101
|
|
|
100
102
|
```bash
|
|
@@ -181,6 +183,13 @@ MIT — see [LICENSE](LICENSE).
|
|
|
181
183
|
|
|
182
184
|
## Changelog
|
|
183
185
|
|
|
186
|
+
### v0.9.0 — Paged view, contents highlight, leaner comics (2026-06-26)
|
|
187
|
+
|
|
188
|
+
- **Paged reading view** — the ≣ / ▭ button switches the reader between continuous scroll and one page per screen (horizontal, arrow-key or swipe navigation), and the choice is remembered. `--paged` makes it the build default; `--rtl` pages it right-to-left.
|
|
189
|
+
- **The table of contents follows along** — the outline entry covering the current page is highlighted as you read.
|
|
190
|
+
- **`--image-format webp` never inflates a comic** — each page keeps WebP or its original, whichever is smaller. An already-compressed scan that used to grow ~2× now stays at its source size, while colour comics still drop to ~×0.3 (measured points in [`examples/BENCH.md`](examples/BENCH.md)).
|
|
191
|
+
- Tests run in CI on Node 24 (the Node 20 deprecation warning is gone), and a flaky reader e2e was de-flaked.
|
|
192
|
+
|
|
184
193
|
### v0.8.0 — Search, dark mode, contents and manga layout (2026-06-26)
|
|
185
194
|
|
|
186
195
|
- **Full-text search** (Ctrl+F or `/`) for PDFs that carry a text layer. `pdftotext` builds an index at conversion time (a `search.json` file, or inlined in single-file mode); matches list the page with a highlighted excerpt and jump there on click. Scans and comics keep the browser's native find.
|
package/dist/cli.js
CHANGED
|
@@ -35,6 +35,7 @@ program
|
|
|
35
35
|
.option('--quality <n>', 'WebP quality (1-100) for lossy raster pages', '80')
|
|
36
36
|
.option('--spread', 'lay pages out two-up (double-page spread)')
|
|
37
37
|
.option('--rtl', 'right-to-left reading order (manga); pairs with --spread')
|
|
38
|
+
.option('--paged', 'default to the paged view (one page per screen); reader can switch back')
|
|
38
39
|
.action(async (input, opts) => {
|
|
39
40
|
try {
|
|
40
41
|
if (!existsSync(input))
|
|
@@ -58,7 +59,7 @@ program
|
|
|
58
59
|
const htmlPath = typeof opts.singleFile === 'string'
|
|
59
60
|
? opts.singleFile
|
|
60
61
|
: basename(input).replace(/\.[^.]+$/, '') + '.html';
|
|
61
|
-
const r = await convert(input, { outDir: '', title: opts.title, onProgress, singleFile: htmlPath, imageFormat, quality, spread: opts.spread, rtl: opts.rtl });
|
|
62
|
+
const r = await convert(input, { outDir: '', title: opts.title, onProgress, singleFile: htmlPath, imageFormat, quality, spread: opts.spread, rtl: opts.rtl, paged: opts.paged });
|
|
62
63
|
if (isTTY)
|
|
63
64
|
process.stderr.write('\x1b[2K\r');
|
|
64
65
|
console.log(`✓ ${r.pageCount} pages → ${htmlPath}`);
|
|
@@ -70,7 +71,7 @@ program
|
|
|
70
71
|
if (existsSync(outDir) && (await readdir(outDir)).length > 0 && !opts.force) {
|
|
71
72
|
throw new Error(`Folder ${outDir} is not empty. Use --force to overwrite.`);
|
|
72
73
|
}
|
|
73
|
-
const r = await convert(input, { outDir, title: opts.title, onProgress, imageFormat, quality, spread: opts.spread, rtl: opts.rtl });
|
|
74
|
+
const r = await convert(input, { outDir, title: opts.title, onProgress, imageFormat, quality, spread: opts.spread, rtl: opts.rtl, paged: opts.paged });
|
|
74
75
|
// Clear progress line before success message
|
|
75
76
|
if (isTTY)
|
|
76
77
|
process.stderr.write('\x1b[2K\r');
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAA;IACtG,iFAAiF;IACjF,uDAAuD;IACvD,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC5E,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAC7B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,qDAAqD,CAAC;KAClE,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,4DAA4D;AAC5D,OAAO;KACJ,OAAO,CAAC,iBAAiB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;KAC/C,WAAW,CAAC,2CAA2C,CAAC;KACxD,MAAM,CAAC,iBAAiB,EAAE,eAAe,CAAC;KAC1C,MAAM,CAAC,qBAAqB,EAAE,gBAAgB,CAAC;KAC/C,MAAM,CAAC,aAAa,EAAE,qCAAqC,CAAC;KAC5D,MAAM,CAAC,SAAS,EAAE,yCAAyC,CAAC;KAC5D,MAAM,CAAC,sBAAsB,EAAE,mEAAmE,CAAC;KACnG,MAAM,CAAC,sBAAsB,EAAE,kDAAkD,EAAE,MAAM,CAAC;KAC1F,MAAM,CAAC,eAAe,EAAE,6CAA6C,EAAE,IAAI,CAAC;KAC5E,MAAM,CAAC,UAAU,EAAE,2CAA2C,CAAC;KAC/D,MAAM,CAAC,OAAO,EAAE,0DAA0D,CAAC;KAC3E,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/convert.js
CHANGED
package/dist/convert.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,MAAM,UAAU,GAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"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"}
|
package/dist/manifest.js
CHANGED
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAiCA,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,IAAU,EAAE,KAAsB,EAAE,SAAyB,EAAE;IAC1G,MAAM,CAAC,GAAa,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAA;IAC9E,IAAI,MAAM,CAAC,UAAU;QAAE,CAAC,CAAC,UAAU,GAAG,IAAI,CAAA;IAC1C,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM;QAAE,CAAC,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;IACvE,IAAI,MAAM,CAAC,MAAM;QAAE,CAAC,CAAC,MAAM,GAAG,IAAI,CAAA;IAClC,IAAI,MAAM,CAAC,GAAG;QAAE,CAAC,CAAC,GAAG,GAAG,IAAI,CAAA;IAC5B,IAAI,MAAM,CAAC,KAAK;QAAE,CAAC,CAAC,MAAM,GAAG,OAAO,CAAA;IACpC,OAAO,CAAC,CAAA;AACV,CAAC"}
|
package/dist/pages.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, copyFile, rm } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile, copyFile, rm, stat } from 'node:fs/promises';
|
|
2
2
|
import { gzipSync } from 'node:zlib';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import sharp from 'sharp';
|
|
@@ -30,12 +30,21 @@ export async function processPages(doc, outDir, opts = {}, onProgress) {
|
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
32
32
|
const ext = (basename(page.imagePath).split('.').pop() ?? 'jpg').toLowerCase();
|
|
33
|
-
// --image-format webp re-encodes comic pages (often large lossless PNGs) to
|
|
34
|
-
//
|
|
33
|
+
// --image-format webp re-encodes comic pages (often large lossless PNGs) to lossy
|
|
34
|
+
// WebP. But an already-compressed source (a low-quality JPEG scan) can come out
|
|
35
|
+
// *bigger* as WebP, so we encode in memory and keep whichever is smaller — WebP
|
|
36
|
+
// never inflates a page. Sources already in WebP are copied (no re-encode).
|
|
35
37
|
let file;
|
|
36
38
|
if (imageFormat === 'webp' && ext !== 'webp') {
|
|
37
|
-
|
|
38
|
-
await
|
|
39
|
+
const webp = await sharp(page.imagePath).webp({ quality, effort: 6 }).toBuffer();
|
|
40
|
+
if (webp.length < (await stat(page.imagePath)).size) {
|
|
41
|
+
file = `pages/${stem}.webp`;
|
|
42
|
+
await writeFile(join(outDir, file), webp);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
file = `pages/${stem}.${ext}`;
|
|
46
|
+
await copyFile(page.imagePath, join(outDir, file));
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
49
|
else {
|
|
41
50
|
file = `pages/${stem}.${ext}`;
|
package/dist/pages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/reader/index.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<body>
|
|
11
11
|
<button id="reduce" type="button" title="Collapse / expand thumbnails" aria-label="Collapse or expand thumbnails">☰</button>
|
|
12
12
|
<button id="theme" type="button" title="Toggle dark mode" aria-label="Toggle dark mode">◐</button>
|
|
13
|
+
<button id="layout" type="button" title="Reading layout: continuous / paged" aria-label="Toggle paged reading view">≣</button>
|
|
13
14
|
<div id="search" class="hidden" role="dialog" aria-label="Search the document">
|
|
14
15
|
<input id="search-input" type="search" placeholder="Search…" aria-label="Search text" autocomplete="off" spellcheck="false" />
|
|
15
16
|
<div id="search-results" aria-live="polite"></div>
|
package/reader/reader.css
CHANGED
|
@@ -28,7 +28,7 @@ html, body { margin: 0; height: 100%; }
|
|
|
28
28
|
body { font-family: verdana, sans-serif; background: var(--bg); }
|
|
29
29
|
#page, #menu { user-select: none; }
|
|
30
30
|
|
|
31
|
-
#reduce, #theme {
|
|
31
|
+
#reduce, #theme, #layout {
|
|
32
32
|
position: fixed; top: 5px; z-index: 4;
|
|
33
33
|
width: 20px; height: 20px; line-height: 18px; text-align: center; padding: 0;
|
|
34
34
|
background: var(--chrome-bg); border: 1px solid var(--chrome-border); border-radius: 3px;
|
|
@@ -36,7 +36,8 @@ body { font-family: verdana, sans-serif; background: var(--bg); }
|
|
|
36
36
|
}
|
|
37
37
|
#reduce { left: 5px; }
|
|
38
38
|
#theme { left: 30px; }
|
|
39
|
-
#
|
|
39
|
+
#layout { left: 55px; }
|
|
40
|
+
#reduce:focus-visible, #theme:focus-visible, #layout:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
40
41
|
|
|
41
42
|
/* Search panel: Ctrl+F (or /) opens it over the top of the page area. */
|
|
42
43
|
#search {
|
|
@@ -76,6 +77,7 @@ body { font-family: verdana, sans-serif; background: var(--bg); }
|
|
|
76
77
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
77
78
|
}
|
|
78
79
|
.toc-item:hover, .toc-item:focus-visible { background: var(--divider-bg); outline: none; }
|
|
80
|
+
.toc-item.active { color: var(--focus); font-weight: 600; }
|
|
79
81
|
#menu .thumb {
|
|
80
82
|
display: block; width: 100%; padding: 0; margin: 0; border: 0;
|
|
81
83
|
background: none; cursor: pointer; text-align: center; font-family: inherit;
|
|
@@ -115,6 +117,23 @@ body { font-family: verdana, sans-serif; background: var(--bg); }
|
|
|
115
117
|
@media (max-width: 700px) {
|
|
116
118
|
#page.spread .page { width: calc(100% - 24px); }
|
|
117
119
|
}
|
|
120
|
+
|
|
121
|
+
/* Paged view: one page per screen with horizontal scroll-snap, a reading mode toggled
|
|
122
|
+
at runtime (the ≣/▭ button). rtl pages it right-to-left. Takes precedence over the
|
|
123
|
+
spread layout when both are set. */
|
|
124
|
+
#page.paged {
|
|
125
|
+
display: flex; flex-direction: row; flex-wrap: nowrap; gap: 0; padding: 0;
|
|
126
|
+
overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory;
|
|
127
|
+
}
|
|
128
|
+
#page.paged .page {
|
|
129
|
+
flex: 0 0 100%; width: 100%; height: 100%; margin: 0; padding: 12px;
|
|
130
|
+
scroll-snap-align: center;
|
|
131
|
+
}
|
|
132
|
+
#page.paged .page object, #page.paged .page img {
|
|
133
|
+
width: 100%; height: 100%; object-fit: contain;
|
|
134
|
+
border: 0; box-shadow: none; opacity: 1;
|
|
135
|
+
}
|
|
136
|
+
#page.paged.rtl { direction: rtl; }
|
|
118
137
|
.page object, .page img {
|
|
119
138
|
display: block; width: 100%; height: 100%;
|
|
120
139
|
border: 1px solid var(--page-border); box-shadow: .2em .2em var(--page-shadow);
|
|
@@ -133,8 +152,9 @@ body { font-family: verdana, sans-serif; background: var(--bg); }
|
|
|
133
152
|
is always full width, and tap targets are a little larger. reader.js collapses
|
|
134
153
|
the column by default on small screens. */
|
|
135
154
|
@media (max-width: 640px) {
|
|
136
|
-
#reduce, #theme { width: 30px; height: 30px; line-height: 28px; font-size: 16px; }
|
|
155
|
+
#reduce, #theme, #layout { width: 30px; height: 30px; line-height: 28px; font-size: 16px; }
|
|
137
156
|
#theme { left: 40px; }
|
|
157
|
+
#layout { left: 75px; }
|
|
138
158
|
#menu { width: 72vw; max-width: 280px; z-index: 3; box-shadow: 2px 0 14px rgba(0, 0, 0, .35); }
|
|
139
159
|
#menu .thumb img { width: 120px; }
|
|
140
160
|
#menu .thumb:hover img { width: 120px; }
|
package/reader/reader.js
CHANGED
|
@@ -105,6 +105,8 @@ function init(manifest) {
|
|
|
105
105
|
|
|
106
106
|
// Table of contents, when the document carries an outline. It sits at the top of the
|
|
107
107
|
// thumbnail column; each entry jumps to its page (goTo is hoisted, defined below).
|
|
108
|
+
// tocEntries keeps each button with its page so the current section can be highlighted.
|
|
109
|
+
const tocEntries = []
|
|
108
110
|
if (manifest.outline && manifest.outline.length) {
|
|
109
111
|
const toc = document.createElement('div')
|
|
110
112
|
toc.id = 'toc'
|
|
@@ -121,10 +123,23 @@ function init(manifest) {
|
|
|
121
123
|
item.style.paddingLeft = `${8 + Math.min(e.depth, 5) * 12}px`
|
|
122
124
|
item.addEventListener('click', () => goTo(e.page))
|
|
123
125
|
toc.append(item)
|
|
126
|
+
tocEntries.push({ el: item, page: e.page })
|
|
124
127
|
}
|
|
125
128
|
menu.append(toc)
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
// Highlights the outline entry covering page n: the last one whose page is ≤ n.
|
|
132
|
+
let activeToc = -1
|
|
133
|
+
function updateTocActive(n) {
|
|
134
|
+
if (!tocEntries.length) return
|
|
135
|
+
let idx = -1
|
|
136
|
+
for (let i = 0; i < tocEntries.length; i++) if (tocEntries[i].page <= n) idx = i
|
|
137
|
+
if (idx === activeToc) return
|
|
138
|
+
tocEntries[activeToc]?.el.classList.remove('active')
|
|
139
|
+
activeToc = idx
|
|
140
|
+
tocEntries[idx]?.el.classList.add('active')
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
// Each thumbnail is a real <button> so it can be reached and activated by
|
|
129
144
|
// keyboard, not just clicked. The .select class lives on the button.
|
|
130
145
|
const thumbs = manifest.pages.map((p) => {
|
|
@@ -155,7 +170,17 @@ function init(manifest) {
|
|
|
155
170
|
pageEl.classList.toggle('spread', !!manifest.spread)
|
|
156
171
|
pageEl.classList.toggle('rtl', !!manifest.rtl)
|
|
157
172
|
|
|
158
|
-
|
|
173
|
+
// Reading layout: 'scroll' (vertical continuous) or 'paged' (one page per screen). A
|
|
174
|
+
// saved choice wins; otherwise the build's default (manifest.layout) applies.
|
|
175
|
+
let layout = (() => {
|
|
176
|
+
try { const saved = localStorage.getItem('tojiru:layout'); if (saved === 'paged' || saved === 'scroll') return saved } catch {}
|
|
177
|
+
return manifest.layout === 'paged' ? 'paged' : 'scroll'
|
|
178
|
+
})()
|
|
179
|
+
const paged = () => layout === 'paged'
|
|
180
|
+
pageEl.classList.toggle('paged', paged())
|
|
181
|
+
|
|
182
|
+
// 800px on every side so the next page preloads whichever way the reader scrolls.
|
|
183
|
+
const io = new IntersectionObserver(onIntersect, { root: pageEl, rootMargin: '800px' })
|
|
159
184
|
const containers = manifest.pages.map((p) => {
|
|
160
185
|
const c = document.createElement('div')
|
|
161
186
|
c.className = 'page'
|
|
@@ -185,21 +210,34 @@ function init(manifest) {
|
|
|
185
210
|
thumbs[current - 1]?.classList.add('select')
|
|
186
211
|
containers[current - 1]?.classList.add('select')
|
|
187
212
|
thumbs[current - 1]?.scrollIntoView({ block: 'nearest' })
|
|
213
|
+
updateTocActive(n)
|
|
188
214
|
history.replaceState(null, '', `#page=${n}`)
|
|
189
215
|
try { localStorage.setItem(key, String(n)) } catch {}
|
|
190
216
|
}
|
|
191
217
|
|
|
192
218
|
function goTo(n) {
|
|
193
219
|
n = Math.min(Math.max(1, n), manifest.pages.length)
|
|
194
|
-
|
|
220
|
+
// Paged scrolls horizontally to centre the page; scroll mode aligns it to the top.
|
|
221
|
+
containers[n - 1].scrollIntoView(paged() ? { inline: 'center', block: 'nearest' } : undefined)
|
|
195
222
|
setCurrent(n)
|
|
196
223
|
}
|
|
197
224
|
|
|
198
225
|
pageEl.addEventListener('scroll', () => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
if (paged()) {
|
|
227
|
+
// Horizontal: the page whose box spans the viewport's centre x is current. Using
|
|
228
|
+
// getBoundingClientRect keeps this correct under rtl, where scrollLeft flips sign.
|
|
229
|
+
const r = pageEl.getBoundingClientRect()
|
|
230
|
+
const cx = r.left + r.width / 2
|
|
231
|
+
for (let i = 0; i < containers.length; i++) {
|
|
232
|
+
const cr = containers[i].getBoundingClientRect()
|
|
233
|
+
if (cr.left <= cx && cx < cr.right) { setCurrent(i + 1); break }
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
const mid = pageEl.scrollTop + pageEl.clientHeight / 2
|
|
237
|
+
for (let i = 0; i < containers.length; i++) {
|
|
238
|
+
const c = containers[i]
|
|
239
|
+
if (c.offsetTop <= mid && c.offsetTop + c.offsetHeight > mid) { setCurrent(i + 1); break }
|
|
240
|
+
}
|
|
203
241
|
}
|
|
204
242
|
}, { passive: true })
|
|
205
243
|
|
|
@@ -226,6 +264,19 @@ function init(manifest) {
|
|
|
226
264
|
try { localStorage.setItem('tojiru:theme', next) } catch {}
|
|
227
265
|
})
|
|
228
266
|
|
|
267
|
+
// Reading-layout toggle: continuous scroll (≣) ↔ paged (▭). The icon shows the
|
|
268
|
+
// current mode; switching re-centres the current page in the new axis.
|
|
269
|
+
const layoutBtn = $('#layout')
|
|
270
|
+
const syncLayoutBtn = () => { layoutBtn.textContent = paged() ? '▭' : '≣' }
|
|
271
|
+
syncLayoutBtn()
|
|
272
|
+
layoutBtn.addEventListener('click', () => {
|
|
273
|
+
layout = paged() ? 'scroll' : 'paged'
|
|
274
|
+
pageEl.classList.toggle('paged', paged())
|
|
275
|
+
try { localStorage.setItem('tojiru:layout', layout) } catch {}
|
|
276
|
+
syncLayoutBtn()
|
|
277
|
+
if (current) goTo(current)
|
|
278
|
+
})
|
|
279
|
+
|
|
229
280
|
// --- Full-text search (only when the build shipped an index) ---
|
|
230
281
|
const searchBox = $('#search')
|
|
231
282
|
const searchInput = $('#search-input')
|