[V1] Fixed Mark as Read Refresh + Auto Feature

This commit is contained in:
Youwes09
2026-03-04 00:00:12 -06:00
parent eb7360ee05
commit bf38e00cf3
6 changed files with 120 additions and 15 deletions
+18
View File
@@ -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);
+13
View File
@@ -45,6 +45,9 @@ const MangaCard = memo(function MangaCard({
{!!manga.downloadCount && (
<span className={s.downloadedBadge}>{manga.downloadCount}</span>
)}
{!!manga.unreadCount && (
<span className={s.unreadBadge}>{manga.unreadCount}</span>
)}
</div>
<p className={s.title}>{manga.title}</p>
</button>
@@ -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<number | null>(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);
+68 -15
View File
@@ -197,6 +197,8 @@ export default function Reader() {
const visibleChapterRef = useRef<number | null>(null);
const stripChaptersRef = useRef<StripChapter[]>([]);
const pageUrlsRef = useRef<string[]>([]);
const activeChapterRef = useRef<typeof activeChapter>(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 ── */}
<div className={[s.topbar, uiVisible ? "" : s.uiHidden].join(" ")}>
<button className={s.iconBtn} onClick={closeReader} title="Close reader"><X size={15} weight="light" /></button>
<button className={s.iconBtn} onClick={() => adjacent.prev && openReader(adjacent.prev, activeChapterList)} disabled={!adjacent.prev} title="Previous chapter">
<button className={s.iconBtn} onClick={() => { if (adjacent.prev) { maybeMarkCurrentRead(); openReader(adjacent.prev, activeChapterList); } }} disabled={!adjacent.prev} title="Previous chapter">
<CaretLeft size={14} weight="light" />
</button>
<span className={s.chLabel}>
@@ -817,7 +858,7 @@ export default function Reader() {
<span>{displayChapter?.name}</span>
</span>
<span className={s.pageLabel}>{visibleChunkPage} / {visibleChunkLastPage || "…"}</span>
<button className={s.iconBtn} onClick={() => adjacent.next && openReader(adjacent.next, activeChapterList)} disabled={!adjacent.next} title="Next chapter">
<button className={s.iconBtn} onClick={() => { if (adjacent.next) { maybeMarkCurrentRead(); openReader(adjacent.next, activeChapterList); } }} disabled={!adjacent.next} title="Next chapter">
<CaretRight size={14} weight="light" />
</button>
<div className={s.topSep} />
@@ -854,6 +895,16 @@ export default function Reader() {
<span className={s.modeBtnLabel}>Auto</span>
</button>
)}
{!autoNext && (
<button
className={[s.modeBtn, markReadOnNext ? s.modeBtnActive : ""].join(" ")}
onClick={() => updateSettings({ markReadOnNext: !markReadOnNext })}
title={markReadOnNext
? "Mark chapter read when advancing to next (click to disable)"
: "Don't mark chapter read on next (click to enable)"}>
<span className={s.modeBtnLabel}>Mk.Read</span>
</button>
)}
<button className={s.modeBtn} onClick={() => setDlOpen(true)} title="Download options">
<Download size={14} weight="light" />
</button>
@@ -920,10 +971,12 @@ export default function Reader() {
{/* ── Bottom nav ── */}
<div className={[s.bottombar, uiVisible ? "" : s.uiHidden].join(" ")}>
<button className={s.navBtn} onClick={goPrev} disabled={loading || (pageNumber === 1 && !adjacent.prev)}>
<button className={s.navBtn} onClick={goPrev}
disabled={loading || (style === "longstrip" ? !adjacent.prev : (pageNumber === 1 && !adjacent.prev))}>
<ArrowLeft size={13} weight="light" />
</button>
<button className={s.navBtn} onClick={goNext} disabled={loading || (pageNumber === lastPage && !adjacent.next)}>
<button className={s.navBtn} onClick={goNext}
disabled={loading || (style === "longstrip" ? !adjacent.next : (pageNumber === lastPage && !adjacent.next))}>
<ArrowRight size={13} weight="light" />
</button>
</div>
+8
View File
@@ -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));
+6
View File
@@ -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) && (
<Toggle label="Mark read when skipping to next chapter"
description="When auto-advance is off, mark the current chapter as read if you tap the next chapter button before finishing it"
checked={settings.markReadOnNext ?? true}
onChange={(v) => update({ markReadOnNext: v })} />
)}
<Stepper label="Pages to preload"
description="Images loaded ahead of the current page"
value={settings.preloadPages} min={0} max={10}
+7
View File
@@ -76,6 +76,12 @@ export interface Settings {
splashCards?: boolean;
storageLimitGb: number | null;
folders: Folder[];
/**
* Mark a chapter as read when the user manually taps the "next chapter"
* button/key while autoNextChapter is off. Has no effect when autoNextChapter
* is on (the scroll-based mark-as-read logic handles that path).
*/
markReadOnNext: boolean;
/** Debounce delay (ms) applied to the reader's scroll/page-change handler. 0 = off. */
readerDebounceMs: number;
/** UI colour theme. Applied as data-theme on <html>. */
@@ -110,6 +116,7 @@ export const DEFAULT_SETTINGS: Settings = {
splashCards: true,
storageLimitGb: null,
folders: [],
markReadOnNext: true,
readerDebounceMs: 120,
theme: "dark",
};