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

413 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
}
]
}
```