New Patch for Reader

This commit is contained in:
Youwes09
2026-02-27 17:49:07 -06:00
parent 1fa1c3a2e0
commit fc68d3ac7e
+75 -67
View File
@@ -192,6 +192,7 @@ export default function Reader() {
const abortRef = useRef<AbortController | null>(null); const abortRef = useRef<AbortController | null>(null);
const visibleChapterRef = useRef<number | null>(null); // single truth for visible chapter const visibleChapterRef = useRef<number | null>(null); // single truth for visible chapter
const stripChaptersRef = useRef<StripChapter[]>([]); const stripChaptersRef = useRef<StripChapter[]>([]);
const pageUrlsRef = useRef<string[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -221,6 +222,7 @@ export default function Reader() {
settingsRef.current = settings; settingsRef.current = settings;
chapterListRef.current = activeChapterList; chapterListRef.current = activeChapterList;
pageUrlsRef.current = pageUrls;
// ── UI autohide ────────────────────────────────────────────────────────────── // ── UI autohide ──────────────────────────────────────────────────────────────
const showUi = useCallback(() => { const showUi = useCallback(() => {
@@ -364,65 +366,68 @@ export default function Reader() {
}, []); // no deps — reads everything from refs }, []); // no deps — reads everything from refs
// ── Longstrip: IntersectionObserver for page number + visible chapter ──────── // ── Longstrip: IntersectionObserver for page number + visible chapter ────────
// Uses a MutationObserver to auto-observe newly added images so we never need // ── Longstrip: scroll-driven page + chapter tracking + mark-as-read ──────────
// to recreate this observer when chapters are appended. // Scroll listener queries the DOM directly on every scroll — no threshold gaps,
// no fast-scroll skipping, works equally scrolling up or down.
useEffect(() => { useEffect(() => {
const el = containerRef.current; const el = containerRef.current;
if (!el || style !== "longstrip") return; if (!el || style !== "longstrip") return;
const pageObs = new IntersectionObserver((entries) => { const READ_LINE_PCT = 0.20; // 20% from top of container = "what the user is reading"
let topPage = Infinity;
let topChId: number | null = null;
let topY = Infinity;
for (const entry of entries) { const onScroll = () => {
if (!entry.isIntersecting) continue; const containerTop = el.getBoundingClientRect().top;
const target = entry.target as HTMLElement; const readLineY = containerTop + el.clientHeight * READ_LINE_PCT;
const p = Number(target.dataset.page); const imgs = el.querySelectorAll<HTMLElement>("img[data-local-page]");
const ch = Number(target.dataset.chapter);
const y = entry.boundingClientRect.top;
// Track the topmost visible image by its position on screen
if (y < topY) { topY = y; topPage = p; topChId = ch; }
}
if (topPage !== Infinity) setPageNumber(topPage); let activeLocalPage: number | null = null;
let activeChId: number | null = null;
if (topChId && topChId !== visibleChapterRef.current) { for (const img of imgs) {
const prev = visibleChapterRef.current; const rect = img.getBoundingClientRect();
// Mark the chapter we just finished scrolling past if (rect.top <= readLineY) {
if (settingsRef.current?.autoMarkRead && prev && !markedReadRef.current.has(prev)) { activeLocalPage = Number(img.dataset.localPage);
markedReadRef.current.add(prev); activeChId = Number(img.dataset.chapter);
gql(MARK_CHAPTER_READ, { id: prev, isRead: true }).catch(console.error); } else {
break; // images are in DOM order top→bottom
} }
visibleChapterRef.current = topChId;
setVisibleChapterId(topChId);
} }
}, { root: el, threshold: 0.1 });
// Observe all existing images // Fallback: nothing above read line yet — use first image
el.querySelectorAll<HTMLElement>("img[data-page]").forEach((img) => pageObs.observe(img)); if (activeLocalPage === null && imgs.length > 0) {
activeLocalPage = Number(imgs[0].dataset.localPage);
activeChId = Number(imgs[0].dataset.chapter);
}
// Auto-observe any images added later (new strip chunks) if (activeLocalPage !== null) setPageNumber(activeLocalPage);
const mutObs = new MutationObserver((mutations) => {
for (const m of mutations) { if (activeChId && activeChId !== visibleChapterRef.current) {
m.addedNodes.forEach((node) => { visibleChapterRef.current = activeChId;
if (node instanceof HTMLElement) { setVisibleChapterId(activeChId);
if (node.matches("img[data-page]")) { }
pageObs.observe(node);
} else { // Mark as read when active page reaches second-to-last or last page
node.querySelectorAll<HTMLElement>("img[data-page]").forEach((img) => pageObs.observe(img)); if (settingsRef.current?.autoMarkRead && activeLocalPage !== null && activeChId) {
} const strip = stripChaptersRef.current;
const chunk = strip.find((c) => c.chapterId === activeChId);
// Fall back to pageUrls length when autoNext is off and strip is empty
const total = chunk ? chunk.urls.length : pageUrlsRef.current.length;
if (total > 0 && activeLocalPage >= total - 1) {
const ch = activeChId;
if (!markedReadRef.current.has(ch)) {
markedReadRef.current.add(ch);
gql(MARK_CHAPTER_READ, { id: ch, isRead: true }).catch((e) => {
markedReadRef.current.delete(ch);
console.error("MARK_CHAPTER_READ failed for chapter", ch, e);
});
} }
}); }
} }
});
mutObs.observe(el, { childList: true, subtree: true });
return () => {
pageObs.disconnect();
mutObs.disconnect();
}; };
// Only recreate when style changes — MutationObserver handles new images
el.addEventListener("scroll", onScroll, { passive: true });
onScroll(); // fire once on mount to set initial state
return () => el.removeEventListener("scroll", onScroll);
}, [style]); }, [style]);
// ── Longstrip: sentinel triggers append ────────────────────────────────────── // ── Longstrip: sentinel triggers append ──────────────────────────────────────
@@ -451,7 +456,7 @@ export default function Reader() {
return () => obs.disconnect(); return () => obs.disconnect();
// Re-run only when mode changes or first chunk arrives (0→N). // Re-run only when mode changes or first chunk arrives (0→N).
// appendNextChapter is stable (no deps), so it won't cause extra re-runs. // appendNextChapter is stable (no deps), so it won't cause extra re-runs.
}, [style, autoNext, stripChapters.length > 0, appendNextChapter]); }, [style, autoNext, stripChapters.length, appendNextChapter]);
// ── Mark last chapter read when reaching the very bottom ───────────────────── // ── Mark last chapter read when reaching the very bottom ─────────────────────
useEffect(() => { useEffect(() => {
@@ -511,17 +516,29 @@ export default function Reader() {
if (behind) preloadImage(behind); if (behind) preloadImage(behind);
}, [pageNumber, pageUrls, settings.preloadPages]); }, [pageNumber, pageUrls, settings.preloadPages]);
// ── Derived display values ───────────────────────────────────────────────────
const lastPage = pageUrls.length;
// In longstrip+autoNext, show the chapter the user is actually looking at,
// not the one that was originally opened.
const displayChapter = useMemo(() => {
if (style !== "longstrip" || !autoNext || !visibleChapterId) return activeChapter;
return activeChapterList.find((c) => c.id === visibleChapterId) ?? activeChapter;
}, [style, autoNext, visibleChapterId, activeChapter, activeChapterList]);
// ── Adjacent chapters + cache eviction ────────────────────────────────────── // ── Adjacent chapters + cache eviction ──────────────────────────────────────
// Uses displayChapter so topbar prev/next navigate from what's visible on screen.
const adjacent = useMemo(() => { const adjacent = useMemo(() => {
if (!activeChapter || !activeChapterList.length) const ref = displayChapter ?? activeChapter;
if (!ref || !activeChapterList.length)
return { prev: null, next: null, remaining: [] }; return { prev: null, next: null, remaining: [] };
const idx = activeChapterList.findIndex((c) => c.id === activeChapter.id); const idx = activeChapterList.findIndex((c) => c.id === ref.id);
return { return {
prev: idx > 0 ? activeChapterList[idx - 1] : null, prev: idx > 0 ? activeChapterList[idx - 1] : null,
next: idx < activeChapterList.length - 1 ? activeChapterList[idx + 1] : null, next: idx < activeChapterList.length - 1 ? activeChapterList[idx + 1] : null,
remaining: activeChapterList.slice(idx + 1), remaining: activeChapterList.slice(idx + 1),
}; };
}, [activeChapter, activeChapterList]); }, [displayChapter, activeChapter, activeChapterList]);
useEffect(() => { useEffect(() => {
const pinned = new Set([activeChapter?.id, adjacent.next?.id, adjacent.prev?.id].filter(Boolean) as number[]); const pinned = new Set([activeChapter?.id, adjacent.next?.id, adjacent.prev?.id].filter(Boolean) as number[]);
@@ -530,25 +547,15 @@ export default function Reader() {
cacheEvict(pinned); cacheEvict(pinned);
}, [adjacent.next?.id, adjacent.prev?.id]); }, [adjacent.next?.id, adjacent.prev?.id]);
// ── Derived display values ───────────────────────────────────────────────────
const lastPage = pageUrls.length;
const displayChapter = useMemo(() => {
if (style !== "longstrip" || !autoNext || !visibleChapterId) return activeChapter;
return activeChapterList.find((c) => c.id === visibleChapterId) ?? activeChapter;
}, [style, autoNext, visibleChapterId, activeChapter, activeChapterList]);
const visibleChunkLastPage = useMemo(() => { const visibleChunkLastPage = useMemo(() => {
if (style !== "longstrip" || !autoNext) return lastPage; if (style !== "longstrip" || !autoNext) return lastPage;
const chunk = stripChapters.find((c) => c.chapterId === (visibleChapterId ?? activeChapter?.id)); const chId = visibleChapterId ?? activeChapter?.id;
const chunk = stripChapters.find((c) => c.chapterId === chId);
return chunk?.urls.length ?? lastPage; return chunk?.urls.length ?? lastPage;
}, [style, autoNext, stripChapters, visibleChapterId, activeChapter?.id, lastPage]); }, [style, autoNext, stripChapters, visibleChapterId, activeChapter?.id, lastPage]);
const visibleChunkPage = useMemo(() => { // pageNumber is always local (per-chapter) — no offset math needed
if (style !== "longstrip" || !autoNext) return pageNumber; const visibleChunkPage = pageNumber;
const chunk = stripChapters.find((c) => c.chapterId === (visibleChapterId ?? activeChapter?.id));
return chunk ? Math.max(1, pageNumber - chunk.startGlobalIdx) : pageNumber;
}, [style, autoNext, stripChapters, visibleChapterId, activeChapter?.id, pageNumber]);
// ── Auto-mark read + history (non-longstrip) ───────────────────────────────── // ── Auto-mark read + history (non-longstrip) ─────────────────────────────────
useEffect(() => { useEffect(() => {
@@ -827,16 +834,17 @@ export default function Reader() {
<> <>
{stripToRender.map((chunk) => {stripToRender.map((chunk) =>
chunk.urls.map((url, i) => { chunk.urls.map((url, i) => {
const globalIdx = chunk.startGlobalIdx + i; const localPage = i + 1;
return ( return (
<img <img
key={`${chunk.chapterId}-${i}`} key={`${chunk.chapterId}-${i}`}
src={url} src={url}
alt={`${chunk.chapterName} Page ${i + 1}`} alt={`${chunk.chapterName} Page ${localPage}`}
data-page={globalIdx + 1} data-local-page={localPage}
data-chapter={chunk.chapterId} data-chapter={chunk.chapterId}
data-total={chunk.urls.length}
className={[imgCls, settings.pageGap ? s.stripGap : ""].join(" ")} className={[imgCls, settings.pageGap ? s.stripGap : ""].join(" ")}
loading={globalIdx < 3 ? "eager" : "lazy"} loading={i < 3 ? "eager" : "lazy"}
decoding="async" decoding="async"
width={aspectCache.has(url) ? Math.round(aspectCache.get(url)! * 1000) : 720} width={aspectCache.has(url) ? Math.round(aspectCache.get(url)! * 1000) : 720}
height={1000} height={1000}