mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Restructure Repository for SvelteKit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { gql } from "@api/client";
|
||||
import { store, addHistory, addBookmark, removeBookmark,
|
||||
checkAndMarkCompleted, DEFAULT_MANGA_PREFS } from "@store/state.svelte";
|
||||
import { MARK_CHAPTER_READ, DELETE_DOWNLOADED_CHAPTERS } from "@api/mutations/chapters";
|
||||
import { ENQUEUE_CHAPTERS_DOWNLOAD } from "@api/mutations/downloads";
|
||||
import { trackingState } from "@features/tracking/store/trackingState.svelte";
|
||||
|
||||
const AVG_MIN_PER_PAGE = 0.33;
|
||||
|
||||
export function getMangaPrefs() {
|
||||
const mangaId = store.activeManga?.id;
|
||||
if (!mangaId) return DEFAULT_MANGA_PREFS;
|
||||
return { ...DEFAULT_MANGA_PREFS, ...(store.settings.mangaPrefs?.[mangaId] ?? {}) };
|
||||
}
|
||||
|
||||
export function markChapterRead(id: number, markedRead: Set<number>) {
|
||||
if (markedRead.has(id)) return;
|
||||
markedRead.add(id);
|
||||
const chapter = store.activeChapterList.find(c => c.id === id) ?? store.activeChapter;
|
||||
const pages = chapter?.pageCount ?? store.pageUrls.length ?? 15;
|
||||
const minutes = Math.max(1, Math.round(pages * AVG_MIN_PER_PAGE));
|
||||
if (store.activeManga && chapter) {
|
||||
addHistory(
|
||||
{ mangaId: store.activeManga.id, mangaTitle: store.activeManga.title, thumbnailUrl: store.activeManga.thumbnailUrl, chapterId: id, chapterName: chapter.name, readAt: Date.now() },
|
||||
true, minutes,
|
||||
);
|
||||
}
|
||||
gql(MARK_CHAPTER_READ, { id, isRead: true })
|
||||
.then(() => {
|
||||
const mangaId = store.activeManga?.id;
|
||||
if (!mangaId) return;
|
||||
const updated = store.activeChapterList.map(c => c.id === id ? { ...c, isRead: true } : c);
|
||||
checkAndMarkCompleted(mangaId, updated);
|
||||
const ch = store.activeChapterList.find(c => c.id === id) ?? store.activeChapter;
|
||||
const prefs = getMangaPrefs();
|
||||
if (ch) trackingState.updateFromRead(mangaId, ch, store.activeChapterList, prefs);
|
||||
if (prefs.deleteOnRead) {
|
||||
const ch = store.activeChapterList.find(c => c.id === id);
|
||||
if (ch?.isDownloaded) {
|
||||
const delayMs = (prefs.deleteDelayHours ?? 0) * 3_600_000;
|
||||
const doDelete = () => gql(DELETE_DOWNLOADED_CHAPTERS, { ids: [id] }).catch(console.error);
|
||||
if (delayMs === 0) doDelete(); else setTimeout(doDelete, delayMs);
|
||||
}
|
||||
}
|
||||
if (prefs.downloadAhead > 0) {
|
||||
const list = store.activeChapterList;
|
||||
const idx = list.findIndex(c => c.id === id);
|
||||
if (idx >= 0) {
|
||||
const toQueue = list.slice(idx + 1, idx + 1 + prefs.downloadAhead).filter(c => !c.isDownloaded && !c.isRead).map(c => c.id);
|
||||
if (toQueue.length) gql(ENQUEUE_CHAPTERS_DOWNLOAD, { chapterIds: toQueue }).catch(console.error);
|
||||
}
|
||||
}
|
||||
if (prefs.maxKeepChapters > 0) {
|
||||
const downloaded = store.activeChapterList.filter(c => c.isDownloaded).sort((a, b) => a.sourceOrder - b.sourceOrder);
|
||||
const excess = downloaded.slice(0, Math.max(0, downloaded.length - prefs.maxKeepChapters));
|
||||
if (excess.length) gql(DELETE_DOWNLOADED_CHAPTERS, { ids: excess.map(c => c.id) }).catch(console.error);
|
||||
}
|
||||
})
|
||||
.catch(e => { markedRead.delete(id); console.error(e); });
|
||||
}
|
||||
|
||||
export function toggleBookmark(
|
||||
displayChapter: import("@types").Chapter | null | undefined,
|
||||
pageNumber: number,
|
||||
) {
|
||||
const ch = displayChapter;
|
||||
const manga = store.activeManga;
|
||||
if (!ch || !manga) return;
|
||||
const isBookmarked = !!store.bookmarks.find(
|
||||
b => b.mangaId === manga.id && b.chapterId === ch.id && b.pageNumber === pageNumber,
|
||||
);
|
||||
if (isBookmarked) {
|
||||
removeBookmark(ch.id);
|
||||
} else {
|
||||
const existing = store.bookmarks.find(b => b.mangaId === manga.id && b.chapterId !== ch.id);
|
||||
if (existing) removeBookmark(existing.chapterId);
|
||||
addBookmark({ mangaId: manga.id, mangaTitle: manga.title, thumbnailUrl: manga.thumbnailUrl, chapterId: ch.id, chapterName: ch.name, pageNumber });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { store } from "@store/state.svelte";
|
||||
import { readerState } from "../store/readerState.svelte";
|
||||
import { fetchPages } from "./pageLoader";
|
||||
import { trackingState } from "@features/tracking/store/trackingState.svelte";
|
||||
import { cancelQueuedFetches } from "@core/cache/imageCache";
|
||||
import { clearResolvedUrlCache } from "@core/cache/pageCache";
|
||||
|
||||
export function scheduleResumeDismiss() {
|
||||
setTimeout(() => { readerState.resumeFading = true; }, 1500);
|
||||
setTimeout(() => { readerState.resumeVisible = false; readerState.resumeFading = false; }, 2500);
|
||||
}
|
||||
|
||||
export async function loadChapter(
|
||||
id: number,
|
||||
useBlob: boolean,
|
||||
abortCtrl: { current: AbortController | null },
|
||||
startAtLastPage: { current: boolean },
|
||||
markedRead: Set<number>,
|
||||
adjacent: { next: { id: number } | null },
|
||||
) {
|
||||
abortCtrl.current?.abort();
|
||||
const ctrl = new AbortController();
|
||||
abortCtrl.current = ctrl;
|
||||
|
||||
cancelQueuedFetches();
|
||||
if (useBlob) clearResolvedUrlCache();
|
||||
|
||||
startAtLastPage.current = false;
|
||||
markedRead.clear();
|
||||
readerState.resetForChapter();
|
||||
store.pageUrls = [];
|
||||
|
||||
const mangaId = store.activeManga?.id;
|
||||
if (mangaId) trackingState.loadForManga(mangaId);
|
||||
|
||||
const bookmark = store.bookmarks.find(b => b.chapterId === id);
|
||||
const resumeTo = bookmark ? bookmark.pageNumber : 0;
|
||||
readerState.resumePage = resumeTo > 1 ? resumeTo : 0;
|
||||
readerState.resumeDismissed = false;
|
||||
readerState.resumeVisible = resumeTo > 1;
|
||||
if (resumeTo > 1) scheduleResumeDismiss();
|
||||
|
||||
store.pageNumber = 1;
|
||||
try {
|
||||
const urls = await fetchPages(id, useBlob, ctrl.signal, resumeTo > 1 ? resumeTo - 1 : 0);
|
||||
if (ctrl.signal.aborted) return;
|
||||
store.pageUrls = urls;
|
||||
if (startAtLastPage.current) store.pageNumber = urls.length;
|
||||
else if (resumeTo > 1) store.pageNumber = Math.min(resumeTo, urls.length || resumeTo);
|
||||
readerState.pageReady = true;
|
||||
readerState.loading = false;
|
||||
if (adjacent.next) fetchPages(adjacent.next.id, useBlob, ctrl.signal).catch(() => {});
|
||||
} catch (e: any) {
|
||||
if (ctrl.signal.aborted) return;
|
||||
readerState.error = e instanceof Error ? e.message : String(e);
|
||||
readerState.loading = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export { readerState } from "./store/readerState.svelte";
|
||||
export type { PageStyle } from "./store/readerState.svelte";
|
||||
export { PAGE_STYLES, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX } from "./store/readerState.svelte";
|
||||
|
||||
export { fetchPages, resolveUrl, preloadImage, measureAspect, buildPageGroups, clearPageCache } from "./lib/pageLoader";
|
||||
export { setupScrollTracking, appendNextChapter } from "./lib/scrollHandler";
|
||||
export type { StripChapter, ScrollHandlerCallbacks } from "./lib/scrollHandler";
|
||||
export { createReaderKeyHandler } from "./lib/readerKeybinds";
|
||||
export type { ReaderKeyActions } from "./lib/readerKeybinds";
|
||||
|
||||
export { markChapterRead, getMangaPrefs, toggleBookmark } from "./lib/chapterActions";
|
||||
export { goForward, goBack, jumpToPage, animateFade } from "./lib/navigation";
|
||||
export { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "./lib/zoomHelpers";
|
||||
export { loadChapter, scheduleResumeDismiss } from "./lib/chapterLoader";
|
||||
@@ -0,0 +1,84 @@
|
||||
import { store, openReader, closeReader } from "@store/state.svelte";
|
||||
import { readerState } from "../store/readerState.svelte";
|
||||
import type { Chapter } from "@types";
|
||||
|
||||
interface Adjacent {
|
||||
prev: Chapter | null;
|
||||
next: Chapter | null;
|
||||
}
|
||||
|
||||
export function advanceGroup(forward: boolean, adjacent: Adjacent, startAtLastPage: () => void) {
|
||||
if (!readerState.pageGroups.length) return;
|
||||
const gi = readerState.pageGroups.findIndex(g => g.includes(store.pageNumber));
|
||||
if (forward) {
|
||||
if (gi < readerState.pageGroups.length - 1) store.pageNumber = readerState.pageGroups[gi + 1][0];
|
||||
else if (adjacent.next) { store.pageNumber = 1; openReader(adjacent.next, store.activeChapterList); }
|
||||
else closeReader();
|
||||
} else {
|
||||
if (gi > 0) store.pageNumber = readerState.pageGroups[gi - 1][0];
|
||||
else if (adjacent.prev) { startAtLastPage(); openReader(adjacent.prev, store.activeChapterList); }
|
||||
}
|
||||
}
|
||||
|
||||
export async function animateFade(fn: () => void) {
|
||||
readerState.fadingOut = true;
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
fn();
|
||||
readerState.fadingOut = false;
|
||||
}
|
||||
|
||||
export function goForward(
|
||||
style: string,
|
||||
adjacent: Adjacent,
|
||||
lastPage: number,
|
||||
onMaybeMarkRead: () => void,
|
||||
startAtLastPage: () => void,
|
||||
) {
|
||||
if (readerState.loading) return;
|
||||
if (style === "longstrip") {
|
||||
if (adjacent.next) { onMaybeMarkRead(); openReader(adjacent.next, store.activeChapterList); }
|
||||
return;
|
||||
}
|
||||
if (style === "double" && readerState.pageGroups.length) { advanceGroup(true, adjacent, startAtLastPage); return; }
|
||||
if (!store.pageUrls.length) return;
|
||||
if (store.pageNumber < lastPage) {
|
||||
if (style === "fade") animateFade(() => { store.pageNumber++; });
|
||||
else store.pageNumber++;
|
||||
} else if (adjacent.next) {
|
||||
onMaybeMarkRead();
|
||||
store.pageNumber = 1;
|
||||
openReader(adjacent.next, store.activeChapterList);
|
||||
} else closeReader();
|
||||
}
|
||||
|
||||
export function goBack(
|
||||
style: string,
|
||||
adjacent: Adjacent,
|
||||
startAtLastPage: () => void,
|
||||
) {
|
||||
if (readerState.loading) return;
|
||||
if (style === "longstrip") {
|
||||
if (adjacent.prev) { startAtLastPage(); openReader(adjacent.prev, store.activeChapterList); }
|
||||
return;
|
||||
}
|
||||
if (style === "double" && readerState.pageGroups.length) { advanceGroup(false, adjacent, startAtLastPage); return; }
|
||||
if (!store.pageUrls.length) return;
|
||||
if (store.pageNumber > 1) {
|
||||
if (style === "fade") animateFade(() => { store.pageNumber--; });
|
||||
else store.pageNumber--;
|
||||
} else if (adjacent.prev) { startAtLastPage(); openReader(adjacent.prev, store.activeChapterList); }
|
||||
}
|
||||
|
||||
export function jumpToPage(page: number, style: string, lastPage: number, containerEl: HTMLElement | null) {
|
||||
if (style === "longstrip") {
|
||||
const chId = readerState.visibleChapterId ?? store.activeChapter?.id;
|
||||
containerEl?.querySelector<HTMLImageElement>(`img[data-local-page="${page}"][data-chapter="${chId}"]`)?.scrollIntoView({ block: "start" });
|
||||
return;
|
||||
}
|
||||
if (style === "double" && readerState.pageGroups.length) {
|
||||
const group = readerState.pageGroups[page - 1];
|
||||
if (group) store.pageNumber = group[0];
|
||||
} else {
|
||||
store.pageNumber = Math.max(1, Math.min(lastPage, page));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export { fetchPages, resolveUrl, preloadImage, measureAspect, clearPageCache } from "@core/cache/pageCache";
|
||||
|
||||
export function buildPageGroups(urls: string[], aspects: number[], offsetSpreads: boolean): number[][] {
|
||||
const groups: number[][] = [[1]];
|
||||
if (offsetSpreads) groups.push([2]);
|
||||
let i = offsetSpreads ? 3 : 2;
|
||||
while (i <= urls.length) {
|
||||
const a = aspects[i - 1];
|
||||
if (a > 1.2 || i === urls.length) { groups.push([i++]); }
|
||||
else { groups.push([i, i + 1]); i += 2; }
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createPinchGesture } from "@core/ui/touchscreen";
|
||||
import { clampZoom } from "./zoomHelpers";
|
||||
import { ZOOM_MIN, ZOOM_MAX } from "../store/readerState.svelte";
|
||||
|
||||
export interface PinchTrackerOptions {
|
||||
getZoom: () => number;
|
||||
setZoom: (z: number) => void;
|
||||
getInspectScale: () => number;
|
||||
setInspectScale: (s: number) => void;
|
||||
resetInspectPan: () => void;
|
||||
isLongstrip: () => boolean;
|
||||
}
|
||||
|
||||
export type { PinchGesture as PinchTracker } from "@core/ui/touchscreen";
|
||||
|
||||
const INSPECT_ZOOM_MAX = 8;
|
||||
|
||||
export function createPinchTracker(opts: PinchTrackerOptions) {
|
||||
let startZoom = 0;
|
||||
let startInspect = 0;
|
||||
|
||||
return createPinchGesture({
|
||||
onPinch(scale) {
|
||||
if (startZoom === 0) {
|
||||
startZoom = opts.getZoom();
|
||||
startInspect = opts.getInspectScale();
|
||||
}
|
||||
if (opts.isLongstrip()) {
|
||||
opts.setZoom(clampZoom(startZoom * scale));
|
||||
} else {
|
||||
const next = Math.max(1, Math.min(INSPECT_ZOOM_MAX, startInspect * scale));
|
||||
if (next !== opts.getInspectScale()) {
|
||||
if (next === 1) opts.resetInspectPan();
|
||||
opts.setInspectScale(next);
|
||||
}
|
||||
}
|
||||
},
|
||||
onPinchEnd() {
|
||||
startZoom = 0;
|
||||
startInspect = 0;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { matchesKeybind, toggleFullscreen, DEFAULT_KEYBINDS } from "@core/keybinds";
|
||||
import type { Keybinds } from "@core/keybinds";
|
||||
|
||||
export interface ReaderKeyActions {
|
||||
goNext: () => void;
|
||||
goPrev: () => void;
|
||||
closeReader: () => void;
|
||||
goToPage: (page: number) => void;
|
||||
lastPage: () => number;
|
||||
adjustZoom: (delta: number) => void;
|
||||
resetZoom: () => void;
|
||||
cycleStyle: () => void;
|
||||
toggleDirection: () => void;
|
||||
openSettings: () => void;
|
||||
toggleBookmark: () => void;
|
||||
toggleMarker: () => void;
|
||||
toggleAutoScroll: () => void;
|
||||
chapterNext: () => void;
|
||||
chapterPrev: () => void;
|
||||
closePopovers: () => boolean;
|
||||
getKeybinds: () => Keybinds;
|
||||
}
|
||||
|
||||
const ZOOM_STEP = 0.10;
|
||||
|
||||
export function createReaderKeyHandler(actions: ReaderKeyActions): (e: KeyboardEvent) => void {
|
||||
return function onKey(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return;
|
||||
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
if (actions.closePopovers()) return;
|
||||
actions.closeReader();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.ctrlKey) {
|
||||
if (e.key === "=" || e.key === "+") { e.preventDefault(); actions.adjustZoom(ZOOM_STEP); return; }
|
||||
if (e.key === "-") { e.preventDefault(); actions.adjustZoom(-ZOOM_STEP); return; }
|
||||
if (e.key === "0") { e.preventDefault(); actions.resetZoom(); return; }
|
||||
}
|
||||
|
||||
const kb = actions.getKeybinds();
|
||||
|
||||
if (matchesKeybind(e, kb.exitReader)) { e.preventDefault(); actions.closeReader(); }
|
||||
else if (matchesKeybind(e, kb.turnPageRight)) { e.preventDefault(); actions.goNext(); }
|
||||
else if (matchesKeybind(e, kb.turnPageLeft)) { e.preventDefault(); actions.goPrev(); }
|
||||
else if (matchesKeybind(e, kb.firstPage)) { e.preventDefault(); actions.goToPage(1); }
|
||||
else if (matchesKeybind(e, kb.lastPage)) { e.preventDefault(); actions.goToPage(actions.lastPage()); }
|
||||
else if (matchesKeybind(e, kb.turnChapterRight)) { e.preventDefault(); actions.chapterNext(); }
|
||||
else if (matchesKeybind(e, kb.turnChapterLeft)) { e.preventDefault(); actions.chapterPrev(); }
|
||||
else if (matchesKeybind(e, kb.togglePageStyle)) { e.preventDefault(); actions.cycleStyle(); }
|
||||
else if (matchesKeybind(e, kb.toggleReadingDirection)) { e.preventDefault(); actions.toggleDirection(); }
|
||||
else if (matchesKeybind(e, kb.toggleFullscreen)) { e.preventDefault(); toggleFullscreen().catch(console.error); }
|
||||
else if (matchesKeybind(e, kb.openSettings)) { e.preventDefault(); actions.openSettings(); }
|
||||
else if (matchesKeybind(e, kb.toggleBookmark)) { e.preventDefault(); actions.toggleBookmark(); }
|
||||
else if (matchesKeybind(e, kb.toggleMarker)) { e.preventDefault(); actions.toggleMarker(); }
|
||||
else if (matchesKeybind(e, kb.toggleAutoScroll)) { e.preventDefault(); actions.toggleAutoScroll(); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
export const READ_LINE_PCT = 0.50;
|
||||
|
||||
export interface StripChapter {
|
||||
chapterId: number;
|
||||
chapterName: string;
|
||||
urls: string[];
|
||||
}
|
||||
|
||||
export interface ScrollHandlerCallbacks {
|
||||
onPageChange: (page: number) => void;
|
||||
onChapterChange: (chapterId: number) => void;
|
||||
onMarkRead: (chapterId: number) => void;
|
||||
onAppend: () => void;
|
||||
getStripChapters: () => StripChapter[];
|
||||
getPageUrls: () => string[];
|
||||
shouldAutoMark: () => boolean;
|
||||
}
|
||||
|
||||
export function setupScrollTracking(
|
||||
containerEl: HTMLElement,
|
||||
callbacks: ScrollHandlerCallbacks,
|
||||
): () => void {
|
||||
const {
|
||||
onPageChange, onChapterChange, onMarkRead,
|
||||
onAppend, getStripChapters, getPageUrls, shouldAutoMark,
|
||||
} = callbacks;
|
||||
|
||||
let rafId: number | null = null;
|
||||
|
||||
function tick() {
|
||||
rafId = null;
|
||||
|
||||
const imgs = containerEl.querySelectorAll<HTMLElement>("img[data-local-page]");
|
||||
if (!imgs.length) return;
|
||||
|
||||
const containerTop = containerEl.getBoundingClientRect().top;
|
||||
const readLineY = containerTop + containerEl.clientHeight * READ_LINE_PCT;
|
||||
|
||||
let lo = 0, hi = imgs.length - 1, best = 0;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (imgs[mid].getBoundingClientRect().top <= readLineY) { best = mid; lo = mid + 1; }
|
||||
else hi = mid - 1;
|
||||
}
|
||||
|
||||
const active = imgs[best];
|
||||
const activePage = Number(active.dataset.localPage);
|
||||
const activeChId = Number(active.dataset.chapter);
|
||||
|
||||
onPageChange(activePage);
|
||||
if (activeChId) onChapterChange(activeChId);
|
||||
|
||||
if (shouldAutoMark() && activeChId) {
|
||||
const chunks = getStripChapters();
|
||||
const chunk = chunks.find(c => c.chapterId === activeChId);
|
||||
const total = chunk ? chunk.urls.length : getPageUrls().length;
|
||||
if (total > 0 && activePage >= total) onMarkRead(activeChId);
|
||||
|
||||
const atBottom = containerEl.scrollTop + containerEl.clientHeight >= containerEl.scrollHeight - 40;
|
||||
if (atBottom) {
|
||||
const last = chunks[chunks.length - 1];
|
||||
if (last) onMarkRead(last.chapterId);
|
||||
}
|
||||
}
|
||||
|
||||
const pct = (containerEl.scrollTop + containerEl.clientHeight) / containerEl.scrollHeight;
|
||||
if (pct >= 0.80) onAppend();
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (rafId !== null) return;
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
containerEl.addEventListener("scroll", onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
containerEl.removeEventListener("scroll", onScroll);
|
||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||
};
|
||||
}
|
||||
|
||||
export function appendNextChapter(
|
||||
stripChapters: StripChapter[],
|
||||
chapterList: { id: number; name: string }[],
|
||||
fetchPages: (chapterId: number) => Promise<string[]>,
|
||||
preloadImage: (url: string) => void,
|
||||
onAppended: (next: StripChapter) => void,
|
||||
onDone: () => void,
|
||||
): void {
|
||||
if (!stripChapters.length) return;
|
||||
|
||||
const lastChunk = stripChapters[stripChapters.length - 1];
|
||||
const lastIdx = chapterList.findIndex(c => c.id === lastChunk.chapterId);
|
||||
if (lastIdx < 0 || lastIdx >= chapterList.length - 1) return;
|
||||
|
||||
const next = chapterList[lastIdx + 1];
|
||||
if (!next || stripChapters.some(c => c.chapterId === next.id)) return;
|
||||
|
||||
fetchPages(next.id)
|
||||
.then(urls => {
|
||||
urls.slice(0, 6).forEach(preloadImage);
|
||||
return urls;
|
||||
})
|
||||
.then(urls => {
|
||||
if (stripChapters.some(c => c.chapterId === next.id)) { onDone(); return; }
|
||||
onAppended({ chapterId: next.id, chapterName: next.name, urls });
|
||||
onDone();
|
||||
})
|
||||
.catch(() => onDone());
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { clampZoom as _clampZoom, captureZoomAnchor, restoreZoomAnchor } from "@core/ui/zoom";
|
||||
import { ZOOM_MIN, ZOOM_MAX } from "../store/readerState.svelte";
|
||||
|
||||
export { captureZoomAnchor, restoreZoomAnchor };
|
||||
|
||||
export function clampZoom(z: number): number {
|
||||
return _clampZoom(z, ZOOM_MIN, ZOOM_MAX);
|
||||
}
|
||||
Reference in New Issue
Block a user