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>
This commit is contained in:
412
ios-app/Sources/DIAGRAMS.md
Normal file
412
ios-app/Sources/DIAGRAMS.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# 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
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user