feat: seguirdo leyendo - progreso de lectura en localStorage (barra de progreso + aviso de continuacion + guardado al salir)

This commit is contained in:
Renato
2026-08-05 08:18:08 +08:00
parent 0cf271e4f1
commit cef75bdaa1
+80 -1
View File
@@ -29,12 +29,51 @@ export default function ReaderPage() {
const [mode, setMode] = useState<"paged" | "webtoon">("paged") const [mode, setMode] = useState<"paged" | "webtoon">("paged")
const [showControls, setShowControls] = useState(true) const [showControls, setShowControls] = useState(true)
const [isFullscreen, setIsFullscreen] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false)
const [resumeNotice, setResumeNotice] = useState<string | null>(null)
const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined) const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const touchStartX = useRef<number | null>(null) const touchStartX = useRef<number | null>(null)
const touchStartY = useRef<number | null>(null) const touchStartY = useRef<number | null>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
// ---- Persistencia del progreso de lectura (localStorage, sin backend) ----
const PROGRESS_KEY = `ws_progress_${gid}`
function saveProgress(p: number, m: "paged" | "webtoon") {
try {
localStorage.setItem(PROGRESS_KEY, JSON.stringify({ page: p, mode: m, at: Date.now() }))
} catch {}
}
function loadProgress(): { page: number; mode: "paged" | "webtoon" } | null {
try {
const raw = localStorage.getItem(PROGRESS_KEY)
if (!raw) return null
const parsed = JSON.parse(raw)
if (typeof parsed.page === "number" && (parsed.mode === "paged" || parsed.mode === "webtoon")) {
return parsed
}
return null
} catch {
return null
}
}
// Restaura el progreso guardado (página + modo) al abrir el lector.
function restoreProgress(total: number) {
const saved = loadProgress()
if (!saved) return
// Solo restaurar si es una página intermedia (no la primera ni la última).
if (saved.page > 0 && saved.page < total - 1) {
setPage(saved.page)
if (saved.mode) setMode(saved.mode)
setResumeNotice(`Continuaste en la página ${saved.page + 1} de ${total}`)
} else if (saved.page >= total - 1) {
// Llegó al final la última vez — empezar de nuevo.
saveProgress(0, "paged")
}
}
// Fetch page list // Fetch page list
useEffect(() => { useEffect(() => {
async function load() { async function load() {
@@ -50,6 +89,7 @@ export default function ReaderPage() {
const urls = Array.from({ length: total }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`) const urls = Array.from({ length: total }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`)
setPageUrls(urls) setPageUrls(urls)
setLoading(false) setLoading(false)
restoreProgress(total)
return return
} }
} }
@@ -65,6 +105,7 @@ export default function ReaderPage() {
const urls = Array.from({ length: summary.num_pages }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`) const urls = Array.from({ length: summary.num_pages }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`)
setPageUrls(urls) setPageUrls(urls)
setLoading(false) setLoading(false)
restoreProgress(summary.num_pages)
return return
} }
} catch {} } catch {}
@@ -77,6 +118,7 @@ export default function ReaderPage() {
const urls = Array.from({ length: rendered.length }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`) const urls = Array.from({ length: rendered.length }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`)
setPageUrls(urls) setPageUrls(urls)
setLoading(false) setLoading(false)
restoreProgress(rendered.length)
return return
} }
} catch {} } catch {}
@@ -103,10 +145,11 @@ export default function ReaderPage() {
(n: number) => { (n: number) => {
setPage((prev) => { setPage((prev) => {
const next = Math.max(0, Math.min(n, totalPages - 1)) const next = Math.max(0, Math.min(n, totalPages - 1))
saveProgress(next, mode)
return next return next
}) })
}, },
[totalPages] [totalPages, mode]
) )
// Preload next 2 images // Preload next 2 images
@@ -133,6 +176,18 @@ export default function ReaderPage() {
return () => window.removeEventListener("keydown", onKey) return () => window.removeEventListener("keydown", onKey)
}, [page, goTo]) }, [page, goTo])
// Guardar progreso al salir del lector (cierre de pestaña o navegación).
useEffect(() => {
function onExit() {
saveProgress(page, mode)
}
window.addEventListener("pagehide", onExit)
return () => {
window.removeEventListener("pagehide", onExit)
saveProgress(page, mode)
}
}, [page, mode])
const handleUserActivity = useCallback(() => { const handleUserActivity = useCallback(() => {
setShowControls(true) setShowControls(true)
if (hideTimer.current) clearTimeout(hideTimer.current) if (hideTimer.current) clearTimeout(hideTimer.current)
@@ -223,6 +278,30 @@ export default function ReaderPage() {
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
{/* Barra de progreso de lectura (siempre visible, arriba) */}
{totalPages > 1 && (
<div className="absolute top-0 left-0 right-0 z-[60] h-1 bg-white/10">
<div
className="h-full bg-gradient-to-r from-[var(--accent)] to-[#a78bfa] transition-all duration-300"
style={{ width: `${((page + 1) / totalPages) * 100}%` }}
/>
</div>
)}
{/* Aviso de "Continuaste leyendo" */}
{resumeNotice && (
<div className="absolute top-3 left-1/2 z-[60] -translate-x-1/2 rounded-full border border-white/15 bg-black/85 px-4 py-2 text-xs text-white/90 backdrop-blur-md shadow-lg">
{resumeNotice}
<button
onClick={() => setResumeNotice(null)}
className="ml-2 text-[var(--accent)] hover:text-white transition-colors"
aria-label="Cerrar aviso"
>
</button>
</div>
)}
{/* Floating Top Controls Header */} {/* Floating Top Controls Header */}
<header <header
className={`fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b border-white/10 bg-[#09090b]/90 backdrop-blur-md px-3 py-2 transition-transform duration-300 ${ className={`fixed top-0 left-0 right-0 z-50 flex items-center justify-between border-b border-white/10 bg-[#09090b]/90 backdrop-blur-md px-3 py-2 transition-transform duration-300 ${