feat: seguirdo leyendo - progreso de lectura en localStorage (barra de progreso + aviso de continuacion + guardado al salir)
This commit is contained in:
@@ -29,12 +29,51 @@ export default function ReaderPage() {
|
||||
const [mode, setMode] = useState<"paged" | "webtoon">("paged")
|
||||
const [showControls, setShowControls] = useState(true)
|
||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||
const [resumeNotice, setResumeNotice] = useState<string | null>(null)
|
||||
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
const touchStartX = useRef<number | null>(null)
|
||||
const touchStartY = useRef<number | null>(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
|
||||
useEffect(() => {
|
||||
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}`)
|
||||
setPageUrls(urls)
|
||||
setLoading(false)
|
||||
restoreProgress(total)
|
||||
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}`)
|
||||
setPageUrls(urls)
|
||||
setLoading(false)
|
||||
restoreProgress(summary.num_pages)
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
@@ -77,6 +118,7 @@ export default function ReaderPage() {
|
||||
const urls = Array.from({ length: rendered.length }, (_, i) => `/api/proxy/galleries/${gid}/page/${i + 1}`)
|
||||
setPageUrls(urls)
|
||||
setLoading(false)
|
||||
restoreProgress(rendered.length)
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
@@ -103,10 +145,11 @@ export default function ReaderPage() {
|
||||
(n: number) => {
|
||||
setPage((prev) => {
|
||||
const next = Math.max(0, Math.min(n, totalPages - 1))
|
||||
saveProgress(next, mode)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[totalPages]
|
||||
[totalPages, mode]
|
||||
)
|
||||
|
||||
// Preload next 2 images
|
||||
@@ -133,6 +176,18 @@ export default function ReaderPage() {
|
||||
return () => window.removeEventListener("keydown", onKey)
|
||||
}, [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(() => {
|
||||
setShowControls(true)
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current)
|
||||
@@ -223,6 +278,30 @@ export default function ReaderPage() {
|
||||
onTouchStart={handleTouchStart}
|
||||
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 */}
|
||||
<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 ${
|
||||
|
||||
Reference in New Issue
Block a user