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 [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 ${
|
||||||
|
|||||||
Reference in New Issue
Block a user