// tomugi-app.jsx — app orchestration and routing (home <-> series) function App() { const { useEffect, useRef, useState } = React; const [t, setTweak] = useTweaks(window.TomugiConfig.tweaks); const { user, signIn, signOut } = useTomugiAuth(); // Restore the last view on reload (per-tab, via sessionStorage) so a refresh // keeps you where you were instead of bouncing back to home. const NAV_KEY = "tomugi:nav"; const savedNav = (() => { try { return JSON.parse(sessionStorage.getItem(NAV_KEY) || "null"); } catch (e) { return null; } })(); const seriesById = (id) => (window.TOMUGI_SERIES || []).find((s) => String(s.id) === String(id)) || null; const restoredCurrent = savedNav && savedNav.view === "series" ? seriesById(savedNav.currentId) : null; const restoredView = !savedNav ? "home" : (savedNav.view === "series" && !restoredCurrent ? "home" : (savedNav.view || "home")); const savedCatalog = (savedNav && savedNav.catalog) || {}; const [view, setView] = useState(restoredView); // home | catalog | series const [previousView, setPreviousView] = useState((savedNav && savedNav.previousView) || "home"); const [current, setCurrent] = useState(restoredCurrent); const [authMode, setAuthMode] = useState(null); const [catalogState, setCatalogState] = useState({ filter: savedCatalog.filter || "All", page: savedCatalog.page || 1, scrollY: 0 }); const [highlightedSeriesId, setHighlightedSeriesId] = useState(null); const [showTop, setShowTop] = useState(false); const restoreCatalogScroll = useRef(null); const catalogScrollY = useRef(0); useEffect(() => { document.documentElement.style.setProperty("--accent", t.accent); }, [t.accent]); useEffect(() => { document.documentElement.setAttribute("data-tone", t.tone); }, [t.tone]); useEffect(() => { const el = document.getElementById("app-loader"); if (!el) return; el.classList.add("is-hidden"); const timer = setTimeout(() => el.remove(), 500); return () => clearTimeout(timer); }, []); useEffect(() => { const onScroll = () => setShowTop(window.scrollY > 480); window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => window.removeEventListener("scroll", onScroll); }, []); useEffect(() => { if (view !== "catalog" || restoreCatalogScroll.current == null) return; const target = restoreCatalogScroll.current; restoreCatalogScroll.current = null; requestAnimationFrame(() => { requestAnimationFrame(() => window.scrollTo({ top: target, behavior: "auto" })); }); }, [view, catalogState.filter, catalogState.page]); useEffect(() => { if (!highlightedSeriesId) return; const timer = setTimeout(() => setHighlightedSeriesId(null), 3200); return () => clearTimeout(timer); }, [highlightedSeriesId]); useEffect(() => { try { sessionStorage.setItem(NAV_KEY, JSON.stringify({ view, previousView, currentId: current ? current.id : null, catalog: { filter: catalogState.filter, page: catalogState.page }, })); } catch (e) { /* storage unavailable — non-fatal */ } }, [view, previousView, current, catalogState.filter, catalogState.page]); function openSeries(s) { setHighlightedSeriesId(null); if (view === "catalog") { catalogScrollY.current = window.scrollY; setCatalogState((prev) => ({ ...prev, scrollY: catalogScrollY.current })); } setCurrent(s); setPreviousView(view === "series" ? previousView : view); setView("series"); window.scrollTo({ top: 0 }); } function openCatalog(opts) { // Fresh entry → page 1, top of the list. If a genre is passed (from a // genre tag), filter the catalog by it and reset the status filter. const genre = opts && opts.genre ? opts.genre : null; restoreCatalogScroll.current = null; catalogScrollY.current = 0; setCatalogState((prev) => ({ ...prev, page: 1, scrollY: 0, genre, filter: genre ? "All" : prev.filter })); setView("catalog"); window.scrollTo({ top: 0 }); } function goHome() { setView("home"); window.scrollTo({ top: 0 }); } function goBackFromSeries() { // "Back to catalog" always lands on the full catalog page. If we came from // there, restore scroll; otherwise (e.g. opened from the home preview) open // the catalog fresh at the top. Either way, highlight the series we left. setHighlightedSeriesId(current?.id || null); if (previousView === "catalog") { restoreCatalogScroll.current = catalogScrollY.current; } else { window.scrollTo({ top: 0 }); } setView("catalog"); } function onNav(id) { if (view !== "home") { setView("home"); setTimeout(() => scrollToId(id), 60); } else scrollToId(id); } return (
You've got the chapters; tomugi knows which volume each one belongs to. Point it at your folder and the whole series is organized in one click.