Files
MangaReader/ios-app/Sources/DIAGRAMS.md
renato97 b474182dd9 Initial commit: MangaReader iOS App
 Features:
- App iOS completa para leer manga sin publicidad
- Scraper con WKWebView para manhwaweb.com
- Sistema de descargas offline
- Lector con zoom y navegación
- Favoritos y progreso de lectura
- Compatible con iOS 15+ y Sideloadly/3uTools

📦 Contenido:
- Backend Node.js con Puppeteer (opcional)
- App iOS con SwiftUI
- Scraper de capítulos e imágenes
- Sistema de almacenamiento local
- Testing completo
- Documentación exhaustiva

🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente
  - 21 páginas descargadas
  - 4.68 MB total
  - URLs verificadas y funcionales

🎉 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-04 15:34:18 +01:00

26 KiB
Raw Permalink Blame History

Diagramas del Sistema de Descarga

1. Arquitectura General

┌─────────────────────────────────────────────────────────────┐
│                        UI Layer                             │
├──────────────────────┬──────────────────┬───────────────────┤
│  MangaDetailView     │  DownloadsView   │  ReaderView       │
│  - Download buttons  │  - Active tab    │  - Read offline   │
│  - Progress bars     │  - Completed tab │  - Use local URLs │
│  - Notifications     │  - Failed tab    │                   │
└──────────┬───────────┴──────────┬───────┴─────────┬─────────┘
           │                      │                 │
           ▼                      ▼                 ▼
┌─────────────────────────────────────────────────────────────┐
│                    Presentation Layer                        │
├──────────────────────┬──────────────────┬───────────────────┤
│  MangaDetailViewModel│ DownloadsViewModel│                  │
│  - downloadChapter() │ - clearAllStorage│                  │
│  - downloadChapters()│ - showClearAlert│                  │
└──────────┬──────────────────────────────┴───────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Business Layer                           │
├─────────────────────────────────────────────────────────────┤
│                   DownloadManager                           │
│  - downloadChapter()                                        │
│  - downloadChapters()                                       │
│  - cancelDownload()                                         │
│  - cancelAllDownloads()                                     │
│  - downloadImages()                                         │
└──────────┬──────────────────────┬───────────────────────────┘
           │                      │
           ▼                      ▼
┌────────────────────────┐ ┌────────────────────────────────┐
│   Scraper Layer        │ │    Storage Layer                │
├────────────────────────┤ ├────────────────────────────────┤
│ ManhwaWebScraper       │ │  StorageService                 │
│ - scrapeChapters()     │ │  - saveImage()                  │
│ - scrapeChapterImages()│ │  - getImageURL()                │
│ - scrapeMangaInfo()    │ │  - isChapterDownloaded()        │
│                        │ │  - getChapterDirectory()        │
│                        │ │  - deleteDownloadedChapter()    │
└────────────────────────┘ └────────────────────────────────┘
           │
           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Network Layer                            │
├─────────────────────────────────────────────────────────────┤
│  URLSession                                                │
│  - downloadImage(from: URL)                                │
│  - data(from: URL)                                         │
└─────────────────────────────────────────────────────────────┘

2. Flujo de Descarga Detallado

USUARIO TOCA "DESCARGAR CAPÍTULO"
            │
            ▼
┌───────────────────────────────────────────┐
│  MangaDetailView                          │
│  Button tapped → downloadChapter()        │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  MangaDetailViewModel                     │
│  1. Verificar si ya está descargado       │
│  2. Llamar downloadManager.downloadChapter()│
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  DownloadManager                          │
│  1. Verificar duplicados                  │
│  2. Crear DownloadTask (state: .pending)  │
│  3. Agregar a activeDownloads[]           │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  ManhwaWebScraper                         │
│  scrapeChapterImages(chapterSlug)         │
│  → Retorna [String] URLs de imágenes      │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  DownloadManager                          │
│  4. Actualizar task.imageURLs             │
│  5. Iniciar downloadImages()              │
│     Task 1: state = .downloading(0.0)     │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  downloadImages() - CONCURRENCIA          │
│  ┌─────────────────────────────────────┐  │
│  │ TaskGroup (max 5 concurrent)        │  │
│  │                                     │  │
│  │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐    │  │
│  │ │Img 0│ │Img 1│ │Img 2│ │Img 3│... │  │
│  │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘    │  │
│  │    │       │       │       │        │  │
│  │    ▼       ▼       ▼       ▼        │  │
│  │  ┌───────────────────────────────┐  │  │
│  │  │ downloadImage(from: URL)      │  │  │
│  │  │  1. URLSession.data(from:)    │  │  │
│  │  │  2. Validar HTTP 200          │  │  │
│  │  │  3. UIImage(data:)            │  │  │
│  │  │  4. optimizedForStorage()     │  │  │
│  │  └───────────────┬───────────────┘  │  │
│  │                  │                   │  │
│  │                  ▼                   │  │
│  │  ┌───────────────────────────────┐  │  │
│  │  │ StorageService.saveImage()    │  │  │
│  │  │  → Documents/Chapters/...     │  │  │
│  │  └───────────────┬───────────────┘  │  │
│  │                  │                   │  │
│  │                  ▼                   │  │
│  │  ┌───────────────────────────────┐  │  │
│  │  │ task.updateProgress()         │  │  │
│  │  │  downloadedPages += 1         │  │  │
│  │  │  progress = new value         │  │  │
│  │  │  @Published → UI updates      │  │  │
│  │  └───────────────────────────────┘  │  │
│  └─────────────────────────────────────┘  │
│                                             │
│  Repetir para todas las imágenes...        │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  DownloadManager                          │
│  6. Todas las imágenes descargadas        │
│  7. Crear DownloadedChapter metadata      │
│  8. storage.saveDownloadedChapter()       │
│  9. task.complete() → state = .completed  │
│  10. Mover de activeDownloads[] a         │
│      completedDownloads[]                 │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  MangaDetailViewModel                     │
│  11. showDownloadCompletionNotification() │
│  12. "1 capítulo(s) descargado(s)"        │
│  13. loadChapters() para actualizar UI    │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  UI ACTUALIZADA                           │
│  - ChapterRow muestra checkmark verde     │
│  - Toast notification aparece             │
│  - DownloadsView actualiza                │
└───────────────────────────────────────────┘

3. Estados de una Descarga

┌─────────────────────────────────────────────────────────────┐
│                    ESTADOS DE DESCARGA                      │
└─────────────────────────────────────────────────────────────┘

PENDING
┌──────────────────────┐
│ state: .pending      │
│ downloadedPages: 0   │
│ progress: 0.0        │
│ UI: Icono gris       │
└──────────┬───────────┘
           │ Usuario inicia descarga
           ▼
DOWNLOADING
┌──────────────────────┐
│ state: .downloading  │
│ downloadedPages: N   │ ← Incrementando
│ progress: N/Total    │ ← 0.0 a 1.0
│ UI: Barra azul       │ ← Animando
└──────────┬───────────┘
           │
           ├──────────────────┐
           │                  │
           ▼                  ▼
      COMPLETADO         CANCELADO/ERROR
┌──────────────────┐ ┌──────────────────────┐
│ state: .completed│ │ state: .cancelled    │
│ downloadedPages: │ │ state: .failed(error)│
│      Total       │ │ downloadedPages: N   │
│ progress: 1.0    │ │ progress: N/Total    │
│ UI: Checkmark    │ │ UI: X rojo / Icono   │
└──────────────────┘ └──────────────────────┘

4. Cancelación de Descarga

USUARIO TOCA "CANCELAR"
            │
            ▼
┌───────────────────────────────────────────┐
│  DownloadManager.cancelDownload(taskId)   │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  1. Encontrar task en activeDownloads[]   │
│  2. task.cancel()                         │
│     → cancellationToken.isCancelled = true│
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  TaskGroup detecta cancelación            │
│  ┌─────────────────────────────────────┐  │
│  │ if task.isCancelled {               │  │
│  │     throw DownloadError.cancelled   │  │
│  │ }                                   │  │
│  └─────────────────────────────────────┘  │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  downloadImages() lanza error             │
│  → Catch block ejecuta cleanup            │
└───────────────┬───────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  LIMPIEZA                                 │
│  1. Remover de activeDownloads[]          │
│  2. storage.deleteDownloadedChapter()     │
│     → Eliminar imágenes parciales         │
│  3. NO agregar a completed[]              │
└───────────────────────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────┐
│  UI ACTUALIZADA                           │
│  - Progress bar desaparece                │
│  - Icono de descarga restaurado           │
└───────────────────────────────────────────┘

5. Concurrencia de Descargas

NIVEL 1: Descarga de Capítulos (max 3 simultáneos)
┌─────────────────────────────────────────────────────────────┐
│  downloadManager.downloadChapters([ch1, ch2, ch3, ch4...])  │
└───────────────┬─────────────────────────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────────────────────────┐
│  TaskGroup (limitado a 3 tasks)                            │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │   Chapter 1 │ │   Chapter 2 │ │   Chapter 3 │ ← Active  │
│  │  downloading│ │  downloading│ │  downloading│           │
│  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘           │
│         │               │               │                    │
│  ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐           │
│  │   Chapter 4 │ │   Chapter 5 │ │   Chapter 6 │ ← Waiting │
│  │     waiting │ │     waiting │ │     waiting │           │
│  └─────────────┘ └─────────────┘ └─────────────┘           │
│                                                             │
│  Cuando Chapter 1 completa → Chapter 4 inicia              │
└─────────────────────────────────────────────────────────────┘

NIVEL 2: Descarga de Imágenes (max 5 simultáneas por capítulo)
┌─────────────────────────────────────────────────────────────┐
│  Chapter 1: downloadImages([img0, img1, ... img50])        │
└───────────────┬─────────────────────────────────────────────┘
                │
                ▼
┌─────────────────────────────────────────────────────────────┐
│  TaskGroup (limitado a 5 tasks)                            │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐                        │
│  │img0│ │img1│ │img2│ │img3│ │img4│ ← Descargando          │
│  └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘                        │
│    │      │      │      │      │                           │
│  ┌─▼──────▼──────▼──────▼──────▼──┐                         │
│  │ img5, img6, img7, img8, img9...│ ← Waiting              │
│  └─────────────────────────────────┘                         │
│                                                             │
│  Cuando img0-4 completan → img5-9 inician                  │
└─────────────────────────────────────────────────────────────┘

RESULTADO: Máximo 15 imágenes descargando simultáneamente
(3 capítulos × 5 imágenes)

6. Gestión de Errores

┌─────────────────────────────────────────────────────────────┐
│                    TIPOS DE ERROR                           │
└─────────────────────────────────────────────────────────────┘

NETWORK ERRORS
┌──────────────────────┐
│ - Timeout (30s)      │ → Reintentar automáticamente
│ - No internet        │ → Error al usuario
│ - HTTP 4xx, 5xx      │ → Error específico
└──────────────────────┘

SCRAPER ERRORS
┌──────────────────────┐
│ - No images found    │ → Error: "No se encontraron imágenes"
│ - Page load failed   │ → Error: "Error al cargar página"
│ - Parsing error      │ → Error: "Error al procesar"
└──────────────────────┘

STORAGE ERRORS
┌──────────────────────┐
│ - No space left      │ → Error: "Espacio insuficiente"
│ - Permission denied   │ → Error: "Sin permisos"
│ - Disk write error    │ → Error: "Error de escritura"
└──────────────────────┘

VALIDATION ERRORS
┌──────────────────────┐
│ - Already downloaded │ → Skip o sobrescribir
│ - Invalid URL        │ → Error: "URL inválida"
│ - Invalid image data │ → Error: "Imagen inválida"
└──────────────────────┘

7. Sincronización de UI

┌─────────────────────────────────────────────────────────────┐
│                 @Published PROPERTIES                       │
└─────────────────────────────────────────────────────────────┘

DownloadManager
┌───────────────────────────────────┐
│ @Published var activeDownloads    │ → Vista observa
│ @Published var completedDownloads │ → Vista observa
│ @Published var failedDownloads    │ → Vista observa
│ @Published var totalProgress      │ → Vista observa
└───────────────────────────────────┘
         │
         │ @Published cambia
         ▼
┌───────────────────────────────────┐
│ SwiftUI View se re-renderiza      │
│ automáticamente                   │
└───────────────────────────────────┘

DownloadTask
┌───────────────────────────────────┐
│ @Published var state              │ → Card observa
│ @Published var downloadedPages    │ → ProgressView observa
│ @Published var progress           │ → ProgressView observa
└───────────────────────────────────┘
         │
         │ @Published cambia
         ▼
┌───────────────────────────────────┐
│ ActiveDownloadCard se actualiza   │
│ automáticamente                   │
└───────────────────────────────────┘

8. Estructura de Archivos

Documents/
└── Chapters/
    └── {mangaSlug}/
        └── Chapter{chapterNumber}/
            ├── page_0.jpg
            ├── page_1.jpg
            ├── page_2.jpg
            ├── ...
            └── page_N.jpg

Ejemplo:
Documents/
└── Chapters/
    └── one-piece_1695365223767/
        └── Chapter1/
            ├── page_0.jpg  (150 KB)
            ├── page_1.jpg  (180 KB)
            ├── page_2.jpg  (165 KB)
            └── ...
        └── Chapter2/
            ├── page_0.jpg
            ├── page_1.jpg
            └── ...

metadata.json
{
  "downloadedChapters": [
    {
      "id": "one-piece_1695365223767-chapter1",
      "mangaSlug": "one-piece_1695365223767",
      "mangaTitle": "One Piece",
      "chapterNumber": 1,
      "pages": [...],
      "downloadedAt": "2026-02-04T10:30:00Z",
      "totalSize": 5242880
    }
  ]
}