mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
[V1] Major Bug Fixes & Loading Screen (WIP)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user