mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 17:29:55 -05:00
Fix: Zoom Values turning to NaN in Reader
This commit is contained in:
Vendored
+1
@@ -1,3 +1,4 @@
|
|||||||
export * from './memoryCache';
|
export * from './memoryCache';
|
||||||
|
export * from './pageCache';
|
||||||
export * from './imageCache';
|
export * from './imageCache';
|
||||||
export * from './queryCache';
|
export * from './queryCache';
|
||||||
|
|||||||
Vendored
+79
@@ -0,0 +1,79 @@
|
|||||||
|
import { gql, plainThumbUrl } from "@api/client";
|
||||||
|
import { getBlobUrl, preloadBlobUrls } from "@core/cache/imageCache";
|
||||||
|
import { dedupeRequest } from "@core/async/batchRequests";
|
||||||
|
import { FETCH_CHAPTER_PAGES } from "@api/mutations/chapters";
|
||||||
|
|
||||||
|
const pageCache = new Map<number, string[]>();
|
||||||
|
const inflight = new Map<number, Promise<string[]>>();
|
||||||
|
const resolvedUrlCache = new Map<string, Promise<string>>();
|
||||||
|
const preloadedUrls = new Set<string>();
|
||||||
|
const aspectCache = new Map<string, number>();
|
||||||
|
|
||||||
|
export function resolveUrl(url: string, useBlob: boolean, priority = 0): Promise<string> {
|
||||||
|
if (!useBlob) return Promise.resolve(url);
|
||||||
|
if (!resolvedUrlCache.has(url)) resolvedUrlCache.set(url, getBlobUrl(url, priority));
|
||||||
|
return resolvedUrlCache.get(url)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchPages(
|
||||||
|
chapterId: number,
|
||||||
|
useBlob: boolean,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
priorityPage = 0,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const cached = pageCache.get(chapterId);
|
||||||
|
if (cached) return Promise.resolve(cached);
|
||||||
|
if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
||||||
|
|
||||||
|
if (!inflight.has(chapterId)) {
|
||||||
|
const p = dedupeRequest(`chapter-pages:${chapterId}`, () =>
|
||||||
|
gql<{ fetchChapterPages: { pages: string[] } }>(FETCH_CHAPTER_PAGES, { chapterId })
|
||||||
|
.then(d => {
|
||||||
|
const urls = d.fetchChapterPages.pages.map(p => plainThumbUrl(p));
|
||||||
|
if (useBlob) {
|
||||||
|
if (urls[priorityPage]) getBlobUrl(urls[priorityPage], urls.length + 999);
|
||||||
|
preloadBlobUrls(urls.filter((_, i) => i !== priorityPage), urls.length);
|
||||||
|
}
|
||||||
|
pageCache.set(chapterId, urls);
|
||||||
|
return urls;
|
||||||
|
})
|
||||||
|
).finally(() => inflight.delete(chapterId));
|
||||||
|
inflight.set(chapterId, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = inflight.get(chapterId)!;
|
||||||
|
if (!signal) return base;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
|
||||||
|
base.then(resolve, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function measureAspect(url: string, useBlob: boolean): Promise<number> {
|
||||||
|
if (aspectCache.has(url)) return Promise.resolve(aspectCache.get(url)!);
|
||||||
|
return resolveUrl(url, useBlob).then(src => new Promise(res => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => { const r = img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : 0.67; aspectCache.set(url, r); res(r); };
|
||||||
|
img.onerror = () => res(0.67);
|
||||||
|
img.src = src;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preloadImage(url: string, useBlob: boolean): void {
|
||||||
|
if (preloadedUrls.has(url)) return;
|
||||||
|
preloadedUrls.add(url);
|
||||||
|
resolveUrl(url, useBlob).then(src => { new Image().src = src; }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPageCache(chapterId?: number): void {
|
||||||
|
if (chapterId !== undefined) {
|
||||||
|
pageCache.delete(chapterId);
|
||||||
|
inflight.delete(chapterId);
|
||||||
|
} else {
|
||||||
|
pageCache.clear();
|
||||||
|
inflight.clear();
|
||||||
|
resolvedUrlCache.clear();
|
||||||
|
preloadedUrls.clear();
|
||||||
|
aspectCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-12
@@ -7,11 +7,9 @@ export function applyZoom() {
|
|||||||
const uiZoom = store.settings.uiZoom ?? 1.0;
|
const uiZoom = store.settings.uiZoom ?? 1.0;
|
||||||
if (uiZoom === _appliedZoom) return;
|
if (uiZoom === _appliedZoom) return;
|
||||||
_appliedZoom = uiZoom;
|
_appliedZoom = uiZoom;
|
||||||
|
|
||||||
document.documentElement.style.setProperty("--ui-zoom", String(uiZoom));
|
document.documentElement.style.setProperty("--ui-zoom", String(uiZoom));
|
||||||
document.documentElement.style.setProperty("--ui-scale", String(uiZoom));
|
document.documentElement.style.setProperty("--ui-scale", String(uiZoom));
|
||||||
document.documentElement.style.zoom = `${uiZoom * 100}%`;
|
document.documentElement.style.zoom = `${uiZoom * 100}%`;
|
||||||
|
|
||||||
if (_vhRafId !== null) cancelAnimationFrame(_vhRafId);
|
if (_vhRafId !== null) cancelAnimationFrame(_vhRafId);
|
||||||
_vhRafId = requestAnimationFrame(() => {
|
_vhRafId = requestAnimationFrame(() => {
|
||||||
_vhRafId = null;
|
_vhRafId = null;
|
||||||
@@ -22,19 +20,42 @@ export function applyZoom() {
|
|||||||
export function handleZoomKey(e: KeyboardEvent) {
|
export function handleZoomKey(e: KeyboardEvent) {
|
||||||
if (!e.ctrlKey) return;
|
if (!e.ctrlKey) return;
|
||||||
const current = store.settings.uiZoom ?? 1.0;
|
const current = store.settings.uiZoom ?? 1.0;
|
||||||
if (e.key === "=" || e.key === "+") {
|
if (e.key === "=" || e.key === "+") { e.preventDefault(); store.settings.uiZoom = Math.min(2.0, Math.round((current + 0.1) * 10) / 10); }
|
||||||
e.preventDefault();
|
else if (e.key === "-") { e.preventDefault(); store.settings.uiZoom = Math.max(0.5, Math.round((current - 0.1) * 10) / 10); }
|
||||||
store.settings.uiZoom = Math.min(2.0, Math.round((current + 0.1) * 10) / 10);
|
else if (e.key === "0") { e.preventDefault(); store.settings.uiZoom = 1.0; }
|
||||||
} else if (e.key === "-") {
|
|
||||||
e.preventDefault();
|
|
||||||
store.settings.uiZoom = Math.max(0.5, Math.round((current - 0.1) * 10) / 10);
|
|
||||||
} else if (e.key === "0") {
|
|
||||||
e.preventDefault();
|
|
||||||
store.settings.uiZoom = 1.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mountZoomKey(): () => void {
|
export function mountZoomKey(): () => void {
|
||||||
window.addEventListener("keydown", handleZoomKey);
|
window.addEventListener("keydown", handleZoomKey);
|
||||||
return () => window.removeEventListener("keydown", handleZoomKey);
|
return () => window.removeEventListener("keydown", handleZoomKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clampZoom(z: number, min: number, max: number): number {
|
||||||
|
return Math.round(Math.min(max, Math.max(min, z)) * 1000) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function captureZoomAnchor(
|
||||||
|
containerEl: HTMLElement | null,
|
||||||
|
style: string,
|
||||||
|
out: { el: HTMLElement | null; offset: number },
|
||||||
|
) {
|
||||||
|
if (!containerEl || style !== "longstrip") return;
|
||||||
|
const containerTop = containerEl.getBoundingClientRect().top;
|
||||||
|
for (const img of containerEl.querySelectorAll<HTMLElement>("img[data-local-page]")) {
|
||||||
|
const rect = img.getBoundingClientRect();
|
||||||
|
if (rect.bottom > containerTop) { out.el = img; out.offset = rect.top - containerTop; return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreZoomAnchor(
|
||||||
|
containerEl: HTMLElement | null,
|
||||||
|
out: { el: HTMLElement | null; offset: number },
|
||||||
|
) {
|
||||||
|
if (!out.el || !containerEl) return;
|
||||||
|
const el = out.el;
|
||||||
|
out.el = null;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const containerTop = containerEl!.getBoundingClientRect().top;
|
||||||
|
containerEl!.scrollTop += (el.getBoundingClientRect().top - containerTop) - out.offset;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ 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 { 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 { fetchPages, resolveUrl, preloadImage, measureAspect, buildPageGroups, clearPageCache } from "./lib/pageLoader";
|
||||||
export { setupScrollTracking, appendNextChapter } from "./lib/scrollHandler";
|
export { setupScrollTracking, appendNextChapter } from "./lib/scrollHandler";
|
||||||
export type { StripChapter, ScrollHandlerCallbacks } from "./lib/scrollHandler";
|
export type { StripChapter, ScrollHandlerCallbacks } from "./lib/scrollHandler";
|
||||||
export { createReaderKeyHandler } from "./lib/readerKeybinds";
|
export { createReaderKeyHandler } from "./lib/readerKeybinds";
|
||||||
export type { ReaderKeyActions } from "./lib/readerKeybinds";
|
export type { ReaderKeyActions } from "./lib/readerKeybinds";
|
||||||
@@ -11,4 +11,4 @@ export type { ReaderKeyActions } from "./lib/readerKeybinds";
|
|||||||
export { markChapterRead, getMangaPrefs, toggleBookmark } from "./lib/chapterActions";
|
export { markChapterRead, getMangaPrefs, toggleBookmark } from "./lib/chapterActions";
|
||||||
export { goForward, goBack, jumpToPage, animateFade } from "./lib/navigation";
|
export { goForward, goBack, jumpToPage, animateFade } from "./lib/navigation";
|
||||||
export { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "./lib/zoomHelpers";
|
export { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "./lib/zoomHelpers";
|
||||||
export { loadChapter, scheduleResumeDismiss } from "./lib/chapterLoader";
|
export { loadChapter, scheduleResumeDismiss } from "./lib/chapterLoader";
|
||||||
@@ -1,83 +1,6 @@
|
|||||||
import { gql, plainThumbUrl } from "@api/client";
|
export { fetchPages, resolveUrl, preloadImage, measureAspect, clearPageCache } from "@core/cache/pageCache";
|
||||||
import { getBlobUrl, preloadBlobUrls } from "@core/cache/imageCache";
|
|
||||||
import { dedupeRequest } from "@core/async/batchRequests";
|
|
||||||
import { FETCH_CHAPTER_PAGES } from "@api/mutations/chapters";
|
|
||||||
|
|
||||||
export interface PageLoaderOptions {
|
export function buildPageGroups(urls: string[], aspects: number[], offsetSpreads: boolean): number[][] {
|
||||||
useBlob: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageCache = new Map<number, string[]>();
|
|
||||||
const inflight = new Map<number, Promise<string[]>>();
|
|
||||||
const resolvedUrlCache = new Map<string, Promise<string>>();
|
|
||||||
const preloadedUrls = new Set<string>();
|
|
||||||
const aspectCache = new Map<string, number>();
|
|
||||||
|
|
||||||
export function resolveUrl(url: string, useBlob: boolean, priority = 0): Promise<string> {
|
|
||||||
if (!useBlob) return Promise.resolve(url);
|
|
||||||
if (!resolvedUrlCache.has(url)) resolvedUrlCache.set(url, getBlobUrl(url, priority));
|
|
||||||
return resolvedUrlCache.get(url)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchPages(
|
|
||||||
chapterId: number,
|
|
||||||
useBlob: boolean,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
priorityPage = 0,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const cached = pageCache.get(chapterId);
|
|
||||||
if (cached) return Promise.resolve(cached);
|
|
||||||
if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
||||||
|
|
||||||
if (!inflight.has(chapterId)) {
|
|
||||||
const p = dedupeRequest(`chapter-pages:${chapterId}`, () =>
|
|
||||||
gql<{ fetchChapterPages: { pages: string[] } }>(FETCH_CHAPTER_PAGES, { chapterId })
|
|
||||||
.then(d => {
|
|
||||||
const urls = d.fetchChapterPages.pages.map(p => plainThumbUrl(p));
|
|
||||||
if (useBlob) {
|
|
||||||
if (urls[priorityPage]) getBlobUrl(urls[priorityPage], urls.length + 999);
|
|
||||||
preloadBlobUrls(urls.filter((_, i) => i !== priorityPage), urls.length);
|
|
||||||
}
|
|
||||||
pageCache.set(chapterId, urls);
|
|
||||||
return urls;
|
|
||||||
})
|
|
||||||
).finally(() => inflight.delete(chapterId));
|
|
||||||
inflight.set(chapterId, p);
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = inflight.get(chapterId)!;
|
|
||||||
if (!signal) return base;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
|
|
||||||
base.then(resolve, reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function measureAspect(url: string, useBlob: boolean): Promise<number> {
|
|
||||||
if (aspectCache.has(url)) return Promise.resolve(aspectCache.get(url)!);
|
|
||||||
return resolveUrl(url, useBlob).then(src => new Promise(res => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
const r = img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : 0.67;
|
|
||||||
aspectCache.set(url, r);
|
|
||||||
res(r);
|
|
||||||
};
|
|
||||||
img.onerror = () => res(0.67);
|
|
||||||
img.src = src;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preloadImage(url: string, useBlob: boolean): void {
|
|
||||||
if (preloadedUrls.has(url)) return;
|
|
||||||
preloadedUrls.add(url);
|
|
||||||
resolveUrl(url, useBlob).then(src => { new Image().src = src; }).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildPageGroups(
|
|
||||||
urls: string[],
|
|
||||||
aspects: number[],
|
|
||||||
offsetSpreads: boolean,
|
|
||||||
): number[][] {
|
|
||||||
const groups: number[][] = [[1]];
|
const groups: number[][] = [[1]];
|
||||||
if (offsetSpreads) groups.push([2]);
|
if (offsetSpreads) groups.push([2]);
|
||||||
let i = offsetSpreads ? 3 : 2;
|
let i = offsetSpreads ? 3 : 2;
|
||||||
@@ -87,17 +10,4 @@ export function buildPageGroups(
|
|||||||
else { groups.push([i, i + 1]); i += 2; }
|
else { groups.push([i, i + 1]); i += 2; }
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
}
|
|
||||||
|
|
||||||
export function clearPageCache(chapterId?: number): void {
|
|
||||||
if (chapterId !== undefined) {
|
|
||||||
pageCache.delete(chapterId);
|
|
||||||
inflight.delete(chapterId);
|
|
||||||
} else {
|
|
||||||
pageCache.clear();
|
|
||||||
inflight.clear();
|
|
||||||
resolvedUrlCache.clear();
|
|
||||||
preloadedUrls.clear();
|
|
||||||
aspectCache.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,8 @@
|
|||||||
import { readerState } from "../store/readerState.svelte";
|
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 {
|
export function clampZoom(z: number): number {
|
||||||
const { ZOOM_MIN, ZOOM_MAX } = readerState;
|
return _clampZoom(z, ZOOM_MIN, ZOOM_MAX);
|
||||||
return Math.round(Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, z)) * 1000) / 1000;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function captureZoomAnchor(
|
|
||||||
containerEl: HTMLElement | null,
|
|
||||||
style: string,
|
|
||||||
out: { el: HTMLElement | null; offset: number },
|
|
||||||
) {
|
|
||||||
if (!containerEl || style !== "longstrip") return;
|
|
||||||
const imgs = containerEl.querySelectorAll<HTMLElement>("img[data-local-page]");
|
|
||||||
const containerTop = containerEl.getBoundingClientRect().top;
|
|
||||||
for (const img of imgs) {
|
|
||||||
const rect = img.getBoundingClientRect();
|
|
||||||
if (rect.bottom > containerTop) {
|
|
||||||
out.el = img;
|
|
||||||
out.offset = rect.top - containerTop;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function restoreZoomAnchor(
|
|
||||||
containerEl: HTMLElement | null,
|
|
||||||
out: { el: HTMLElement | null; offset: number },
|
|
||||||
) {
|
|
||||||
if (!out.el || !containerEl) return;
|
|
||||||
const el = out.el;
|
|
||||||
out.el = null;
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const containerTop = containerEl!.getBoundingClientRect().top;
|
|
||||||
const newRect = el.getBoundingClientRect();
|
|
||||||
containerEl!.scrollTop += (newRect.top - containerTop) - out.offset;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user