diff --git a/src/application/ports.ts b/src/application/ports.ts index 4af0fa9..200ab6b 100644 --- a/src/application/ports.ts +++ b/src/application/ports.ts @@ -29,6 +29,7 @@ export interface Repositorio { // Progreso agregarProgreso(entry: ProgresoEntry): Promise; listarProgreso(obraId: string): Promise; + eliminarProgreso(id: string): Promise; } /** RF-017 / RNF-008: verifica del lado servidor si una URL responde (≤ 5 s, no bloqueante). */ diff --git a/src/application/progreso-service.ts b/src/application/progreso-service.ts index 08a6b5f..aebe626 100644 --- a/src/application/progreso-service.ts +++ b/src/application/progreso-service.ts @@ -21,4 +21,31 @@ export class ProgresoService { async actual(obraId: string): Promise { return puntoActual(await this.repo.listarProgreso(obraId)); } + + /** Fija un capítulo y auto-rellena todos los capítulos enteros anteriores si no existen. */ + async fijar(obraId: string, input: NuevoProgreso): Promise { + const target = input.capitulo; + const historialActual = await this.repo.listarProgreso(obraId); + const setCapitulos = new Set(historialActual.map((e) => e.capitulo)); + + // Rellenar enteros anteriores (ej: si target es 50, rellenar 1..49) + if (target > 1) { + const tope = Math.floor(target); + // Iteramos creando entradas con la hora actual ligeramente desfasada + // (aunque al final todos tendrán tiempos similares, el último será el target) + for (let i = 1; i <= tope; i++) { + if (!setCapitulos.has(i) && i !== target) { + const entry = registrarProgreso(obraId, { capitulo: i }, this.id.nuevo(), this.reloj.ahora()); + await this.repo.agregarProgreso(entry); + } + } + } + + // Registrar finalmente el capítulo deseado para que quede como el más reciente + return this.registrar(obraId, input); + } + + async eliminar(id: string): Promise { + await this.repo.eliminarProgreso(id); + } } diff --git a/src/infrastructure/persistence/dexie-repositorio.ts b/src/infrastructure/persistence/dexie-repositorio.ts index 4496466..2c3748e 100644 --- a/src/infrastructure/persistence/dexie-repositorio.ts +++ b/src/infrastructure/persistence/dexie-repositorio.ts @@ -47,4 +47,8 @@ export class DexieRepositorio implements Repositorio { async listarProgreso(obraId: string): Promise { return this.db.progreso.where('obraId').equals(obraId).toArray(); } + + async eliminarProgreso(id: string): Promise { + await this.db.progreso.delete(id); + } } diff --git a/src/infrastructure/persistence/memory-repositorio.ts b/src/infrastructure/persistence/memory-repositorio.ts index 6e044f8..8db4f4d 100644 --- a/src/infrastructure/persistence/memory-repositorio.ts +++ b/src/infrastructure/persistence/memory-repositorio.ts @@ -40,4 +40,13 @@ export class MemoryRepositorio implements Repositorio { async listarProgreso(obraId: string): Promise { return [...(this.progreso.get(obraId) ?? [])]; } + async eliminarProgreso(id: string): Promise { + for (const [obraId, entries] of this.progreso.entries()) { + const filtered = entries.filter((e) => e.id !== id); + if (filtered.length !== entries.length) { + this.progreso.set(obraId, filtered); + break; + } + } + } } diff --git a/src/ui/pages/CatalogoPage.tsx b/src/ui/pages/CatalogoPage.tsx index 7eaa3a6..321f0c2 100644 --- a/src/ui/pages/CatalogoPage.tsx +++ b/src/ui/pages/CatalogoPage.tsx @@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from 'react'; import { IonBadge, IonButton, IonButtons, IonChip, IonContent, IonFab, IonFabButton, IonHeader, IonIcon, IonItem, IonLabel, IonList, IonNote, IonPage, IonSearchbar, IonSelect, - IonSelectOption, IonTitle, IonToolbar, useIonViewWillEnter, + IonSelectOption, IonTitle, IonToolbar, useIonViewWillEnter, IonModal, } from '@ionic/react'; import { add, cloudOutline, downloadOutline, folderOpenOutline } from 'ionicons/icons'; import { useHistory } from 'react-router-dom'; @@ -26,6 +26,7 @@ export default function CatalogoPage() { const [total, setTotal] = useState(0); const [modal, setModal] = useState(false); const [syncPanel, setSyncPanel] = useState(false); + const [welcomeModal, setWelcomeModal] = useState(false); const recargar = useCallback(async (f: Filtro) => { // Universo de tags disponibles (a partir del catálogo completo) para el filtro RF-014. @@ -41,6 +42,9 @@ export default function CatalogoPage() { }), ); setFilas(filas); + if (todas.length === 0) { + setWelcomeModal(true); + } }, []); useIonViewWillEnter(() => { void recargar(filtro); }); @@ -140,6 +144,9 @@ export default function CatalogoPage() { {total === 0 ? ( <>

Tu catálogo está vacío.

+ setWelcomeModal(true)}> + Ver bienvenida + { await sembrarDemo(); await recargar(filtro); }}> Cargar datos de ejemplo @@ -192,6 +199,33 @@ export default function CatalogoPage() { onClose={() => setSyncPanel(false)} onSyncCompleto={() => void recargar(filtro)} /> + + setWelcomeModal(false)}> + + + ¡Bienvenido a CapMark! + + + +
+

Tu gestor de lecturas personal

+

+ CapMark está diseñado para ayudarte a gestionar las obras (mangas, webtoons, novelas) que llevas en distintas páginas de forma sencilla. +

+

+ 100% Autoalojado: Todos tus datos se guardan en tu propio dispositivo. Nada se comparte con terceros sin tu permiso. Si deseas, puedes sincronizar tu progreso usando tu propio Google Drive. +

+
+ { setWelcomeModal(false); setModal(true); }}> + Agregar mi primera obra + + setWelcomeModal(false)} style={{ marginTop: '10px' }}> + Explorar la app + +
+
+
+
); } diff --git a/src/ui/pages/ObraDetallePage.tsx b/src/ui/pages/ObraDetallePage.tsx index e01cf4c..e4c5124 100644 --- a/src/ui/pages/ObraDetallePage.tsx +++ b/src/ui/pages/ObraDetallePage.tsx @@ -57,7 +57,7 @@ export default function ObraDetallePage() { { text: 'Guardar', handler: async (d) => { - await container.progreso.registrar(id, { capitulo: Number.parseFloat(d.capitulo), punto: d.punto }); + await container.progreso.fijar(id, { capitulo: Number.parseFloat(d.capitulo), punto: d.punto }); await cargar(); }, }, @@ -140,6 +140,11 @@ export default function ObraDetallePage() { const marcarPrincipal = async (f: Fuente) => setFuentes(await container.fuentes.marcarPrincipal(id, f.id)); const eliminarFuente = async (f: Fuente) => setFuentes(await container.fuentes.eliminar(id, f.id)); const abrir = (f: Fuente) => window.open(f.url, '_blank', 'noopener'); + const eliminarProgreso = async (progresoId: string) => { + await container.progreso.eliminar(progresoId); + toast({ message: 'Registro de progreso eliminado', duration: 1500 }); + await cargar(); + }; if (!obra) { return ( @@ -244,7 +249,16 @@ export default function ObraDetallePage() {

Cap. {capFmt(h.capitulo)}

{h.punto &&

{h.punto}

} - {fechaCorta(h.registradoEn)} + {fechaCorta(h.registradoEn)} + eliminarProgreso(h.id)} + title="Deshacer (Eliminar registro)" + > + + ))}