[V1] Major Bug Fixes & Loading Screen (WIP)

This commit is contained in:
Youwes09
2026-02-24 16:14:46 -06:00
parent ac1c0520c5
commit f866d4d0e9
10 changed files with 929 additions and 122 deletions
+30 -11
View File
@@ -58,13 +58,14 @@ function fetchLibrary() {
}
export default function Library() {
const [allManga, setAllManga] = useState<Manga[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [ctx, setCtx] = useState<{ x: number; y: number; manga: Manga } | null>(null);
const [emptyCtx, setEmptyCtx] = useState<{ x: number; y: number } | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [allManga, setAllManga] = useState<Manga[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
const [search, setSearch] = useState("");
const [ctx, setCtx] = useState<{ x: number; y: number; manga: Manga } | null>(null);
const [emptyCtx, setEmptyCtx] = useState<{ x: number; y: number } | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const setActiveManga = useStore((state) => state.setActiveManga);
const libraryFilter = useStore((state) => state.libraryFilter);
@@ -80,18 +81,30 @@ export default function Library() {
const loadData = useCallback((showLoading = false) => {
if (showLoading) setLoading(true);
// Clear a previously failed cache entry so we actually retry the network call
if (!cache.has(CACHE_KEYS.LIBRARY)) {
// cache miss — fresh fetch, nothing to clear
}
fetchLibrary()
.then((nodes) => { setAllManga(nodes); setError(null); })
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, []);
// Initial load — delayed on first mount so the server has time to start.
// retryCount bumps force a re-run; manual retries clear the cache first.
useEffect(() => {
loadData(true);
// Re-fetch when library cache is invalidated (e.g. by Explore or GenreDrillPage)
setLoading(true);
setError(null);
if (retryCount > 0) cache.clear(CACHE_KEYS.LIBRARY);
loadData(false);
// Re-fetch when library cache is invalidated by other pages
const unsub = cache.subscribe(CACHE_KEYS.LIBRARY, () => loadData(false));
return unsub;
}, [loadData]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [retryCount]);
useEffect(() => {
scrollRef.current?.scrollTo({ top: 0 });
@@ -271,7 +284,13 @@ export default function Library() {
if (error) return (
<div className={s.center}>
<p className={s.errorMsg}>Could not reach Suwayomi</p>
<p className={s.errorDetail}>{error}</p>
<p className={s.errorDetail}>Make sure the server is running, then retry.</p>
<button
style={{ marginTop: "var(--sp-3)", padding: "6px 16px", borderRadius: "var(--radius-md)", border: "1px solid var(--border-dim)", background: "var(--bg-raised)", color: "var(--text-muted)", cursor: "pointer", fontFamily: "var(--font-ui)", fontSize: "var(--text-xs)", letterSpacing: "var(--tracking-wide)" }}
onClick={() => setRetryCount((c) => c + 1)}
>
Retry
</button>
</div>
);
+37 -11
View File
@@ -256,9 +256,14 @@ export default function Reader() {
* currently reading (for topbar display) without triggering a full reload.
*/
const [visibleChapterId, setVisibleChapterId] = useState<number | null>(null);
// Ref mirror so the scroll handler always reads the latest value without
// closing over a stale state snapshot from a previous effect render.
const visibleChapterIdRef = useRef<number | null>(null);
// Keep the ref mirror in sync so the scroll handler always sees current strip state
useEffect(() => { stripChaptersRef.current = stripChapters; }, [stripChapters]);
// Keep visibleChapterId ref in sync
useEffect(() => { visibleChapterIdRef.current = visibleChapterId; }, [visibleChapterId]);
// Restore scroll position synchronously after a head-trim, before the browser paints
useLayoutEffect(() => {
@@ -681,33 +686,54 @@ export default function Reader() {
// ── Infinite append ──────────────────────────────────────────────────
if (!autoNext) {
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 80;
// Only navigate when the strip genuinely overflows the viewport.
// If pages are short/zoomed-out, scrollHeight === clientHeight and
// atBottom would always be true, causing unwanted chapter switches.
const isScrollable = el.scrollHeight > el.clientHeight + 4;
const atBottom = isScrollable && el.scrollTop + el.clientHeight >= el.scrollHeight - 80;
if (atBottom && adjacent.next) openReader(adjacent.next, activeChapterList);
return;
}
const strip = stripChaptersRef.current;
// Silently update visibleChapterId as we scroll into each chunk
// Silently update visibleChapterId as we scroll into each chunk.
// Use the ref so we always compare against the current value, not a
// stale closure snapshot from when the effect was last set up.
for (const chunk of strip) {
const chunkEnd = chunk.startGlobalIdx + chunk.urls.length;
if (n - 1 >= chunk.startGlobalIdx && n - 1 < chunkEnd) {
if (chunk.chapterId !== visibleChapterId) {
setVisibleChapterId(chunk.chapterId);
if (chunk.chapterId !== visibleChapterIdRef.current) {
// Mark the chapter we just *left* as read before updating the ref.
if (settings.autoMarkRead) {
const prevChunk = strip[strip.indexOf(chunk) - 1];
if (prevChunk) {
if (!markedReadRef.current.has(prevChunk.chapterId)) {
markedReadRef.current.add(prevChunk.chapterId);
gql(MARK_CHAPTER_READ, { id: prevChunk.chapterId, isRead: true }).catch(console.error);
}
const chunkIdx = strip.indexOf(chunk);
const prevChunk = chunkIdx > 0 ? strip[chunkIdx - 1] : null;
if (prevChunk && !markedReadRef.current.has(prevChunk.chapterId)) {
markedReadRef.current.add(prevChunk.chapterId);
gql(MARK_CHAPTER_READ, { id: prevChunk.chapterId, isRead: true }).catch(console.error);
}
}
visibleChapterIdRef.current = chunk.chapterId;
setVisibleChapterId(chunk.chapterId);
}
break;
}
}
// When the user reaches the very bottom of the full strip, mark the
// last chapter as read (it never triggers the "crossed into next chunk" path).
if (settings.autoMarkRead) {
const isScrollable = el.scrollHeight > el.clientHeight + 4;
const atVeryBottom = isScrollable && el.scrollTop + el.clientHeight >= el.scrollHeight - 40;
if (atVeryBottom) {
const lastChunk = strip[strip.length - 1];
if (lastChunk && !markedReadRef.current.has(lastChunk.chapterId)) {
markedReadRef.current.add(lastChunk.chapterId);
gql(MARK_CHAPTER_READ, { id: lastChunk.chapterId, isRead: true }).catch(console.error);
}
}
}
// Append next chapter when within 300px of the bottom
const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 300;
if (!nearBottom) return;
@@ -751,7 +777,7 @@ export default function Reader() {
el.removeEventListener("scroll", onScroll);
cancelAnimationFrame(rafRef.current);
};
}, [style, autoNext, activeChapterList, activeChapter?.id, adjacent.next, fetchPages, visibleChapterId]);
}, [style, autoNext, activeChapterList, activeChapter?.id, adjacent.next, fetchPages]);
// Reset scroll position when switching chapters in non-longstrip modes
useEffect(() => {