From bf38e00cf3a677c6d040f605f0bc63055567f85e Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Wed, 4 Mar 2026 00:00:12 -0600 Subject: [PATCH] [V1] Fixed Mark as Read Refresh + Auto Feature --- src/components/pages/Library.module.css | 18 ++++++ src/components/pages/Library.tsx | 13 ++++ src/components/pages/Reader.tsx | 83 ++++++++++++++++++++----- src/components/pages/SeriesDetail.tsx | 8 +++ src/components/settings/Settings.tsx | 6 ++ src/store/index.ts | 7 +++ 6 files changed, 120 insertions(+), 15 deletions(-) diff --git a/src/components/pages/Library.module.css b/src/components/pages/Library.module.css index ebe9ab8..c39e04d 100644 --- a/src/components/pages/Library.module.css +++ b/src/components/pages/Library.module.css @@ -175,6 +175,24 @@ border: 1px solid var(--accent-muted); } +.unreadBadge { + position: absolute; + top: var(--sp-1); + left: var(--sp-1); + min-width: 18px; + height: 18px; + padding: 0 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: bold; + background: var(--bg-void); + color: var(--text-primary); + border-radius: var(--radius-sm); + border: 1px solid var(--border-strong); +} + .title { margin-top: var(--sp-2); font-size: var(--text-sm); diff --git a/src/components/pages/Library.tsx b/src/components/pages/Library.tsx index ae26ddc..84c4cd3 100644 --- a/src/components/pages/Library.tsx +++ b/src/components/pages/Library.tsx @@ -45,6 +45,9 @@ const MangaCard = memo(function MangaCard({ {!!manga.downloadCount && ( {manga.downloadCount} )} + {!!manga.unreadCount && ( + {manga.unreadCount} + )}

{manga.title}

@@ -78,6 +81,16 @@ export default function Library() { const addFolder = useStore((state) => state.addFolder); const assignMangaToFolder = useStore((state) => state.assignMangaToFolder); const removeMangaFromFolder = useStore((state) => state.removeMangaFromFolder); + const activeChapter = useStore((state) => state.activeChapter); + + + const prevChapterRef = useRef(null); + useEffect(() => { + const wasOpen = prevChapterRef.current !== null; + prevChapterRef.current = activeChapter?.id ?? null; + if (!wasOpen || activeChapter) return; + cache.clear(CACHE_KEYS.LIBRARY); + }, [activeChapter]); const loadData = useCallback((showLoading = false) => { if (showLoading) setLoading(true); diff --git a/src/components/pages/Reader.tsx b/src/components/pages/Reader.tsx index ba57230..931aa87 100644 --- a/src/components/pages/Reader.tsx +++ b/src/components/pages/Reader.tsx @@ -197,6 +197,8 @@ export default function Reader() { const visibleChapterRef = useRef(null); const stripChaptersRef = useRef([]); const pageUrlsRef = useRef([]); + const activeChapterRef = useRef(null); + const markReadOnNextRef = useRef(true); // Captured before a head-trim; useLayoutEffect restores scroll synchronously const scrollAnchorRef = useRef<{ scrollTop: number; scrollHeight: number } | null>(null); @@ -239,10 +241,29 @@ export default function Reader() { const style = settings.pageStyle ?? "single"; const maxW = settings.maxPageWidth ?? 900; const autoNext = settings.autoNextChapter ?? false; + const markReadOnNext = settings.markReadOnNext ?? true; - settingsRef.current = settings; - chapterListRef.current = activeChapterList; - pageUrlsRef.current = pageUrls; + settingsRef.current = settings; + chapterListRef.current = activeChapterList; + pageUrlsRef.current = pageUrls; + activeChapterRef.current = activeChapter; + markReadOnNextRef.current = markReadOnNext; + + // Mark the current chapter read when the user manually skips to another chapter. + // Uses refs only — safe to call from any callback without stale-closure issues. + // markReadOnNext gates this; autoNextChapter does NOT block it because a manual + // chapter-skip is always intentional regardless of the auto-advance setting. + const maybeMarkCurrentRead = useCallback(() => { + const ch = activeChapterRef.current; + if (!ch) return; + if (!markReadOnNextRef.current) return; + if (markedReadRef.current.has(ch.id)) return; + markedReadRef.current.add(ch.id); + gql(MARK_CHAPTER_READ, { id: ch.id, isRead: true }).catch((e) => { + markedReadRef.current.delete(ch.id); + console.error("MARK_CHAPTER_READ (manual next) failed:", e); + }); + }, []); // ── UI autohide ────────────────────────────────────────────────────────────── const showUi = useCallback(() => { @@ -294,8 +315,9 @@ export default function Reader() { fetchPages(targetId, ctrl.signal) .then(async (urls) => { if (ctrl.signal.aborted) return; - if (style !== "longstrip") await decodeImage(urls[0]); - if (ctrl.signal.aborted) return; + // Don't block the render on decoding — set URLs immediately so the + // browser can start painting the first image without waiting for the + // full decode. The img element's own decoding="async" handles the rest. setPageUrls(urls); setPageReady(true); if (style === "longstrip" && autoNext) { @@ -532,6 +554,12 @@ export default function Reader() { if (style !== "longstrip" && containerRef.current) containerRef.current.scrollTop = 0; }, [pageNumber, style]); + // Always scroll to top when a new chapter opens — even if pageNumber stays at 1 + // (navigating chapter→chapter while already on page 1 won't trigger the effect above). + useEffect(() => { + if (containerRef.current) containerRef.current.scrollTop = 0; + }, [activeChapter?.id]); + // ── Preload adjacent pages ─────────────────────────────────────────────────── useEffect(() => { const ahead = settings.preloadPages ?? 3; @@ -671,26 +699,39 @@ export default function Reader() { }, [pageGroups, pageNumber, adjacent, activeChapterList]); const goForward = useCallback(() => { - if (loading || !pageUrls.length) return; + if (loading) return; + // Longstrip: bottom arrows always switch chapters, not pages + if (style === "longstrip") { + if (adjacent.next) { maybeMarkCurrentRead(); openReader(adjacent.next, activeChapterList); } + return; + } if (style === "double" && pageGroups.length) { advanceGroup(true); return; } + if (!pageUrls.length) return; if (pageNumber < lastPage) { decodeImage(pageUrls[pageNumber]).then(() => setPageNumber(pageNumber + 1)); } else if (adjacent.next) { + maybeMarkCurrentRead(); setPageNumber(1); openReader(adjacent.next, activeChapterList); } else { closeReader(); } - }, [loading, pageNumber, lastPage, pageUrls, adjacent, activeChapterList, style, pageGroups, advanceGroup]); + }, [loading, style, pageNumber, lastPage, pageUrls, adjacent, activeChapterList, pageGroups, advanceGroup, maybeMarkCurrentRead]); const goBack = useCallback(() => { - if (loading || !pageUrls.length) return; + if (loading) return; + // Longstrip: bottom arrows always switch chapters, not pages + if (style === "longstrip") { + if (adjacent.prev) openReader(adjacent.prev, activeChapterList); + return; + } if (style === "double" && pageGroups.length) { advanceGroup(false); return; } + if (!pageUrls.length) return; if (pageNumber > 1) { decodeImage(pageUrls[pageNumber - 2]).then(() => setPageNumber(pageNumber - 1)); } else if (adjacent.prev) { openReader(adjacent.prev, activeChapterList); } - }, [loading, pageNumber, pageUrls, adjacent, activeChapterList, style, pageGroups, advanceGroup]); + }, [loading, style, pageNumber, pageUrls, adjacent, activeChapterList, pageGroups, advanceGroup]); const goNext = rtl ? goBack : goForward; const goPrev = rtl ? goForward : goBack; @@ -752,7 +793,7 @@ export default function Reader() { const list = chapterListRef.current; const idx = list.findIndex((c) => c.id === loadingIdRef.current); const next = idx >= 0 && idx < list.length - 1 ? list[idx + 1] : null; - if (next) openReader(next, list); + if (next) { maybeMarkCurrentRead(); openReader(next, list); } } else if (matchesKeybind(e, kb.chapterLeft)) { e.preventDefault(); @@ -768,7 +809,7 @@ export default function Reader() { }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [zoomOpen, dlOpen, lastPage]); + }, [zoomOpen, dlOpen, lastPage, maybeMarkCurrentRead]); // ── Render ─────────────────────────────────────────────────────────────────── function handleTap(e: React.MouseEvent) { @@ -808,7 +849,7 @@ export default function Reader() { {/* ── Topbar ── */}
- @@ -817,7 +858,7 @@ export default function Reader() { {displayChapter?.name} {visibleChunkPage} / {visibleChunkLastPage || "…"} -
@@ -854,6 +895,16 @@ export default function Reader() { Auto )} + {!autoNext && ( + + )} @@ -920,10 +971,12 @@ export default function Reader() { {/* ── Bottom nav ── */}
- -
diff --git a/src/components/pages/SeriesDetail.tsx b/src/components/pages/SeriesDetail.tsx index feebbc2..3619f67 100644 --- a/src/components/pages/SeriesDetail.tsx +++ b/src/components/pages/SeriesDetail.tsx @@ -301,6 +301,7 @@ export default function SeriesDetail() { const activeManga = useStore((state) => state.activeManga); const setActiveManga = useStore((state) => state.setActiveManga); const openReader = useStore((state) => state.openReader); + const activeChapter = useStore((state) => state.activeChapter); const settings = useStore((state) => state.settings); const updateSettings = useStore((state) => state.updateSettings); const addToast = useStore((state) => state.addToast); @@ -515,6 +516,13 @@ export default function SeriesDetail() { }); }, [applyChapters]); + // Reload chapters whenever the reader is closed so read/unread state is always current. + useEffect(() => { + if (activeChapter || !activeManga) return; + reloadChapters(activeManga.id); + cache.clear(CACHE_KEYS.LIBRARY); + }, [activeChapter, activeManga, reloadChapters]); + async function enqueue(chapter: Chapter, e: React.MouseEvent) { e.stopPropagation(); setEnqueueing((prev) => new Set(prev).add(chapter.id)); diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index e6eb912..d5b8376 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -265,6 +265,12 @@ function ReaderTab({ settings, update }: { settings: Settings; update: (p: Parti description="Automatically open the next chapter at the end of a long strip" checked={settings.autoNextChapter ?? false} onChange={(v) => update({ autoNextChapter: v })} /> + {!(settings.autoNextChapter ?? false) && ( + update({ markReadOnNext: v })} /> + )} . */ @@ -110,6 +116,7 @@ export const DEFAULT_SETTINGS: Settings = { splashCards: true, storageLimitGb: null, folders: [], + markReadOnNext: true, readerDebounceMs: 120, theme: "dark", };