# 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 } ] } ```