pressy 0.1.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.
@@ -0,0 +1,660 @@
1
+ // src/runtime/client.tsx
2
+ import { render } from "preact";
3
+ import { useState, useEffect } from "preact/hooks";
4
+ import { signal as signal2, effect } from "@preact/signals";
5
+ import { Reader, DownloadBook, BookProgress } from "@pressy-pub/components";
6
+ import { useMDXComponents } from "@pressy-pub/components/content";
7
+
8
+ // src/runtime/offline.ts
9
+ import { signal } from "@preact/signals";
10
+ var CACHED_BOOKS_KEY = "pressy-cached-books";
11
+ var offlineStatus = signal("online");
12
+ var cacheProgress = signal(null);
13
+ var swReady = signal(false);
14
+ function loadCachedBooks() {
15
+ if (typeof localStorage === "undefined") return /* @__PURE__ */ new Set();
16
+ try {
17
+ const stored = localStorage.getItem(CACHED_BOOKS_KEY);
18
+ if (stored) return new Set(JSON.parse(stored));
19
+ } catch {
20
+ }
21
+ return /* @__PURE__ */ new Set();
22
+ }
23
+ var cachedBooks = signal(loadCachedBooks());
24
+ if (typeof window !== "undefined") {
25
+ offlineStatus.value = navigator.onLine ? "online" : "offline";
26
+ window.addEventListener("online", () => {
27
+ offlineStatus.value = "online";
28
+ });
29
+ window.addEventListener("offline", () => {
30
+ offlineStatus.value = "offline";
31
+ });
32
+ }
33
+ async function registerServiceWorker(basePath2 = "") {
34
+ if (!("serviceWorker" in navigator)) {
35
+ console.warn("Service workers are not supported");
36
+ return null;
37
+ }
38
+ const swUrl = basePath2 ? `${basePath2}/sw.js` : "/sw.js";
39
+ const scope = basePath2 ? `${basePath2}/` : "/";
40
+ try {
41
+ const registration = await navigator.serviceWorker.register(swUrl, {
42
+ scope
43
+ });
44
+ navigator.serviceWorker.addEventListener("message", handleSWMessage);
45
+ if (navigator.serviceWorker.controller) {
46
+ swReady.value = true;
47
+ } else {
48
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
49
+ swReady.value = true;
50
+ });
51
+ }
52
+ registration.addEventListener("updatefound", () => {
53
+ const newWorker = registration.installing;
54
+ if (!newWorker) return;
55
+ newWorker.addEventListener("statechange", () => {
56
+ if (newWorker.state === "activated") {
57
+ swReady.value = true;
58
+ }
59
+ });
60
+ });
61
+ return registration;
62
+ } catch (err) {
63
+ console.error("Service worker registration failed:", err);
64
+ return null;
65
+ }
66
+ }
67
+ function persistCachedBooks() {
68
+ try {
69
+ localStorage.setItem(CACHED_BOOKS_KEY, JSON.stringify([...cachedBooks.value]));
70
+ } catch {
71
+ }
72
+ }
73
+ function handleSWMessage(event) {
74
+ const { type } = event.data;
75
+ if (type === "CACHE_PROGRESS") {
76
+ const { bookSlug, current, total } = event.data;
77
+ cacheProgress.value = { bookSlug, current, total };
78
+ }
79
+ if (type === "CACHE_COMPLETE") {
80
+ const { bookSlug } = event.data;
81
+ cacheProgress.value = null;
82
+ const newCached = new Set(cachedBooks.value);
83
+ newCached.add(bookSlug);
84
+ cachedBooks.value = newCached;
85
+ persistCachedBooks();
86
+ }
87
+ if (type === "CACHE_STATUS") {
88
+ const { cached } = event.data;
89
+ const newCached = /* @__PURE__ */ new Set();
90
+ for (const url of cached) {
91
+ const match = url.match(/\/books\/([^/]+)/);
92
+ if (match) {
93
+ newCached.add(match[1]);
94
+ }
95
+ }
96
+ cachedBooks.value = newCached;
97
+ persistCachedBooks();
98
+ }
99
+ if (type === "CACHE_CLEARED") {
100
+ const { bookSlug } = event.data;
101
+ const newCached = new Set(cachedBooks.value);
102
+ newCached.delete(bookSlug);
103
+ cachedBooks.value = newCached;
104
+ persistCachedBooks();
105
+ }
106
+ }
107
+ async function downloadBookForOffline(bookSlug, chapterUrls) {
108
+ if (!("serviceWorker" in navigator) || !navigator.serviceWorker.controller) {
109
+ console.warn("Service worker not available");
110
+ return false;
111
+ }
112
+ const urls = chapterUrls.map(
113
+ (url) => url.startsWith("http") ? url : `${window.location.origin}${url}`
114
+ );
115
+ cacheProgress.value = { bookSlug, current: 0, total: urls.length };
116
+ const newCached = new Set(cachedBooks.value);
117
+ newCached.add(bookSlug);
118
+ cachedBooks.value = newCached;
119
+ persistCachedBooks();
120
+ navigator.serviceWorker.controller.postMessage({
121
+ type: "CACHE_BOOK",
122
+ bookSlug,
123
+ urls
124
+ });
125
+ return true;
126
+ }
127
+ async function clearBookCache(bookSlug) {
128
+ const newCached = new Set(cachedBooks.value);
129
+ newCached.delete(bookSlug);
130
+ cachedBooks.value = newCached;
131
+ persistCachedBooks();
132
+ if (!("serviceWorker" in navigator) || !navigator.serviceWorker.controller) {
133
+ try {
134
+ const cache = await caches.open("pressy-offline-books");
135
+ const keys = await cache.keys();
136
+ for (const request of keys) {
137
+ if (request.url.includes(`/books/${bookSlug}`)) {
138
+ await cache.delete(request);
139
+ }
140
+ }
141
+ return true;
142
+ } catch (err) {
143
+ console.error("Failed to clear cache:", err);
144
+ return false;
145
+ }
146
+ }
147
+ navigator.serviceWorker.controller.postMessage({
148
+ type: "CLEAR_BOOK_CACHE",
149
+ bookSlug
150
+ });
151
+ return true;
152
+ }
153
+
154
+ // src/runtime/client.tsx
155
+ import { jsx, jsxs } from "preact/jsx-runtime";
156
+ var currentRoute = signal2("/");
157
+ var currentTheme = signal2("light");
158
+ var isOffline = signal2(!navigator.onLine);
159
+ var DB_NAME = "pressy";
160
+ var DB_VERSION = 1;
161
+ var PROGRESS_STORE = "reading-progress";
162
+ var UNLOCKS_STORE = "unlocks";
163
+ var db = null;
164
+ async function initDB() {
165
+ if (db) return db;
166
+ return new Promise((resolve, reject) => {
167
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
168
+ request.onerror = () => reject(request.error);
169
+ request.onsuccess = () => {
170
+ db = request.result;
171
+ resolve(db);
172
+ };
173
+ request.onupgradeneeded = (event) => {
174
+ const database = event.target.result;
175
+ if (!database.objectStoreNames.contains(PROGRESS_STORE)) {
176
+ database.createObjectStore(PROGRESS_STORE, { keyPath: "chapterSlug" });
177
+ }
178
+ if (!database.objectStoreNames.contains(UNLOCKS_STORE)) {
179
+ database.createObjectStore(UNLOCKS_STORE, { keyPath: "bookSlug" });
180
+ }
181
+ };
182
+ });
183
+ }
184
+ async function saveReadingProgress(progress) {
185
+ const database = await initDB();
186
+ return new Promise((resolve, reject) => {
187
+ const tx = database.transaction(PROGRESS_STORE, "readwrite");
188
+ const store = tx.objectStore(PROGRESS_STORE);
189
+ const request = store.put(progress);
190
+ request.onsuccess = () => resolve();
191
+ request.onerror = () => reject(request.error);
192
+ });
193
+ }
194
+ async function getReadingProgress(chapterSlug) {
195
+ const database = await initDB();
196
+ return new Promise((resolve, reject) => {
197
+ const tx = database.transaction(PROGRESS_STORE, "readonly");
198
+ const store = tx.objectStore(PROGRESS_STORE);
199
+ const request = store.get(chapterSlug);
200
+ request.onsuccess = () => resolve(request.result || null);
201
+ request.onerror = () => reject(request.error);
202
+ });
203
+ }
204
+ async function getAllReadingProgress() {
205
+ const database = await initDB();
206
+ return new Promise((resolve, reject) => {
207
+ const tx = database.transaction(PROGRESS_STORE, "readonly");
208
+ const store = tx.objectStore(PROGRESS_STORE);
209
+ const request = store.getAll();
210
+ request.onsuccess = () => resolve(request.result || []);
211
+ request.onerror = () => reject(request.error);
212
+ });
213
+ }
214
+ async function isBookUnlocked(bookSlug) {
215
+ const database = await initDB();
216
+ return new Promise((resolve, reject) => {
217
+ const tx = database.transaction(UNLOCKS_STORE, "readonly");
218
+ const store = tx.objectStore(UNLOCKS_STORE);
219
+ const request = store.get(bookSlug);
220
+ request.onsuccess = () => resolve(!!request.result);
221
+ request.onerror = () => reject(request.error);
222
+ });
223
+ }
224
+ async function unlockBook(bookSlug, orderId) {
225
+ const database = await initDB();
226
+ return new Promise((resolve, reject) => {
227
+ const tx = database.transaction(UNLOCKS_STORE, "readwrite");
228
+ const store = tx.objectStore(UNLOCKS_STORE);
229
+ const request = store.put({ bookSlug, orderId, unlockedAt: Date.now() });
230
+ request.onsuccess = () => resolve();
231
+ request.onerror = () => reject(request.error);
232
+ });
233
+ }
234
+ function navigate(path, replace = false) {
235
+ if (replace) {
236
+ history.replaceState(null, "", path);
237
+ } else {
238
+ history.pushState(null, "", path);
239
+ }
240
+ currentRoute.value = path;
241
+ }
242
+ function resolveSystemTheme() {
243
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
244
+ }
245
+ function setTheme(theme) {
246
+ currentTheme.value = theme;
247
+ localStorage.setItem("pressy-theme", theme);
248
+ const resolved = theme === "system" ? resolveSystemTheme() : theme;
249
+ document.documentElement.setAttribute("data-theme", resolved);
250
+ }
251
+ function loadTheme() {
252
+ const saved = localStorage.getItem("pressy-theme");
253
+ if (saved) {
254
+ setTheme(saved);
255
+ } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
256
+ setTheme("dark");
257
+ }
258
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
259
+ if (currentTheme.value === "system") {
260
+ document.documentElement.setAttribute("data-theme", resolveSystemTheme());
261
+ }
262
+ });
263
+ }
264
+ function setupOfflineDetection() {
265
+ window.addEventListener("online", () => {
266
+ isOffline.value = false;
267
+ });
268
+ window.addEventListener("offline", () => {
269
+ isOffline.value = true;
270
+ });
271
+ }
272
+ function renderBookPage(book, articles = []) {
273
+ const chapterUrls = book.chapters.map(
274
+ (ch) => `/books/${book.slug}/${ch.slug}`
275
+ );
276
+ return /* @__PURE__ */ jsxs("div", { class: "pressy-home", children: [
277
+ /* @__PURE__ */ jsxs("header", { class: "pressy-home-header", children: [
278
+ /* @__PURE__ */ jsx("h1", { children: book.metadata.title }),
279
+ /* @__PURE__ */ jsxs("p", { class: "pressy-home-author", children: [
280
+ "by ",
281
+ book.metadata.author
282
+ ] }),
283
+ book.metadata.description && /* @__PURE__ */ jsx("p", { class: "pressy-home-desc", children: book.metadata.description })
284
+ ] }),
285
+ /* @__PURE__ */ jsx(
286
+ DownloadBook,
287
+ {
288
+ bookSlug: book.slug,
289
+ chapterUrls,
290
+ cachedBooks,
291
+ cacheProgress,
292
+ onDownload: downloadBookForOffline,
293
+ onRemove: clearBookCache
294
+ }
295
+ ),
296
+ /* @__PURE__ */ jsxs("section", { class: "pressy-home-section", children: [
297
+ /* @__PURE__ */ jsx("h2", { children: "Chapters" }),
298
+ /* @__PURE__ */ jsx(
299
+ BookProgress,
300
+ {
301
+ bookSlug: book.slug,
302
+ chapters: book.chapters.map((ch) => ({
303
+ slug: ch.slug,
304
+ title: ch.title,
305
+ order: ch.order,
306
+ wordCount: ch.wordCount || 0
307
+ })),
308
+ basePath,
309
+ loadAllProgress: getAllReadingProgress
310
+ }
311
+ )
312
+ ] }),
313
+ articles.length > 0 && /* @__PURE__ */ jsxs("section", { class: "pressy-home-section", children: [
314
+ /* @__PURE__ */ jsx("h2", { children: "Articles" }),
315
+ /* @__PURE__ */ jsx("nav", { class: "pressy-chapter-list", children: articles.map((article) => /* @__PURE__ */ jsx(
316
+ "a",
317
+ {
318
+ href: `${basePath}/articles/${article.slug}`,
319
+ class: "pressy-chapter-link",
320
+ children: article.metadata.title
321
+ }
322
+ )) })
323
+ ] }),
324
+ /* @__PURE__ */ jsx("style", { children: HOME_STYLES })
325
+ ] });
326
+ }
327
+ function renderHomePage(manifest) {
328
+ if (manifest.books.length === 1) {
329
+ return renderBookPage(manifest.books[0], manifest.articles);
330
+ }
331
+ const siteTitle = manifest.books[0]?.metadata.title || "Library";
332
+ return /* @__PURE__ */ jsxs("div", { class: "pressy-home", children: [
333
+ /* @__PURE__ */ jsxs("header", { class: "pressy-home-header", children: [
334
+ /* @__PURE__ */ jsx("h1", { children: siteTitle }),
335
+ manifest.books[0]?.metadata.description && /* @__PURE__ */ jsx("p", { class: "pressy-home-desc", children: manifest.books[0].metadata.description })
336
+ ] }),
337
+ manifest.books.length > 0 && /* @__PURE__ */ jsxs("section", { class: "pressy-home-section", children: [
338
+ /* @__PURE__ */ jsx("h2", { children: "Books" }),
339
+ /* @__PURE__ */ jsx("nav", { class: "pressy-chapter-list", children: manifest.books.map((book) => /* @__PURE__ */ jsx(
340
+ "a",
341
+ {
342
+ href: `${basePath}/books/${book.slug}`,
343
+ class: "pressy-chapter-link",
344
+ children: book.metadata.title
345
+ }
346
+ )) })
347
+ ] }),
348
+ manifest.articles.length > 0 && /* @__PURE__ */ jsxs("section", { class: "pressy-home-section", children: [
349
+ /* @__PURE__ */ jsx("h2", { children: "Articles" }),
350
+ /* @__PURE__ */ jsx("nav", { class: "pressy-chapter-list", children: manifest.articles.map((article) => /* @__PURE__ */ jsx(
351
+ "a",
352
+ {
353
+ href: `${basePath}/articles/${article.slug}`,
354
+ class: "pressy-chapter-link",
355
+ children: article.metadata.title
356
+ }
357
+ )) })
358
+ ] }),
359
+ /* @__PURE__ */ jsx("style", { children: HOME_STYLES })
360
+ ] });
361
+ }
362
+ function calculateBookProgressPercent(book, currentChapterSlug, currentPage, currentTotalPages, allProgress) {
363
+ const totalWords = book.chapters.reduce((sum, ch) => sum + (ch.wordCount || 0), 0);
364
+ if (totalWords === 0) return 0;
365
+ const progressMap = new Map(allProgress.map((p) => [p.chapterSlug, p]));
366
+ let wordsRead = 0;
367
+ for (const ch of book.chapters) {
368
+ const chapterWords = ch.wordCount || 0;
369
+ if (ch.slug === currentChapterSlug) {
370
+ if (currentTotalPages > 0) {
371
+ wordsRead += currentPage / Math.max(1, currentTotalPages - 1) * chapterWords;
372
+ }
373
+ } else {
374
+ const progress = progressMap.get(ch.slug);
375
+ if (!progress) continue;
376
+ if (progress.totalPages > 0 && progress.page >= progress.totalPages - 1) {
377
+ wordsRead += chapterWords;
378
+ } else if (progress.page > 0 && progress.totalPages > 0) {
379
+ wordsRead += progress.page / progress.totalPages * chapterWords;
380
+ }
381
+ }
382
+ }
383
+ return Math.min(100, wordsRead / totalWords * 100);
384
+ }
385
+ function ChapterReaderWithProgress({
386
+ book,
387
+ chapterSlug,
388
+ chapter,
389
+ prevChapter,
390
+ nextChapter,
391
+ paginationMode,
392
+ Content,
393
+ chapterMapData
394
+ }) {
395
+ const [bookProgressPercent, setBookProgressPercent] = useState(void 0);
396
+ useEffect(() => {
397
+ getAllReadingProgress().then((allProgress) => {
398
+ const percent = calculateBookProgressPercent(book, chapterSlug, 0, 0, allProgress);
399
+ setBookProgressPercent(percent);
400
+ });
401
+ }, [book, chapterSlug]);
402
+ const handleSaveProgress = (data) => {
403
+ saveReadingProgress({
404
+ chapterSlug,
405
+ page: data.page,
406
+ totalPages: data.totalPages,
407
+ scrollPosition: data.scrollPosition,
408
+ timestamp: Date.now()
409
+ });
410
+ if (data.totalPages > 0) {
411
+ getAllReadingProgress().then((allProgress) => {
412
+ const percent = calculateBookProgressPercent(
413
+ book,
414
+ chapterSlug,
415
+ data.page,
416
+ data.totalPages,
417
+ allProgress
418
+ );
419
+ setBookProgressPercent(percent);
420
+ });
421
+ }
422
+ };
423
+ const handleRestoreProgress = async () => {
424
+ const progress = await getReadingProgress(chapterSlug);
425
+ if (!progress) return null;
426
+ return {
427
+ page: progress.page,
428
+ totalPages: progress.totalPages,
429
+ scrollPosition: progress.scrollPosition
430
+ };
431
+ };
432
+ const handleChapterChange = (slug, page, chapterTotalPages) => {
433
+ saveReadingProgress({
434
+ chapterSlug: slug,
435
+ page,
436
+ totalPages: chapterTotalPages,
437
+ scrollPosition: 0,
438
+ timestamp: Date.now()
439
+ });
440
+ getAllReadingProgress().then((allProgress) => {
441
+ const percent = calculateBookProgressPercent(book, slug, page, chapterTotalPages, allProgress);
442
+ setBookProgressPercent(percent);
443
+ });
444
+ };
445
+ useEffect(() => {
446
+ if (prevChapter) {
447
+ const link = document.createElement("link");
448
+ link.rel = "prefetch";
449
+ link.href = prevChapter.slug;
450
+ document.head.appendChild(link);
451
+ return () => {
452
+ document.head.removeChild(link);
453
+ };
454
+ }
455
+ }, [prevChapter]);
456
+ return /* @__PURE__ */ jsx(
457
+ Reader,
458
+ {
459
+ title: chapter?.title || chapterSlug,
460
+ bookTitle: book.metadata.title,
461
+ chapterSlug,
462
+ prevChapter,
463
+ nextChapter,
464
+ paginationMode,
465
+ onSaveProgress: handleSaveProgress,
466
+ onRestoreProgress: handleRestoreProgress,
467
+ bookProgressPercent,
468
+ initialContent: Content,
469
+ chapterMapData,
470
+ currentChapterSlug: chapterSlug,
471
+ allChapters: book.chapters.map((ch) => ({ slug: ch.slug, title: ch.title, wordCount: ch.wordCount })),
472
+ bookBasePath: `${basePath}/books/${book.slug}`,
473
+ onChapterChange: handleChapterChange,
474
+ mdxComponents: useMDXComponents(),
475
+ children: /* @__PURE__ */ jsx(Content, { components: useMDXComponents() })
476
+ }
477
+ );
478
+ }
479
+ function renderChapterPage(manifest, route, Content, paginationMode, chapterMapData) {
480
+ const parts = route.split("/");
481
+ const bookSlug = parts[2];
482
+ const chapterSlug = parts[3];
483
+ const book = manifest.books.find((b) => b.slug === bookSlug);
484
+ const chapterIdx = book ? book.chapters.findIndex((c) => c.slug === chapterSlug) : -1;
485
+ const chapter = book?.chapters[chapterIdx];
486
+ const chapterPath = (ch) => `${basePath}/books/${bookSlug}/${ch.slug}`;
487
+ const prevChapter = book && chapterIdx > 0 ? { slug: chapterPath(book.chapters[chapterIdx - 1]), title: book.chapters[chapterIdx - 1].title } : book ? { slug: `${basePath}/books/${bookSlug}`, title: book.metadata.title } : void 0;
488
+ const nextChapter = book && chapterIdx >= 0 && chapterIdx < book.chapters.length - 1 ? { slug: chapterPath(book.chapters[chapterIdx + 1]), title: book.chapters[chapterIdx + 1].title } : book ? { slug: `${basePath}/books/${bookSlug}`, title: book.metadata.title } : void 0;
489
+ const MDXContent = Content;
490
+ if (!book) {
491
+ return /* @__PURE__ */ jsx(
492
+ Reader,
493
+ {
494
+ title: chapter?.title || chapterSlug,
495
+ prevChapter,
496
+ nextChapter,
497
+ paginationMode,
498
+ children: /* @__PURE__ */ jsx(MDXContent, { components: useMDXComponents() })
499
+ }
500
+ );
501
+ }
502
+ return /* @__PURE__ */ jsx(
503
+ ChapterReaderWithProgress,
504
+ {
505
+ book,
506
+ chapterSlug,
507
+ chapter,
508
+ prevChapter,
509
+ nextChapter,
510
+ paginationMode,
511
+ Content: MDXContent,
512
+ chapterMapData
513
+ }
514
+ );
515
+ }
516
+ function renderArticlePage(manifest, route, Content) {
517
+ const articleSlug = route.split("/")[2];
518
+ const article = manifest.articles.find((a) => a.slug === articleSlug);
519
+ const MDXContent = Content;
520
+ return /* @__PURE__ */ jsx(Reader, { title: article?.metadata.title || articleSlug, children: /* @__PURE__ */ jsx(MDXContent, { components: useMDXComponents() }) });
521
+ }
522
+ var HOME_STYLES = `
523
+ .pressy-home {
524
+ max-width: 65ch;
525
+ margin: 0 auto;
526
+ padding: 2rem 1.5rem;
527
+ font-family: var(--font-body, Georgia, 'Times New Roman', serif);
528
+ color: var(--color-text, #1a1a1a);
529
+ }
530
+ .pressy-home-header {
531
+ margin-bottom: 3rem;
532
+ text-align: center;
533
+ }
534
+ .pressy-home-header h1 {
535
+ font-size: 2rem;
536
+ margin-bottom: 0.5rem;
537
+ }
538
+ .pressy-home-author {
539
+ font-style: italic;
540
+ color: var(--color-text-muted, #666);
541
+ }
542
+ .pressy-home-desc {
543
+ color: var(--color-text-muted, #666);
544
+ line-height: 1.6;
545
+ max-width: 50ch;
546
+ margin: 0.5rem auto 0;
547
+ }
548
+ .pressy-home-section {
549
+ margin-bottom: 2rem;
550
+ }
551
+ .pressy-home-section h2 {
552
+ font-size: 1.25rem;
553
+ margin-bottom: 1rem;
554
+ border-bottom: 1px solid var(--color-border, #e5e5e5);
555
+ padding-bottom: 0.5rem;
556
+ }
557
+ .pressy-chapter-list {
558
+ display: flex;
559
+ flex-direction: column;
560
+ gap: 0.25rem;
561
+ }
562
+ .pressy-chapter-link {
563
+ display: flex;
564
+ gap: 0.75rem;
565
+ padding: 0.75rem 1rem;
566
+ text-decoration: none;
567
+ color: var(--color-text, #1a1a1a);
568
+ border-radius: 0.5rem;
569
+ transition: background 0.15s;
570
+ }
571
+ .pressy-chapter-link:hover {
572
+ background: var(--color-bg-subtle, #f5f5f5);
573
+ }
574
+ .pressy-chapter-order {
575
+ color: var(--color-text-muted, #666);
576
+ min-width: 2ch;
577
+ }
578
+ `;
579
+ function getBasePath(route) {
580
+ const pathname = window.location.pathname;
581
+ const cleanPath = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
582
+ const cleanRoute = route.endsWith("/") ? route.slice(0, -1) : route;
583
+ if (cleanRoute === "" || cleanRoute === "/") {
584
+ return cleanPath.replace(/\/index\.html$/, "") || "";
585
+ }
586
+ const idx = cleanPath.indexOf(cleanRoute);
587
+ if (idx > 0) {
588
+ return cleanPath.slice(0, idx);
589
+ }
590
+ return "";
591
+ }
592
+ var basePath = "";
593
+ function hydrate(data, Content, chapterMapData) {
594
+ basePath = getBasePath(data.route);
595
+ currentRoute.value = data.route;
596
+ loadTheme();
597
+ setupOfflineDetection();
598
+ initDB();
599
+ registerServiceWorker(basePath);
600
+ window.addEventListener("popstate", () => {
601
+ currentRoute.value = window.location.pathname;
602
+ });
603
+ document.addEventListener("click", (e) => {
604
+ const link = e.target.closest("a");
605
+ if (!link) return;
606
+ const href = link.getAttribute("href");
607
+ if (!href) return;
608
+ if (href.startsWith("http") || href.startsWith("#")) return;
609
+ if (href.startsWith(data.route + "#")) {
610
+ e.preventDefault();
611
+ const hash = href.slice(href.indexOf("#"));
612
+ const target = document.querySelector(hash);
613
+ if (target) target.scrollIntoView({ behavior: "smooth" });
614
+ return;
615
+ }
616
+ });
617
+ let page;
618
+ switch (data.routeType) {
619
+ case "home":
620
+ page = renderHomePage(data.manifest);
621
+ break;
622
+ case "book": {
623
+ const bookSlug = data.route.split("/")[2];
624
+ const book = data.manifest.books.find((b) => b.slug === bookSlug);
625
+ page = book ? renderBookPage(book) : /* @__PURE__ */ jsx("div", { children: "Book not found" });
626
+ break;
627
+ }
628
+ case "chapter":
629
+ page = Content ? renderChapterPage(data.manifest, data.route, Content, data.pagination?.defaultMode, chapterMapData) : /* @__PURE__ */ jsx("div", { children: "Loading..." });
630
+ break;
631
+ case "article":
632
+ page = Content ? renderArticlePage(data.manifest, data.route, Content) : /* @__PURE__ */ jsx("div", { children: "Loading..." });
633
+ break;
634
+ case "books":
635
+ page = renderHomePage(data.manifest);
636
+ break;
637
+ case "articles":
638
+ page = renderHomePage(data.manifest);
639
+ break;
640
+ default:
641
+ page = /* @__PURE__ */ jsx("div", { children: "Page not found" });
642
+ }
643
+ render(page, document.getElementById("app"));
644
+ }
645
+ export {
646
+ currentRoute,
647
+ currentTheme,
648
+ effect,
649
+ getAllReadingProgress,
650
+ getReadingProgress,
651
+ hydrate,
652
+ isBookUnlocked,
653
+ isOffline,
654
+ navigate,
655
+ saveReadingProgress,
656
+ setTheme,
657
+ signal2 as signal,
658
+ unlockBook
659
+ };
660
+ //# sourceMappingURL=client.js.map