chore: clean unnecessary markdown files for CV sharing

This commit is contained in:
Renato97
2026-03-31 01:16:14 -03:00
parent 89cdb5468f
commit f2a6d682c6
23 changed files with 0 additions and 10513 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,381 +0,0 @@
# VPS Integration - Code Changes Reference
## File: VPSAPIClient.swift (NEW FILE)
Created a complete API client with these sections:
1. **Configuration & Initialization**
- Singleton pattern
- URLSession setup with appropriate timeouts
- Base URL configuration
2. **Health Check**
```swift
func checkHealth() async throws -> Bool
```
3. **Download Operations**
```swift
func downloadChapter(...) async throws -> VPSDownloadResult
```
- Progress tracking via `@Published` properties
- Active download tracking
4. **Status Checking**
```swift
func getChapterManifest(...) async throws -> VPSChapterManifest?
func listDownloadedChapters(...) async throws -> [VPSChapterInfo]
```
5. **Image URLs**
```swift
func getImageURL(...) -> String
```
6. **Management**
```swift
func deleteChapter(...) async throws -> Bool
func getStorageStats() async throws -> VPSStorageStats
```
---
## File: MangaDetailView.swift
### Change 1: Import VPS Client
```swift
// Added to MangaDetailView struct
@StateObject private var vpsClient = VPSAPIClient.shared
```
### Change 2: Add VPS Download Button to Toolbar
```swift
// In toolbar
Button {
viewModel.showingVPSDownloadAll = true
} label: {
Image(systemName: "icloud.and.arrow.down")
}
.disabled(viewModel.chapters.isEmpty)
```
### Change 3: Add VPS Download Alert
```swift
.alert("Descargar a VPS", isPresented: $viewModel.showingVPSDownloadAll) {
Button("Cancelar", role: .cancel) { }
Button("Últimos 10 a VPS") {
Task {
await viewModel.downloadLastChaptersToVPS(count: 10)
}
}
Button("Todos a VPS") {
Task {
await viewModel.downloadAllChaptersToVPS()
}
}
} message: {
Text("¿Cuántos capítulos quieres descargar al servidor VPS?")
}
```
### Change 4: Update ChapterRowView
```swift
struct ChapterRowView: View {
// Added:
let onVPSDownloadToggle: () async -> Void
@ObservedObject var vpsClient = VPSAPIClient.shared
@State private var isVPSDownloaded = false
@State private var isVPSChecked = false
```
### Change 5: Add VPS Status Indicator in ChapterRowView Body
```swift
// VPS Download Button / Status
if isVPSChecked {
if isVPSDownloaded {
Image(systemName: "icloud.fill")
.foregroundColor(.blue)
} else {
Button {
Task {
await onVPSDownloadToggle()
}
} label: {
Image(systemName: "icloud.and.arrow.up")
.foregroundColor(.blue)
}
.buttonStyle(.plain)
}
}
```
### Change 6: Add VPS Progress Display
```swift
// Mostrar progreso de descarga VPS
if vpsClient.activeDownloads.contains("\(mangaSlug)-\(chapter.number)"),
let progress = vpsClient.downloadProgress["\(mangaSlug)-\(chapter.number)"] {
HStack {
Image(systemName: "icloud.and.arrow.down")
.font(.caption2)
.foregroundColor(.blue)
ProgressView(value: progress)
.progressViewStyle(.linear)
.frame(maxWidth: 100)
Text("VPS \(Int(progress * 100))%")
.font(.caption2)
.foregroundColor(.blue)
}
}
```
### Change 7: Add VPS Status Check Function
```swift
private func checkVPSStatus() async {
do {
let manifest = try await vpsClient.getChapterManifest(
mangaSlug: mangaSlug,
chapterNumber: chapter.number
)
isVPSDownloaded = manifest != nil
isVPSChecked = true
} catch {
isVPSDownloaded = false
isVPSChecked = true
}
}
```
### Change 8: Update chaptersList to Pass VPS Callback
```swift
ChapterRowView(
chapter: chapter,
mangaSlug: manga.slug,
onTap: {
viewModel.selectedChapter = chapter
},
onDownloadToggle: {
await viewModel.downloadChapter(chapter)
},
onVPSDownloadToggle: { // NEW
await viewModel.downloadChapterToVPS(chapter)
}
)
```
### Change 9: ViewModel Additions
```swift
// New Published Property
@Published var showingVPSDownloadAll = false
// New Dependency
private let vpsClient = VPSAPIClient.shared
// New Methods
func downloadChapterToVPS(_ chapter: Chapter) async {
do {
let imageUrls = try await scraper.scrapeChapterImages(chapterSlug: chapter.slug)
let result = try await vpsClient.downloadChapter(
mangaSlug: manga.slug,
chapterNumber: chapter.number,
chapterSlug: chapter.slug,
imageUrls: imageUrls
)
// Handle result and show notification
} catch {
// Handle error
}
}
func downloadAllChaptersToVPS() async { /* ... */ }
func downloadLastChaptersToVPS(count: Int) async { /* ... */ }
```
---
## File: ReaderView.swift
### Change 1: Add VPS Client
```swift
// Added to ReaderView struct
@ObservedObject private var vpsClient = VPSAPIClient.shared
```
### Change 2: Update PageView for VPS Support
```swift
struct PageView: View {
// Added:
@ObservedObject var vpsClient = VPSAPIClient.shared
@State private var useVPS = false
var body: some View {
// ...
if let localURL = StorageService.shared.getImageURL(...) {
// Load from local cache
} else if useVPS {
// Load from VPS
let vpsImageURL = vpsClient.getImageURL(
mangaSlug: mangaSlug,
chapterNumber: chapterNumber,
pageIndex: page.index + 1
)
AsyncImage(url: URL(string: vpsImageURL)) { /* ... */ }
} else {
// Load from original URL
fallbackImage
}
// ...
.task {
// Check if VPS has this chapter
if let manifest = try? await vpsClient.getChapterManifest(...) {
useVPS = true
}
}
}
private var fallbackImage: some View { /* ... */ }
}
```
### Change 3: Update ReaderViewModel
```swift
// New Published Property
@Published var isVPSDownloaded = false
// New Dependency
private let vpsClient = VPSAPIClient.shared
// Updated loadPages()
func loadPages() async {
// 1. Check VPS first
if let vpsManifest = try await vpsClient.getChapterManifest(...) {
// Load from VPS
isVPSDownloaded = true
}
// 2. Then local storage
else if let downloadedChapter = storage.getDownloadedChapter(...) {
// Load from local
isDownloaded = true
}
// 3. Finally scrape
else {
// Scrape from original
}
}
```
### Change 4: Update Reader Footer
```swift
// Page indicator section
if viewModel.isVPSDownloaded {
Label("VPS", systemImage: "icloud.fill")
.font(.caption)
.foregroundColor(.blue)
}
if viewModel.isDownloaded {
Label("Local", systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundColor(.green)
}
```
---
## Data Models (in VPSAPIClient.swift)
### VPSDownloadResult
```swift
struct VPSDownloadResult {
let success: Bool
let alreadyDownloaded: Bool
let manifest: VPSChapterManifest?
let downloaded: Int?
let failed: Int?
}
```
### VPSChapterManifest
```swift
struct VPSChapterManifest: Codable {
let mangaSlug: String
let chapterNumber: Int
let totalPages: Int
let downloadedPages: Int
let failedPages: Int
let downloadDate: String
let totalSize: Int
let images: [VPSImageInfo]
}
```
### VPSChapterInfo
```swift
struct VPSChapterInfo: Codable {
let chapterNumber: Int
let downloadDate: String
let totalPages: Int
let downloadedPages: Int
let totalSize: Int
let totalSizeMB: String
}
```
### VPSStorageStats
```swift
struct VPSStorageStats: Codable {
let totalMangas: Int
let totalChapters: Int
let totalSize: Int
let totalSizeMB: String
let totalSizeFormatted: String
let mangaDetails: [VPSMangaDetail]
}
```
---
## Priority Order for Image Loading
1. **Local Device Storage** (fastest, offline)
2. **VPS Storage** (fast, online)
3. **Original URL** (slowest, may fail)
This ensures best performance and reliability.
---
## Error Handling Pattern
All VPS operations follow this pattern:
```swift
do {
let result = try await vpsClient.someMethod(...)
// Handle success
} catch {
// Handle error - show user notification
notificationMessage = "Error: \(error.localizedDescription)"
showDownloadNotification = true
}
```
---
## Progress Tracking Pattern
For downloads:
```swift
// Track active downloads
vpsClient.activeDownloads.contains("downloadId")
// Get progress
vpsClient.downloadProgress["downloadId"]
// Display in UI
ProgressView(value: progress)
Text("\(Int(progress * 100))%")
```

View File

@@ -1,587 +0,0 @@
# Quick Implementation Guide - MangaReader Optimizations
## 📂 Archivos Creados
```
ios-app/Sources/Services/
├── ManhwaWebScraperOptimized.swift # Scraper con cache inteligente
├── StorageServiceOptimized.swift # Storage con compresión y thumbnails
├── ImageCache.swift # Sistema de cache de imágenes
└── CacheManager.swift # Gerente central de cache
ios-app/Sources/Views/
└── ReaderViewOptimized.swift # Reader optimizado
OPTIMIZATION_SUMMARY.md # Documentación completa
IMPLEMENTATION_GUIDE.md # Este archivo
```
---
## 🔄 Migración Rápida
### Opción 1: Reemplazo Directo (Recomendado para Testing)
```swift
// 1. En tus ViewModels, cambia:
private let scraper = ManhwaWebScraper.shared
// Por:
private let scraper = ManhwaWebScraperOptimized.shared
// 2. En tus Views, cambia:
private let storage = StorageService.shared
// Por:
private let storage = StorageServiceOptimized.shared
// 3. En ReaderView:
// Reemplaza completamente por ReaderViewOptimized
```
### Opción 2: Migración Gradual (Más Segura)
```swift
// Usa alias de tipo para migrar gradualmente
typealias Scraper = ManhwaWebScraperOptimized
typealias Storage = StorageServiceOptimized
// Código existente funciona sin cambios
private let scraper = Scraper.shared
private let storage = Storage.shared
```
---
## 🔧 Configuración Inicial
### 1. Inicializar CacheManager en AppDelegate
```swift
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions ...
) -> Bool {
// Inicializar cache manager
let cacheManager = CacheManager.shared
#if DEBUG
// Imprimir reporte inicial en debug
cacheManager.printCacheReport()
#endif
return true
}
// Monitoring de background
func applicationDidEnterBackground(_ application: UIApplication) {
// El CacheManager ya maneja esto automáticamente
// pero puedes agregar lógica adicional aquí
}
// Monitoring de memory warnings
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
// CacheManager ya responde automáticamente
print("⚠️ Memory warning received")
}
}
```
### 2. Configurar Ajustes de Cache
```swift
// En algún lugar de configuración (ej: SettingsViewModel)
struct CacheSettings {
// Ajusta según tus necesidades
// Tamaño máximo de cache (en bytes)
static let maxImageCacheSize: Int64 = 500 * 1024 * 1024 // 500 MB
static let maxMetadataCacheSize: Int64 = 50 * 1024 * 1024 // 50 MB
// Tiempos de expiración (en segundos)
static let htmlCacheDuration: TimeInterval = 1800 // 30 min
static let imageCacheDuration: TimeInterval = 7 * 86400 // 7 días
static let thumbnailCacheDuration: TimeInterval = 30 * 86400 // 30 días
// Preloading
static let preloadAdjacentPages: Int = 2 // 2 páginas antes/después
static let enablePreloading: Bool = true
}
```
---
## 📊 Monitoreo y Debugging
### Ver Estadísticas en Debug
```swift
#if DEBUG
// Agregar botón de debug en settings
struct DebugSettingsView: View {
var body: some View {
VStack {
Button("Print Cache Report") {
CacheManager.shared.printCacheReport()
}
Button("Print Image Statistics") {
ImageCache.shared.printStatistics()
}
Button("Clear All Cache") {
CacheManager.shared.clearAllCache()
}
Button("Simulate Memory Warning") {
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
}
}
}
#endif
```
### Instruments Configuration
```bash
# Para perfilado de memoria
1. Xcode → Product → Profile
2. Seleccionar "Allocations"
3. Monitorear:
- Overall memory usage
- Anonymous VM
- Image cache size
# Para perfilado de tiempo
1. Seleccionar "Time Profiler"
2. Buscar funciones lentas
3. Verificar que cache esté funcionando
```
---
## 🧪 Testing Checklist
### Tests Funcionales
```swift
class CachePerformanceTests: XCTestCase {
func testScraperCache() async throws {
let scraper = ManhwaWebScraperOptimized.shared
// Primer scraping (debe ser lento)
let start1 = Date()
_ = try await scraper.scrapeChapters(mangaSlug: "test-manga")
let time1 = Date().timeIntervalSince(start1)
// Segundo scraping (debe ser rápido por cache)
let start2 = Date()
_ = try await scraper.scrapeChapters(mangaSlug: "test-manga")
let time2 = Date().timeIntervalSince(start2)
// El segundo debe ser >10x más rápido
XCTAssertLessThan(time2, time1 / 10)
}
func testImageCompression() {
let storage = StorageServiceOptimized.shared
// Crear imagen de prueba grande (4 MB)
let largeImage = createTestImage(size: CGSize(width: 2000, height: 3000))
let expectation = XCTestExpectation(description: "Compress and save")
Task {
let url = try await storage.saveImage(
largeImage,
mangaSlug: "test",
chapterNumber: 1,
pageIndex: 0
)
// Verificar que archivo resultante es < 2 MB
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
let fileSize = attributes[.size] as! Int64
XCTAssertLessThan(fileSize, 2 * 1024 * 1024)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
func testPreloading() {
let viewModel = ReaderViewModelOptimized(
manga: testManga,
chapter: testChapter
)
// Cargar páginas
let expectation = XCTestExpectation(description: "Load pages")
Task {
await viewModel.loadPages()
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
// Simular navegación a página 5
viewModel.currentPage = 5
// Verificar que páginas 3, 4, 6, 7 están en cache
let imageCache = ImageCache.shared
XCTAssertNotNil(imageCache.image(for: viewModel.pages[3].url))
XCTAssertNotNil(imageCache.image(for: viewModel.pages[4].url))
XCTAssertNotNil(imageCache.image(for: viewModel.pages[6].url))
XCTAssertNotNil(imageCache.image(for: viewModel.pages[7].url))
}
}
```
### Tests de Memoria
```swift
class MemoryTests: XCTestCase {
func testMemoryUnderLoad() {
let viewModel = ReaderViewModelOptimized(
manga: largeManga, // 100 páginas
chapter: largeChapter
)
let memoryBefore = getMemoryUsage()
let expectation = XCTestExpectation(description: "Load all pages")
Task {
await viewModel.loadPages()
// Navegar por todas las páginas
for i in 0..<viewModel.pages.count {
viewModel.currentPage = i
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 60.0)
let memoryAfter = getMemoryUsage()
let memoryIncrease = memoryAfter - memoryBefore
// No debe aumentar más de 150 MB
XCTAssertLessThan(memoryIncrease, 150 * 1024 * 1024)
}
func testMemoryWarningResponse() {
// Llenar cache
let cache = ImageCache.shared
for i in 0..<100 {
cache.setImage(createTestImage(), for: "test_\(i)")
}
let itemsBefore = cache.getCacheStatistics().memoryCacheHits
// Simular memory warning
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
// Esperar un poco
Thread.sleep(forTimeInterval: 0.5)
// Cache debe estar vacío o casi vacío
let itemsAfter = cache.getCacheStatistics().memoryCacheHits
XCTAssertLessThan(itemsAfter, itemsBefore / 2)
}
private func getMemoryUsage() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? info.resident_size : 0
}
}
```
---
## 🎯 Optimizaciones por Categoría
### Scraper Optimizations
| Optimización | Archivo | Líneas Clave |
|--------------|---------|--------------|
| WKWebView reuse | `ManhwaWebScraperOptimized.swift` | 30-45 |
| HTML cache | `ManhwaWebScraperOptimized.swift` | 48-62 |
| Precompiled JS | `ManhwaWebScraperOptimized.swift` | 67-122 |
| Adaptive timeout | `ManhwaWebScraperOptimized.swift` | 127-165 |
| Concurrency control | `ManhwaWebScraperOptimized.swift` | 170-195 |
### Storage Optimizations
| Optimización | Archivo | Líneas Clave |
|--------------|---------|--------------|
| Adaptive compression | `StorageServiceOptimized.swift` | 28-52 |
| Thumbnail system | `StorageServiceOptimized.swift` | 57-85 |
| Lazy loading | `StorageServiceOptimized.swift` | 478-488 |
| Auto cleanup | `StorageServiceOptimized.swift` | 330-380 |
| Batch operations | `StorageServiceOptimized.swift` | 220-245 |
### Reader Optimizations
| Optimización | Archivo | Líneas Clave |
|--------------|---------|--------------|
| NSCache | `ImageCache.swift` | 22-45 |
| Preloading | `ReaderViewOptimized.swift` | 195-215 |
| Memory management | `ReaderViewOptimized.swift` | 420-445 |
| Debouncing | `ReaderViewOptimized.swift` | 505-525 |
---
## 🔍 Troubleshooting
### Problema: Cache no funciona
```swift
// Verificar que el singleton se esté usando correctamente
let scraper = ManhwaWebScraperOptimized.shared
// NO: let scraper = ManhwaWebScraperOptimized()
// Verificar que los métodos tengan @MainActor si es necesario
@MainActor
func loadChapters() async throws {
// código
}
```
### Problema: Memoria sigue alta
```swift
// 1. Verificar que se estén liberando referencias
deinit {
progressSaveTimer?.invalidate()
NotificationCenter.default.removeObserver(self)
}
// 2. Usizar weak en closures
Task { [weak self] in
await self?.loadPages()
}
// 3. Verificar en Instruments
// - Revisar "Anonymous VM" (debe ser bajo)
// - Buscar "Malloc" leaks
```
### Problema: Preloading no funciona
```swift
// Verificar que esté habilitado
viewModel.enablePreloading = true // Debe ser true
// Verificar que onAppear se llame
PageView(page: page)
.onAppear {
print("Page \(page.index) appeared") // Debug
viewModel.preloadAdjacentPages(...)
}
// Verificar priority queue
Task(priority: .utility) {
// Debe usar .utility, no .background
}
```
### Problema: Thumbnails no se generan
```swift
// Verificar directorio de thumbnails
let thumbDir = storage.getThumbnailDirectory(...)
print("Thumb dir: \(thumbDir.path)")
// Verificar que se cree el directorio
try? FileManager.default.createDirectory(
at: thumbDir,
withIntermediateDirectories: true
)
// Verificar que se guarde correctamente
try? thumbData.write(to: thumbnailURL)
```
---
## 📈 Métricas de Éxito
### Objetivos de Rendimiento
```swift
struct PerformanceTargets {
// Tiempos de carga
static let maxChapterLoadTime: TimeInterval = 2.0 // segundos
static let maxPageLoadTime: TimeInterval = 0.5 // segundos (con cache)
static let maxAppLaunchTime: TimeInterval = 1.0 // segundos
// Memoria
static let maxReaderMemory: UInt64 = 120 * 1024 * 1024 // 120 MB
static let maxScraperMemory: UInt64 = 50 * 1024 * 1024 // 50 MB
// Cache
static let minCacheHitRate: Double = 0.80 // 80%
static let maxCacheSize: Int64 = 500 * 1024 * 1024 // 500 MB
}
// Función de verificación
func verifyPerformanceTargets() {
let cacheStats = ImageCache.shared.getCacheStatistics()
let hitRate = cacheStats.hitRate
XCTAssertGreaterThan(hitRate, PerformanceTargets.minCacheHitRate,
"Cache hit rate \(hitRate) below target")
// ... más verificaciones
}
```
---
## 🚀 Próximos Pasos
### Implementación Inmediata
1. **Backup del código existente**
```bash
cp -r ios-app/Sources ios-app/Sources.backup
```
2. **Reemplazar archivos uno por uno**
- Empezar con ManhwaWebScraperOptimized
- Probar exhaustivamente
- Continuar con StorageServiceOptimized
- etc.
3. **Testing completo**
- Unit tests
- Integration tests
- Manual testing
### Optimizaciones Futuras (Opcionales)
1. **Prefetching Predictivo**
```swift
// Usar ML para predecir próximo capítulo
func predictNextChapter(userHistory: [Chapter]) -> Chapter? {
// Implementación básica de ML
}
```
2. **Compresión HEIC**
```swift
// Usar HEIC en lugar de JPEG (50% más eficiente)
if #available(iOS 11.0, *) {
let heicData = image.heicData()
}
```
3. **Progressive Image Loading**
```swift
// Cargar versión baja resolución primero
// Luego reemplazar con alta resolución
```
4. **Background Sync**
```swift
// Sincronizar en background usando BGTaskScheduler
func scheduleBackgroundSync() {
let request = BGAppRefreshTaskRequest(...)
try? BGTaskScheduler.shared.submit(request)
}
```
---
## 💡 Tips y Best Practices
### DO ✅
```swift
// 1. Usar singletons para caches
static let shared = ImageCache()
// 2. Usar colas para I/O
private let ioQueue = DispatchQueue(label: "...", qos: .utility)
// 3. Weak references en closures
Task { [weak self] in
await self?.loadData()
}
// 4. Responder a memory warnings
@objc func handleMemoryWarning() {
cache.removeAllObjects()
}
// 5. Debouncing de operaciones frecuentes
func saveDebounced() {
timer?.invalidate()
timer = Timer.scheduledTimer(...)
}
```
### DON'T ❌
```swift
// 1. NO crear múltiples instancias de cache
// let cache1 = ImageCache()
// let cache2 = ImageCache() // ❌
// 2. NO hacer I/O en main thread
// let data = try Data(contentsOf: url) // ❌ Bloquea
// 3. NO olvidar weak en closures
// Task {
// self.doSomething() // ❌ Memory leak
// }
// 4. NO ignorar memory warnings
// @objc func handleMemoryWarning() {
// // ❌ No hacer nada
// }
// 5. NO guardar en cada cambio
// func onChange() {
// saveToDisk() // ❌ Demasiado frecuente
// }
```
---
## 📞 Soporte
Si encuentras problemas:
1. Revisa `OPTIMIZATION_SUMMARY.md` para detalles técnicos
2. Usa los tests de ejemplo como guía
3. Habilita logging debug en development
4. Usa Instruments para perfilado
---
**Happy Optimizing! 🚀**

View File

@@ -1,865 +0,0 @@
# MangaReader - Optimizaciones de Rendimiento y Memoria
## Resumen Ejecutivo
Se han implementado optimizaciones comprehensivas en el proyecto MangaReader para mejorar el rendimiento, reducir el uso de memoria y optimizar el tamaño final de la aplicación.
## 📊 Métricas Esperadas de Mejora
| Métrica | BEFORE | AFTER | Mejora |
|---------|--------|-------|--------|
| Tiempo de carga de capítulos | 3-5 segundos | 0.5-2 segundos | **60-85%** |
| Uso de memoria en lectura | 150-300 MB | 50-100 MB | **50-65%** |
| Tamaño de cache de imágenes | Ilimitado | Max 500 MB | **Controlado** |
| Re-scraping de páginas | Siempre | Cache 30 min | **80% reducción** |
| Preloading de páginas | Ninguno | 2 páginas adelante/atrás | **Experiencia fluida** |
| Compresión de imágenes | JPEG 0.8 fijo | Adaptativa (0.6-0.9) | **30-40% espacio** |
| Thumbnails | No | Sí (150x200) | **Navegación rápida** |
---
## 1. Optimización del Scraper (ManhwaWebScraperOptimized.swift)
### 🎯 Optimizaciones Implementadas
#### 1.1 WKWebView Reutilizable
```swift
// BEFORE: Creaba nueva instancia cada vez
private var webView: WKWebView?
// AFTER: Singleton con reutilización
static let shared = ManhwaWebScraperOptimized()
private var webView: WKWebView? // Una sola instancia
```
**Impacto:**
- Reducción de 70-80% en tiempo de inicialización
- Menor uso de memoria (sin múltiples WKWebView)
- Evita crashes por límite de WKWebViews simultáneos
#### 1.2 Cache Inteligente de HTML
```swift
// BEFORE: Siempre descargaba y parseaba HTML
func scrapeChapters(mangaSlug: String) async throws -> [Chapter] {
// Siempre scraping
}
// AFTER: NSCache + Disco con expiración
private var htmlCache: NSCache<NSString, NSString>
private let cacheValidDuration: TimeInterval = 1800 // 30 minutos
func scrapeChapters(mangaSlug: String) async throws -> [Chapter] {
if let cachedResult = getCachedResult(for: cacheKey) {
return parseChapters(from: cachedResult)
}
// Solo scraping si no hay cache válido
}
```
**Impacto:**
- 80-90% de requests sirven desde cache
- Reducción drástica de uso de red
- Tiempo de respuesta: 3-5s → 0.1-0.5s
#### 1.3 JavaScript Injection Optimizado
```swift
// BEFORE: Strings literales en cada llamada
chapters = try await webView.evaluateJavaScript("""
(function() {
// 50 líneas de JavaScript
})();
""") as! [[String: Any]]
// AFTER: Scripts precompilados (enum)
private enum JavaScriptScripts: String {
case extractChapters = """
(function() {
// Script optimizado
})();
"""
}
chapters = try await webView.evaluateJavaScript(
JavaScriptScripts.extractChapters.rawValue
) as! [[String: Any]]
```
**Impacto:**
- Menor memoria (strings no se recrean)
- Ejecución 10-15% más rápida
- Código más mantenible
#### 1.4 Timeout Adaptativo
```swift
// BEFORE: Siempre 3-5 segundos fijos
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
continuation.resume()
}
// AFTER: Basado en historial de rendimiento
private var averageLoadTime: TimeInterval = 3.0
private func getAdaptiveTimeout() -> TimeInterval {
return averageLoadTime + 1.0 // Margen de seguridad
}
// Se ajusta automáticamente según condiciones de red
private func updateLoadTimeHistory(_ loadTime: TimeInterval) {
loadTimeHistory.append(loadTime)
averageLoadTime = calculateAverage()
}
```
**Impacto:**
- 20-30% más rápido en conexiones buenas
- Más robusto en conexiones lentas
- Timeout óptimo: 2-8 segundos (adaptativo)
#### 1.5 Control de Concurrencia
```swift
// BEFORE: Sin límite de scraping simultáneo
// Podía crashear por demasiados WKWebViews
// AFTER: Semaphore para máximo 2 scrapings
private let scrapingSemaphore = DispatchSemaphore(value: 2)
func scrapeChapters(mangaSlug: String) async throws -> [Chapter] {
await withCheckedContinuation { continuation in
scrapingSemaphore.wait()
continuation.resume()
}
defer { scrapingSemaphore.signal() }
// Scraping con límite de concurrencia
}
```
**Impacto:**
- Previene crashes por sobrecarga
Mejora estabilidad general
- Uso de memoria controlado
---
## 2. Optimización del StorageService (StorageServiceOptimized.swift)
### 🎯 Optimizaciones Implementadas
#### 2.1 Compresión Inteligente de Imágenes
```swift
// BEFORE: JPEG quality 0.8 fijo
let data = image.jpegData(compressionQuality: 0.8)
// AFTER: Calidad adaptativa basada en tamaño
private enum ImageCompression {
static func quality(for imageSize: Int) -> CGFloat {
let sizeMB = Double(imageSize) / (1024 * 1024)
if sizeMB > 3.0 {
return 0.6 // Imágenes muy grandes
} else if sizeMB > 1.5 {
return 0.75 // Imágenes medianas
} else {
return 0.9 // Imágenes pequeñas
}
}
}
let quality = ImageCompression.quality(for: imageSize)
let data = image.jpegData(compressionQuality: quality)
```
**Impacto:**
- 30-40% reducción en espacio de almacenamiento
- Calidad visual imperceptible
- Ahorro significativo en ancho de banda
#### 2.2 Sistema de Thumbnails
```swift
// BEFORE: Sin thumbnails - cargaba imágenes completas
struct MangaPage {
var thumbnailURL: String {
return url // No había thumbnails
}
}
// AFTER: Generación automática de thumbnails
private enum ThumbnailSize {
static let small = CGSize(width: 150, height: 200) // Para lista
static let medium = CGSize(width: 300, height: 400) // Para preview
}
func saveImage(_ image: UIImage, ...) async throws -> URL {
// Guardar imagen completa
try data.write(to: fileURL)
// Crear thumbnail automáticamente en background
Task {
await createThumbnail(for: fileURL, ...)
}
}
private func createThumbnail(for imageURL: URL, ...) async {
let thumbnail = await resizeImage(image, to: targetSize)
let thumbData = thumbnail.jpegData(compressionQuality: 0.5)
try? thumbData.write(to: thumbnailURL)
}
```
**Impacto:**
- Navegación 10x más rápida en listas
- 90-95% menos memoria en previews
- Mejor experiencia de usuario
#### 2.3 Lazy Loading de Capítulos
```swift
// BEFORE: Cargaba todos los capítulos en memoria
func getDownloadedChapters() -> [DownloadedChapter] {
let all = try decodeJSON() // Todo en memoria
return all
}
// AFTER: Paginación y filtros eficientes
func getDownloadedChapters(offset: Int, limit: Int) -> [DownloadedChapter] {
let all = getAllDownloadedChapters()
let start = min(offset, all.count)
let end = min(offset + limit, all.count)
return Array(all[start..<end]) // Solo lo necesario
}
func getDownloadedChapters(forManga mangaSlug: String) -> [DownloadedChapter] {
return getAllDownloadedChapters()
.filter { $0.mangaSlug == mangaSlug } // Solo del manga específico
}
```
**Impacto:**
- Inicio de app 50-70% más rápido
- Menor uso de memoria en startup
- UI más fluida
#### 2.4 Purga Automática de Cache
```swift
// BEFORE: Sin limpieza automática
// El cache crecía indefinidamente
// AFTER: Limpieza automática periódica
private let maxCacheAge: TimeInterval = 30 * 24 * 3600 // 30 días
private let maxCacheSize: Int64 = 2 * 1024 * 1024 * 1024 // 2 GB
private func setupAutomaticCleanup() {
Timer.scheduledTimer(withTimeInterval: 86400, repeats: true) { _ in
self.performCleanupIfNeeded()
}
}
private func performCleanupIfNeeded() {
if currentSize > maxCacheSize {
cleanupOldFiles()
}
}
private func cleanupOldFiles() {
let now = Date()
// Eliminar archivos más viejos que maxCacheAge
for fileURL in oldFiles {
if modificationDate < now.addingTimeInterval(-maxCacheAge) {
try? fileManager.removeItem(at: fileURL)
}
}
}
```
**Impacto:**
- Almacenamiento controlado automáticamente
- No requiere intervención del usuario
- Previene problemas de espacio
#### 2.5 Batch Operations
```swift
// BEFORE: Operaciones I/O individuales
func saveReadingProgress(_ progress: ReadingProgress) {
var allProgress = getAllReadingProgress() // Leer
allProgress.append(progress) // Modificar
saveProgressToDisk(allProgress) // Escribir
}
// AFTER: Escritura diferida en background
func saveReadingProgress(_ progress: ReadingProgress) {
var allProgress = getAllReadingProgress()
allProgress.append(progress)
// Guardar en background, no bloquear
Task(priority: .utility) {
await saveProgressToDiskAsync(allProgress)
}
}
```
**Impacto:**
- UI más fluida (no bloquea en I/O)
- Mejor experiencia de usuario
- Operaciones en paralelo
---
## 3. Optimización del ReaderView (ReaderViewOptimized.swift)
### 🎯 Optimizaciones Implementadas
#### 3.1 Image Caching con NSCache
```swift
// BEFORE: Sin cache en memoria
struct PageView: View {
var body: some View {
AsyncImage(url: URL(string: page.url)) { phase in
// Recargaba siempre
}
}
}
// AFTER: NSCache con prioridades
final class ImageCache {
static let shared = ImageCache()
private let cache: NSCache<NSString, UIImage>
func image(for url: String) -> UIImage? {
// Verificar memoria cache primero
if let cachedImage = getCachedImage(for: url) {
return cachedImage
}
// Verificar disco cache
if let diskImage = loadImageFromDisk(for: url) {
setImage(diskImage, for: url)
return diskImage
}
return nil // No en cache, necesita descarga
}
}
```
**Impacto:**
- 80-90% de páginas cargan instantáneamente
- Hit rate de cache: 85-95%
- Navegación fluida sin recargas
#### 3.2 Preloading de Páginas Adyacentes
```swift
// BEFORE: Sin preloading
TabView(selection: $currentPage) {
ForEach(pages) { page in
PageView(page: page)
.onAppear {
// Solo carga la página actual
}
}
}
// AFTER: Precarga 2 páginas adelante y atrás
func preloadAdjacentPages(currentIndex: Int, total: Int) {
let startIndex = max(0, currentIndex - 2)
let endIndex = min(total - 1, currentIndex + 2)
for index in startIndex...endIndex {
guard index != currentIndex else { continue }
Task(priority: .utility) {
// Precargar en background
await loadImage(pageIndex: index)
}
}
}
TabView(selection: $currentPage) {
ForEach(pages) { page in
PageView(page: page)
.onAppear {
preloadAdjacentPages(
currentIndex: page.index,
total: pages.count
)
}
}
}
```
**Impacto:**
- Navegación instantánea en 80% de casos
- Mejor experiencia de lectura
- No afecta rendimiento significativamente
#### 3.3 Memory Management para Imágenes Grandes
```swift
// BEFORE: Imágenes a resolución completa
if let image = UIImage(data: data) {
Image(uiImage: image)
.resizable()
}
// AFTER: Redimensiona automáticamente
private func optimizeImageSize(_ image: UIImage) -> UIImage {
let maxDimension: CGFloat = 2048
// Si ya es pequeña, no cambiar
if image.size.width <= maxDimension {
return image
}
// Redimensionar manteniendo aspect ratio
let newSize = calculateNewSize()
let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
}
// Response a memory warnings
@objc private func handleMemoryWarning() {
cache.removeAllObjects()
preloadQueue.removeAll()
}
```
**Impacto:**
- 50-70% menos memoria en imágenes
- Sin crashes por memory pressure
- Rendering más rápido
#### 3.4 Optimización de TabView
```swift
// BEFORE: Cargaba todas las vistas
TabView(selection: $currentPage) {
ForEach(viewModel.pages) { page in
PageView(page: page)
.tag(page.index)
}
// Todas las páginas se creaban de una vez
}
// AFTER: View recycling + lazy loading
TabView(selection: $currentPage) {
ForEach(viewModel.pages) { page in
PageViewOptimized(
page: page,
mangaSlug: manga.slug,
chapterNumber: chapter.number,
viewModel: viewModel
)
.id(page.index) // View recycling
.tag(page.index)
.onAppear {
viewModel.preloadAdjacentPages(
currentIndex: page.index,
total: viewModel.pages.count
)
}
}
}
```
**Impacto:**
- Inicio 60-70% más rápido
- Menor uso de memoria
- Scroll más fluido
#### 3.5 Debouncing de Progreso
```swift
// BEFORE: Guardaba progreso en cada cambio de página
.onChange(of: currentPage) { _, newValue in
saveProgress() // I/O cada cambio
}
// AFTER: Debouncing de 2 segundos
private var progressSaveTimer: Timer?
func currentPageChanged(from: Int, to: Int) {
saveProgressDebounced()
preloadAdjacentPages(currentIndex: to, total: pages.count)
}
private func saveProgressDebounced() {
progressSaveTimer?.invalidate()
progressSaveTimer = Timer.scheduledTimer(
withTimeInterval: 2.0,
repeats: false
) { [weak self] _ in
self?.saveProgress()
}
}
```
**Impacto:**
- 95% menos escrituras a disco
- Mejor rendimiento de navegación
- No se pierde progreso
---
## 4. Cache Manager (CacheManager.swift)
### 🎯 Optimizaciones Implementadas
#### 4.1 Políticas LRU (Least Recently Used)
```swift
// BEFORE: Sin política clara de eliminación
// FIFO simple sin considerar uso
// AFTER: LRU completo con tracking
private struct CacheItem {
let key: String
let size: Int64
var lastAccess: Date
var accessCount: Int
}
func trackAccess(key: String, type: CacheType, size: Int64) {
cacheItems[key] = CacheItem(
key: key,
size: size,
lastAccess: Date(),
accessCount: existingItem.accessCount + 1
)
}
// Limpieza por prioridad + recencia
let sortedItems = cacheItems.sorted {
if $0.type.priority != $1.type.priority {
return $0.type.priority < $1.type.priority
}
return $0.lastAccess < $1.lastAccess
}
```
**Impacto:**
- Items más usados permanecen más tiempo
- Cache más eficiente
- Mejor hit rate
#### 4.2 Priorización por Tipo de Contenido
```swift
enum CacheType: String {
case images // Prioridad alta
case thumbnails // Prioridad media
case html // Prioridad baja
case metadata // Prioridad baja
var priority: CachePriority {
switch self {
case .images: return .high
case .thumbnails: return .medium
case .html, .metadata: return .low
}
}
}
// En limpieza, eliminar baja prioridad primero
let lowPriorityItems = cacheItems.filter {
$0.type.priority == .low
}
```
**Impacto:**
- Preserva contenido importante
- Limpieza más inteligente
- Mejor experiencia de usuario
#### 4.3 Análisis de Patrones de Uso
```swift
// BEFORE: Sin análisis de uso
// AFTER: Tracking completo de patrones
struct CacheItem {
var lastAccess: Date
var accessCount: Int
let created: Date
}
func getCacheReport() -> CacheReport {
let averageAge = cacheItems.values
.map { now.timeIntervalSince($0.created) }
.reduce(0, +) / Double(cacheItems.count)
let averageAccessCount = cacheItems.values
.map { $0.accessCount }
.reduce(0, +) / Double(cacheItems.count)
return CacheReport(
averageAge: averageAge,
averageAccessCount: averageAccessCount
)
}
```
**Impacto:**
- Decisiones basadas en datos reales
- Optimización continua
- Mejora iterativa
#### 4.4 Emergency Cleanup
```swift
// BEFORE: Sin respuesta a baja memoria
// AFTER: Limpieza agresiva cuando es necesario
func performEmergencyCleanup() {
print("🚨 EMERGENCY CLEANUP")
// Eliminar todo de baja prioridad
let lowPriorityItems = cacheItems.filter {
$0.type.priority == .low
}
for (key, item) in lowPriorityItems {
removeCacheItem(key: key, type: item.type)
}
// Si aún es crítico, eliminar media prioridad vieja
if availableStorage < minFreeSpace {
let oldMediumItems = cacheItems.filter {
$0.type.priority == .medium &&
$0.lastAccess.addingTimeInterval(7 * 24 * 3600) < now
}
for (key, item) in oldMediumItems {
removeCacheItem(key: key, type: item.type)
}
}
}
```
**Impacto:**
- Previene crashes por falta de espacio
- Respuesta automática a situaciones críticas
- App más robusta
---
## 5. Optimizaciones Generales
### 5.1 Reducción del Tamaño Final del App
**Estrategias implementadas:**
1. **Compresión de assets**
- Imágenes comprimidas con calidad adaptativa
- Eliminación de imágenes duplicadas
2. **Code optimization**
- Eliminación de código muerto
- Uso eficiente de librerías
3. **Stripping de símbolos**
- Configuración de release mode
- Optimización del compilador
**Estimación de reducción:**
- BEFORE: ~45 MB
- AFTER: ~30-35 MB
- **Reducción: 20-25%**
### 5.2 Optimización del Tiempo de Lanzamiento
**Estrategias:**
```swift
// BEFORE: Todo sincrónico en init
init() {
loadFavorites() // Bloquea
loadReadingProgress() // Bloquea
loadDownloaded() // Bloquea
}
// AFTER: Carga diferida y en background
init() {
// Solo carga esencial sincrónica
setupViews()
}
func loadContent() async {
// Todo lo demás en background
Task {
await loadFavorites()
await loadReadingProgress()
await loadDownloaded()
}
}
```
**Mejoras:**
- BEFORE: 2-3 segundos hasta UI usable
- AFTER: 0.5-1 segundo hasta UI usable
- **Mejora: 60-70%**
### 5.3 Reducción de Uso de Memoria en Background
**Estrategias:**
```swift
// BEFORE: No liberaba memoria en background
// AFTER: Limpieza agresiva al entrar en background
@objc private func handleBackgroundTransition() {
// Limpiar caches innecesarios
ImageCache.shared.clearAllCache()
// Guardar estado
saveCurrentState()
// Sugerir al sistema que puede liberar memoria
URLCache.shared.removeAllCachedResponses()
}
```
**Impacto:**
- App menos probable de ser matada por el sistema
- Reanudación más rápida
- Mejor experiencia general
---
## 📈 Métricas de Rendimiento
### Métricas Antes/Después
| Operación | Antes | Después | Mejora |
|-----------|-------|---------|--------|
| **Scraper** |
| Primer scraping | 5-8s | 3-5s | 40% |
| Scraping con cache | 5-8s | 0.1-0.5s | 90% |
| Memoria del scraper | 50-80 MB | 20-40 MB | 50% |
| **Storage** |
| Guardar imagen | 0.5-1s | 0.3-0.6s | 40% |
| Cargar imagen local | 0.2-0.5s | 0.05-0.1s | 80% |
| Espacio por capítulo | 15-25 MB | 8-15 MB | 40% |
| **Reader** |
| Cargar página (sin cache) | 2-4s | 1-2s | 50% |
| Cargar página (con cache) | 2-4s | 0.05-0.1s | 95% |
| Memoria en lectura | 150-300 MB | 50-100 MB | 60% |
| **General** |
| Tiempo de launch | 2-3s | 0.5-1s | 70% |
| Tamaño del app | ~45 MB | ~30-35 MB | 25% |
| Crashes por memoria | 2-3/semana | 0-1/semana | 80% |
### Impacto en Experiencia de Usuario
| Aspecto | Mejora Percibida |
|---------|-----------------|
| Velocidad de carga | ⭐⭐⭐⭐⭐ (5/5) |
| Fluidez de navegación | ⭐⭐⭐⭐⭐ (5/5) |
| Estabilidad | ⭐⭐⭐⭐⭐ (5/5) |
| Uso de espacio | ⭐⭐⭐⭐ (4/5) |
| Consumo de datos | ⭐⭐⭐⭐⭐ (5/5) |
---
## 🚀 Implementación y Testing
### Pasos para Implementación
1. **Reemplazar componentes originales:**
```bash
# Backup de archivos originales
mv ManhwaWebScraper.swift ManhwaWebScraper.swift.backup
mv StorageService.swift StorageService.swift.backup
mv ReaderView.swift ReaderView.swift.backup
# Usar versiones optimizadas
# (Ya sea reemplazando o usando imports condicionales)
```
2. **Configuración de Cache:**
```swift
// En AppDelegate o SceneDelegate
let cacheManager = CacheManager.shared
cacheManager.printCacheReport() // Debug inicial
```
3. **Testing:**
- Pruebas de carga con diferentes tamaños de capítulo
- Testing de memoria con Instruments
- Medición de tiempos de carga
- Verificación de funcionalidad de cache
### Tests Recomendados
```swift
// Test de cache efficiency
func testCacheHitRate() {
let report = CacheManager.shared.getCacheReport()
XCTAssertGreaterThan(report.itemsRemoved, 0)
}
// Test de memory management
func testMemoryUsageUnderLoad() {
// Monitorear memoria durante carga de 100 páginas
// Debe mantenerse bajo límite crítico
}
// Test de preloading
func testPreloadingEfficiency() {
// Verificar que páginas adyacentes se cargan
// antes de ser visibles
}
```
---
## 📝 Notas de Mantenimiento
### Monitoreo Continuo
```swift
// Imprimir reportes periódicamente
Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { _ in
CacheManager.shared.printCacheReport()
ImageCache.shared.printStatistics()
}
```
### Ajustes de Configuración
Según los patrones de uso reales, ajustar:
1. **Tamaños de cache:**
- `maxCacheSize` en CacheManager
- `memoryCacheLimit` en ImageCache
- `cacheValidDuration` en Scraper
2. **Thresholds de limpieza:**
- `maxCacheAge` según retención deseada
- `minFreeSpace` según dispositivo target
3. **Niveles de preloading:**
- Cantidad de páginas adyacentes
- Prioridades de descarga
---
## ✅ Checklist de Verificación
- [ ] WKWebView reutiliza correctamente
- [ ] Cache de HTML funciona con expiración
- [ ] JavaScript precompilado ejecuta correctamente
- [ ] Timeout adaptativo responde a condiciones de red
- [ ] Compresión de imágenes mantiene calidad aceptable
- [ ] Thumbnails se generan correctamente
- [ ] Lazy loading funciona en listas grandes
- [ ] Purga automática no elimina contenido reciente
- [ ] NSCache responde a memory warnings
- [ ] Preloading no afecta performance
- [ ] Debouncing de progreso funciona
- [ ] LRU elimina items correctos
- [ ] Emergency cleanup funciona cuando es necesario
- [ ] Métricas de rendimiento se mantienen positivas
---
## 🎓 Conclusión
Las optimizaciones implementadas mejoran significativamente el rendimiento y uso de memoria del MangaReader:
- **Rendimiento:** 50-90% de mejora en tiempos de carga
- **Memoria:** 50-65% de reducción en uso
- **Tamaño:** 20-25% de reducción en app final
- **Estabilidad:** 80% de reducción en crashes por memoria
- **Experiencia:** Calificación 4.5-5/5 en fluidez
Los archivos optimizados mantienen compatibilidad con el código existente mientras agregan capas de optimización inteligentes y automáticas.

View File

@@ -1,620 +0,0 @@
# MangaReader - Archivos de Optimización
## 📦 Contenido
Este directorio contiene las versiones optimizadas de los componentes principales de MangaReader, junto con documentación completa.
### 🔧 Archivos de Código (Source Files)
#### Servicios Optimizados
1. **`ManhwaWebScraperOptimized.swift`**
- Reemplazo de `ManhwaWebScraper.swift`
- Cache inteligente de HTML (30 min)
- JavaScript precompilado
- Timeout adaptativo (2-8s según red)
- Control de concurrencia (máx 2 scrapings)
- **Mejora**: 80-90% más rápido con cache, 40% en primera carga
2. **`StorageServiceOptimized.swift`**
- Reemplazo de `StorageService.swift`
- Compresión adaptativa de imágenes (0.6-0.9 según tamaño)
- Sistema automático de thumbnails (150x200)
- Lazy loading con paginación
- Purga automática de cache viejo (>30 días)
- **Mejora**: 40% menos espacio, 70% más rápido en startup
3. **`ImageCache.swift`**
- **NUEVO**: Sistema completo de cache de imágenes
- NSCache en memoria + cache en disco
- Preloading de páginas adyacentes
- Prioridades de carga (current, adjacent, prefetch)
- Compresión automática de imágenes >2048px
- **Mejora**: 80-90% hit rate, navegación instantánea
4. **`CacheManager.swift`**
- **NUEVO**: Gerente centralizado de cache
- Políticas LRU (Least Recently Used)
- Priorización por tipo (images > thumbnails > html > metadata)
- Análisis de patrones de uso
- Emergency cleanup para situaciones críticas
- **Mejora**: Control total de cache, decisiones inteligentes
#### Vistas Optimizadas
5. **`ReaderViewOptimized.swift`**
- Reemplazo de `ReaderView.swift`
- Integración con ImageCache
- Preloading automático de 4 páginas adyacentes
- Debouncing de guardado de progreso (2s)
- Memory management para imágenes grandes
- **Mejora**: 60% menos memoria, 95% más rápido con cache
### 📚 Documentación (Documentation Files)
1. **`OPTIMIZATION_SUMMARY.md`**
- Documentación completa de todas las optimizaciones
- Explicación detallada de cada mejora
- Métricas antes/después
- Impacto en experiencia de usuario
- **Léelo primero para entender todo**
2. **`IMPLEMENTATION_GUIDE.md`**
- Guía paso a paso para implementación
- Configuración inicial
- Testing checklist
- Troubleshooting
- Tips y best practices
- **Usa esto como guía práctica**
3. **`BEFORE_AFTER_COMPARISON.md`**
- Comparaciones lado a lado de código
- ❌ BEFORE vs ✅ AFTER
- Explicación de problemas y soluciones
- Snippets de código comentados
- **Ideal para entender los cambios**
---
## 🚀 Comenzando (Quick Start)
### Paso 1: Backup
```bash
# Hacer backup de archivos originales
cd /home/ren/ios/MangaReader/ios-app/Sources
cp Services/ManhwaWebScraper.swift Services/ManhwaWebScraper.swift.backup
cp Services/StorageService.swift Services/StorageService.swift.backup
cp Views/ReaderView.swift Views/ReaderView.swift.backup
```
### Paso 2: Integración Gradual
#### Opción A: Usar alias de tipo (Recomendado para empezar)
```swift
// Agrega esto en tu código:
typealias Scraper = ManhwaWebScraperOptimized
typealias Storage = StorageServiceOptimized
// Tu código existente funciona sin cambios:
private let scraper = Scraper.shared
private let storage = Storage.shared
```
#### Opción B: Reemplazo directo (Para testing completo)
```swift
// En tus ViewModels, cambiar:
private let scraper = ManhwaWebScraperOptimized.shared
private let storage = StorageServiceOptimized.shared
// En tu ReaderView, usar:
ReaderViewOptimized(manga: manga, chapter: chapter)
```
### Paso 3: Inicializar CacheManager
```swift
// En AppDelegate.swift
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions ...
) -> Bool {
// Inicializar cache manager
_ = CacheManager.shared
#if DEBUG
// Verificar configuración en debug
CacheManager.shared.printCacheReport()
#endif
return true
}
}
```
---
## 📊 Métricas de Mejora
### Rendimiento
| Operación | Antes | Después | Mejora |
|-----------|-------|---------|--------|
| Primer scraping | 5-8s | 3-5s | **40%** |
| Scraping con cache | 5-8s | 0.1-0.5s | **90%** |
| Cargar página (sin cache) | 2-4s | 1-2s | **50%** |
| Cargar página (con cache) | 2-4s | 0.05-0.1s | **95%** |
| Inicio de app | 2-3s | 0.5-1s | **70%** |
### Memoria y Espacio
| Recurso | Antes | Después | Mejora |
|---------|-------|---------|--------|
| Memoria en lectura | 150-300 MB | 50-100 MB | **60%** |
| Espacio por capítulo | 15-25 MB | 8-15 MB | **40%** |
| Tamaño del app | ~45 MB | ~30-35 MB | **25%** |
### Estabilidad
| Métrica | Antes | Después | Mejora |
|---------|-------|---------|--------|
| Crashes por memoria | 2-3/semana | 0-1/semana | **80%** |
| Hit rate de cache | N/A | 85-95% | **Nuevo** |
| Preloading de páginas | No | 4 páginas | **Nuevo** |
---
## 🧪 Testing
### Tests Funcionales
```bash
# Ejecutar tests
xcodebuild test -scheme MangaReader -destination 'platform=iOS Simulator,name=iPhone 14'
# Con cobertura
xcodebuild test -scheme MangaReader -enableCodeCoverage YES
```
### Tests de Memoria
```bash
# Usar Instruments
1. Xcode → Product → Profile (⌘I)
2. Seleccionar "Allocations"
3. Monitorear:
- Overall memory usage
- Anonymous VM
- Image cache size
```
### Tests de Rendimiento
```bash
# Time Profiler
1. Product → Profile
2. Seleccionar "Time Profiler"
3. Buscar funciones lentas
4. Verificar que cache esté funcionando
```
---
## 📖 Estructura de Archivos
```
MangaReader/
├── ios-app/Sources/
│ ├── Services/
│ │ ├── ManhwaWebScraper.swift [ORIGINAL]
│ │ ├── ManhwaWebScraperOptimized.swift ✨ [OPTIMIZADO]
│ │ ├── StorageService.swift [ORIGINAL]
│ │ ├── StorageServiceOptimized.swift ✨ [OPTIMIZADO]
│ │ ├── ImageCache.swift ✨ [NUEVO]
│ │ └── CacheManager.swift ✨ [NUEVO]
│ └── Views/
│ ├── ReaderView.swift [ORIGINAL]
│ └── ReaderViewOptimized.swift ✨ [OPTIMIZADO]
├── OPTIMIZATION_SUMMARY.md 📖 [DOCUMENTACIÓN]
├── IMPLEMENTATION_GUIDE.md 📖 [GUÍA PRÁCTICA]
├── BEFORE_AFTER_COMPARISON.md 📖 [COMPARACIONES]
└── README_OPTIMIZATIONS.md 📖 [ESTE ARCHIVO]
```
---
## 🎯 Características Principales
### 1. Scraper Optimizado
**Cache Inteligente**
- HTML cacheado por 30 minutos
- Reducción de 80-90% en requests
**JavaScript Precompilado**
- Scripts precompilados en enum
- 10-15% más rápido en ejecución
**Timeout Adaptativo**
- Se ajusta a condiciones de red (2-8s)
- Aprende del historial de rendimiento
**Control de Concurrencia**
- Máximo 2 scrapings simultáneos
- Previene crashes por sobrecarga
### 2. Storage Optimizado
**Compresión Adaptativa**
- Calidad: 0.9 (pequeñas), 0.75 (medianas), 0.6 (grandes)
- 30-40% menos espacio
**Sistema de Thumbnails**
- Generación automática (150x200)
- Navegación 10x más rápida
**Lazy Loading**
- Paginación de capítulos
- Carga solo lo necesario
**Purga Automática**
- Limpieza cada 24 horas
- Elimina archivos >30 días
### 3. Reader Optimizado
**Image Caching**
- NSCache en memoria
- Cache en disco para persistencia
- 85-95% hit rate
**Preloading Inteligente**
- Precarga 2 páginas antes y después
- Navegación instantánea en 80% de casos
**Memory Management**
- Optimiza imágenes >2048px
- Respuesta a memory warnings
- 60% menos memoria
**Debouncing**
- Guarda progreso cada 2s de inactividad
- 95% menos escrituras a disco
### 4. Cache Manager
**Políticas LRU**
- Elimina menos usados primero
- Preserva contenido importante
**Priorización**
- Images > Thumbnails > HTML > Metadata
- Limpieza graduada
**Emergency Cleanup**
- Respuesta automática a baja memoria
- Previene crashes
---
## ⚙️ Configuración
### Ajustes de Cache
```swift
// En CacheManager.swift
struct CacheLimits {
static let maxCacheSizePercentage: Double = 0.15 // 15% del almacenamiento
static let minFreeSpace: Int64 = 500 * 1024 * 1024 // 500 MB mínimo libre
static let maxAge: TimeInterval = 30 * 24 * 3600 // 30 días
static let maxItemCount: Int = 1000 // Máximo items
}
```
### Ajustes de Imagen
```swift
// En ImageCache.swift
private let maxImageDimension: CGFloat = 2048 // Máximo 2048x2048
private let diskCacheLimit: Int64 = 500 * 1024 * 1024 // 500 MB cache
```
### Ajustes de Preloading
```swift
// En ReaderViewOptimized.swift
@Published var enablePreloading = true // Habilitar preloading
private let preloadRange = 2 // 2 páginas antes/después
```
---
## 🔍 Debugging
### Ver Estadísticas
```swift
#if DEBUG
// En tu vista de debug
Button("Print Cache Report") {
CacheManager.shared.printCacheReport()
}
Button("Print Image Stats") {
ImageCache.shared.printStatistics()
}
Button("Simulate Memory Warning") {
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
#endif
```
### Logs Importantes
Busca estos logs en console:
```
✅ Cache HIT → Cache funcionando
❌ Cache MISS - Scraping → Scrapeando (normal la primera vez)
🗑️ Removed old file → Limpieza automática
⚠️ Memory warning received → Memory warning (respuesta automática)
📥 Loaded image in 0.23s → Tiempo de carga de imagen
```
---
## 🐛 Troubleshooting
### Problema: Cache no funciona
**Solución:**
```swift
// Verificar que uses el singleton
let scraper = ManhwaWebScraperOptimized.shared // Correcto
let scraper = ManhwaWebScraperOptimized() // Incorrecto
```
### Problema: Memoria sigue alta
**Solución:**
```swift
// 1. Verificar weak references
Task { [weak self] in // Correcto
await self?.loadData()
}
// 2. Verificar deinit
deinit {
progressSaveTimer?.invalidate()
NotificationCenter.default.removeObserver(self)
}
```
### Problema: Preloading no funciona
**Solución:**
```swift
// Habilitar preloading
viewModel.enablePreloading = true //
// Verificar onAppear
.onAppear {
viewModel.preloadAdjacentPages(...)
}
```
---
## 📈 Monitoreo Continuo
### Métricas Clave
Monitorea regularmente:
1. **Cache Hit Rate**
```swift
let stats = ImageCache.shared.getCacheStatistics()
print("Hit rate: \(stats.hitRate * 100)%")
```
Objetivo: >80%
2. **Tamaño de Cache**
```swift
let size = CacheManager.shared.getCurrentCacheSize()
print("Cache size: \(formatBytes(size))")
```
Objetivo: <500 MB
3. **Uso de Memoria**
```bash
# Usar Instruments
# Objetivo: <100 MB en lectura
```
4. **Tiempo de Carga**
```swift
let start = Date()
await loadPages()
let time = Date().timeIntervalSince(start)
print("Load time: \(time)s")
```
Objetivo: <2s
---
## 🎓 Recursos de Aprendizaje
### Documentación
1. **`OPTIMIZATION_SUMMARY.md`**
- Lee primero para overview completo
- Detalle técnico de cada optimización
2. **`BEFORE_AFTER_COMPARISON.md`**
- Compara código lado a lado
- Entiende qué cambió y por qué
3. **`IMPLEMENTATION_GUIDE.md`**
- Sigue esto para implementar
- Incluye tests y troubleshooting
### Apple Documentation
- [NSCache](https://developer.apple.com/documentation/foundation/nscache)
- [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview)
- [Memory Management](https://developer.apple.com/documentation/swift/memory_safety)
- [Instruments](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/)
---
## ✅ Checklist de Verificación
Antes de considerar la implementación completa:
### Funcionalidad
- [ ] WKWebView se reutiliza correctamente
- [ ] Cache de HTML funciona con expiración
- [ ] JavaScript precompilado ejecuta correctamente
- [ ] Timeout adaptativo responde a condiciones de red
- [ ] Compresión de imágenes mantiene calidad aceptable
- [ ] Thumbnails se generan correctamente
- [ ] Lazy loading funciona en listas grandes
- [ ] Purga automática no elimina contenido reciente
- [ ] NSCache responde a memory warnings
- [ ] Preloading no afecta performance
- [ ] Debouncing de progreso funciona
- [ ] LRU elimina items correctos
### Rendimiento
- [ ] Tiempo de carga < 2s para capítulos
- [ ] Hit rate de cache > 80%
- [ ] Memoria en lectura < 100 MB
- [ ] App launch < 1s
- [ ] Tamaño de cache < 500 MB
### Estabilidad
- [ ] Sin crashes por memoria
- [ ] Sin memory leaks
- [ ] Respuesta correcta a warnings
- [ ] Cleanup automático funciona
---
## 🚀 Próximos Pasos
### Implementación Inmediata
1. ✅ Hacer backup del código existente
2. ✅ Reemplazar archivos uno por uno
3. ✅ Probar exhaustivamente
4. ✅ Monitorear métricas
### Optimizaciones Futuras (Opcionales)
- [ ] Prefetching predictivo con ML
- [ ] Compresión HEIC (50% más eficiente)
- [ ] Progressive image loading
- [ ] Background sync con BGTaskScheduler
---
## 💡 Tips y Best Practices
### DO ✅
```swift
// 1. Usar singletons para caches
static let shared = ImageCache()
// 2. Usar colas para I/O
private let ioQueue = DispatchQueue(label: "...", qos: .utility)
// 3. Weak references en closures
Task { [weak self] in
await self?.loadData()
}
// 4. Responder a memory warnings
@objc func handleMemoryWarning() {
cache.removeAllObjects()
}
// 5. Debouncing de operaciones frecuentes
func saveDebounced() {
timer?.invalidate()
timer = Timer.scheduledTimer(...)
}
```
### DON'T ❌
```swift
// 1. NO crear múltiples instancias
// let cache1 = ImageCache()
// let cache2 = ImageCache() // ❌
// 2. NO hacer I/O en main thread
// let data = try Data(contentsOf: url) // ❌ Bloquea
// 3. NO olvidar weak en closures
// Task {
// self.doSomething() // ❌ Memory leak
// }
// 4. NO ignorar memory warnings
// @objc func handleMemoryWarning() {
// // ❌ No hacer nada
// }
// 5. NO guardar en cada cambio
// func onChange() {
// saveToDisk() // ❌ Demasiado frecuente
// }
```
---
## 📞 Soporte
Si encuentras problemas:
1. Revisa la documentación:
- `OPTIMIZATION_SUMMARY.md` para detalles técnicos
- `IMPLEMENTATION_GUIDE.md` para guía práctica
- `BEFORE_AFTER_COMPARISON.md` para comparaciones
2. Usa los tests de ejemplo
3. Habilita logging debug en development
4. Usa Instruments para perfilado
---
## 🎉 Conclusión
Estas optimizaciones mejoran significativamente MangaReader:
- **Rendimiento**: 50-90% de mejora en tiempos de carga
- **Memoria**: 50-65% de reducción en uso
- **Tamaño**: 20-25% de reducción en app final
- **Estabilidad**: 80% de reducción en crashes por memoria
- **Experiencia**: Calificación 4.5-5/5 en fluidez
Los archivos optimizados mantienen compatibilidad con el código existente mientras agregan capas de optimización inteligentes y automáticas.
---
**¡Happy Optimizing! 🚀**
Para cualquier pregunta o sugerencia, consulta los archivos de documentación incluidos.

View File

@@ -1,230 +0,0 @@
# MangaReader - Configuración del Servidor VPS
## 📋 Información del Servidor
**URL:** https://manga.cbcren.online
**Subdominio:** manga.cbcren.online (dedicado, sin interferencias)
**Protocolo:** HTTPS (SSL automático por Let's Encrypt)
**Puerto backend:** 3001 (interno, vía Docker proxy)
**Estado:** ✅ Activo y corriendo
## 🚀 Servicios Configurados
### 1. Backend API (Node.js/Express)
- **Puerto:** 3001
- **Servicio systemd:** `mangareader-backend.service`
- **Directorio:** `/home/ren/ios/MangaReader/backend`
- **Auto-reinicio:** ✅ Sí (systemd)
- **Acceso externo:** ✅ Sí (firewall ufw)
### 2. Proxy Reverse (Caddy en Docker)
- **Contenedor:** gitea-proxy
- **Imagen:** caddy:2
- **Rol:** Proxy HTTPS con certificado SSL automático
- **Backend interno:** 172.17.0.1:3001 (Docker bridge gateway)
- **SSL:** Let's Encrypt automático
- **No interfiere con:** Gitea, Nextcloud, DNS, Finanzas
### 3. DNS
- **Registro A:** manga.cbcren.online → 194.163.191.200
- **Propietario:** Usuario
- **Propósito:** Subdominio dedicado exclusivo para MangaReader
### 4. Storage
- **Directorio:** `/home/ren/ios/MangaReader/storage/`
- **Estructura:**
```
storage/
└── manga/
└── {mangaSlug}/
└── chapter_{chapterNumber}/
├── page_001.jpg
├── page_002.jpg
└── manifest.json
```
## 🌐 Endpoints API
### Manga Endpoints
- `GET /api/health` - Health check
- `GET /api/manga/:slug` - Info de manga
- `GET /api/manga/:slug/chapters` - Lista de capítulos
- `GET /api/chapter/:slug/images` - Imágenes de capítulo
- `GET /api/manga/:slug/full` - Info completa
### Storage Endpoints
- `POST /api/download` - Descargar capítulo a VPS
- `GET /api/storage/chapters/:mangaSlug` - Listar capítulos descargados
- `GET /api/storage/chapter/:mangaSlug/:chapterNumber` - Verificar capítulo
- `GET /api/storage/image/:mangaSlug/:chapterNumber/:pageIndex` - Obtener imagen
- `DELETE /api/storage/chapter/:mangaSlug/:chapterNumber` - Eliminar capítulo
- `GET /api/storage/stats` - Estadísticas de almacenamiento
## 🔧 Gestión del Servicio
### Verificar estado
```bash
sudo systemctl status mangareader-backend.service
```
### Iniciar servicio
```bash
sudo systemctl start mangareader-backend.service
```
### Detener servicio
```bash
sudo systemctl stop mangareader-backend.service
```
### Reiniciar servicio
```bash
sudo systemctl restart mangareader-backend.service
```
### Ver logs
```bash
sudo journalctl -u mangareader-backend.service -f
```
### Ver logs recientes
```bash
sudo journalctl -u mangareader-backend.service -n 50
```
## 📱 Configuración iOS App
### APIConfig.swift
```swift
static let serverURL = "https://manga.cbcren.online"
static let port: Int? = nil // Usa puerto estándar HTTPS (443)
```
### URL Base Completa
```
https://manga.cbcren.online
```
## 🧪 Tests
### Health Check
```bash
curl https://manga.cbcren.online/api/health
```
### Storage Stats
```bash
curl https://manga.cbcren.online/api/storage/stats
```
### Test Local (directo al backend)
```bash
curl http://localhost:3001/api/health
```
## 🔒 Seguridad
### SSL/TLS
- **Certificado:** Let's Encrypt (automático via Caddy)
- **Renovación:** Automática
- **Protocolo:** HTTPS/TLS 1.2+
- **Proxy:** Caddy maneja SSL termination
### Firewall
- Puerto 3001 abierto solo para acceso local (Docker)
- No requiere puerto abierto al público (Caddy maneja el proxy)
### Recomendaciones Futuras
1. ✅ SSL/HTTPS (implementado con Let's Encrypt)
2. Implementar autenticación JWT
3. Rate limiting
4. Validación de input
5. Sanitización de rutas de archivos
## 📊 Monitoreo
### Revisar uso de disco
```bash
du -sh /home/ren/ios/MangaReader/storage/
```
### Listar capítulos descargados
```bash
find /home/ren/ios/MangaReader/storage/ -name "manifest.json"
```
### Ver tamaño por manga
```bash
du -sh /home/ren/ios/MangaReader/storage/manga/*/
```
## 🚨 Troubleshooting
### El servicio no inicia
```bash
# Ver logs de error
sudo journalctl -u mangareader-backend.service -n 100 --no-pager
# Verificar que node esté instalado
which node
node --version
# Verificar puerto disponible
sudo ss -tlnp | grep 3001
```
### No se puede acceder desde el exterior
```bash
# Verificar firewall
sudo ufw status
# Verificar que el servicio esté corriendo
sudo systemctl status mangareader-backend.service
# Verificar puerto desde adentro
curl http://localhost:3001/api/health
```
### Puerto ya en uso
```bash
# Ver qué está usando el puerto
sudo ss -tlnp | grep 3001
# Matar proceso si es necesario
sudo kill -9 <PID>
```
## 📝 Notas Importantes
1. **Auto-inicio:** El servicio se inicia automáticamente al reiniciar el servidor
2. **Auto-reinicio:** Si el servicio falla, se reinicia automáticamente después de 10 segundos
3. **Subdominio dedicado:** manga.cbcren.online no interfiere con Gitea ni otros servicios
4. **HTTPS automático:** Caddy obtiene y renueva certificados SSL automáticamente
5. **Sin puertos públicos:** El puerto 3001 es interno, solo se expone vía proxy HTTPS
6. **Almacenamiento:** Usa /home/ren/ios/MangaReader/storage/ con espacio disponible (200GB)
## 🎯 Próximos Pasos
1. ✅ Backend configurado y corriendo
2. ✅ Subdominio dedicado configurado
3. ✅ SSL/HTTPS automático
4. ✅ Servicio systemd activo
5. ✅ Proxy Caddy configurado
6. ⏭️ Clonar repo en Mac
7. ⏭️ Compilar app iOS
8. ⏭️ Instalar en iPad/iPhone via Sideloadly/3uTools
9. ⏭️ Probar descarga de capítulos
10. ⏭️ Verificar lectura offline
## 📞 Soporte
Si tienes problemas:
1. Verificar los logs: `sudo journalctl -u mangareader-backend.service -f`
2. Verificar el estado: `sudo systemctl status mangareader-backend.service`
3. Verificar firewall: `sudo ufw status`
4. Verificar conectividad: `curl http://localhost:3001/api/health`
---
**Última actualización:** 2026-02-04 16:42 CET
**Versión:** 2.0.0
**Estado:** ✅ Producción (manga.cbcren.online con HTTPS)

View File

@@ -1,221 +0,0 @@
# VPS Backend Integration - iOS App Updates
## Overview
Successfully integrated VPS backend storage into the iOS MangaReader app, allowing users to download chapters to a remote VPS server and read them from there.
## Files Created
### 1. VPSAPIClient.swift
**Location:** `/home/ren/ios/MangaReader/ios-app/Sources/Services/VPSAPIClient.swift`
**Purpose:** Complete API client for communicating with the VPS backend server.
**Key Features:**
- Singleton pattern for shared instance
- Health check endpoint
- Download chapters to VPS storage
- Check chapter download status (manifest)
- List downloaded chapters
- Get image URLs from VPS
- Delete chapters from VPS
- Get storage statistics
- Progress tracking for downloads
- Comprehensive error handling
**Main Methods:**
```swift
// Download chapter to VPS
func downloadChapter(mangaSlug:chapterNumber:chapterSlug:imageUrls:) async throws -> VPSDownloadResult
// Check if chapter exists on VPS
func getChapterManifest(mangaSlug:chapterNumber:) async throws -> VPSChapterManifest?
// List all downloaded chapters for a manga
func listDownloadedChapters(mangaSlug:) async throws -> [VPSChapterInfo]
// Get URL for specific page image
func getImageURL(mangaSlug:chapterNumber:pageIndex:) -> String
// Delete chapter from VPS
func deleteChapter(mangaSlug:chapterNumber:) async throws -> Bool
// Get storage statistics
func getStorageStats() async throws -> VPSStorageStats
```
## Files Modified
### 2. MangaDetailView.swift
**Location:** `/home/ren/ios/MangaReader/ios-app/Sources/Views/MangaDetailView.swift`
**Changes Made:**
#### View Level Updates:
- Added `@StateObject private var vpsClient = VPSAPIClient.shared`
- Added VPS download button to toolbar (icloud.and.arrow.down icon)
- Added alert for VPS bulk download options
- Updated chapter list to pass VPS download callback
#### ChapterRowView Updates:
- Added VPS download button/status indicator (icloud.fill when downloaded, icloud.and.arrow.up to download)
- Added VPS download progress display
- Added `checkVPSStatus()` async function to check if chapter is on VPS
- Shows cloud icon when chapter is available on VPS
- Shows VPS download progress with percentage
#### ViewModel Updates:
**New Published Properties:**
```swift
@Published var showingVPSDownloadAll = false
```
**New Methods:**
```swift
// Download single chapter to VPS
func downloadChapterToVPS(_ chapter: Chapter) async
// Download all chapters to VPS
func downloadAllChaptersToVPS() async
// Download last N chapters to VPS
func downloadLastChaptersToVPS(count: Int) async
```
**Features:**
- Scrapes image URLs from original source
- Sends download request to VPS
- Shows success/failure notifications
- Tracks download progress
- Handles errors gracefully
### 3. ReaderView.swift
**Location:** `/home/ren/ios/MangaReader/ios-app/Sources/Views/ReaderView.swift`
**Changes Made:**
#### View Level Updates:
- Added `@ObservedObject private var vpsClient = VPSAPIClient.shared`
#### PageView Updates:
- Added VPS image loading capability
- Checks if chapter is available on VPS on load
- Loads images from VPS when available (priority order: local → VPS → original URL)
- Falls back to original URL if VPS fails
- Added `useVPS` state variable
#### ViewModel Updates:
**New Published Properties:**
```swift
@Published var isVPSDownloaded = false
```
**New Dependencies:**
```swift
private let vpsClient = VPSAPIClient.shared
```
**Updated loadPages() Method:**
Now checks sources in this priority order:
1. VPS storage (if available)
2. Local device storage
3. Scrape from original website
**Footer Updates:**
- Shows "VPS" label with cloud icon when reading from VPS
- Shows "Local" label with checkmark when reading from local storage
## User Experience Flow
### Downloading to VPS:
1. **Single Chapter:**
- User taps cloud upload icon (icloud.and.arrow.up) next to chapter
- App scrapes image URLs
- Sends download request to VPS
- Shows progress indicator (VPS XX%)
- Shows success notification
- Cloud icon changes to filled (icloud.fill)
2. **Multiple Chapters:**
- User taps cloud download button in toolbar
- Chooses "Últimos 10 a VPS" or "Todos a VPS"
- Downloads sequentially with progress tracking
- Shows summary notification
### Reading from VPS:
1. User opens chapter
2. App checks if chapter is on VPS
3. If available, loads images from VPS URLs
4. Shows "VPS" indicator in footer
5. Falls back to local or original if VPS fails
### Visual Indicators:
**Chapter List:**
- ✓ Green checkmark: Downloaded locally
- ☁️ Blue cloud: Available on VPS
- ☁️↑ Cloud upload: Download to VPS button
- Progress bar: Shows VPS download progress
**Reader View:**
- "VPS" label with cloud icon: Reading from VPS
- "Local" label with checkmark: Reading from local cache
## Error Handling
All VPS operations include comprehensive error handling:
- Network errors caught and displayed
- Timeout handling (5 min request, 10 min resource)
- Graceful fallback to alternative sources
- User-friendly error messages in Spanish
- Silent failures for non-critical operations
## Configuration
**Default VPS URL:** `http://localhost:3000/api`
To change the VPS URL, modify the `baseURL` in VPSAPIClient initialization or add a configuration method.
## API Endpoints Used
From the backend server (`/home/ren/ios/MangaReader/backend/server.js`):
- `POST /api/download` - Request chapter download
- `GET /api/storage/chapter/:mangaSlug/:chapterNumber` - Check chapter status
- `GET /api/storage/chapters/:mangaSlug` - List downloaded chapters
- `GET /api/storage/image/:mangaSlug/:chapterNumber/:pageIndex` - Get image
- `DELETE /api/storage/chapter/:mangaSlug/:chapterNumber` - Delete chapter
- `GET /api/storage/stats` - Get statistics
## Next Steps
To complete the integration:
1. **Update VPS URL:** Change `baseURL` in VPSAPIClient to your actual VPS address
2. **Test:** Run the app and test download/read functionality
3. **Optional Enhancements:**
- Add settings screen to configure VPS URL
- Add authentication token support
- Implement retry logic for failed downloads
- Add download queue management
- Show VPS storage usage in UI
## Benefits
✅ Saves local device storage
✅ Faster downloads from VPS vs original source
✅ Access chapters from multiple devices
✅ Offline reading capability (when cached from VPS)
✅ Centralized manga library management
✅ Progressive enhancement (works without VPS)
## Technical Highlights
- Async/await for all network operations
- Combine for reactive state management
- Priority-based image loading (local → VPS → original)
- Progress tracking for better UX
- Comprehensive error handling
- Clean separation of concerns
- Follows existing code patterns and conventions

View File

@@ -1,197 +0,0 @@
# Quick Start Guide: Integration Tests
## Prerequisites
```bash
# Install dependencies (if not already installed)
cd /home/ren/ios/MangaReader/backend
npm install
```
## Method 1: Using npm scripts (Recommended)
### Run individual tests:
```bash
# Terminal 1: Start server
npm start
# Terminal 2: Run VPS flow test
npm run test:vps
# Terminal 3: Run concurrent downloads test
npm run test:concurrent
```
### Clean up test data:
```bash
npm run test:clean
```
## Method 2: Using the test runner script
### Basic commands:
```bash
# Start server in background
./run-tests.sh start
# Check server status
./run-tests.sh status
# View server logs
./run-tests.sh logs
# Run VPS flow test
./run-tests.sh vps-flow
# Run concurrent downloads test
./run-tests.sh concurrent
# Run all tests
./run-tests.sh all
# Clean up test data
./run-tests.sh cleanup
# Stop server
./run-tests.sh stop
```
### Complete workflow (one command):
```bash
./run-tests.sh start && ./run-tests.sh all && ./run-tests.sh cleanup && ./run-tests.sh stop
```
## Method 3: Manual execution
```bash
# Terminal 1: Start server
node server.js
# Terminal 2: Run VPS flow test
node test-vps-flow.js
# Terminal 3: Run concurrent downloads test
node test-concurrent-downloads.js
```
## What Gets Tested
### VPS Flow Test (`test-vps-flow.js`)
- ✓ Server health check
- ✓ Chapter image scraping
- ✓ Download to VPS storage
- ✓ File verification
- ✓ Storage statistics
- ✓ Chapter deletion
- ✓ Complete cleanup
### Concurrent Downloads Test (`test-concurrent-downloads.js`)
- ✓ 5 chapters downloaded concurrently
- ✓ No race conditions
- ✓ No file corruption
- ✓ Independent manifests
- ✓ Concurrent deletion
- ✓ Thread-safe operations
## Expected Output
### Success:
```
✓ ALL TESTS PASSED
✓ No race conditions detected
✓ No file corruption found
✓ Storage handles concurrent access properly
```
### Test Results:
```
Total Tests: 11
Passed: 11
Failed: 0
```
## Troubleshooting
### Port already in use:
```bash
lsof -ti:3000 | xargs kill -9
```
### Server not responding:
```bash
# Check if server is running
./run-tests.sh status
# View logs
./run-tests.sh logs
```
### Clean everything and start fresh:
```bash
# Stop server
./run-tests.sh stop
# Clean test data
./run-tests.sh cleanup
# Remove logs
rm -rf logs/
# Start fresh
./run-tests.sh start
```
## Test Duration
- **VPS Flow Test**: ~2-3 minutes
- **Concurrent Test**: ~3-5 minutes
Total time: ~5-8 minutes for both tests
## Files Created
| File | Purpose |
|------|---------|
| `test-vps-flow.js` | End-to-end VPS flow tests |
| `test-concurrent-downloads.js` | Concurrent download tests |
| `run-tests.sh` | Test automation script |
| `TEST_README.md` | Detailed documentation |
| `TEST_QUICK_START.md` | This quick reference |
## Getting Help
```bash
# Show test runner help
./run-tests.sh help
# View detailed documentation
cat TEST_README.md
```
## Next Steps
After tests pass:
1. ✓ Verify storage directory structure
2. ✓ Check image quality in downloaded chapters
3. ✓ Monitor storage stats in production
4. ✓ Set up CI/CD integration (see TEST_README.md)
## Storage Location
Downloaded test chapters are stored in:
```
/home/ren/ios/MangaReader/storage/manga/one-piece_1695365223767/
├── chapter_787/
├── chapter_788/
├── chapter_789/
├── chapter_790/
└── chapter_791/
```
Each chapter contains:
- `page_001.jpg`, `page_002.jpg`, etc. - Downloaded images
- `manifest.json` - Chapter metadata and image list

View File

@@ -1,246 +0,0 @@
# Integration Tests for MangaReader VPS Backend
This directory contains comprehensive integration tests for the complete VPS flow: iOS app → VPS backend → Storage → Images served back.
## Test Files
### 1. `test-vps-flow.js`
Tests the complete end-to-end flow of downloading and serving manga chapters.
**Test Coverage:**
- Server health check
- Chapter image scraping from source
- Download to VPS storage
- Storage verification
- Image file validation
- Image path retrieval
- Chapter listing
- Storage statistics
- Chapter deletion
- Post-deletion verification
- Storage stats update verification
**Usage:**
```bash
# Make sure the server is running first
node server.js &
# In another terminal, run the test
node test-vps-flow.js
```
**Expected Output:**
- Color-coded test progress
- Detailed assertions with success/failure indicators
- Storage statistics
- Final summary with pass/fail counts
### 2. `test-concurrent-downloads.js`
Tests concurrent download operations to verify thread safety and data integrity.
**Test Coverage:**
- Pre-download cleanup
- Concurrent chapter downloads (5 chapters, max 3 concurrent)
- Post-download verification
- File integrity checks (no corruption, no missing files)
- Manifest independence verification
- Storage statistics accuracy
- Chapter listing functionality
- Concurrent deletion
- Complete cleanup verification
- Race condition detection
**Usage:**
```bash
# Make sure the server is running first
node server.js &
# In another terminal, run the test
node test-concurrent-downloads.js
```
**Expected Output:**
- Progress tracking for each operation
- Batch processing information
- Detailed integrity reports per chapter
- Summary of valid/missing/corrupted images
- Concurrent delete tracking
- Final summary with race condition analysis
## Test Configuration
Both tests use the following configuration:
```javascript
{
mangaSlug: 'one-piece_1695365223767',
chapters: [787, 788, 789, 790, 791], // For concurrent test
baseUrl: 'http://localhost:3000',
timeout: 120000-180000 // 2-3 minutes
}
```
You can modify these values in the test files if needed.
## Prerequisites
1. **Dependencies installed:**
```bash
npm install
```
2. **Server running on port 3000:**
```bash
node server.js
```
3. **Storage directory structure:**
The tests will automatically create the required storage structure:
```
/storage
/manga
/one-piece_1695365223767
/chapter_789
page_001.jpg
page_002.jpg
...
manifest.json
```
## Running All Tests
Run both test suites:
```bash
# Terminal 1: Start server
cd /home/ren/ios/MangaReader/backend
node server.js
# Terminal 2: Run VPS flow test
node test-vps-flow.js
# Terminal 3: Run concurrent downloads test
node test-concurrent-downloads.js
```
## Test Results
### Success Indicators
- ✓ Green checkmarks for passing assertions
- 🎉 "ALL TESTS PASSED!" message
- Exit code 0
### Failure Indicators
- ✗ Red X marks for failing assertions
- ❌ "SOME TESTS FAILED" message
- Detailed error messages
- Exit code 1
## Color Codes
The tests use color-coded output for easy reading:
- **Green**: Success/passing assertions
- **Red**: Errors/failing assertions
- **Blue**: Information messages
- **Cyan**: Test titles
- **Yellow**: Warnings
- **Magenta**: Operation tracking (concurrent tests)
## Cleanup
Tests automatically clean up after themselves:
- Delete test chapters from storage
- Remove temporary files
- Reset storage statistics
However, you can manually clean up:
```bash
# Remove all test data
rm -rf /home/ren/ios/MangaReader/storage/manga/one-piece_1695365223767
```
## Troubleshooting
### Server Not Responding
```
Error: Failed to fetch
```
**Solution:** Make sure the server is running on port 3000:
```bash
node server.js
```
### Chapter Already Exists
Tests will automatically clean up existing chapters. If you see warnings, that's normal behavior.
### Timeout Errors
If tests timeout, the scraper might be taking too long. You can:
1. Increase the timeout value in TEST_CONFIG
2. Check your internet connection
3. Verify the source website is accessible
### Port Already in Use
```
Error: listen EADDRINUSE: address already in use :::3000
```
**Solution:** Kill the existing process:
```bash
lsof -ti:3000 | xargs kill -9
```
## Test Coverage Summary
| Feature | VPS Flow Test | Concurrent Test |
|---------|---------------|-----------------|
| Server Health | ✓ | - |
| Image Scraping | ✓ | ✓ |
| Download to Storage | ✓ | ✓ (5 chapters) |
| File Verification | ✓ | ✓ |
| Manifest Validation | ✓ | ✓ |
| Storage Stats | ✓ | ✓ |
| Chapter Listing | ✓ | ✓ |
| Deletion | ✓ | ✓ (concurrent) |
| Race Conditions | - | ✓ |
| Corruption Detection | - | ✓ |
## Integration with CI/CD
These tests can be integrated into a CI/CD pipeline:
```yaml
# Example GitHub Actions workflow
- name: Start Server
run: node server.js &
- name: Wait for Server
run: sleep 5
- name: Run VPS Flow Tests
run: node test-vps-flow.js
- name: Run Concurrent Tests
run: node test-concurrent-downloads.js
```
## Performance Notes
- **VPS Flow Test**: ~2-3 minutes (downloads 5 images from 1 chapter)
- **Concurrent Test**: ~3-5 minutes (downloads 5 images from 5 chapters with max 3 concurrent)
Times vary based on:
- Network speed to source website
- VPS performance
- Current load on source website
## Contributing
When adding new features:
1. Add corresponding tests in `test-vps-flow.js`
2. If feature involves concurrent operations, add tests in `test-concurrent-downloads.js`
3. Update this README with new test coverage
4. Ensure all tests pass before submitting
## License
Same as the main MangaReader project.

View File

@@ -1,316 +0,0 @@
# Integration Tests: Creation Summary
## Overview
I have created comprehensive integration tests for the complete VPS flow: iOS app → VPS backend → Storage → Images served back.
## Files Created
### 1. `/home/ren/ios/MangaReader/backend/test-vps-flow.js`
**Purpose**: End-to-end integration test for the complete VPS download and serving flow
**Test Cases (11 tests)**:
- Server health check
- Get chapter images from scraper
- Download chapter to storage
- Verify chapter exists in storage
- Verify image files exist on disk
- Get image path from storage service
- List downloaded chapters
- Get storage statistics
- Delete chapter from storage
- Verify chapter was removed
- Verify storage stats updated after deletion
**Features**:
- Color-coded output for easy reading
- Detailed assertions with success/failure indicators
- Comprehensive error reporting
- Automatic cleanup
- Progress tracking
**Usage**:
```bash
npm run test:vps
# or
node test-vps-flow.js
```
### 2. `/home/ren/ios/MangaReader/backend/test-concurrent-downloads.js`
**Purpose**: Test concurrent download operations to verify thread safety and data integrity
**Test Cases (10 tests)**:
- Pre-download verification and cleanup
- Concurrent downloads (5 chapters, max 3 concurrent)
- Post-download verification
- Integrity check (no corruption, no missing files)
- Manifest independence verification
- Storage statistics accuracy
- Chapter listing functionality
- Concurrent deletion of all chapters
- Complete cleanup verification
- Race condition detection
**Features**:
- Progress tracker with operation IDs
- Batch processing (max 3 concurrent)
- Detailed integrity reports per chapter
- Corruption detection
- Missing file detection
- Concurrent operation tracking
- Race condition analysis
**Usage**:
```bash
npm run test:concurrent
# or
node test-concurrent-downloads.js
```
### 3. `/home/ren/ios/MangaReader/backend/run-tests.sh`
**Purpose**: Automation script for easy test execution and server management
**Commands**:
- `start` - Start server in background
- `stop` - Stop server
- `restart` - Restart server
- `logs` - Show server logs (tail -f)
- `status` - Check server status
- `vps-flow` - Run VPS flow test
- `concurrent` - Run concurrent downloads test
- `all` - Run all tests
- `cleanup` - Clean up test data
- `help` - Show help message
**Features**:
- Automatic server management
- PID tracking
- Log management
- Color-coded output
- Error handling
**Usage**:
```bash
./run-tests.sh start && ./run-tests.sh all && ./run-tests.sh cleanup && ./run-tests.sh stop
```
### 4. `/home/ren/ios/MangaReader/backend/TEST_README.md`
**Purpose**: Comprehensive documentation for integration tests
**Contents**:
- Detailed test descriptions
- Configuration options
- Prerequisites
- Usage examples
- Troubleshooting guide
- Test coverage table
- CI/CD integration examples
- Performance notes
### 5. `/home/ren/ios/MangaReader/backend/TEST_QUICK_START.md`
**Purpose**: Quick reference guide for running tests
**Contents**:
- Quick start instructions
- Multiple execution methods
- What gets tested
- Expected output
- Troubleshooting
- Test duration estimates
- Storage location info
### 6. Updated `/home/ren/ios/MangaReader/backend/package.json`
**Added npm scripts**:
- `test` - Run default tests
- `test:vps` - Run VPS flow test
- `test:concurrent` - Run concurrent downloads test
- `test:all` - Run all tests
- `test:clean` - Clean up test data
## Test Coverage Summary
| Feature | VPS Flow Test | Concurrent Test | Total Tests |
|---------|---------------|-----------------|-------------|
| Server Health | ✓ | - | 1 |
| Image Scraping | ✓ | ✓ | 2 |
| Download to Storage | ✓ | ✓ | 2 |
| File Verification | ✓ | ✓ | 2 |
| Manifest Validation | ✓ | ✓ | 2 |
| Storage Stats | ✓ | ✓ | 2 |
| Chapter Listing | ✓ | ✓ | 2 |
| Deletion | ✓ | ✓ | 2 |
| Cleanup | ✓ | ✓ | 2 |
| Race Conditions | - | ✓ | 1 |
| Corruption Detection | - | ✓ | 1 |
| **TOTAL** | **11** | **10** | **21** |
## Key Features Implemented
### 1. Comprehensive Logging
- Color-coded output (green for success, red for errors, blue for info)
- Detailed progress tracking
- Error messages with stack traces
- Operation tracking with IDs (for concurrent tests)
### 2. Robust Assertions
- Custom assertion functions with detailed messages
- Immediate feedback on failures
- Clear error context
### 3. Automatic Cleanup
- Tests clean up after themselves
- No residual test data
- Storage state restored
### 4. Progress Tracking
- Real-time operation status
- Duration tracking
- Batch processing information
- Summary statistics
### 5. Integrity Verification
- File existence checks
- Size validation
- Manifest validation
- Corruption detection
- Race condition detection
## Test Configuration
Both tests use these defaults (configurable in files):
```javascript
{
mangaSlug: 'one-piece_1695365223767',
chapters: [787, 788, 789, 790, 791], // Concurrent test only
baseUrl: 'http://localhost:3000',
timeout: 120000-180000 // 2-3 minutes
}
```
## Running the Tests
### Quick Start:
```bash
cd /home/ren/ios/MangaReader/backend
# Method 1: Using npm scripts
npm start # Terminal 1: Start server
npm run test:vps # Terminal 2: Run VPS flow test
npm run test:concurrent # Terminal 3: Run concurrent test
# Method 2: Using automation script
./run-tests.sh start
./run-tests.sh all
./run-tests.sh cleanup
./run-tests.sh stop
# Method 3: All in one
./run-tests.sh start && ./run-tests.sh all && ./run-tests.sh cleanup && ./run-tests.sh stop
```
## Expected Results
### Success Output:
```
============================================================
TEST RESULTS SUMMARY
============================================================
Total Tests: 11
Passed: 11
Failed: 0
======================================================================
🎉 ALL TESTS PASSED!
======================================================================
```
### Test Files Created During Execution:
```
/home/ren/ios/MangaReader/storage/manga/one-piece_1695365223767/
├── chapter_789/
│ ├── page_001.jpg
│ ├── page_002.jpg
│ ├── ...
│ └── manifest.json
```
## Assertions Included
Each test includes multiple assertions:
- **Equality checks** - Verify expected values match actual values
- **Truthy checks** - Verify conditions are met
- **File system checks** - Verify files and directories exist
- **Data validation** - Verify data integrity
- **Operation checks** - Verify operations complete successfully
## Error Handling
- Try-catch blocks around all operations
- Detailed error messages
- Stack traces for debugging
- Graceful failure handling
- Cleanup even on failure
## Performance Characteristics
- **VPS Flow Test**: Downloads 5 images (1 chapter) in ~2-3 minutes
- **Concurrent Test**: Downloads 25 images (5 chapters × 5 images) in ~3-5 minutes
- **Memory Usage**: Efficient concurrent processing with max 3 parallel downloads
- **Disk I/O**: Optimized for SSD/NVMe storage
## Next Steps
1. **Run the tests**:
```bash
cd /home/ren/ios/MangaReader/backend
./run-tests.sh all
```
2. **Verify results**: Check for green checkmarks and "ALL TESTS PASSED" message
3. **Review logs**: Check `logs/server.log` for any issues
4. **Inspect storage**: Verify downloaded images in storage directory
5. **Integrate into CI/CD**: Add to your CI/CD pipeline (see TEST_README.md)
## Maintenance
### Adding New Tests:
1. Create test function in appropriate test file
2. Add assertions using provided helper functions
3. Record test results
4. Update documentation
### Modifying Configuration:
- Edit `TEST_CONFIG` object in test files
- Update documentation if defaults change
### Extending Coverage:
- Add new test cases to existing suites
- Create new test files for new features
- Update TEST_README.md with coverage table
## Support
For issues or questions:
- Check TEST_README.md for detailed documentation
- Check TEST_QUICK_START.md for quick reference
- Review test output for specific error messages
- Check logs/server.log for server-side issues
## Summary
✅ Created 2 comprehensive test files with 21 total tests
✅ Created automation script for easy test execution
✅ Created detailed documentation (3 markdown files)
✅ Added npm scripts to package.json
✅ Implemented color-coded output and progress tracking
✅ Added comprehensive error handling and cleanup
✅ Verified thread safety and race condition detection
✅ Implemented integrity checks for file corruption
✅ Ready for CI/CD integration
All tests are production-ready and can be run immediately!

View File

@@ -1,992 +0,0 @@
# API Documentation - MangaReader
Este documento proporciona la documentación completa de la API de MangaReader, incluyendo modelos de datos, servicios, ViewModels y sus responsabilidades.
## Tabla de Contenidos
- [Modelos de Datos](#modelos-de-datos)
- [Servicios](#servicios)
- [ViewModels](#viewmodels)
- [Views](#views)
- [Errores y Manejo de Excepciones](#errores-y-manejo-de-excepciones)
---
## Modelos de Datos
Los modelos de datos se encuentran en `ios-app/Sources/Models/Manga.swift`. Son estructuras inmutables que conforman a protocolos Swift estándar para serialización e identificación.
### Manga
Representa la información completa de un manga.
```swift
struct Manga: Codable, Identifiable, Hashable {
let id: String { slug }
let slug: String
let title: String
let description: String
let genres: [String]
let status: String
let url: String
let coverImage: String?
enum CodingKeys: String, CodingKey {
case slug, title, description, genres, status, url, coverImage
}
var displayStatus: String { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `String` | Identificador único (computed, igual a `slug`) | `"one-piece_1695365223767"` |
| `slug` | `String` | Slug único del manga usado en URLs | `"one-piece_1695365223767"` |
| `title` | `String` | Título del manga | `"One Piece"` |
| `description` | `String` | Descripción o sinopsis | `"La historia de piratas..."` |
| `genres` | `[String]` | Array de géneros literarios | `["Acción", "Aventura"]` |
| `status` | `String` | Estado de publicación (crudo) | `"PUBLICANDOSE"` |
| `url` | `String` | URL completa del manga | `"https://manhwaweb.com/manga/..."` |
| `coverImage` | `String?` | URL de imagen de portada (opcional) | `"https://..."` |
#### Métodos Computados
**`displayStatus: String`**
- Retorna el estado traducido y formateado para mostrar en UI
- Mapeos:
- `"PUBLICANDOSE"``"En publicación"`
- `"FINALIZADO"``"Finalizado"`
- `"EN_PAUSA"`, `"EN_ESPERA"``"En pausa"`
- Otro → retorna valor original
#### Ejemplo de Uso
```swift
let manga = Manga(
slug: "one-piece_1695365223767",
title: "One Piece",
description: "La historia de Monkey D. Luffy...",
genres: ["Acción", "Aventura", "Comedia"],
status: "PUBLICANDOSE",
url: "https://manhwaweb.com/manga/one-piece_1695365223767",
coverImage: "https://example.com/cover.jpg"
)
print(manga.displayStatus) // "En publicación"
print(manga.id) // "one-piece_1695365223767"
```
---
### Chapter
Representa un capítulo individual de un manga.
```swift
struct Chapter: Codable, Identifiable, Hashable {
let id: Int { number }
let number: Int
let title: String
let url: String
let slug: String
var isRead: Bool = false
var isDownloaded: Bool = false
var lastReadPage: Int = 0
var displayNumber: String { ... }
var progress: Double { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `Int` | Identificador único (computed, igual a `number`) | `1` |
| `number` | `Int` | Número del capítulo | `1` |
| `title` | `String` | Título del capítulo | `"El inicio de la aventura"` |
| `url` | `String` | URL completa del capítulo | `"https://manhwaweb.com/leer/..."` |
| `slug` | `String` | Slug para identificar el capítulo | `"one-piece/capitulo-1"` |
| `isRead` | `Bool` | Estado de lectura (mutable) | `false` |
| `isDownloaded` | `Bool` | Estado de descarga (mutable) | `false` |
| `lastReadPage` | `Int` | Última página leída (mutable) | `5` |
#### Métodos Computados
**`displayNumber: String`**
- Retorna string formateado para mostrar
- Formato: `"Capítulo {number}"`
**`progress: Double`**
- Retorna progreso como `Double` para ProgressViews
- Valor: `Double(lastReadPage)`
#### Ejemplo de Uso
```swift
var chapter = Chapter(
number: 1,
title: "El inicio",
url: "https://manhwaweb.com/leer/one-piece/1",
slug: "one-piece/1"
)
chapter.isRead = true
chapter.lastReadPage = 15
print(chapter.displayNumber) // "Capítulo 1"
print(chapter.progress) // 15.0
```
---
### MangaPage
Representa una página individual (imagen) de un capítulo.
```swift
struct MangaPage: Codable, Identifiable, Hashable {
let id: String { url }
let url: String
let index: Int
var isCached: Bool = false
var thumbnailURL: String { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción | Ejemplo |
|-----------|------|-------------|---------|
| `id` | `String` | Identificador único (computed, igual a `url`) | `"https://..."` |
| `url` | `String` | URL completa de la imagen | `"https://example.com/page1.jpg"` |
| `index` | `Int` | Índice de la página en el capítulo | `0` |
| `isCached` | `Bool` | Estado de cache local (mutable) | `false` |
#### Métodos Computados
**`thumbnailURL: String`**
- Actualmente retorna la misma URL
- Futuro: implementar versión thumbnail optimizada
---
### ReadingProgress
Almacena el progreso de lectura de un usuario.
```swift
struct ReadingProgress: Codable {
let mangaSlug: String
let chapterNumber: Int
let pageNumber: Int
let timestamp: Date
var isCompleted: Bool { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `mangaSlug` | `String` | Slug del manga |
| `chapterNumber` | `Int` | Número del capítulo |
| `pageNumber` | `Int` | Página actual |
| `timestamp` | `Date` | Fecha/hora de lectura |
#### Métodos Computados
**`isCompleted: Bool`**
- Retorna `true` si el usuario leyó más de 5 páginas
- Lógica: `return pageNumber > 5`
---
### DownloadedChapter
Representa un capítulo descargado localmente.
```swift
struct DownloadedChapter: Codable, Identifiable {
let id: String { "\(mangaSlug)-chapter\(chapterNumber)" }
let mangaSlug: String
let mangaTitle: String
let chapterNumber: Int
let pages: [MangaPage]
let downloadedAt: Date
var totalSize: Int64 = 0
var displayTitle: String { ... }
}
```
#### Propiedades
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `id` | `String` | ID compuesto único |
| `mangaSlug` | `String` | Slug del manga |
| `mangaTitle` | `String` | Título del manga |
| `chapterNumber` | `Int` | Número del capítulo |
| `pages` | `[MangaPage]` | Array de páginas |
| `downloadedAt` | `Date` | Fecha de descarga |
| `totalSize` | `Int64` | Tamaño total en bytes |
---
## Servicios
Los servicios encapsulan la lógica de negocio y se encuentran en `ios-app/Sources/Services/`.
### ManhwaWebScraper
Servicio responsable del scraping de contenido web desde manhwaweb.com.
```swift
@MainActor
class ManhwaWebScraper: NSObject, ObservableObject
```
#### Propiedades
| Propiedad | Tipo | Acceso | Descripción |
|-----------|------|--------|-------------|
| `shared` | `ManhwaWebScraper` | `static` | Instancia singleton compartida |
| `webView` | `WKWebView?` | `private` | WebView para ejecutar JavaScript |
#### Métodos Públicos
##### `scrapeMangaInfo(mangaSlug:)`
Obtiene la información completa de un manga.
**Firma:**
```swift
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga
```
**Parámetros:**
- `mangaSlug`: `String` - Slug único del manga
**Retorna:**
- `Manga` - Objeto con información completa del manga
**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
- `ScrapingError.noContentFound`
**Ejemplo:**
```swift
do {
let manga = try await ManhwaWebScraper.shared.scrapeMangaInfo(
mangaSlug: "one-piece_1695365223767"
)
print("Manga: \(manga.title)")
} catch {
print("Error: \(error.localizedDescription)")
}
```
**Proceso Interno:**
1. Construye URL: `https://manhwaweb.com/manga/{slug}`
2. Carga URL en WKWebView
3. Espera 3 segundos a que JavaScript renderice
4. Ejecuta JavaScript para extraer:
- Título (de `<h1>` o `<title>`)
- Descripción (de `<p>` con >100 caracteres)
- Géneros (de links `/genero/*`)
- Estado (via regex)
- Cover image (de `.cover img`)
5. Parsea resultado a `Manga`
---
##### `scrapeChapters(mangaSlug:)`
Obtiene la lista de capítulos de un manga.
**Firma:**
```swift
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
```
**Parámetros:**
- `mangaSlug`: `String` - Slug del manga
**Retorna:**
- `[Chapter]` - Array de capítulos ordenados descendente
**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
- `ScrapingError.parsingError`
**Ejemplo:**
```swift
do {
let chapters = try await ManhwaWebScraper.shared.scrapeChapters(
mangaSlug: "one-piece_1695365223767"
)
print("Found \(chapters.count) chapters")
} catch {
print("Error: \(error.localizedDescription)")
}
```
**Proceso Interno:**
1. Carga página del manga
2. Ejecuta JavaScript que:
- Busca todos los links `/leer/*`
- Extrae número de capítulo via regex
- Filtra duplicados
- Ordena descendente
3. Parsea a array de `Chapter`
---
##### `scrapeChapterImages(chapterSlug:)`
Obtiene las URLs de las imágenes de un capítulo.
**Firma:**
```swift
func scrapeChapterImages(chapterSlug: String) async throws -> [String]
```
**Parámetros:**
- `chapterSlug`: `String` - Slug del capítulo
**Retorna:**
- `[String]` - Array de URLs de imágenes en orden
**Errors:**
- `ScrapingError.webViewNotInitialized`
- `ScrapingError.pageLoadFailed`
**Ejemplo:**
```swift
do {
let images = try await ManhwaWebScraper.shared.scrapeChapterImages(
chapterSlug: "one-piece/1"
)
print("Found \(images.count) pages")
for (index, imageUrl) in images.enumerated() {
print("Page \(index + 1): \(imageUrl)")
}
} catch {
print("Error: \(error.localizedDescription)")
}
```
**Proceso Interno:**
1. Carga URL del capítulo
2. Espera 5 segundos (para imágenes)
3. Ejecuta JavaScript que:
- Selecciona todas las etiquetas `<img>`
- Filtra elementos de UI (avatars, icons, logos)
- Elimina duplicados
4. Retorna array de URLs
---
#### Métodos Privados
##### `setupWebView()`
Configura el WKWebView con preferencias optimizadas.
##### `loadURLAndWait(_:waitForImages:)`
Carga una URL y espera a que JavaScript termine de renderizar.
**Parámetros:**
- `url`: `URL` - URL a cargar
- `waitForImages`: `Bool` - Si `true`, espera 5 segundos; si `false`, 3 segundos
---
### StorageService
Servicio responsable del almacenamiento local de datos.
```swift
class StorageService
```
#### Propiedades
| Propiedad | Tipo | Acceso | Descripción |
|-----------|------|--------|-------------|
| `shared` | `StorageService` | `static` | Instancia singleton compartida |
| `documentsDirectory` | `URL` | `private` | Directorio de documentos |
| `chaptersDirectory` | `URL` | `private` | Directorio de capítulos |
#### Gestión de Favoritos
##### `getFavorites()`
```swift
func getFavorites() -> [String]
```
Retorna array de slugs de mangas favoritos.
##### `saveFavorite(mangaSlug:)`
```swift
func saveFavorite(mangaSlug: String)
```
Guarda un manga como favorito (no duplica si ya existe).
##### `removeFavorite(mangaSlug:)`
```swift
func removeFavorite(mangaSlug: String)
```
Elimina un manga de favoritos.
##### `isFavorite(mangaSlug:)`
```swift
func isFavorite(mangaSlug: String) -> Bool
```
Verifica si un manga es favorito.
**Ejemplo:**
```swift
let storage = StorageService.shared
// Guardar favorito
storage.saveFavorite(mangaSlug: "one-piece_1695365223767")
// Verificar
if storage.isFavorite(mangaSlug: "one-piece_1695365223767") {
print("Es favorito")
}
// Listar todos
let favorites = storage.getFavorites()
print("Tienes \(favorites.count) favoritos")
// Eliminar
storage.removeFavorite(mangaSlug: "one-piece_1695365223767")
```
---
#### Gestión de Progreso de Lectura
##### `saveReadingProgress(_:)`
```swift
func saveReadingProgress(_ progress: ReadingProgress)
```
Guarda o actualiza el progreso de lectura.
##### `getReadingProgress(mangaSlug:chapterNumber:)`
```swift
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?
```
Retorna el progreso de un capítulo específico.
##### `getAllReadingProgress()`
```swift
func getAllReadingProgress() -> [ReadingProgress]
```
Retorna todo el progreso guardado.
##### `getLastReadChapter(mangaSlug:)`
```swift
func getLastReadChapter(mangaSlug: String) -> ReadingProgress?
```
Retorna el capítulo más reciente leído de un manga.
**Ejemplo:**
```swift
let storage = StorageService.shared
// Guardar progreso
let progress = ReadingProgress(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1,
pageNumber: 15,
timestamp: Date()
)
storage.saveReadingProgress(progress)
// Recuperar progreso
if let savedProgress = storage.getReadingProgress(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1
) {
print("Última página: \(savedProgress.pageNumber)")
}
// Último capítulo leído
if let lastChapter = storage.getLastReadChapter(mangaSlug: "one-piece_1695365223767") {
print("Último capítulo: \(lastChapter.chapterNumber)")
}
```
---
#### Gestión de Capítulos Descargados
##### `saveDownloadedChapter(_:)`
```swift
func saveDownloadedChapter(_ chapter: DownloadedChapter)
```
Guarda metadata de un capítulo descargado.
##### `getDownloadedChapters()`
```swift
func getDownloadedChapters() -> [DownloadedChapter]
```
Retorna todos los capítulos descargados.
##### `isChapterDownloaded(mangaSlug:chapterNumber:)`
```swift
func isChapterDownloaded(mangaSlug: String, chapterNumber: Int) -> Bool
```
Verifica si un capítulo está descargado.
##### `deleteDownloadedChapter(mangaSlug:chapterNumber:)`
```swift
func deleteDownloadedChapter(mangaSlug: String, chapterNumber: Int)
```
Elimina un capítulo descargado (archivos + metadata).
**Ejemplo:**
```swift
let storage = StorageService.shared
// Verificar si está descargado
if storage.isChapterDownloaded(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1
) {
print("Capítulo ya descargado")
} else {
print("Capítulo no descargado")
}
// Eliminar capítulo
storage.deleteDownloadedChapter(
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1
)
```
---
#### Gestión de Imágenes
##### `saveImage(_:mangaSlug:chapterNumber:pageIndex:)`
```swift
func saveImage(_ image: UIImage, mangaSlug: String, chapterNumber: Int, pageIndex: Int) async throws -> URL
```
Guarda una imagen en disco local.
**Parámetros:**
- `image`: `UIImage` - Imagen a guardar
- `mangaSlug`: `String` - Slug del manga
- `chapterNumber`: `Int` - Número del capítulo
- `pageIndex`: `Int` - Índice de la página
**Retorna:**
- `URL` - Ruta del archivo guardado
**Errors:**
- Error si no se puede convertir o guardar
**Ejemplo:**
```swift
let storage = StorageService.shared
do {
let imageURL = try await storage.saveImage(
image: myUIImage,
mangaSlug: "one-piece_1695365223767",
chapterNumber: 1,
pageIndex: 0
)
print("Imagen guardada en: \(imageURL.path)")
} catch {
print("Error guardando imagen: \(error)")
}
```
##### `loadImage(mangaSlug:chapterNumber:pageIndex:)`
```swift
func loadImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> UIImage?
```
Carga una imagen desde disco.
##### `getImageURL(mangaSlug:chapterNumber:pageIndex:)`
```swift
func getImageURL(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> URL?
```
Retorna la URL local de una imagen si existe.
---
#### Gestión de Almacenamiento
##### `getStorageSize()`
```swift
func getStorageSize() -> Int64
```
Retorna el tamaño total usado en bytes.
##### `clearAllDownloads()`
```swift
func clearAllDownloads()
```
Elimina todos los capítulos descargados.
##### `formatFileSize(_:)`
```swift
func formatFileSize(_ bytes: Int64) -> String
```
Formatea bytes a string legible (KB, MB, GB).
**Ejemplo:**
```swift
let storage = StorageService.shared
// Obtener tamaño usado
let size = storage.getStorageSize()
let formatted = storage.formatFileSize(size)
print("Almacenamiento usado: \(formatted)")
// Limpiar todo
storage.clearAllDownloads()
```
---
## ViewModels
Los ViewModels coordinan entre Services y Views.
### MangaListViewModel
ViewModel para la lista principal de mangas.
```swift
@MainActor
class MangaListViewModel: ObservableObject
```
#### Propiedades Publicadas
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `mangas` | `[Manga]` | Lista de mangas cargados |
| `isLoading` | `Bool` | Indicador de carga |
| `searchText` | `String` | Texto de búsqueda |
| `filter` | `MangaFilter` | Filtro actual (all/favorites/downloaded) |
| `newMangaSlug` | `String` | Slug del nuevo manga a agregar |
#### Métodos Públicos
##### `loadMangas()`
```swift
func loadMangas() async
```
Carga los mangas guardados en favoritos.
##### `addManga(_:)`
```swift
func addManga(_ slug: String) async
```
Agrega un nuevo manga mediante scraping.
#### Propiedades Computadas
##### `filteredMangas`
```swift
var filteredMangas: [Manga]
```
Retorna mangas filtrados por búsqueda y categoría.
**Ejemplo:**
```swift
@StateObject private var viewModel = MangaListViewModel()
// Cargar mangas
await viewModel.loadMangas()
// Agregar manga
await viewModel.addManga("one-piece_1695365223767")
// Filtrar
viewModel.filter = .favorites
viewModel.searchText = "one"
```
---
### MangaDetailViewModel
ViewModel para el detalle de un manga.
```swift
@MainActor
class MangaDetailViewModel: ObservableObject
```
#### Propiedades Publicadas
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `chapters` | `[Chapter]` | Lista de capítulos |
| `isLoadingChapters` | `Bool` | Indicador de carga |
| `isFavorite` | `Bool` | Estado de favorito |
| `selectedChapter` | `Chapter?` | Capítulo seleccionado |
| `showingDownloadAll` | `Bool` | Mostrar diálogo de descarga |
#### Métodos Públicos
##### `loadChapters()`
```swift
func loadChapters() async
```
Carga los capítulos del manga.
##### `toggleFavorite()`
```swift
func toggleFavorite()
```
Alterna el estado de favorito.
##### `downloadAllChapters()`
```swift
func downloadAllChapters()
```
Inicia descarga de todos los capítulos.
##### `downloadLastChapters(count:)`
```swift
func downloadLastChapters(count: Int)
```
Descarga los últimos N capítulos.
**Ejemplo:**
```swift
@StateObject private var viewModel = MangaDetailViewModel(manga: manga)
// Cargar capítulos
await viewModel.loadChapters()
// Marcar favorito
viewModel.toggleFavorite()
// Descargar últimos 10
viewModel.downloadLastChapters(count: 10)
```
---
### ReaderViewModel
ViewModel para el lector de capítulos.
```swift
@MainActor
class ReaderViewModel: ObservableObject
```
#### Propiedades Publicadas
| Propiedad | Tipo | Descripción |
|-----------|------|-------------|
| `pages` | `[MangaPage]` | Lista de páginas |
| `currentPage` | `Int` | Página actual |
| `isLoading` | `Bool` | Indicador de carga |
| `showError` | `Bool` | Mostrar error |
| `showControls` | `Bool` | Mostrar controles UI |
| `isFavorite` | `Bool` | Estado de favorito |
| `isDownloaded` | `Bool` | Capítulo descargado |
| `backgroundColor` | `Color` | Color de fondo |
| `readingMode` | `ReadingMode` | Modo de lectura |
#### Métodos Públicos
##### `loadPages()`
```swift
func loadPages() async
```
Carga las páginas del capítulo (desde local o web).
##### `cachePage(_:image:)`
```swift
func cachePage(_ page: MangaPage, image: Image) async
```
Cachea una página localmente (TODO: implementar).
##### `toggleFavorite()`
```swift
func toggleFavorite()
```
Alterna favorito del manga actual.
##### `cycleBackgroundColor()`
```swift
func cycleBackgroundColor()
```
Cicla entre colores de fondo (blanco/negro/sepia).
#### Propiedades Computadas
- `currentPageIndex`: `Int` - Índice de página actual
- `totalPages`: `Int` - Total de páginas
**Ejemplo:**
```swift
@StateObject private var viewModel = ReaderViewModel(manga: manga, chapter: chapter)
// Cargar páginas
await viewModel.loadPages()
// Ir a página específica
viewModel.currentPage = 10
// Cambiar fondo
viewModel.cycleBackgroundColor()
// Cambiar modo lectura
viewModel.readingMode = .horizontal
```
---
## Views
Las vistas son componentes SwiftUI que presentan la UI.
### ContentView
Vista principal que muestra la lista de mangas.
**Componentes:**
- `MangaRowView`: Fila individual de manga
- `MangaListViewModel`: ViewModel asociado
**Funcionalidades:**
- Búsqueda de mangas
- Filtros (todos/favoritos/descargados)
- Agregar manga manualmente
- Pull-to-refresh
### MangaDetailView
Vista de detalle de un manga específico.
**Componentes:**
- `ChapterRowView`: Fila de capítulo
- `FlowLayout`: Layout de géneros
- `MangaDetailViewModel`: ViewModel asociado
**Funcionalidades:**
- Mostrar información del manga
- Listar capítulos
- Marcar favorito
- Descargar capítulos
### ReaderView
Vista de lectura de capítulos.
**Componentes:**
- `PageView`: Vista de página individual
- `ReaderViewModel`: ViewModel asociado
**Funcionalidades:**
- Mostrar páginas con zoom/pan
- Navegación entre páginas
- Configurar fondo
- Cambiar modo de lectura
- Slider de navegación
---
## Errores y Manejo de Excepciones
### ScrapingError
Errores específicos del scraper.
```swift
enum ScrapingError: LocalizedError {
case webViewNotInitialized
case pageLoadFailed
case noContentFound
case parsingError
var errorDescription: String? { ... }
}
```
#### Casos
| Error | Descripción | Mensaje (Español) |
|-------|-------------|-------------------|
| `webViewNotInitialized` | WKWebView no configurado | "WebView no está inicializado" |
| `pageLoadFailed` | Error cargando página | "Error al cargar la página" |
| `noContentFound` | No se encontró contenido | "No se encontró contenido" |
| `parsingError` | Error procesando datos | "Error al procesar el contenido" |
### Estrategias de Manejo de Errores
1. **ViewModels**: Capturan errores del scraper
2. **Views**: Muestran alertas al usuario
3. **Servicios**: Propagan errores con `throws`
**Ejemplo:**
```swift
do {
let manga = try await scraper.scrapeMangaInfo(mangaSlug: slug)
// Manejar éxito
} catch ScrapingError.webViewNotInitialized {
// Mostrar alerta específica
errorMessage = "Error de configuración"
} catch {
// Error genérico
errorMessage = error.localizedDescription
}
```
---
**Última actualización**: Febrero 2026
**Versión**: 1.0.0

View File

@@ -1,642 +0,0 @@
# Arquitectura de MangaReader
Este documento describe la arquitectura general del proyecto MangaReader, explicando cómo funcionan los componentes Backend y iOS App, y cómo fluyen los datos desde el scraping hasta el display en pantalla.
## Tabla de Contenidos
- [Visión General](#visión-general)
- [Arquitectura del Sistema](#arquitectura-del-sistema)
- [Arquitectura de la App iOS](#arquitectura-de-la-app-ios)
- [Flujo de Datos](#flujo-de-datos)
- [Patrones de Diseño](#patrones-de-diseño)
- [Diagramas de Secuencia](#diagramas-de-secuencia)
## Visión General
MangaReader es una aplicación nativa de iOS para leer manga sin publicidad. El proyecto consta de dos componentes opcionales:
1. **Backend (Opcional)**: Servidor Node.js con Express que realiza scraping usando Puppeteer
2. **iOS App**: Aplicación nativa SwiftUI que puede hacer scraping localmente usando WKWebView
```
┌─────────────────────────────────────────────────────────────────┐
│ MangaReader │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Backend │ │ iOS App │ │
│ │ (Opcional) │ │ (Principal) │ │
│ │ │ │ │ │
│ │ • Node.js │ │ • SwiftUI │ │
│ │ • Express │ │ • WKWebView │ │
│ │ • Puppeteer │ │ • Core Data │ │
│ └─────────────────┘ └─────────────────┘ │
│ ▲ │ │
│ │ │ │
│ └──────────────────────────────────┘ │
│ Scraping Independiente │
└─────────────────────────────────────────────────────────────────┘
```
## Arquitectura del Sistema
### Componentes Principales
#### 1. Backend (Opcional)
El backend es una API REST opcional que puede actuar como intermediario:
```
backend/
├── scraper.js # Scraper con Puppeteer
├── server.js # API REST con Express
└── package.json
```
**Responsabilidades:**
- Realizar scraping de manhwaweb.com
- Servir datos vía API REST
- Cachear respuestas para mejorar rendimiento
**API Endpoints:**
- `GET /api/health` - Health check
- `GET /api/manga/:slug` - Información de un manga
- `GET /api/manga/:slug/chapters` - Lista de capítulos
- `GET /api/chapter/:slug/images` - Imágenes de un capítulo
**Nota Importante**: El backend es completamente opcional. La app iOS está diseñada para funcionar de manera autónoma sin necesidad del backend.
#### 2. iOS App (Principal)
La aplicación iOS es el componente principal y puede operar independientemente:
```
ios-app/
├── MangaReaderApp.swift # Entry point
├── Info.plist
└── Sources/
├── Models/ # Modelos de datos
│ └── Manga.swift
├── Services/ # Lógica de negocio
│ ├── ManhwaWebScraper.swift
│ └── StorageService.swift
└── Views/ # UI SwiftUI
├── ContentView.swift
├── MangaDetailView.swift
└── ReaderView.swift
```
## Arquitectura de la App iOS
### MVVM Pattern (Model-View-ViewModel)
La app iOS sigue el patrón MVVM para separar la UI de la lógica de negocio:
```
┌─────────────────────────────────────────────────────────────────┐
│ MVVM Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────────────┐ ┌─────────┐ │
│ │ View │◄────────│ ViewModel │◄────────│ Model │ │
│ │(SwiftUI)│ │ (Observable) │ │(Struct) │ │
│ └─────────┘ └─────────────────┘ └─────────┘ │
│ ▲ │ │ │
│ │ │ │ │
│ └───────────────────────┴───────────────────────────┘ │
│ Data Binding & Commands │
└─────────────────────────────────────────────────────────────────┘
```
### Estructura de Componentes
#### 1. Models (Datos)
**Ubicación**: `ios-app/Sources/Models/Manga.swift`
Los modelos son estructuras inmutables que representan los datos:
```swift
- Manga: Información del manga (título, descripción, géneros, estado)
- Chapter: Capítulo individual (número, título, URL, estado de lectura)
- MangaPage: Página individual del capítulo (URL de imagen, índice)
- ReadingProgress: Progreso de lectura del usuario
- DownloadedChapter: Capítulo descargado localmente
```
**Características:**
- Inmutables (`struct`)
- Conformes a `Codable` para serialización
- Conformes a `Identifiable` para SwiftUI
- Conformes a `Hashable` para comparaciones
#### 2. Services (Lógica de Negocio)
**Ubicación**: `ios-app/Sources/Services/`
##### ManhwaWebScraper.swift
Responsable del scraping de contenido web:
```swift
class ManhwaWebScraper: NSObject, ObservableObject {
// Singleton instance
static let shared = ManhwaWebScraper()
// Funciones principales:
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
func scrapeChapterImages(chapterSlug: String) async throws -> [String]
}
```
**Características:**
- Usa `WKWebView` para ejecutar JavaScript
- Implementa `async/await` para operaciones asíncronas
- Patrón Singleton para compartir instancia
- Manejo de errores con `ScrapingError`
##### StorageService.swift
Responsable del almacenamiento local:
```swift
class StorageService {
// Singleton instance
static let shared = StorageService()
// Gestión de favoritos:
func getFavorites() -> [String]
func saveFavorite(mangaSlug: String)
func removeFavorite(mangaSlug: String)
// Gestión de progreso:
func saveReadingProgress(_ progress: ReadingProgress)
func getReadingProgress(mangaSlug: String, chapterNumber: Int) -> ReadingProgress?
// Gestión de descargas:
func saveDownloadedChapter(_ chapter: DownloadedChapter)
func getDownloadedChapters() -> [DownloadedChapter]
// Gestión de imágenes:
func saveImage(_ image: UIImage, ...) async throws -> URL
func loadImage(...) -> UIImage?
}
```
**Características:**
- Almacena favoritos en `UserDefaults`
- Almacena progreso en `UserDefaults`
- Guarda imágenes en el sistema de archivos
- Usa `FileManager` para gestión de archivos
#### 3. ViewModels (Presentación)
**Ubicación**: Integrados en los archivos de Views
Los ViewModels coordinan entre Services y Views:
```swift
@MainActor
class MangaListViewModel: ObservableObject {
@Published var mangas: [Manga] = []
@Published var isLoading = false
private let scraper = ManhwaWebScraper.shared
private let storage = StorageService.shared
func loadMangas() async
func addManga(_ slug: String) async
}
```
**Responsabilidades:**
- Mantener estado de la UI
- Transformar datos para presentación
- Manejar lógica de navegación
- Coordinar llamadas a servicios
#### 4. Views (UI)
**Ubicación**: `ios-app/Sources/Views/`
##### ContentView.swift
- Vista principal de la app
- Lista de mangas con filtros
- Búsqueda y añadir manga
##### MangaDetailView.swift
- Detalle de un manga específico
- Lista de capítulos
- Descarga de capítulos
##### ReaderView.swift
- Lector de imágenes
- Gestos de zoom y pan
- Configuración de lectura
## Flujo de Datos
### 1. Flujo de Scraping de Manga
```
Usuario ingresa slug
┌───────────────────────────────────────────────────────────────┐
│ ContentView -> MangaListViewModel.addManga(slug) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ManhwaWebScraper.scrapeMangaInfo(mangaSlug) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ WKWebView carga URL de manhwaweb.com │
│ https://manhwaweb.com/manga/{slug} │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ JavaScript ejecutado en WKWebView: │
│ - Extrae título, descripción, géneros │
│ - Extrae estado, imagen de portada │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Datos parseados a struct Manga │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ViewModel actualiza @Published var mangas │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ SwiftUI detecta cambio y re-renderiza UI │
└───────────────────────────────────────────────────────────────┘
```
### 2. Flujo de Lectura de Capítulo
```
Usuario selecciona capítulo
┌───────────────────────────────────────────────────────────────┐
│ MangaDetailView -> ReaderView(manga, chapter) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ReaderViewModel.loadPages() │
└───────────────────────────────────────────────────────────────┘
├──► ¿Capítulo descargado?
│ │
│ SÍ │ NO
│ ▼
│ ┌─────────────────────────────────────────────────┐
│ │ StorageService.getDownloadedChapter() │
│ │ Cargar páginas locales │
│ └─────────────────────────────────────────────────┘
│ │
│ └──────────────────┐
│ │
└─────────────────────────────┼──────┐
│ │
▼ ▼
┌───────────────────────────────────────────────────────────────┐
│ ManhwaWebScraper.scrapeChapterImages(chapterSlug) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ WKWebView carga URL del capítulo │
│ https://manhwaweb.com/leer/{slug} │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ JavaScript extrae URLs de imágenes: │
│ - Selecciona todas las etiquetas <img> │
│ - Filtra elementos de UI (avatars, icons) │
│ - Elimina duplicados │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Array de strings con URLs de imágenes │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Convertir a [MangaPage] y mostrar en ReaderView │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ReaderView muestra imágenes con AsyncImage │
│ Cache automático de imágenes en visualización │
└───────────────────────────────────────────────────────────────┘
```
### 3. Flujo de Guardado de Progreso
```
Usuario navega a página X
┌───────────────────────────────────────────────────────────────┐
│ ReaderViewModel.currentPage cambia a X │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ ReaderViewModel.saveProgress() │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ Crear ReadingProgress(mangaSlug, chapterNumber, pageNumber) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ StorageService.saveReadingProgress(progress) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ JSONEncoder codifica a Data │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ UserDefaults.set(data, forKey: "readingProgress") │
└───────────────────────────────────────────────────────────────┘
```
## Patrones de Diseño
### 1. Singleton Pattern
**Uso**: Services compartidos
```swift
class StorageService {
static let shared = StorageService()
private init() { ... }
}
class ManhwaWebScraper {
static let shared = ManhwaWebScraper()
private init() { ... }
}
```
**Beneficios:**
- Unica instancia compartida en toda la app
- Reduce consumo de memoria
- Facilita acceso desde cualquier View/ViewModel
### 2. MVVM (Model-View-ViewModel)
**Uso**: Arquitectura general de la app
**Separación de responsabilidades:**
- **Model**: Datos puras (`struct`, `Codable`)
- **View**: UI pura (`SwiftUI`, reactive)
- **ViewModel**: Lógica de presentación (`ObservableObject`)
**Beneficios:**
- Testabilidad de ViewModels sin UI
- Reutilización de ViewModels
- Separación clara de concerns
### 3. Repository Pattern
**Uso**: Abstracción de fuentes de datos
```swift
class StorageService {
// Abstrae UserDefaults, FileManager, etc.
func getFavorites() -> [String]
func saveFavorite(mangaSlug: String)
}
```
**Beneficios:**
- Interfaz unificada para diferentes storage
- Fácil cambiar implementación
- Centraliza lógica de persistencia
### 4. Async/Await Pattern
**Uso**: Operaciones de scraping
```swift
func scrapeMangaInfo(mangaSlug: String) async throws -> Manga {
// Operación asíncrona
try await loadURLAndWait(url)
let info = try await webView.evaluateJavaScript(...)
return Manga(...)
}
```
**Beneficios:**
- Código asíncrono legible
- Manejo de errores claro
- No bloquea el hilo principal
### 5. Observable Object Pattern
**Uso**: Reactividad en SwiftUI
```swift
@MainActor
class MangaListViewModel: ObservableObject {
@Published var mangas: [Manga] = []
func loadMangas() async {
mangas = ... // SwiftUI detecta cambio
}
}
```
**Beneficios:**
- UI se actualiza automáticamente
- Código declarativo
- Menos código boilerplate
### 6. Factory Pattern (Implícito)
**Uso**: Creación de modelos
```swift
// Funciones estáticas que crean instancias
Chapter(number: 1, title: "...", url: "...", slug: "...")
MangaPage(url: "...", index: 0)
```
**Beneficios:**
- Creación consistente de objetos
- Validación en inicialización
- Fácil de mantener
## Diagramas de Secuencia
### Secuencia 1: Agregar Manga
```
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌────────────┐
│ Usuario │ │ ViewModel │ │ Scraper │ │ WKWebView │
└────┬────┘ └──────┬───────┘ └────────┬────────┘ └─────┬──────┘
│ │ │ │
│ ingresa slug │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ scrapeMangaInfo() │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ load(URL) │
│ │ │───────────────────>│
│ │ │ │
│ │ │ wait 3 seconds │
│ │ │<───────────────────┤
│ │ │ │
│ │ │ evaluateJavaScript │
│ │ │───────────────────>│
│ │ │ (extrae datos) │
│ │ │<───────────────────┤
│ │ │ │
│ │ Manga │ │
│ │<────────────────────│ │
│ │ │ │
│ actualiza │ │ │
│ UI │ │ │
│<───────────────│ │ │
│ │ │ │
┌────┴────┐ ┌──────┴───────┘ └────────┴────────┘ └─────┴──────┘
```
### Secuencia 2: Leer Capítulo
```
┌─────────┐ ┌──────────────┐ ┌─────────────────┐ ┌──────────┐
│ Usuario │ │ ReaderView │ │ ViewModel │ │ Storage │
└────┬────┘ └──────┬───────┘ └────────┬────────┘ └────┬─────┘
│ │ │ │
│ tap capítulo │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ loadPages() │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ isDownloaded()? │
│ │ │──────────────────>│
│ │ │ │
│ │ │ NO │
│ │ │<──────────────────┤
│ │ │ │
│ │ │ scrapeChapterImages│
│ │ │ (via Scraper) │
│ │ │ │
│ │ [MangaPage] │ │
│ │<────────────────────│ │
│ │ │ │
│ muestra páginas│ │ │
│<───────────────│ │ │
│ │ │ │
┌────┴────┐ ┌──────┴───────┘ └────────┴────────┘ └────┴─────┘
```
### Secuencia 3: Guardar Favorito
```
┌─────────┐ ┌──────────────┐ ┌─────────────────┐
│ Usuario │ │ View │ │ StorageService │
└────┬────┘ └──────┬───────┘ └────────┬────────┘
│ │ │
│ tap corazón │ │
│───────────────>│ │
│ │ │
│ │ toggleFavorite() │
│ │────────────────────>│
│ │ │
│ │ │ getFavorites()
│ │ │ (UserDefaults)
│ │ │
│ │ │ saveFavorite()
│ │ │ (UserDefaults)
│ │ │
│ │ actualiza UI │
│ │<────────────────────│
│ │ │
┌────┴────┘ ┌──────┴───────┘ └────────┴────────┘
```
## Decisiones de Arquitectura
### ¿Por qué WKWebView para scraping?
1. **JavaScript Rendering**: manhwaweb.com usa JavaScript para cargar contenido
2. **Sin dependencias externas**: No requiere librerías de terceros
3. **Aislamiento**: El scraping ocurre en contexto separado
4. **Control**: Full control sobre timeouts, cookies, headers
### ¿Por qué UserDefaults para favoritos/progreso?
1. **Simplicidad**: Datos pequeños y simples
2. **Sincronización**: iCloud sync automático disponible
3. **Rendimiento**: Lectura/escritura rápida
4. **Persistencia**: Survive app reinstalls (si iCloud)
### ¿Por qué FileManager para imágenes?
1. **Tamaño**: Imágenes pueden ser grandes (MBs)
2. **Performance**: Acceso directo a archivos
3. **Cache control**: Control manual de qué guardar
4. **Escalabilidad**: No limitado por UserDefaults
### ¿Por qué MVVM?
1. **SwiftUI nativo**: SwiftUI está diseñado para MVVM
2. **Testabilidad**: ViewModels testeables sin UI
3. **Reactibilidad**: `@Published` y `ObservableObject`
4. **Separación**: UI separada de lógica de negocio
## Consideraciones de Escalabilidad
### Futuras Mejoras
1. **Database**: Migrar de UserDefaults a Core Data o SQLite
2. **Background Tasks**: Descargas en background
3. **Caching Strategy**: LRU cache para imágenes
4. **Pagination**: Cargar capítulos bajo demanda
5. **Sync Service**: Sincronización entre dispositivos
### Rendimiento
- **Lazy Loading**: Cargar imágenes bajo demanda
- **Image Compression**: JPEG 80% calidad
- **Request Batching**: Descargar páginas en paralelo
- **Memory Management**: Liberar imágenes no visibles
## Seguridad
### Consideraciones
1. **No se almacenan credenciales**: La app no requiere login
2. **SSL Pinning**: Considerar para producción
3. **Input Validation**: Validar slugs antes de scraping
4. **Rate Limiting**: No sobrecargar el servidor objetivo
---
**Última actualización**: Febrero 2026
**Versión**: 1.0.0

View File

@@ -1,735 +0,0 @@
# Contributing to MangaReader
Gracias por tu interés en contribuir a MangaReader. Este documento proporciona una guía completa para contribuyentes, incluyendo cómo agregar nuevas fuentes de manga, modificar el scraper, estándares de código y testing.
## Tabla de Contenidos
- [Configuración del Entorno de Desarrollo](#configuración-del-entorno-de-desarrollo)
- [Cómo Agregar Nuevas Fuentes de Manga](#cómo-agregar-nuevas-fuentes-de-manga)
- [Cómo Modificar el Scraper](#cómo-modificar-el-scraper)
- [Estándares de Código](#estándares-de-código)
- [Testing](#testing)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Troubleshooting](#troubleshooting)
---
## Configuración del Entorno de Desarrollo
### Requisitos Previos
1. **MacOS** con Xcode 15+
2. **iOS 15+** device o simulator
3. **Git** para control de versiones
4. **Cuenta de Developer** de Apple (opcional, para firmar)
### Pasos Iniciales
1. **Fork el repositorio** (si es proyecto open source)
2. **Clona tu fork**:
```bash
git clone https://github.com/tu-usuario/MangaReader.git
cd MangaReader/ios-app
```
3. **Abre el proyecto en Xcode**:
```bash
open MangaReader.xcodeproj
```
4. **Configura el signing**:
- Selecciona el proyecto en el sidebar
- En "Signing & Capabilities", elige tu Team
- Asegúrate que "Automatically manage signing" esté activado
5. **Ejecuta el proyecto**:
- Selecciona un dispositivo o simulador
- Presiona `Cmd + R`
---
## Cómo Agregar Nuevas Fuentes de Manga
MangaReader está diseñado para soportar múltiples fuentes de manga. Actualmente soporta manhwaweb.com, pero puedes agregar más fuentes siguiendo estos pasos.
### Arquitectura de Fuentes
Las fuentes se implementan usando un protocolo común:
```swift
protocol MangaSource {
var name: String { get }
var baseURL: String { get }
func fetchMangaInfo(slug: String) async throws -> Manga
func fetchChapters(mangaSlug: String) async throws -> [Chapter]
func fetchChapterImages(chapterSlug: String) async throws -> [String]
}
```
### Paso 1: Crear el Scraper de la Nueva Fuente
Crea un nuevo archivo en `ios-app/Sources/Services/`:
**Ejemplo: `MangaTownScraper.swift`**
```swift
import Foundation
import WebKit
/// Scraper para mangatown.com
/// Implementa el protocolo MangaSource para agregar soporte de esta fuente
@MainActor
class MangaTownScraper: NSObject, ObservableObject, MangaSource {
// MARK: - MangaSource Protocol
let name = "MangaTown"
let baseURL = "https://www.mangatown.com"
// MARK: - Properties
static let shared = MangaTownScraper()
private var webView: WKWebView?
// MARK: - Initialization
private override init() {
super.init()
setupWebView()
}
// MARK: - Setup
private func setupWebView() {
let configuration = WKWebViewConfiguration()
configuration.applicationNameForUserAgent = "Mozilla/5.0"
webView = WKWebView(frame: .zero, configuration: configuration)
webView?.navigationDelegate = self
}
// MARK: - MangaSource Implementation
/// Obtiene la información de un manga desde MangaTown
/// - Parameter slug: Identificador único del manga
/// - Returns: Objeto Manga con información completa
/// - Throws: ScrapingError si falla el scraping
func fetchMangaInfo(slug: String) async throws -> Manga {
guard let webView = webView else {
throw ScrapingError.webViewNotInitialized
}
let url = URL(string: "\(baseURL)/manga/\(slug)")!
try await loadURLAndWait(url, webView: webView)
// Extraer información usando JavaScript
let info: [String: Any] = try await webView.evaluateJavaScript("""
(function() {
// Implementación específica para MangaTown
return {
title: document.querySelector('.manga-title')?.textContent || '',
description: document.querySelector('.manga-summary')?.textContent || '',
// ... más extracciones
};
})();
""") as! [String: Any]
return Manga(
slug: slug,
title: info["title"] as? String ?? "",
description: info["description"] as? String ?? "",
genres: [],
status: "",
url: url.absoluteString,
coverImage: nil
)
}
/// Obtiene los capítulos de un manga
/// - Parameter mangaSlug: Slug del manga
/// - Returns: Array de capítulos ordenados
/// - Throws: ScrapingError si falla
func fetchChapters(mangaSlug: String) async throws -> [Chapter] {
// Implementación similar a scrapeChapters
// ...
return []
}
/// Obtiene las imágenes de un capítulo
/// - Parameter chapterSlug: Slug del capítulo
/// - Returns: Array de URLs de imágenes
/// - Throws: ScrapingError si falla
func fetchChapterImages(chapterSlug: String) async throws -> [String] {
// Implementación similar a scrapeChapterImages
// ...
return []
}
// MARK: - Helper Methods
private func loadURLAndWait(_ url: URL, webView: WKWebView, waitTime: Double = 3.0) async throws {
try await withCheckedThrowingContinuation { continuation in
webView.load(URLRequest(url: url))
DispatchQueue.main.asyncAfter(deadline: .now() + waitTime) {
continuation.resume()
}
}
}
}
// MARK: - WKNavigationDelegate
extension MangaTownScraper: WKNavigationDelegate {
nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Navegación completada
}
nonisolated func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Navigation failed: \(error.localizedDescription)")
}
}
```
### Paso 2: Registrar la Nueva Fuente
Agrega la fuente al registro de fuentes disponibles. Crea un archivo `MangaSourceRegistry.swift`:
```swift
/// Registro de fuentes de manga disponibles
enum MangaSourceRegistry {
static let allSources: [MangaSource] = [
ManhwaWebScraper.shared,
MangaTownScraper.shared,
]
static func source(named name: String) -> MangaSource? {
allSources.first { $0.name == name }
}
}
```
### Paso 3: Actualizar la UI para Seleccionar Fuente
Modifica `ContentView.swift` para permitir selección de fuente:
```swift
// En MangaListViewModel
@Published var selectedSource: MangaSource = ManhwaWebScraper.shared
// En ContentView
Picker("Fuente", selection: $viewModel.selectedSource) {
ForEach(MangaSourceRegistry.allSources, id: \.name) { source in
Text(source.name).tag(source)
}
}
```
### Paso 4: Testing de la Nueva Fuente
Crea tests para verificar que el scraper funciona correctamente:
```swift
import XCTest
@testable import MangaReader
class MangaTownScraperTests: XCTestCase {
var scraper: MangaTownScraper!
override func setUp() {
super.setUp()
scraper = MangaTownScraper.shared
}
func testFetchMangaInfo() async throws {
let manga = try await scraper.fetchMangaInfo(slug: "one-piece")
XCTAssertFalse(manga.title.isEmpty)
XCTAssertFalse(manga.description.isEmpty)
}
func testFetchChapters() async throws {
let chapters = try await scraper.fetchChapters(mangaSlug: "one-piece")
XCTAssertFalse(chapters.isEmpty)
XCTAssertTrue(chapters.first?.number == 1)
}
}
```
---
## Cómo Modificar el Scraper
Si la estructura de manhwaweb.com cambia y el scraper deja de funcionar, sigue estos pasos para actualizarlo.
### Paso 1: Investigar la Nueva Estructura
1. Abre manhwaweb.com en Safari/Chrome
2. Abre Web Inspector (F12 o Cmd+Option+I)
3. Navega a un manga/capítulo
4. Inspecciona el HTML para encontrar los nuevos selectores
### Paso 2: Identificar Selectores Clave
Busca los siguientes elementos:
**Para Manga Info:**
- Selector del título (ej: `h1`, `.title`, `[class*="title"]`)
- Selector de la descripción (ej: `p`, `.description`)
- Selector de géneros (ej: `a[href*="/genero/"]`, `.genres a`)
- Selector de estado (ej: regex en body o `.status`)
- Selector de cover (ej: `.cover img`, `[class*="cover"] img`)
**Para Capítulos:**
- Selector de links (ej: `a[href*="/leer/"]`, `.chapter-link`)
- Cómo extraer el número de capítulo (regex o atributo)
**Para Imágenes:**
- Selector de imágenes (ej: `img`, `.page-image img`)
- Cómo distinguir imágenes de contenido de UI
### Paso 3: Actualizar el JavaScript
En `ManhwaWebScraper.swift`, actualiza el JavaScript en los métodos correspondientes:
**Ejemplo: Actualizar `scrapeMangaInfo`**
```swift
let mangaInfo: [String: Any] = try await webView.evaluateJavaScript("""
(function() {
// ACTUALIZAR: Nuevo selector de título
let title = '';
const titleEl = document.querySelector('.nuevo-selector-titulo');
if (titleEl) {
title = titleEl.textContent?.trim() || '';
}
// ACTUALIZAR: Nueva lógica de descripción
let description = '';
const descEl = document.querySelector('.nuevo-selector-desc');
if (descEl) {
description = descEl.textContent?.trim() || '';
}
// Resto de extracciones...
return {
title: title,
description: description,
// ...
};
})();
""") as! [String: Any]
```
### Paso 4: Probar los Cambios
1. Compila y ejecuta la app
2. Intenta agregar un manga existente
3. Verifica que la información se muestre correctamente
4. Intenta leer un capítulo
5. Verifica que las imágenes carguen
### Paso 5: Manejo de Errores
Agrega manejo de errores robusto:
```swift
// Agregar fallbacks
let titleEl = document.querySelector('.nuevo-selector') ||
document.querySelector('.selector-respalgo') ||
document.querySelector('h1'); // Último recurso
if (titleEl) {
title = titleEl.textContent?.trim() || '';
}
```
### Consejos para Troubleshooting
1. **Incrementa el tiempo de espera** si la página carga lento:
```swift
try await loadURLAndWait(url, waitForImages: true)
// Aumenta el tiempo en loadURLAndWait
```
2. **Verifica que JavaScript esté habilitado** en WKWebView
3. **Revisa la consola** del WebView agregando logging:
```swift
webView.evaluateJavaScript("console.log('Debug info')")
```
4. **Usa `try?`** en vez de `try!` temporalmente para evitar crashes:
```swift
let info = try? webView.evaluateJavaScript(...) as? [String: Any]
print("Info: \(info ?? [:])")
```
---
## Estándares de Código
### Swift Style Guide
Sigue las convenciones de Swift para código limpio y mantenible.
#### 1. Nomenclatura
**Clases y Structs**: PascalCase
```swift
class ManhwaWebScraper { }
struct Manga { }
```
**Propiedades y Métodos**: camelCase
```swift
var mangaTitle: String
func scrapeMangaInfo() { }
```
**Constantes Privadas**: camelCase con prefijo si es necesario
```swift
private let favoritesKey = "favoriteMangas"
```
#### 2. Organización de Código
Usa MARK comments para organizar:
```swift
class ManhwaWebScraper {
// MARK: - Properties
private var webView: WKWebView?
// MARK: - Initialization
init() { }
// MARK: - Public Methods
func scrapeMangaInfo() { }
// MARK: - Private Methods
private func loadURLAndWait() { }
}
```
#### 3. Documentación
Documenta todas las funciones públicas:
```swift
/// Obtiene la lista de capítulos de un manga
///
/// Este método carga la página del manga en un WKWebView,
/// ejecuta JavaScript para extraer los capítulos, y retorna
/// un array ordenado de manera descendente.
///
/// - Parameter mangaSlug: El slug único del manga
/// - Returns: Array de capítulos ordenados por número (descendente)
/// - Throws: `ScrapingError` si el WebView no está inicializado
/// o si falla la extracción de contenido
///
/// # Example
/// ```swift
/// do {
/// let chapters = try await scraper.scrapeChapters(mangaSlug: "one-piece")
/// print("Found \(chapters.count) chapters")
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
func scrapeChapters(mangaSlug: String) async throws -> [Chapter] {
// Implementación
}
```
#### 4. Async/Await
Usa async/await para código asíncrono:
```swift
// BUENO
func loadManga() async throws -> Manga {
let manga = try await scraper.scrapeMangaInfo(slug: slug)
return manga
}
// MAL (evitar completion handlers si es posible)
func loadManga(completion: @escaping (Result<Manga, Error>) -> Void) {
// ...
}
```
#### 5. Error Handling
Usa errores tipados en vez de genéricos:
```swift
// BUENO
enum ScrapingError: LocalizedError {
case webViewNotInitialized
case pageLoadFailed
}
// MAL
func scrape() throws {
throw NSError(domain: "Error", code: 1, userInfo: nil)
}
```
#### 6. Opcionalidad
Usa opcionales con cuidado:
```swift
// BUENO - unwrap seguro
if let coverImage = manga.coverImage {
// Usar coverImage
}
// BUENO - nil coalescing
let title = manga.title ?? "Unknown"
// MAL - force unwrap (evitar a menos que estés 100% seguro)
let image = UIImage(contentsOfFile: path)!
```
#### 7. Closures
Usa trailing closure syntax cuando sea el último parámetro:
```swift
// BUENO
DispatchQueue.main.async {
print("Async code")
}
// ACEPTABLE
DispatchQueue.main.async(execute: {
print("Async code")
})
```
---
## Testing
### Escribir Tests
Crea tests para nuevas funcionalidades:
**Ejemplo de Unit Test:**
```swift
import XCTest
@testable import MangaReader
class StorageServiceTests: XCTestCase {
var storage: StorageService!
override func setUp() {
super.setUp()
storage = StorageService.shared
}
override func tearDown() {
// Limpiar después de cada test
storage.clearAllDownloads()
super.tearDown()
}
func testSaveAndRetrieveFavorite() {
// Given
let slug = "test-manga"
// When
storage.saveFavorite(mangaSlug: slug)
let isFavorite = storage.isFavorite(mangaSlug: slug)
// Then
XCTAssertTrue(isFavorite)
}
func testRemoveFavorite() {
// Given
let slug = "test-manga"
storage.saveFavorite(mangaSlug: slug)
// When
storage.removeFavorite(mangaSlug: slug)
let isFavorite = storage.isFavorite(mangaSlug: slug)
// Then
XCTAssertFalse(isFavorite)
}
}
```
### Ejecutar Tests
1. En Xcode, presiona `Cmd + U` para ejecutar todos los tests
2. Para ejecutar un test específico, click en el diamante junto al nombre del test
3. Los tests deben ejecutarse en el simulator o en un dispositivo real
### Cobertura de Código
Apunta a tener al menos 70% de cobertura de código:
- Servicios: 80%+
- ViewModels: 70%+
- Models: 90%+ (son datos simples)
- Views: 50%+ (UI testing es más difícil)
---
## Pull Request Guidelines
### Antes de Abrir un PR
1. **Actualiza tu rama**:
```bash
git checkout main
git pull upstream main
git checkout tu-rama
git rebase main
```
2. **Resuelve conflicts** si los hay
3. **Ejecuta los tests**:
```bash
xcodebuild test -scheme MangaReader -destination 'platform=iOS Simulator,name=iPhone 15'
```
4. **Limpia el código**:
- Remueve `print()` statements de debugging
- Formatea el código (Xcode puede hacerlo automáticamente: Ctrl + I)
- Remueve comentarios TODO/FIXME implementados
### Estructura del PR
Usa esta plantilla para tu PR:
```markdown
## Descripción
Breve descripción de los cambios.
## Tipo de Cambio
- [ ] Bug fix (non-breaking change que arregla un issue)
- [ ] New feature (non-breaking change que agrega funcionalidad)
- [ ] Breaking change (fix or feature que causaría breaking changes)
- [ ] Documentation update
## Testing
Describe los tests que ejecutaste:
- [ ] Unit tests pasan
- [ ] Manual testing en device/simulator
- [ ] Probado en iOS 15, iOS 16, iOS 17
## Screenshots (si aplica)
Before/After screenshots para cambios de UI.
## Checklist
- [ ] Mi código sigue los style guidelines
- [ ] He realizado self-review de mi código
- [ ] He comentado código complejo
- [ ] He actualizado la documentación
- [ ] No hay nuevos warnings
- [ ] He agregado tests que prueban mis cambios
- [ ] Todos los tests pasan
```
### Review Process
1. **Automated Checks**: CI ejecutará tests automáticamente
2. **Code Review**: Al menos 1 revisor debe aprobar
3. **Testing**: El revisor probará los cambios en un dispositivo
4. **Merge**: El maintainer mergeará si todo está bien
---
## Troubleshooting
### Issues Comunes
#### 1. WebView no carga contenido
**Síntoma**: `scrapeMangaInfo` retorna datos vacíos
**Solución**:
- Aumenta el tiempo de espera en `loadURLAndWait`
- Verifica que la URL sea correcta
- Agrega logging para ver qué JavaScript retorna
#### 2. Tests fallan en CI pero pasan localmente
**Síntoma**: Tests pasan en tu máquina pero fallan en GitHub Actions
**Solución**:
- Asegúrate de que los tests no dependen de datos locales
- Usa mocks en vez de scrapers reales en tests
- Verifica que la configuración de iOS sea la misma
#### 3. Imágenes no cargan
**Síntoma**: ReaderView muestra placeholders en vez de imágenes
**Solución**:
- Verifica que las URLs sean válidas
- Agrega logging en `scrapeChapterImages`
- Prueba las URLs en un navegador
- Verifica que no haya bloqueo de red
#### 4. El proyecto no compila
**Síntoma**: Errores de "Cannot find type" o "No such module"
**Solución**:
1. Limpia el proyecto: `Cmd + Shift + K`
2. Cierra Xcode
3. Borra `DerivedData`: `rm -rf ~/Library/Developer/Xcode/DerivedData`
4. Abre Xcode y rebuild
### Pedir Ayuda
Si estás atascado:
1. **Busca en Issues existentes**: Puede que alguien ya tuvo el mismo problema
2. **Crea un Issue** con:
- Descripción detallada del problema
- Pasos para reproducir
- Logs relevantes
- Tu entorno (Xcode version, iOS version, macOS version)
3. **Únete a Discord/Slack** (si existe) para ayuda en tiempo real
---
## Recursos Adicionales
### Documentación de Referencia
- [Swift Language Guide](https://docs.swift.org/swift-book/)
- [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui)
- [WKWebView Reference](https://developer.apple.com/documentation/webkit/wkwebview)
- [Swift Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
### Herramientas Útiles
- **SwiftLint**: Linter para Swift
- **SwiftFormat**: Formateador de código
- **SwiftGen**: Generador de código para recursos
### Comunidad
- [Swift Forums](https://forums.swift.org/)
- [Stack Overflow - swift tag](https://stackoverflow.com/questions/tagged/swift)
- [r/swift subreddit](https://www.reddit.com/r/swift/)
---
**Última actualización**: Febrero 2026
**Versión**: 1.0.0

View File

@@ -1,277 +0,0 @@
# MangaReader - Documentación Técnica
Bienvenido a la documentación técnica de MangaReader. Este directorio contiene toda la información necesaria para entender, desarrollar y contribuir al proyecto.
## Documentos Disponibles
### [ARCHITECTURE.md](./ARCHITECTURE.md)
Documentación completa de la arquitectura del sistema.
**Contenido:**
- Visión general del sistema (Backend + iOS App)
- Arquitectura MVVM de la app iOS
- Diagramas de flujo de datos (scraping, lectura, guardado)
- Patrones de diseño implementados (Singleton, MVVM, Repository, etc.)
- Diagramas de secuencia en ASCII
- Decisiones arquitectónicas y su justificación
**Para quién es:**
- Desarrolladores que necesitan entender la estructura general
- Arquitectos que evalúan el diseño del sistema
- Nuevos contribuyentes que necesitan orientación
---
### [API.md](./API.md)
Documentación de la API y modelos de datos.
**Contenido:**
- Modelos de datos (Manga, Chapter, MangaPage, ReadingProgress, etc.)
- Documentación completa de servicios (ManhwaWebScraper, StorageService)
- Descripción de ViewModels y sus responsabilidades
- Errores y manejo de excepciones
- Ejemplos de uso para cada método público
**Para quién es:**
- Desarrolladores que integran funcionalidades
- Equipo QA que necesita entender comportamientos esperados
- Contribuyentes que agregan nuevas features
---
### [CONTRIBUTING.md](./CONTRIBUTING.md)
Guía para contribuir al proyecto.
**Contenido:**
- Configuración del entorno de desarrollo
- Cómo agregar nuevas fuentes de manga
- Cómo modificar el scraper existente
- Estándares de código (Swift style guide)
- Testing (unit tests, integración)
- Pull request guidelines
- Troubleshooting común
**Para quién es:**
- Nuevos contribuyentes
- Desarrolladores que agregan features
- Maintainers que revisan PRs
---
## Estructura del Proyecto
```
MangaReader/
├── docs/ # Documentación técnica
│ ├── README.md # Este archivo (índice)
│ ├── ARCHITECTURE.md # Arquitectura y diagramas
│ ├── API.md # API y modelos de datos
│ └── CONTRIBUTING.md # Guía para contribuyentes
├── ios-app/ # Aplicación iOS
│ ├── MangaReaderApp.swift # Entry point de la app
│ ├── Sources/
│ │ ├── Models/ # Modelos de datos
│ │ │ └── Manga.swift # Models documentados
│ │ ├── Services/ # Lógica de negocio
│ │ │ ├── ManhwaWebScraper.swift # Scraper documentado
│ │ │ └── StorageService.swift # Storage documentado
│ │ └── Views/ # UI SwiftUI
│ │ ├── ContentView.swift
│ │ ├── MangaDetailView.swift
│ │ └── ReaderView.swift
│ └── MangaReader.xcodeproj
└── backend/ # Backend opcional (Node.js)
├── scraper.js
├── server.js
└── package.json
```
---
## Resumen Rápido de Componentes
### Modelos de Datos
- **Manga**: Información completa de un manga
- **Chapter**: Capítulo individual con estado de lectura
- **MangaPage**: Página individual (imagen)
- **ReadingProgress**: Progreso de lectura del usuario
- **DownloadedChapter**: Capítulo descargado localmente
### Servicios
- **ManhwaWebScraper**: Scraper usando WKWebView para manhwaweb.com
- **StorageService**: Gestión de almacenamiento local (UserDefaults + FileManager)
### ViewModels
- **MangaListViewModel**: Lista principal de mangas
- **MangaDetailViewModel**: Detalle de un manga
- **ReaderViewModel**: Lector de capítulos
### Views
- **ContentView**: Vista principal con lista de mangas
- **MangaDetailView**: Detalle y capítulos de un manga
- **ReaderView**: Lector de imágenes con zoom/pan
---
## Comenzar Rápidamente
### Para Entender la Arquitectura
1. Lee [ARCHITECTURE.md](./ARCHITECTURE.md) - Sección "Visión General"
2. Revisa los diagramas de flujo de datos
3. Estudia los patrones de diseño usados
### Para Usar la API
1. Consulta [API.md](./API.md) - Sección "Modelos de Datos"
2. Revisa los servicios disponibles
3. Mira los ejemplos de uso
### Para Contribuir
1. Lee [CONTRIBUTING.md](./CONTRIBUTING.md) - "Configuración del Entorno"
2. Configura tu entorno de desarrollo
3. Revisa los estándares de código
4. Sigue el workflow de Pull Requests
---
## Tecnologías Utilizadas
### iOS App
- **SwiftUI**: Framework de UI declarativo
- **Combine**: Programación reactiva
- **WKWebView**: Rendering de JavaScript
- **UserDefaults**: Almacenamiento de preferencias
- **FileManager**: Almacenamiento de archivos
### Backend (Opcional)
- **Node.js**: Runtime de JavaScript
- **Express**: Framework web
- **Puppeteer**: Headless Chrome automation
---
## Patrones de Diseño Principales
| Patrón | Implementación | Propósito |
|--------|---------------|-----------|
| **MVVM** | ViewModels separados de Views | Separar UI de lógica |
| **Singleton** | `StorageService.shared`, `ManhwaWebScraper.shared` | Instancia única compartida |
| **Repository** | `StorageService` abstrae UserDefaults/FileManager | Interfaz unificada de datos |
| **Async/Await** | Métodos `async throws` en scraper | Código asíncrono legible |
| **Observable** | `@Published`, `ObservableObject` | Reactividad en SwiftUI |
---
## Flujos Principales
### 1. Agregar Manga
```
Usuario → ContentView → MangaListViewModel
ManhwaWebScraper.scrapeMangaInfo()
WKWebView + JavaScript
Manga actualizado en UI
```
### 2. Leer Capítulo
```
Usuario → MangaDetailView → ReaderView
ReaderViewModel.loadPages()
¿Descargado? ─NO→ ManhwaWebScraper.scrapeChapterImages()
│S ↓
└─→ StorageService.getDownloadedChapter()
Mostrar páginas en ReaderView
```
### 3. Guardar Progreso
```
Usuario navega a página X
ReaderViewModel.currentPage = X
ReaderViewModel.saveProgress()
StorageService.saveReadingProgress()
UserDefaults (JSON codificado)
```
---
## Preguntas Frecuentes
**¿Puedo agregar nuevas fuentes de manga?**
Sí. Lee [CONTRIBUTING.md](./CONTRIBUTING.md) - Sección "Cómo Agregar Nuevas Fuentes de Manga".
**¿El backend es obligatorio?**
No. La app iOS funciona completamente de manera autónoma. El backend es opcional y puede servir como cache/API intermedia.
**¿Cómo cambio el scraper si manhwaweb.com cambia su estructura?**
Consulta [CONTRIBUTING.md](./CONTRIBUTING.md) - Sección "Cómo Modificar el Scraper".
**¿Cómo ejecuto los tests?**
Ve a [CONTRIBUTING.md](./CONTRIBUTING.md) - Sección "Testing".
---
## Convenciones de Documentación
### En Código Swift
- **///**: Comentarios de documentación públicos (soportan Markdown)
- **//:**: Comentarios de sección (MARK)
- **//**: Comentarios de implementación
### Ejemplo de Documentación de Método
```swift
/// Obtiene la lista de capítulos de un manga.
///
/// Este método carga la página del manga, espera a que JavaScript renderice
/// el contenido, y extrae todos los links de capítulos disponibles.
///
/// - Parameter mangaSlug: Slug único del manga
/// - Returns: Array de `Chapter` ordenados por número (descendente)
/// - Throws: `ScrapingError` si el WebView no está inicializado
///
/// # Example
/// ```swift
/// let chapters = try await scraper.scrapeChapters(mangaSlug: "one-piece")
/// ```
func scrapeChapters(mangaSlug: String) async throws -> [Chapter]
```
---
## Recursos Adicionales
### Documentación Oficial
- [Swift Language Guide](https://docs.swift.org/swift-book/)
- [SwiftUI Documentation](https://developer.apple.com/documentation/swiftui)
- [WKWebView Reference](https://developer.apple.com/documentation/webkit/wkwebview)
### Herramientas
- **SwiftLint**: Linter para código Swift
- **SwiftFormat**: Formateador automático
- **Jazzy**: Generador de documentación (Objective-C/Swift)
---
## Soporte y Contribución
¿Encontraste un error en la documentación? ¿Falta algo?
1. Abre un issue en el repositorio
2. O envía un Pull Request con las mejoras
Para más detalles sobre cómo contribuir, revisa [CONTRIBUTING.md](./CONTRIBUTING.md).
---
**Última actualización**: Febrero 2026
**Versión**: 1.0.0
**Mantenedor**: MangaReader Team

View File

@@ -1,266 +0,0 @@
# Checklist de Implementación - Sistema de Descarga
## ✅ Componentes Core
### DownloadManager
- [x] Crear clase `DownloadManager` con patrón Singleton
- [x] Implementar `downloadChapter()` con async/await
- [x] Implementar `downloadChapters()` para múltiples capítulos
- [x] Implementar `downloadImages()` con concurrencia limitada
- [x] Implementar `cancelDownload(taskId:)` para cancelación individual
- [x] Implementar `cancelAllDownloads()` para cancelación masiva
- [x] Crear `DownloadTask` con propiedades @Published
- [x] Crear enum `DownloadState` con todos los estados
- [x] Crear enum `DownloadError` con tipos de error
- [x] Crear `CancellationChecker` para cancelación asíncrona
- [x] Integrar con `StorageService` para guardar imágenes
- [x] Integrar con `ManhwaWebScraper` para obtener URLs
- [x] Implementar manejo robusto de errores
- [x] Implementar actualización de progreso en tiempo real
- [x] Mantener historial de descargas (completadas y fallidas)
- [x] Verificar duplicados antes de descargar
### MangaDetailView
- [x] Añadir botón de descarga en toolbar
- [x] Crear alert para seleccionar cantidad de capítulos
- [x] Actualizar `ChapterRowView` con botón de descarga
- [x] Mostrar progreso de descarga en cada fila
- [x] Añadir indicador visual de capítulo descargado
- [x] Actualizar `MangaDetailViewModel`:
- [x] Integrar `DownloadManager`
- [x] Implementar `downloadChapter()` async
- [x] Implementar `downloadAllChapters()`
- [x] Implementar `downloadLastChapters(count:)`
- [x] Implementar `getDownloadProgress(for:)`
- [x] Implementar `isDownloadingChapter(_:)`
- [x] Implementar notificaciones de estado
- [x] Crear overlay de notificaciones
- [x] Manejar estados de error y éxito
### DownloadsView
- [x] Crear `DownloadsView` con 3 tabs
- [x] Tab "Activas"
- [x] Tab "Completadas"
- [x] Tab "Fallidas"
- [x] Crear `ActiveDownloadCard` con progreso
- [x] Crear `CompletedDownloadCard`
- [x] Crear `FailedDownloadCard` con reintentar
- [x] Implementar `DownloadsViewModel`
- [x] Añadir botón "Cancelar todas"
- [x] Añadir botones "Limpiar historial"
- [x] Mostrar tamaño de almacenamiento
- [x] Añadir botón "Limpiar todo" con alert
- [x] Crear estados vacíos descriptivos
- [x] Implementar picker segmentado para tabs
## ✅ Extensiones y Utilidades
### DownloadExtensions
- [x] Extensión de `DownloadTask`
- [x] `formattedSize` - tamaño estimado
- [x] `estimatedTimeRemaining` - tiempo restante
- [x] Extensión de `DownloadManager`
- [x] `downloadStats` - estadísticas
- [x] `hasActiveDownloads` - check de activas
- [x] `totalDownloads` - contador total
- [x] Extensión de `UIImage`
- [x] `compressedData(quality:)` - compresión JPEG
- [x] `resized(maxWidth:maxHeight:)` - redimensionado
- [x] `optimizedForStorage()` - optimización completa
- [x] Crear `DownloadStats` modelo
- [x] Definir nombres de notificaciones
- [x] Crear `URLSession.downloadSession()`
## ✅ Integración
### StorageService
- [x] Verificar que `saveImage()` existe y funciona
- [x] Verificar que `getImageURL()` existe y funciona
- [x] Verificar que `isChapterDownloaded()` existe y funciona
- [x] Verificar que `getChapterDirectory()` existe y funciona
- [x] Verificar que `deleteDownloadedChapter()` existe y funciona
- [x] Verificar que `getStorageSize()` existe y funciona
- [x] Verificar que `formatFileSize()` existe y funciona
### Models
- [x] Verificar que `DownloadedChapter` modelo existe
- [x] Verificar que `MangaPage` modelo existe
- [x] Verificar que `Chapter` modelo tiene propiedades necesarias
## ✅ UI/UX
### Notificaciones
- [x] Toast notification al completar descarga
- [x] Icono verde para éxito
- [x] Icono rojo para error
- [x] Auto-ocultado después de 3 segundos
- [x] Animación desde abajo
- [x] Overlay con blur shadow
### Progreso Visual
- [x] ProgressView lineal
- [x] Porcentaje numérico
- [x] Páginas descargadas/total
- [x] Barra animada
- [x] Colores significativos (azul descargando, verde completado)
### Estados de Descarga
- [x] Icono para pending (gris)
- [x] Icono para downloading (azul animado)
- [x] Icono para completed (checkmark verde)
- [x] Icono para failed (X rojo)
- [x] Icono para cancelled (gris)
### Estados Vacíos
- [x] Icono grande y descriptivo
- [x] Mensaje claro
- [x] Llamada a la acción si aplica
## ✅ Manejo de Errores
### Tipos de Error
- [x] `alreadyDownloaded` - Capítulo ya descargado
- [x] `noImagesFound` - Scraper no encontró imágenes
- [x] `invalidURL` - URL malformada
- [x] `invalidResponse` - Respuesta HTTP inválida
- [x] `httpError(statusCode)` - Error HTTP específico
- [x] `invalidImageData` - Datos no son imagen válida
- [x] `cancelled` - Usuario canceló
- [x] `storageError(String)` - Error de almacenamiento
### Recuperación
- [x] Limpieza de archivos parciales al cancelar
- [x] Mensajes descriptivos al usuario
- [x] Logging de errores para debugging
- [x] Estado `failed` en FailedDownloadCard
- [x] Opción de reintentar (preparado)
## ✅ Concurrencia y Performance
### Estrategia de Concurrencia
- [x] Usar Swift Concurrency (async/await)
- [x] Usar `@MainActor` para UI
- [x] Usar `TaskGroup` para descargas en paralelo
- [x] Limitar a 3 capítulos simultáneos
- [x] Limitar a 5 imágenes simultáneas por capítulo
- [x] Usar `CancellationChecker` para cancelación segura
### Optimizaciones
- [x] Comprimir imágenes al 75-80% JPEG
- [x] Redimensionar si > 2048px
- [x] Concurrencia limitada para evitar picos
- [x] Limpieza automática de historiales (50 completadas, 20 fallidas)
## ✅ Configuración
### Parámetros
- [x] `maxConcurrentDownloads = 3`
- [x] `maxConcurrentImagesPerChapter = 5`
- [x] JPEG compression quality 0.8
- [x] Optimized storage quality 0.75
- [x] Max dimension 2048px
### Timeouts
- [x] URLSession request: 30 segundos
- [x] URLSession resource: 5 minutos
- [x] Scraper page load: 3-5 segundos
## ✅ Documentación
### Archivos de Documentación
- [x] `DOWNLOAD_SYSTEM_README.md` - Guía completa
- [x] `IMPLEMENTATION_SUMMARY.md` - Resumen ejecutivo
- [x] `DIAGRAMS.md` - Diagramas de flujo
- [x] `IntegrationExample.swift` - Ejemplos de código
### Código
- [x] Comentarios en código complejo
- [x] Documentación de métodos públicos
- [x] Ejemplos de uso en README
## ✅ Testing
### Testing Manual
- [x] Verificar descarga de un capítulo
- [x] Verificar descarga de múltiples capítulos
- [x] Verificar cancelación de descarga
- [x] Verificar manejo de errores
- [x] Verificar progreso visual
- [x] Verificar notificaciones
- [x] Verificar limpieza de almacenamiento
### Casos de Prueba
- [ ] Descargar capítulo sin internet
- [ ] Descargar capítulo ya descargado
- [ ] Cancelar descarga a mitad
- [ ] Descargar capítulo con 0 imágenes
- [ ] Llenar almacenamiento del dispositivo
- [ ] Probar con diferentes tamaños de capítulo
- [ ] Probar concurrentemente múltiples descargas
## 📋 Próximos Pasos (Opcionales)
### Mejoras Futuras
- [ ] Background downloads con URLSession
- [ ] Reanudar descargas pausadas
- [ ] Priorización de descargas
- [ ] Descarga automática de nuevos capítulos
- [ ] Soporte para formato WebP
- [ ] Batch operations en StorageService
- [ ] Metrics y analytics
### Testing Automatizado
- [ ] Unit tests para DownloadManager
- [ ] Integration tests
- [ ] UI tests para DownloadsView
- [ ] Performance tests
- [ ] Memory leak tests con XCTest
### UI Adicional
- [ ] SettingsView con preferencias de descarga
- [ ] ActiveDownloadsWidget para home
- [ ] ActiveDownloadsBanner modifier
- [ ] Badge en TabView
- [ ] Sheet para descargas desde cualquier vista
---
## 📊 Estadísticas de Implementación
**Fecha**: 2026-02-04
**Versión**: 1.0
**Estado**: ✅ COMPLETO
### Archivos
- **Nuevos**: 5 archivos principales
- **Modificados**: 2 archivos existentes
- **Total de líneas**: ~1,500 líneas
### Tiempos
- **Desarrollo**: 4-6 horas
- **Testing**: 1-2 horas
- **Documentación**: 2-3 horas
- **Total**: 7-11 horas
### Cobertura
- **DownloadManager**: 100% completo
- **MangaDetailView**: 100% completo
- **DownloadsView**: 100% completo
- **Extensiones**: 100% completo
- **Integración**: 100% completo
- **Documentación**: 100% completo
## 🎉 Checklist Final
- [x] Todos los componentes core implementados
- [x] UI/UX pulida y funcional
- [x] Manejo de errores robusto
- [x] Concurrencia optimizada
- [x] Integración completa con servicios existentes
- [x] Documentación exhaustiva
- [x] Ejemplos de integración
- [x] Diagramas de flujo
- [x] Testing manual completado
- [x] Código limpio y mantenible
**ESTADO FINAL**: ✅ LISTO PARA PRODUCCIÓN

View File

@@ -1,352 +0,0 @@
# API Configuration for MangaReader iOS App
## Overview
This directory contains the API configuration for connecting the iOS app to the VPS backend. The configuration is centralized in `APIConfig.swift` and includes all necessary settings for API communication.
## Files
- **APIConfig.swift**: Main configuration file with all API settings, endpoints, and helper methods
- **APIConfigExample.swift**: Comprehensive usage examples and demonstrations
- **README.md** (this file): Documentation and usage guide
## Current Configuration
### Server Connection
- **Server URL**: `https://gitea.cbcren.online`
- **Port**: `3001`
- **Full Base URL**: `https://gitea.cbcren.online:3001`
- **API Version**: `v1`
- **API Base Path**: `https://gitea.cbcren.online:3001/api/v1`
### Timeouts
- **Default Request Timeout**: `30.0` seconds (for regular API calls)
- **Resource Download Timeout**: `300.0` seconds (5 minutes, for large downloads)
### Retry Policy
- **Max Retries**: `3` attempts
- **Base Retry Delay**: `1.0` second (with exponential backoff)
### Cache Configuration
- **Max Memory Usage**: `100` cached responses
- **Cache Expiry**: `300.0` seconds (5 minutes)
## Usage
### Basic URL Construction
```swift
// Method 1: Use the helper function
let url = APIConfig.url(for: "manga/popular")
// Result: "https://gitea.cbcren.online:3001/manga/popular"
// Method 2: Get a URL object
if let urlObj = APIConfig.urlObject(for: "manga/popular") {
var request = URLRequest(url: urlObj)
// Make request...
}
// Method 3: Use predefined endpoints
let endpoint = APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1089)
// Result: "https://gitea.cbcren.online:3001/api/v1/download/one-piece/1089"
```
### URLSession Configuration
```swift
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = APIConfig.defaultTimeout
configuration.timeoutIntervalForResource = APIConfig.downloadTimeout
let session = URLSession(configuration: configuration)
```
### URLRequest with Headers
```swift
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
// Add common headers
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Add authentication if needed
if let token = authToken {
let authHeaders = APIConfig.authHeader(token: token)
for (key, value) in authHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
}
```
## Available Endpoints
### Download Endpoints
```swift
// Request chapter download
APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1089)
// Check if chapter is downloaded
APIConfig.Endpoints.checkDownloaded(mangaSlug: "one-piece", chapterNumber: 1089)
// List all downloaded chapters for a manga
APIConfig.Endpoints.listChapters(mangaSlug: "one-piece")
// Get specific image from chapter
APIConfig.Endpoints.getImage(mangaSlug: "one-piece", chapterNumber: 1089, pageIndex: 0)
// Delete a chapter
APIConfig.Endpoints.deleteChapter(mangaSlug: "one-piece", chapterNumber: 1089)
```
### Server Endpoints
```swift
// Get storage statistics
APIConfig.Endpoints.storageStats()
// Health check
APIConfig.Endpoints.health()
```
## Environment Configuration
The configuration includes presets for different environments:
### Development
```swift
APIConfig.development
// - serverURL: "http://192.168.1.100"
// - port: 3001
// - timeout: 60.0s
// - logging: true
```
### Staging
```swift
APIConfig.staging
// - serverURL: "https://staging.cbcren.online"
// - port: 3001
// - timeout: 30.0s
// - logging: true
```
### Production (Current)
```swift
APIConfig.production
// - serverURL: "https://gitea.cbcren.online"
// - port: 3001
// - timeout: 30.0s
// - logging: false
```
### Testing (Debug Only)
```swift
#if DEBUG
APIConfig.testing
// - serverURL: "http://localhost:3001"
// - port: 3001
// - timeout: 5.0s
// - logging: true
#endif
```
## Changing the Server URL
To change the API server URL, modify the `serverURL` property in `APIConfig.swift`:
```swift
// In APIConfig.swift, line 37
static let serverURL = "https://gitea.cbcren.online" // Change this
```
For environment-specific URLs, use compile-time conditionals:
```swift
#if DEBUG
static let serverURL = "http://192.168.1.100" // Local development
#else
static let serverURL = "https://gitea.cbcren.online" // Production
#endif
```
## Error Codes
The API defines specific error codes for different scenarios:
```swift
APIConfig.ErrorCodes.chapterNotFound // 40401
APIConfig.ErrorCodes.chapterAlreadyDownloaded // 40901
APIConfig.ErrorCodes.storageLimitExceeded // 50701
APIConfig.ErrorCodes.invalidImageFormat // 42201
APIConfig.ErrorCodes.downloadFailed // 50001
```
## Validation
The configuration includes a validation method:
```swift
if APIConfig.isValid {
print("Configuration is valid")
} else {
print("Configuration is invalid")
}
```
This checks:
- Server URL is not empty
- Port is in valid range (1-65535)
- Timeout values are positive
- Retry count is non-negative
## Debug Support
In debug builds, you can print the current configuration:
```swift
#if DEBUG
APIConfig.printConfiguration()
#endif
```
This outputs:
```
=== API Configuration ===
Server URL: https://gitea.cbcren.online
Port: 3001
Base URL: https://gitea.cbcren.online:3001
API Version: v1
Default Timeout: 30.0s
Download Timeout: 300.0s
Max Retries: 3
Logging Enabled: false
Cache Enabled: true
=========================
```
## Best Practices
1. **Always use predefined endpoints** when available instead of manually constructing URLs
2. **Use appropriate timeouts** - `defaultTimeout` for regular calls, `downloadTimeout` for large downloads
3. **Validate configuration** on app startup
4. **Use the helper methods** (`url()`, `urlObject()`) for URL construction
5. **Include common headers** in all requests
6. **Handle specific error codes** defined in `APIConfig.ErrorCodes`
7. **Enable logging only in debug builds** for security
## Example: Making an API Call
```swift
func fetchPopularManga() async throws -> [Manga] {
// Construct URL
guard let url = APIConfig.urlObject(for: "manga/popular") else {
throw APIError.invalidURL
}
// Create request
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
// Add headers
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Make request
let (data, response) = try await URLSession.shared.data(for: request)
// Validate response
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw APIError.requestFailed
}
// Decode response
let mangas = try JSONDecoder().decode([Manga].self, from: data)
return mangas
}
```
## Example: Downloading with Retry
```swift
func downloadChapterWithRetry(
mangaSlug: String,
chapterNumber: Int
) async throws -> Data {
let endpoint = APIConfig.Endpoints.download(
mangaSlug: mangaSlug,
chapterNumber: chapterNumber
)
return try await fetchWithRetry(endpoint: endpoint, retryCount: 0)
}
func fetchWithRetry(endpoint: String, retryCount: Int) async throws -> Data {
guard let url = URL(string: endpoint),
retryCount < APIConfig.maxRetries else {
throw APIError.retryLimitExceeded
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.downloadTimeout
do {
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
return data
} else {
throw APIError.requestFailed
}
} catch {
// Calculate exponential backoff delay
let delay = APIConfig.baseRetryDelay * pow(2.0, Double(retryCount))
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
return try await fetchWithRetry(endpoint: endpoint, retryCount: retryCount + 1)
}
}
```
## Troubleshooting
### Connection Issues
1. **Verify server URL**: Check that `serverURL` is correct and accessible
2. **Check port**: Ensure `port` matches the backend server configuration
3. **Test connectivity**: Use the health endpoint: `APIConfig.Endpoints.health()`
4. **Enable logging**: Set `loggingEnabled = true` to see request details
### Timeout Issues
1. **For regular API calls**: Use `APIConfig.defaultTimeout` (30 seconds)
2. **For large downloads**: Use `APIConfig.downloadTimeout` (300 seconds)
3. **Slow networks**: Increase timeout values if needed
### SSL Certificate Issues
If using HTTPS with a self-signed certificate:
1. Add the certificate to the app's bundle
2. Configure URLSession to trust the certificate
3. Or use HTTP for development (not recommended for production)
## Migration Notes
When migrating from the old configuration:
1. Replace hardcoded URLs with `APIConfig.url(for:)` or predefined endpoints
2. Use `APIConfig.commonHeaders` instead of manually setting headers
3. Replace hardcoded timeouts with `APIConfig.defaultTimeout` or `APIConfig.downloadTimeout`
4. Add validation on app startup with `APIConfig.isValid`
5. Use specific error codes from `APIConfig.ErrorCodes`
## Additional Resources
- See `APIConfigExample.swift` for more comprehensive examples
- Check the backend API documentation for available endpoints
- Review the iOS app's Services directory for integration examples

View File

@@ -1,412 +0,0 @@
# 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
}
]
}
```

View File

@@ -1,355 +0,0 @@
# Sistema de Descarga de Capítulos - Resumen de Implementación
## Archivos Creados/Modificados
### Archivos Nuevos Creados
1. **`/Sources/Services/DownloadManager.swift`** (470 líneas)
- Clase principal `DownloadManager` con patrón Singleton
- `DownloadTask`: Representa una tarea de descarga individual
- `DownloadState`: Enum con estados de descarga
- `DownloadProgress`: Modelo de progreso
- `CancellationChecker`: Sistema de cancelación asíncrona
- `DownloadError`: Tipos de errores específicos
2. **`/Sources/Views/DownloadsView.swift`** (350 líneas)
- Vista principal de gestión de descargas
- 3 tabs: Activas, Completadas, Fallidas
- `ActiveDownloadCard`: Card con progreso en tiempo real
- `CompletedDownloadCard`: Card de descargas exitosas
- `FailedDownloadCard`: Card con opción de reintentar
- `DownloadsViewModel`: ViewModel para la vista
3. **`/Sources/Extensions/DownloadExtensions.swift`** (180 líneas)
- Extensiones de `DownloadTask` para formateo
- Extensiones de `DownloadManager` para estadísticas
- Extensiones de `UIImage` para compresión y optimización
- Constantes de notificaciones
- `DownloadStats` modelo
4. **`/Sources/Examples/IntegrationExample.swift`** (250 líneas)
- Ejemplos de integración con TabView
- Ejemplo de navegación desde MangaDetailView
- Badge en TabView para descargas activas
- Sheet para descargas
- Vista de configuración
- Widget de descargas activas
- Modificador de banner
5. **`/Sources/Services/DOWNLOAD_SYSTEM_README.md`** (400 líneas)
- Documentación completa del sistema
- Guía de uso de todos los componentes
- Ejemplos de código
- Configuración y parámetros
- Best practices
- Troubleshooting
### Archivos Modificados
1. **`/Sources/Views/MangaDetailView.swift`**
- Actualizado `ChapterRowView` para mostrar progreso de descarga
- Añadido botón de descarga individual por capítulo
- Actualizado `MangaDetailViewModel`:
- Integración con `DownloadManager`
- Métodos para descargar capítulos
- Notificaciones de completado/error
- Seguimiento de progreso
- Añadido overlay de notificaciones
2. **`/Sources/Models/Manga.swift`** (sin cambios)
- Ya contiene los modelos necesarios:
- `DownloadedChapter`
- `ReadingProgress`
- `MangaPage`
3. **`/Sources/Services/StorageService.swift`** (sin cambios)
- Ya contiene métodos necesarios:
- `saveImage()`
- `getImageURL()`
- `isChapterDownloaded()`
- `getChapterDirectory()`
- `getStorageSize()`
## Características Implementadas
### 1. DownloadManager (Gerente de Descargas)
- ✅ Descarga asíncrona de imágenes con async/await
- ✅ Concurrencia controlada (3 capítulos, 5 imágenes simultáneas)
- ✅ Cancelación de descargas (individual o masiva)
- ✅ Progreso en tiempo real
- ✅ Manejo robusto de errores
- ✅ Historial de descargas (completadas y fallidas)
- ✅ Integración con StorageService
- ✅ Verificación de duplicados
### 2. MangaDetailView Actualizado
- ✅ Botón de descarga en toolbar
- ✅ Descarga individual por capítulo
- ✅ Progreso visible en cada fila
- ✅ Notificaciones de estado
- ✅ Alert para descargar múltiples capítulos
- ✅ Indicador visual de capítulos descargados
### 3. DownloadsView (Vista de Descargas)
- ✅ Tabs: Activas, Completadas, Fallidas
- ✅ Cards con información detallada
- ✅ Cancelación de descargas
- ✅ Limpieza de historiales
- ✅ Información de almacenamiento usado
- ✅ Alert para limpiar todo
- ✅ Estados vacíos descriptivos
### 4. Extensiones y Utilidades
- ✅ Formateo de tamaños de archivo
- ✅ Estimación de tiempo restante
- ✅ Optimización de imágenes
- ✅ Compresión JPEG configurable
- ✅ Notificaciones del sistema
- ✅ URLSession configurada
## Flujo de Descarga Completo
```
1. Usuario toca botón de descarga
2. DownloadManager.downloadChapter()
3. ManhwaWebScraper.scrapeChapterImages()
4. Se crea DownloadTask con estado .pending
5. downloadImages() inicia con TaskGroup
6. Por cada imagen:
- downloadImage() desde URL
- UIImage.optimizedForStorage()
- StorageService.saveImage()
- Actualizar progreso
7. Al completar todas:
- StorageService.saveDownloadedChapter()
- Mover tarea a completadas
- Notificar usuario
8. Capítulo marcado como descargado
```
## Concurrencia y Performance
### Estrategia de Concurrencia
```swift
// Nivel 1: Descarga de capítulos (máximo 3 en paralelo)
await withTaskGroup(of: Void.self) { group in
for chapter in chapters {
group.addTask {
try await downloadChapter(chapter)
}
}
}
// Nivel 2: Descarga de imágenes por capítulo (máximo 5 en paralelo)
try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
for (index, imageURL) in imageURLs.enumerated() {
group.addTask {
return (index, try await downloadImage(from: imageURL))
}
}
}
```
### Optimizaciones de Memoria
- Imágenes comprimidas al 75-80% JPEG
- Redimensionado si > 2048px
- Concurrencia limitada para evitar picos
- Limpieza automática de historiales
## Manejo de Errores
### Tipos de Errores
```swift
enum DownloadError {
case alreadyDownloaded // Ya existe
case noImagesFound // Scraper falló
case invalidURL // URL malformada
case invalidResponse // Error HTTP
case httpError(statusCode) // 4xx, 5xx
case invalidImageData // No es imagen
case cancelled // Usuario canceló
case storageError(String) // Error disco
}
```
### Recuperación
- Reintentos automáticos en errores de red
- Limpieza de archivos parciales
- Logging de errores para debugging
- Mensajes descriptivos al usuario
## Integración con StorageService
### Guardado de Imágenes
```swift
try await storage.saveImage(
image, // UIImage optimizada
mangaSlug: "manga-slug",
chapterNumber: 1,
pageIndex: 0
)
// Guarda en: Documents/Chapters/manga-slug/Chapter1/page_0.jpg
```
### Verificación de Descarga
```swift
if storage.isChapterDownloaded(
mangaSlug: "manga-slug",
chapterNumber: 1
) {
// Ya está descargado
}
```
### Lectura de Imágenes
```swift
if let imageURL = storage.getImageURL(
mangaSlug: "manga-slug",
chapterNumber: 1,
pageIndex: 0
) {
// Usar URL local
AsyncImage(url: imageURL) { image in
image.resizable()
}
}
```
## UI/UX Implementada
### Notificaciones
- Toast notification al completar
- Icono verde (éxito) o rojo (error)
- Auto-ocultado después de 3 segundos
- Animación desde abajo
### Progreso Visual
- Barra de progreso lineal
- Porcentaje numérico
- Páginas descargadas/total
- Tiempo estimado restante
### Estados Vacíos
- Iconos grandes y descriptivos
- Mensajes claros
- Llamadas a la acción
### Estados de Descarga
- ⏳ Pending: Gris
- 🔄 Downloading: Azul con progreso
- ✅ Completed: Verde
- ❌ Failed: Rojo con mensaje
- ❌ Cancelled: Gris
## Testing y Debugging
### Logs Implementados
```swift
print("Downloading chapter \(chapter.number)")
print("Error downloading chapter: \(error.localizedDescription)")
```
### Puntos de Verificación
- ¿El capítulo ya está descargado?
- ¿Se encontraron imágenes?
- ¿Las URLs son válidas?
- ¿Las imágenes son válidas?
- ¿Hay espacio disponible?
### Métricas Disponibles
- Número de descargas activas
- Progreso general
- Tiempo restante estimado
- Tamaño de almacenamiento
- Tasa de éxito
## Configuración
### Parámetros Ajustables
```swift
// En DownloadManager
private let maxConcurrentDownloads = 3
private let maxConcurrentImagesPerChapter = 5
// En StorageService.saveImage()
image.jpegData(compressionQuality: 0.8)
// En DownloadExtensions
let maxDimension: CGFloat = 2048
return resized.compressedData(quality: 0.75)
```
### Timeouts
- URLSession request: 30 segundos
- URLSession resource: 5 minutos
- Espera carga de página scraper: 3-5 segundos
## Uso Recomendado
### En Tu App Principal
1. Agregar `DownloadsView` a tu TabView principal
2. Opcional: Añadir badge con count de descargas activas
3. Usar `ActiveDownloadsWidget` en home
4. Implementar navegación desde `MangaDetailView`
### En ReaderView
1. Verificar si capítulo está descargado
2. Usar `storage.getImageURL()` para imágenes locales
3. Fallback a URLs remotas si no existe
### En SettingsView
1. Mostrar tamaño de almacenamiento usado
2. Botón para limpiar descargas
3. Estadísticas de descargas
4. Preferencias (solo Wi-Fi, etc.)
## Archivos de Configuración No Necesarios
El sistema no requiere:
- ❌ Info.plist modifications (permisos estándar)
- ❌ Entitlements especiales
- ❌ Background modes (opcional para futuro)
- ❌ Network configurations (usa URLSession por defecto)
## Next Steps Opcionales
### Mejoras Futuras
- [ ] Background downloads con URLSession
- [ ] Reanudar descargas pausadas
- [ ] Priorización de descargas
- [ ] Descarga automática de nuevos capítulos
- [ ] Compresión adicional (WebP)
- [ ] Batch operations
- [ ] Metrics y analytics
### Testing
- [ ] Unit tests para DownloadManager
- [ ] Integration tests
- [ ] UI tests para DownloadsView
- [ ] Performance tests
- [ ] Memory leak tests
### Documentación
- [ ] Vídeo demostrativo
- [ ] Screenshots en README
- [ ] Diagramas de secuencia
- [ ] API documentation
## Resumen Ejecutivo
**Tiempo de Desarrollo**: ~4-6 horas
**Líneas de Código**: ~1,500 líneas
**Archivos Creados**: 5 nuevos
**Archivos Modificados**: 2 existentes
**Complejidad**: Media-Alta
**Robustez**: Alta
**UX**: Excelente
**Estado**: ✅ COMPLETO Y FUNCIONAL

View File

@@ -1,300 +0,0 @@
# Quick Start - Sistema de Descarga
## Integración Rápida (5 minutos)
### Paso 1: Verificar Archivos
Los siguientes archivos ya están creados en tu proyecto:
```
ios-app/Sources/
├── Services/
│ ├── DownloadManager.swift ✅ 13KB
│ └── DOWNLOAD_SYSTEM_README.md ✅ Documentación completa
├── Views/
│ ├── DownloadsView.swift ✅ 13KB
│ └── MangaDetailView.swift ✅ Actualizado
├── Extensions/
│ └── DownloadExtensions.swift ✅ 4.7KB
├── Examples/
│ └── IntegrationExample.swift ✅ Ejemplos de integración
└── Tests/
└── DownloadManagerTests.swift ✅ Tests unitarios
```
### Paso 2: Agregar DownloadsView a Tu App
Si tienes un TabView, simplemente agrega:
```swift
// En tu ContentView o App principal
TabView {
ContentView() // Tu vista actual
.tabItem {
Label("Biblioteca", systemImage: "books.vertical")
}
DownloadsView() // NUEVA VISTA
.tabItem {
Label("Descargas", systemImage: "arrow.down.circle")
}
.badge(downloadManager.activeDownloads.count) // Opcional: badge
SettingsView()
.tabItem {
Label("Ajustes", systemImage: "gear")
}
}
```
### Paso 3: Probar la Descarga
1. Abre `MangaDetailView` (ya está actualizado)
2. Toca el botón de descarga (icono de flecha hacia abajo) en la toolbar
3. Selecciona "Descargar últimos 10" o "Descargar todos"
4. Observa el progreso en cada fila de capítulo
5. Ve a la tab "Descargas" para ver el progreso detallado
¡Eso es todo! El sistema está completamente integrado.
## Características Incluidas
### ✅ Ya Funciona
- Descarga de capítulos individuales
- Descarga masiva (todos o últimos N)
- Progreso en tiempo real
- Cancelación de descargas
- Historial de descargas
- Notificaciones de estado
- Gestión de almacenamiento
- Manejo de errores
### 📱 UI Components
- `DownloadsView` - Vista completa con tabs
- `ActiveDownloadCard` - Card con progreso
- `CompletedDownloadCard` - Card de completados
- `FailedDownloadCard` - Card con reintentar
- Toast notifications
- Progress bars
### 🔧 Services
- `DownloadManager` - Singleton gerente de descargas
- `DownloadTask` - Modelo de tarea individual
- `DownloadState` - Estados de descarga
- `DownloadError` - Tipos de error
## Uso Básico
### Desde MangaDetailView
```swift
// Ya está implementado en MangaDetailView
// El usuario solo necesita tocar el botón de descarga
```
### Programáticamente
```swift
let downloadManager = DownloadManager.shared
// Descargar un capítulo
try await downloadManager.downloadChapter(
mangaSlug: manga.slug,
mangaTitle: manga.title,
chapter: chapter
)
// Descargar múltiples
await downloadManager.downloadChapters(
mangaSlug: manga.slug,
mangaTitle: manga.title,
chapters: chapters
)
// Cancelar descarga
downloadManager.cancelDownload(taskId: taskId)
// Cancelar todas
downloadManager.cancelAllDownloads()
```
### Verificar Descargas
```swift
let storage = StorageService.shared
// ¿Está descargado?
if storage.isChapterDownloaded(
mangaSlug: manga.slug,
chapterNumber: 1
) {
// Usar imagen local
let imageURL = storage.getImageURL(
mangaSlug: manga.slug,
chapterNumber: 1,
pageIndex: 0
)
}
```
## Personalización Opcional
### Ajustar Concurrencia
En `DownloadManager.swift`:
```swift
private let maxConcurrentDownloads = 3 // Capítulos simultáneos
private let maxConcurrentImagesPerChapter = 5 // Imágenes simultáneas
```
### Ajustar Calidad de Imagen
En `StorageService.swift`:
```swift
image.jpegData(compressionQuality: 0.8) // 80% de calidad
```
En `DownloadExtensions.swift`:
```swift
let maxDimension: CGFloat = 2048 // Redimensionar si es mayor
return resized.compressedData(quality: 0.75) // 75% de calidad
```
## Solución de Problemas
### Las descargas no inician
1. Verificar conexión a internet
2. Verificar que ManhwaWebScraper funciona
3. Verificar logs en consola
### El progreso no se actualiza
1. Asegurar que estás en @MainActor
2. Verificar que las propiedades son @Published
3. Verificar que observas DownloadManager
### Error "Already downloaded"
1. Es normal - el capítulo ya existe
2. Usa `storage.deleteDownloadedChapter()` para eliminar
3. O permite sobrescribir
### Las imágenes no se guardan
1. Verificar permisos de la app
2. Verificar espacio disponible
3. Verificar que directorios existen
## Próximos Pasos
### Opcional: Badge en TabView
```swift
struct MainTabView: View {
@StateObject private var downloadManager = DownloadManager.shared
var body: some View {
TabView {
// ...
DownloadsView()
.tabItem {
Label("Descargas", systemImage: "arrow.down.circle")
}
.badge(downloadManager.activeDownloads.count) // Badge
}
}
}
```
### Opcional: Widget en Home
```swift
struct ContentView: View {
@ObservedObject var downloadManager = DownloadManager.shared
var body: some View {
ScrollView {
// Tu contenido actual
if downloadManager.hasActiveDownloads {
ActiveDownloadsWidget()
}
}
}
}
```
### Opcional: Banner de Descargas
```swift
struct ContentView: View {
var body: some View {
MangaDetailView(manga: manga)
.activeDownloadsBanner() // Modificador personalizado
}
}
```
## Testing
### Manual
1. Descargar un capítulo
2. Cancelar una descarga
3. Descargar múltiples capítulos
4. Probar sin internet
5. Limpiar almacenamiento
### Automatizado
Los tests están en `/Sources/Tests/DownloadManagerTests.swift`
Para ejecutar en Xcode:
1. Cmd + U
2. O Product → Test
## Archivos de Referencia
### Documentación
- `DOWNLOAD_SYSTEM_README.md` - Guía completa (400 líneas)
- `IMPLEMENTATION_SUMMARY.md` - Resumen ejecutivo
- `DIAGRAMS.md` - Diagramas de flujo
- `CHECKLIST.md` - Checklist de implementación
### Código
- `DownloadManager.swift` - Core del sistema
- `DownloadsView.swift` - Vista principal
- `DownloadExtensions.swift` - Extensiones útiles
- `IntegrationExample.swift` - Ejemplos de integración
## Soporte
### Problemas Comunes
**"No se compila"**
- Asegúrate de tener iOS 15+
- Verificar que todos los archivos están en el target
- Limpiar carpeta de builds (Cmd + Shift + K)
**"Las descargas fallan"**
- Verificar que ManhwaWebScraper funciona correctamente
- Probar con diferentes capítulos
- Verificar logs en consola
**"No se guardan las imágenes"**
- Verificar permisos en Info.plist
- Probar en dispositivo real (no simulador)
- Verificar espacio disponible
### Contacto
Para más ayuda, consulta:
1. `DOWNLOAD_SYSTEM_README.md` - Documentación completa
2. `DIAGRAMS.md` - Diagramas de flujo
3. `IntegrationExample.swift` - Ejemplos de código
---
**Tiempo de integración**: 5 minutos
**Dificultad**: Fácil
**Estado**: ✅ COMPLETO
¡Happy coding! 🚀

View File

@@ -1,343 +0,0 @@
# Sistema de Descarga de Capítulos - MangaReader iOS
## Overview
El sistema de descarga de capítulos permite a los usuarios descargar capítulos completos de manga para lectura offline. El sistema está diseñado con arquitectura asíncrona moderna usando Swift async/await.
## Componentes Principales
### 1. DownloadManager (`/Sources/Services/DownloadManager.swift`)
Gerente centralizado que maneja todas las operaciones de descarga.
**Características:**
- Descarga asíncrona de imágenes con concurrencia controlada
- Máximo 3 descargas simultáneas de capítulos
- Máximo 5 imágenes simultáneas por capítulo
- Cancelación de descargas individuales o masivas
- Seguimiento de progreso en tiempo real
- Manejo robusto de errores
- Historial de descargas completadas y fallidas
**Uso básico:**
```swift
let downloadManager = DownloadManager.shared
// Descargar un capítulo
try await downloadManager.downloadChapter(
mangaSlug: "one-piece",
mangaTitle: "One Piece",
chapter: chapter
)
// Descargar múltiples capítulos
await downloadManager.downloadChapters(
mangaSlug: "one-piece",
mangaTitle: "One Piece",
chapters: chapters
)
// Cancelar descarga
downloadManager.cancelDownload(taskId: "taskId")
// Cancelar todas
downloadManager.cancelAllDownloads()
```
### 2. MangaDetailView (`/Sources/Views/MangaDetailView.swift`)
Vista de detalles del manga con funcionalidad de descarga integrada.
**Características añadidas:**
- Botón de descarga en la toolbar
- Descarga individual por capítulo
- Progreso de descarga visible en cada fila de capítulo
- Notificaciones de completado/error
- Alert para descargar últimos 10 o todos los capítulos
**Flujo de descarga:**
1. Usuario toca botón de descarga en toolbar → muestra alert
2. Selecciona cantidad de capítulos a descargar
3. Cada capítulo muestra progreso de descarga en tiempo real
4. Notificación aparece al completar todas las descargas
5. Capítulos descargados muestran checkmark verde
### 3. DownloadsView (`/Sources/Views/DownloadsView.swift`)
Vista dedicada para gestionar todas las descargas.
**Tabs:**
- **Activas**: Descargas en progreso con barra de progreso
- **Completadas**: Historial de descargas exitosas
- **Fallidas**: Descargas con errores, permite reintentar
**Funcionalidades:**
- Cancelar descargas individuales
- Cancelar todas las descargas activas
- Limpiar historiales (completadas/fallidas)
- Ver tamaño de almacenamiento usado
- Limpiar todo el almacenamiento descargado
### 4. StorageService (`/Sources/Services/StorageService.swift`)
Servicio de almacenamiento ya existente, ahora con soporte para descargas.
**Métodos utilizados:**
```swift
// Guardar imagen descargada
try await storage.saveImage(
image,
mangaSlug: "manga-slug",
chapterNumber: 1,
pageIndex: 0
)
// Verificar si capítulo está descargado
storage.isChapterDownloaded(mangaSlug: "manga-slug", chapterNumber: 1)
// Obtener directorio del capítulo
let chapterDir = storage.getChapterDirectory(
mangaSlug: "manga-slug",
chapterNumber: 1
)
// Obtener URL de imagen local
if let imageURL = storage.getImageURL(
mangaSlug: "manga-slug",
chapterNumber: 1,
pageIndex: 0
) {
// Usar imagen local
}
// Eliminar capítulo descargado
storage.deleteDownloadedChapter(
mangaSlug: "manga-slug",
chapterNumber: 1
)
// Obtener tamaño de almacenamiento
let size = storage.getStorageSize()
let formatted = storage.formatFileSize(size)
```
## Modelos de Datos
### DownloadTask
Representa una tarea de descarga individual:
```swift
class DownloadTask: ObservableObject {
let id: String
let mangaSlug: String
let mangaTitle: String
let chapterNumber: Int
let imageURLs: [String]
@Published var state: DownloadState
@Published var downloadedPages: Int
@Published var progress: Double
}
```
### DownloadState
Estados posibles de una descarga:
```swift
enum DownloadState {
case pending
case downloading(progress: Double)
case completed
case failed(error: String)
case cancelled
}
```
### DownloadError
Tipos de errores de descarga:
```swift
enum DownloadError: LocalizedError {
case alreadyDownloaded
case noImagesFound
case invalidURL
case invalidResponse
case httpError(statusCode: Int)
case invalidImageData
case cancelled
case storageError(String)
}
```
## Configuración
### Parámetros de Descarga
En `DownloadManager`:
```swift
private let maxConcurrentDownloads = 3 // Máximo de capítulos simultáneos
private let maxConcurrentImagesPerChapter = 5 // Máximo de imágenes simultáneas por capítulo
```
### Calidad de Imagen
En `StorageService.saveImage()`:
```swift
image.jpegData(compressionQuality: 0.8) // 80% de calidad JPEG
```
En `DownloadExtensions`:
```swift
func optimizedForStorage() -> Data? {
// Redimensiona si > 2048px
// Comprime a 75% de calidad
}
```
## Integración con ReaderView
Para leer capítulos descargados:
```swift
struct ReaderView: View {
let chapter: Chapter
let mangaSlug: String
@StateObject private var storage = StorageService.shared
var body: some View {
ScrollView {
LazyVStack {
ForEach(pageIndices, id: \.self) { index in
if let imageURL = storage.getImageURL(
mangaSlug: mangaSlug,
chapterNumber: chapter.number,
pageIndex: index
) {
// Usar imagen local
AsyncImage(url: imageURL) { image in
image.resizable()
} placeholder: {
ProgressView()
}
} else {
// Fallback a URL remota
RemoteChapterPage(url: remoteURL)
}
}
}
}
}
}
```
## Notificaciones
El sistema emite notificaciones para seguimiento:
```swift
extension Notification.Name {
static let downloadDidStart = Notification.Name("downloadDidStart")
static let downloadDidUpdate = Notification.Name("downloadDidUpdate")
static let downloadDidComplete = Notification.Name("downloadDidComplete")
static let downloadDidFail = Notification.Name("downloadDidFail")
static let downloadDidCancel = Notification.Name("downloadDidCancel")
}
```
## Manejo de Errores
### Errores de Red
- Timeout: 30 segundos por imagen
- Reintentos: Manejados por URLSession
- HTTP errors: Capturados y reportados en UI
### Errores de Almacenamiento
- Espacio insuficiente: Error con mensaje descriptivo
- Permisos: Manejados por FileManager
- Corrupción de archivos: Archivos eliminados y descarga reiniciada
### Errores de Scraping
- No se encontraron imágenes: Error `noImagesFound`
- Página no carga: Error del scraper propagado
- Cambios en la web: Requieren actualización del scraper
## Best Practices
### 1. Concurrencia
El sistema usa Swift Concurrency:
- `async/await` para operaciones asíncronas
- `Task` para crear contextos de concurrencia
- `@MainActor` para actualizaciones de UI
- `TaskGroup` para descargas en paralelo
### 2. Memoria
- Imágenes comprimidas antes de guardar
- Descarga limitada a 5 imágenes simultáneas
- Limpieza automática de historiales (50 completadas, 20 fallidas)
### 3. UX
- Progreso visible en tiempo real
- Cancelación en cualquier punto
- Notificaciones de estado
- Estados vacíos descriptivos
- Feedback inmediato de acciones
### 4. Robustez
- Validación de estados antes de descargar
- Limpieza de archivos parciales al cancelar
- Verificación de archivos existentes
- Manejo exhaustivo de errores
## Testing
### Pruebas Unitarias
```swift
func testDownloadManager() async throws {
let manager = DownloadManager.shared
// Probar descarga individual
try await manager.downloadChapter(
mangaSlug: "test",
mangaTitle: "Test Manga",
chapter: testChapter
)
XCTAssertTrue(manager.activeDownloads.isEmpty)
XCTAssertEqual(manager.completedDownloads.count, 1)
}
```
### Pruebas de Integración
- Descargar capítulo completo
- Cancelar descarga a mitad
- Descargar múltiples capítulos
- Probar con y sin conexión
- Verificar persistencia de archivos
## Troubleshooting
### Descargas no inician
- Verificar conexión a internet
- Verificar que el scraper puede acceder a la web
- Revisar logs del scraper
### Progreso no actualiza
- Asegurar que las vistas están en @MainActor
- Verificar que DownloadTask es @ObservedObject
- Chequear que las propiedades son @Published
### Archivos no se guardan
- Verificar permisos de la app
- Chequear espacio disponible
- Revisar que directorios existen
### Imágenes corruptas
- Verificar calidad de compresión
- Chequear que URLs sean válidas
- Probar redimensionado de imágenes
## Futuras Mejoras
- [ ] Soporte para reanudar descargas pausadas
- [ ] Priorización de descargas
- [ ] Descarga automática de nuevos capítulos
- [ ] Compresión adicional de imágenes
- [ ] Soporte para formatos WebP
- [ ] Batch operations en StorageService
- [ ] Background downloads con URLSession
- [ ] Metrics y analytics de descargas

View File

@@ -1,224 +0,0 @@
# MangaReader - Suite de Tests Completa
## Resumen Ejecutivo
He creado una suite completa de tests para el proyecto MangaReader que incluye **~120 tests** distribuidos en **4,900+ líneas de código**.
## Archivos Creados (11 archivos)
### Tests Principales (4 archivos, ~1,850 líneas)
1. **ModelTests.swift** (350 líneas) - Tests para modelos de datos
2. **StorageServiceTests.swift** (500 líneas) - Tests para servicio de almacenamiento
3. **ManhwaWebScraperTests.swift** (450 líneas) - Tests para web scraper
4. **IntegrationTests.swift** (550 líneas) - Tests de integración completa
### Helpers y Utilidades (4 archivos, ~1,300 líneas)
5. **TestHelpers.swift** (400 líneas) - Factories y helpers para tests
6. **XCTestSuiteExtensions.swift** (250 líneas) - Extensiones de XCTest
7. **XCTestManifests.swift** (200 líneas) - Manifests de test suites
8. **TestExamples.swift** (450 líneas) - Ejemplos y plantillas
### Documentación (3 archivos, ~1,800 líneas)
9. **README.md** (400 líneas) - Documentación completa de tests
10. **TEST_SUMMARY.md** (500 líneas) - Resumen detallado de la suite
11. **run_tests.sh** (200 líneas) - Script para ejecutar tests
## Cobertura de Tests
### Por Componente
| Componente | Tests | Cobertura | Estado |
|------------|-------|-----------|--------|
| **Modelos** | 35 | 95%+ | ✅ Completo |
| **StorageService** | 40 | 90%+ | ✅ Completo |
| **ManhwaWebScraper** | 25 | 85%+ | ✅ Completo |
| **Integración** | 20 | 80%+ | ✅ Completo |
### Por Tipo de Test
- **Tests Unitarios**: 100 tests (83%)
- **Tests de Integración**: 20 tests (17%)
- **Tests de Performance**: 7 tests
- **Tests de Concurrencia**: 6 tests
- **Tests de Edge Cases**: 20+ tests
## Características Implementadas
### 1. Tests de Modelos (ModelTests.swift)
- ✅ Codable serialization/deserialization
- ✅ Validación de datos
- ✅ Hashable compliance
- ✅ Cálculo de propiedades derivadas
- ✅ Edge cases (valores vacíos, nil, negativos)
- ✅ Performance tests
### 2. Tests de Storage (StorageServiceTests.swift)
- ✅ Gestión de favoritos (CRUD completo)
- ✅ Reading progress tracking
- ✅ Downloaded chapters management
- ✅ Image caching
- ✅ Storage management (size, cleanup)
- ✅ Operaciones concurrentes
- ✅ Tests de gran escala (1000+ operaciones)
### 3. Tests de Scraper (ManhwaWebScraperTests.swift)
- ✅ Mock de WKWebView responses
- ✅ Parsing de JavaScript results
- ✅ Chapter parsing y deduplication
- ✅ Image filtering
- ✅ Manga info extraction
- ✅ URL construction
- ✅ Error handling
- ✅ Performance tests (1000+ items)
### 4. Tests de Integración (IntegrationTests.swift)
- ✅ Flujo completo scraper -> storage
- ✅ Descarga de capítulos con imágenes
- ✅ Reading progress tracking
- ✅ Favorite management
- ✅ Multi-manga scenarios
- ✅ Concurrent operations
- ✅ Data persistence
- ✅ Large scale operations
### 5. Helpers y Utilities
- ✅ TestDataFactory (crear objetos de prueba)
- ✅ ImageTestHelpers (crear imágenes)
- ✅ FileSystemTestHelpers (operaciones de archivos)
- ✅ StorageTestHelpers (limpieza y seed data)
- ✅ AsyncTestHelpers (operaciones asíncronas)
- ✅ ScraperTestHelpers (mocks de HTML/JS)
- ✅ AssertionHelpers (asserts personalizados)
- ✅ PerformanceTestHelpers (medición de rendimiento)
### 6. Extensiones de XCTest
- ✅ Async helpers (wait, waitForOperation)
- ✅ Error assertions (assertThrowsError, assertNoThrow)
- ✅ Custom assertions (assertDatesEqual, assertEmpty, etc.)
- ✅ Memory leak detection
- ✅ Test logging
- ✅ Metrics tracking
## Cómo Usar
### Ejecutar Todos los Tests
```bash
# Desde Xcode
Cmd + U
# Desde terminal
./run_tests.sh --all
# Con cobertura
./run_tests.sh --all --coverage
```
### Ejecutar Tests Específicos
```bash
# Solo unitarios
./run_tests.sh --unit
# Solo integración
./run_tests.sh --integration
# Con output detallado
./run_tests.sh --all --verbose
```
### En Xcode
- **Cmd + U**: Ejecutar todos los tests
- **Cmd + 6**: Abrir Test Navigator
- **Click derecho en test**: Run individual test
## Archivos de Tests
```
/home/ren/ios/MangaReader/ios-app/Tests/
├── ModelTests.swift # Tests de modelos (35 tests)
├── StorageServiceTests.swift # Tests de storage (40 tests)
├── ManhwaWebScraperTests.swift # Tests de scraper (25 tests)
├── IntegrationTests.swift # Tests de integración (20 tests)
├── TestHelpers.swift # Helpers y factories
├── XCTestSuiteExtensions.swift # Extensiones de XCTest
├── XCTestManifests.swift # Manifests de test suites
├── TestExamples.swift # Ejemplos y plantillas
├── README.md # Documentación completa
├── TEST_SUMMARY.md # Resumen detallado
└── run_tests.sh # Script de ejecución
```
## Estadísticas Finales
- **Total Tests**: ~120
- **Total Líneas de Código**: ~4,900
- **Cobertura Promedio**: 87%+
- **Tests Unitarios**: 100 (83%)
- **Tests de Integración**: 20 (17%)
- **Tests de Performance**: 7
- **Tests de Concurrencia**: 6
- **Tests de Edge Cases**: 20+
## Próximos Pasos
1. **Agregar tests al target de Xcode**
- Abrir el proyecto en Xcode
- Agregar los archivos de tests
- Configurar el test target
2. **Ejecutar los tests**
- Cmd + U para ejecutar todos
- Verificar que pasan
- Ajustar si es necesario
3. **Configurar CI/CD**
- Agregar ejecución de tests en GitHub Actions
- Reportes de cobertura
- Tests en cada PR
4. **Mantener los tests**
- Actualizar cuando se agregan features
- Mantener cobertura > 85%
- Agregar tests para bugs encontrados
## Beneficios
### Calidad del Código
- ✅ Bugs detectados temprano
- ✅ Refactorización segura
- ✅ Documentación viva del código
### Confianza
- ✅ Tests independientes y ejecutables en cualquier orden
- ✅ Setup/teardown apropiado
- ✅ Mocks de dependencias externas
### Mantenibilidad
- ✅ Helpers reutilizables
- ✅ Ejemplos y plantillas
- ✅ Documentación completa
### Performance
- ✅ Tests de performance incluidos
- ✅ Tests de gran escala
- ✅ Métricas y benchmarks
## Recursos
- **README.md**: Guía completa de uso
- **TEST_SUMMARY.md**: Descripción detallada de cada test
- **TestExamples.swift**: Ejemplos y plantillas para nuevos tests
- **run_tests.sh --help**: Ayuda del script
## Contacto
Para preguntas o sugerencias sobre los tests, consultar:
- README.md para documentación general
- TestExamples.swift para ejemplos de código
- TEST_SUMMARY.md para detalles de cada test
---
**Creado**: 2026-02-04
**Versión**: 1.0
**Framework**: XCTest
**Plataforma**: iOS 15+

View File

@@ -1,426 +0,0 @@
# MangaReader Test Suite
Suite completa de tests para el proyecto MangaReader usando XCTest.
## Tabla de Contenidos
- [Descripción General](#descripción-general)
- [Estructura de Tests](#estructura-de-tests)
- [Ejecutar Tests](#ejecutar-tests)
- [Guía de Tests](#guía-de-tests)
- [Mejores Prácticas](#mejores-prácticas)
## Descripción General
Esta suite de tests cubre todos los componentes principales del proyecto MangaReader:
1. **Modelos de Datos** - Validación de Codable, edge cases, y lógica de negocio
2. **StorageService** - Almacenamiento local, favoritos, progreso de lectura
3. **ManhwaWebScraper** - Web scraping y parsing de HTML/JavaScript
4. **Integración** - Flujos completos que conectan múltiples componentes
## Estructura de Tests
```
Tests/
├── ModelTests.swift # Tests para modelos de datos
├── StorageServiceTests.swift # Tests para servicio de almacenamiento
├── ManhwaWebScraperTests.swift # Tests para web scraper
├── IntegrationTests.swift # Tests de integración
├── TestHelpers.swift # Helpers y factories para tests
└── XCTestSuiteExtensions.swift # Extensiones de XCTest
```
## Ejecutar Tests
### Desde Xcode
1. Abrir el proyecto en Xcode
2. Cmd + U para ejecutar todos los tests
3. Cmd + 6 para abrir el Test Navigator
4. Click derecho en un test específico para ejecutarlo
### Desde Línea de Comandos
```bash
# Ejecutar todos los tests
xcodebuild test -scheme MangaReader -destination 'platform=iOS Simulator,name=iPhone 15'
# Ejecutar tests específicos
xcodebuild test -scheme MangaReader -only-testing:MangaReaderTests/ModelTests
# Ejecutar con cobertura
xcodebuild test -scheme MangaReader -enableCodeCoverage YES
```
## Guía de Tests
### ModelTests.swift
Prueba todos los modelos de datos del proyecto.
#### Tests Incluidos:
**Manga Model:**
- `testMangaInitialization` - Verifica inicialización correcta
- `testMangaCodableSerialization` - Prueba encoding/decoding JSON
- `testMangaDisplayStatus` - Verifica traducción de estados
- `testMangaHashable` - Prueba conformidad con Hashable
**Chapter Model:**
- `testChapterInitialization` - Inicialización con valores por defecto
- `testChapterDisplayNumber` - Formato de número de capítulo
- `testChapterProgress` - Cálculo de progreso de lectura
- `testChapterCodableSerialization` - Serialización JSON
**MangaPage Model:**
- `testMangaPageInitialization` - Creación de páginas
- `testMangaPageThumbnailURL` - URLs de thumbnails
- `testMangaPageCodableSerialization` - Serialización
**ReadingProgress Model:**
- `testReadingProgressInitialization` - Creación de progreso
- `testReadingProgressIsCompleted` - Lógica de completación
- `testReadingProgressCodableSerialization` - Persistencia
**DownloadedChapter Model:**
- `testDownloadedChapterInitialization` - Creación de capítulos descargados
- `testDownloadedChapterDisplayTitle` - Formato de títulos
- `testDownloadedChapterCodableSerialization` - Serialización completa
**Edge Cases:**
- `testMangaWithEmptyGenres` - Manejo de arrays vacíos
- `testMangaWithNilCoverImage` - Imagen de portada opcional
- `testChapterWithZeroNumber` - Capítulo cero
- `testMangaPageWithNegativeIndex` - Índices negativos
### StorageServiceTests.swift
Prueba el servicio de almacenamiento local.
#### Tests Incluidos:
**Favorites:**
- `testSaveFavorite` - Guardar un favorito
- `testSaveMultipleFavorites` - Guardar varios favoritos
- `testSaveDuplicateFavorite` - Evitar duplicados
- `testRemoveFavorite` - Eliminar favorito
- `testIsFavorite` - Verificar si es favorito
**Reading Progress:**
- `testSaveReadingProgress` - Guardar progreso
- `testSaveMultipleReadingProgress` - Múltiples progresos
- `testUpdateExistingReadingProgress` - Actualizar progreso
- `testGetLastReadChapter` - Obtener último capítulo leído
- `testGetReadingProgressWhenNotExists` - Progreso inexistente
**Downloaded Chapters:**
- `testSaveDownloadedChapter` - Guardar metadatos de capítulo
- `testIsChapterDownloaded` - Verificar descarga
- `testGetDownloadedChapters` - Listar capítulos
- `testDeleteDownloadedChapter` - Eliminar capítulo
**Image Caching:**
- `testSaveAndLoadImage` - Guardar y cargar imagen
- `testLoadNonExistentImage` - Imagen inexistente
- `testGetImageURL` - Obtener URL de imagen
**Storage Management:**
- `testGetStorageSize` - Calcular tamaño usado
- `testClearAllDownloads` - Limpiar todo el almacenamiento
- `testFormatFileSize` - Formatear tamaño a legible
**Concurrent Operations:**
- `testConcurrentImageSave` - Guardar imágenes concurrentemente
### ManhwaWebScraperTests.swift
Prueba el web scraper con mocks de WKWebView.
#### Tests Incluidos:
**Error Handling:**
- `testScrapingErrorDescriptions` - Descripciones de errores
- `testScrapingErrorLocalizedError` - Conformidad con LocalizedError
**Chapter Parsing:**
- `testChapterParsingFromJavaScriptResult` - Parsear respuesta JS
- `testChapterParsingWithInvalidData` - Manejar datos inválidos
- `testChapterDeduplication` - Eliminar capítulos duplicados
- `testChapterSorting` - Ordenar capítulos
**Image Parsing:**
- `testImageParsingFromJavaScriptResult` - Parsear URLs de imágenes
- `testImageParsingWithEmptyArray` - Array vacío de imágenes
- `testImageParsingWithInvalidURLs` - Filtrar URLs inválidas
**Manga Info Parsing:**
- `testMangaInfoParsingFromJavaScriptResult` - Extraer info de manga
- `testMangaInfoParsingWithEmptyFields` - Campos vacíos
- `testMangaStatusParsing` - Normalizar estados
**URL Construction:**
- `testMangaURLConstruction` - Construir URLs de manga
- `testChapterURLConstruction` - Construir URLs de capítulo
- `testURLConstructionWithSpecialCharacters` - Caracteres especiales
**Edge Cases:**
- `testChapterNumberExtraction` - Extraer números de capítulo
- `testChapterSlugExtraction` - Extraer slugs
- `testDuplicateRemovalPreservingOrder` - Eliminar duplicados manteniendo orden
### IntegrationTests.swift
Prueba flujos completos que integran múltiples componentes.
#### Tests Incluidos:
**Complete Flow:**
- `testCompleteScrapingAndStorageFlow` - Scraper -> Storage
- `testChapterDownloadFlow` - Descarga completa de capítulo
- `testReadingProgressTrackingFlow` - Seguimiento de lectura
**Multi-Manga Scenarios:**
- `testMultipleMangasProgressTracking` - Varios mangas
- `testMultipleChapterDownloads` - Descargas de múltiples capítulos
**Error Handling:**
- `testDownloadFlowWithMissingImages` - Imágenes faltantes
- `testStorageCleanupFlow` - Limpieza de almacenamiento
**Data Persistence:**
- `testDataPersistenceAcrossOperations` - Persistencia de datos
**Concurrent Operations:**
- `testConcurrentFavoriteOperations` - Operaciones concurrentes favoritos
- `testConcurrentProgressOperations` - Operaciones concurrentes progreso
- `testConcurrentImageOperations` - Guardado concurrente de imágenes
**Large Scale:**
- `testLargeScaleFavoriteOperations` - 1000 favoritos
- `testLargeScaleProgressOperations` - 500 progresos
## TestHelpers.swift
Proporciona helpers y factories para crear datos de prueba:
### TestDataFactory
Crea objetos de prueba:
```swift
let manga = TestDataFactory.createManga(
slug: "test-manga",
title: "Test Manga"
)
let chapter = TestDataFactory.createChapter(number: 1)
let chapters = TestDataFactory.createChapters(count: 10)
```
### ImageTestHelpers
Crea imágenes de prueba:
```swift
let image = ImageTestHelpers.createTestImage(
color: .blue,
size: CGSize(width: 800, height: 1200)
)
```
### FileSystemTestHelpers
Operaciones de sistema de archivos:
```swift
let tempDir = try FileSystemTestHelpers.createTemporaryDirectory()
try FileSystemTestHelpers.createTestChapterStructure(
mangaSlug: "test",
chapterNumber: 1,
pageCount: 10,
in: tempDir
)
```
### StorageTestHelpers
Limpieza y preparación de almacenamiento:
```swift
StorageTestHelpers.clearAllStorage()
StorageTestHelpers.seedTestData(
favoriteCount: 5,
progressCount: 10
)
```
## Mejores Prácticas
### 1. Independencia de Tests
Cada test debe ser independiente y poder ejecutarse solo:
```swift
override func setUp() {
super.setUp()
// Limpiar estado antes del test
UserDefaults.standard.removeObject(forKey: "favoritesKey")
}
override func tearDown() {
// Limpiar estado después del test
super.tearDown()
}
```
### 2. Nombres Descriptivos
Usa nombres que describan qué se está probando:
```swift
// Bueno
func testSaveDuplicateFavoriteDoesNotAddDuplicate()
// Malo
func testFavorite()
```
### 3. Un Assert por Test
Cuando sea posible, usa un assert por test:
```swift
// Bueno
func testFavoriteIsSaved() {
storageService.saveFavorite(mangaSlug: "test")
XCTAssertTrue(storageService.isFavorite(mangaSlug: "test"))
}
func testFavoriteIsRemoved() {
storageService.saveFavorite(mangaSlug: "test")
storageService.removeFavorite(mangaSlug: "test")
XCTAssertFalse(storageService.isFavorite(mangaSlug: "test"))
}
// Evitar
func testFavoriteOperations() {
storageService.saveFavorite(mangaSlug: "test")
XCTAssertTrue(storageService.isFavorite(mangaSlug: "test"))
storageService.removeFavorite(mangaSlug: "test")
XCTAssertFalse(storageService.isFavorite(mangaSlug: "test"))
}
```
### 4. AAA Pattern
Usa el patrón Arrange-Act-Assert:
```swift
func testChapterProgressCalculation() {
// Arrange - Preparar el test
var chapter = Chapter(number: 1, title: "Test", url: "", slug: "")
let expectedPage = 5
// Act - Ejecutar la acción
chapter.lastReadPage = expectedPage
// Assert - Verificar el resultado
XCTAssertEqual(chapter.progress, Double(expectedPage))
}
```
### 5. Mock de Dependencias
No hagas llamadas de red reales en tests unitarios:
```swift
// Bueno - Mock
let mockJSResult = [["number": 10, "title": "Chapter 10"]]
let chapters = parseChaptersFromJS(mockJSResult)
// Malo - Llamada real
let chapters = await scraper.scrapeChapters(mangaSlug: "test")
```
### 6. Tests Asíncronos
Usa `async/await` apropiadamente:
```swift
func testAsyncImageSave() async throws {
let image = createTestImage()
let url = try await storageService.saveImage(
image,
mangaSlug: "test",
chapterNumber: 1,
pageIndex: 0
)
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
}
```
## Cobertura de Código
Objetivos de cobertura:
- **Modelos**: 95%+ (lógica crítica de datos)
- **StorageService**: 90%+ (manejo de archivos y persistencia)
- **Scraper**: 85%+ (con mocks de WKWebView)
- **Integración**: 80%+ (flujos críticos de usuario)
## Troubleshooting
### Tests Fallan Intermittentemente
Si un test falla solo algunas veces:
1. Verifica que hay cleanup adecuado en `tearDown()`
2. Asegura que los tests son independientes
3. Usa `waitFor` apropiadamente para operaciones asíncronas
### Tests de Performance Fallan
Si los tests de rendimiento fallan en diferentes máquinas:
1. Ajusta las métricas según el hardware
2. Usa medidas relativas en lugar de absolutas
3. Considera deshabilitar tests de performance en CI
### Memory Leaks en Tests
Para detectar memory leaks:
```swift
func testNoMemoryLeak() {
let instance = MyClass()
assertNoMemoryLeak(instance)
}
```
## Recursos Adicionales
- [XCTest Documentation](https://developer.apple.com/documentation/xctest)
- [Testing with Xcode](https://developer.apple.com/documentation/xcode/testing)
- [Unit Testing Best Practices](https://www.objc.io/books/unit-testing/)
## Contribuir
Para agregar nuevos tests:
1. Decide si es unit test, integration test, o performance test
2. Agrega el test al archivo apropiado
3. Usa los helpers en `TestHelpers.swift` cuando sea posible
4. Asegura que el test es independiente
5. Agrega documentación si el test es complejo
6. Ejecuta todos los tests para asegurar que nada se rompe
## Licencia
Mismo que el proyecto principal.

View File

@@ -1,372 +0,0 @@
# Resumen de Tests Creados - MangaReader
## Archivos Creados
### 1. ModelTests.swift (~17 KB, 350+ líneas)
**Tests para modelos de datos:**
- **Manga Model Tests** (6 tests)
- Inicialización y validación de datos
- Codable serialization/deserialization
- displayStatus (traducción de estados)
- Hashable compliance
- Arrays vacíos y coverImage nil
- **Chapter Model Tests** (5 tests)
- Inicialización con valores por defecto
- displayNumber formatting
- Cálculo de progreso
- Codable y Hashable
- **MangaPage Model Tests** (4 tests)
- Creación de páginas
- thumbnailURL
- Codable y Hashable
- **ReadingProgress Model Tests** (3 tests)
- Inicialización
- Lógica isCompleted (páginas > 5)
- Codable con timestamp
- **DownloadedChapter Model Tests** (3 tests)
- Inicialización
- displayTitle formatting
- Codable
- **Edge Cases** (7 tests)
- Empty genres
- Nil coverImage
- Zero chapter numbers
- Large page numbers
- Negative indices
- Zero progress
- **Performance Tests** (2 tests)
- Manga encoding (1000 iteraciones)
- Chapter array equality lookup
### 2. StorageServiceTests.swift (~20 KB, 500+ líneas)
**Tests para servicio de almacenamiento:**
- **Favorites Tests** (7 tests)
- Guardar favorito único
- Guardar múltiples favoritos
- Evitar duplicados
- Remover favorito
- Verificar isFavorite
- Manejo de favoritos inexistentes
- **Reading Progress Tests** (7 tests)
- Guardar progreso individual
- Guardar múltiples progresos
- Actualizar progreso existente
- Obtener último capítulo leído
- Manejo de progreso inexistente
- **Downloaded Chapters Tests** (5 tests)
- Guardar metadatos de capítulo
- Verificar isChapterDownloaded
- Listar capítulos descargados
- Eliminar capítulos
- Manejo de capítulos inexistentes
- **Image Caching Tests** (5 tests)
- Guardar y cargar imágenes
- Cargar imágenes inexistentes
- Obtener URL de imagen
- Verificar existencia de archivos
- **Storage Management Tests** (4 tests)
- Calcular tamaño de almacenamiento
- Limpiar todos los downloads
- Formatear tamaño de archivo
- Verificar varios tamaños
- **Directory Management Tests** (2 tests)
- Obtener directorio de capítulo
- Creación automática de directorios
- **Edge Cases** (5 tests)
- Slug vacío
- Caracteres especiales
- Progreso con cero páginas
- Capítulo número cero
- Guardado concurrente de imágenes
- **Performance Tests** (2 tests)
- Guardar 1000 favoritos
- Guardar 100 progresos
### 3. ManhwaWebScraperTests.swift (~18 KB, 450+ líneas)
**Tests para web scraper:**
- **Error Handling Tests** (2 tests)
- Descripciones de errores
- LocalizedError compliance
- **Chapter Parsing Tests** (4 tests)
- Parsear respuesta de JavaScript
- Manejar datos inválidos
- Eliminar duplicados
- Ordenar capítulos
- **Image Parsing Tests** (3 tests)
- Parsear URLs de imágenes
- Filtrar UI elements
- Manejar arrays vacíos
- **Manga Info Parsing Tests** (3 tests)
- Extraer información completa
- Manejar campos vacíos
- Parsear estados
- **URL Construction Tests** (3 tests)
- Construir URLs de manga
- Construir URLs de capítulo
- Manejar caracteres especiales
- **Edge Cases** (3 tests)
- Extraer número de capítulo con regex
- Extraer slug
- Eliminar duplicados preservando orden
- **Performance Tests** (3 tests)
- Parsear 1000 capítulos
- Filtrar 10,000 imágenes
- Ordenar 1000 capítulos
- **Integration Simulation** (1 test)
- Flujo completo simulado
### 4. IntegrationTests.swift (~20 KB, 550+ líneas)
**Tests de integración completa:**
- **Complete Flow Tests** (4 tests)
- Scraper -> Storage completo
- Descarga de capítulo con imágenes
- Tracking de progreso de lectura
- Gestión de favoritos
- **Multi-Manga Scenarios** (2 tests)
- Tracking de múltiples mangas
- Descargas de múltiples capítulos
- **Error Handling Scenarios** (2 tests)
- Descarga con imágenes faltantes
- Limpieza de almacenamiento
- **Data Persistence Tests** (1 test)
- Persistencia a través de operaciones
- **Concurrent Operations** (3 tests)
- Operaciones concurrentes en favoritos
- Operaciones concurrentes en progreso
- Guardado concurrente de imágenes (20 imágenes)
- **Large Scale Tests** (2 tests)
- 1000 operaciones de favoritos
- 500 operaciones de progreso
### 5. TestHelpers.swift (~17 KB, 400+ líneas)
**Helpers y utilities:**
- **TestDataFactory**
- createManga, createChapter, createMangaPage
- createReadingProgress, createDownloadedChapter
- createChapters(count:), createPages(count:)
- **ImageTestHelpers**
- createTestImage(color:size:)
- createTestImageWithText(size:)
- compareImages, isImageNotEmpty
- **FileSystemTestHelpers**
- createTemporaryDirectory, removeTemporaryDirectory
- createTestFile, fileExists, fileSize
- createTestChapterStructure
- **StorageTestHelpers**
- clearAllStorage
- seedTestData
- assertStorageIsEmpty
- **AsyncTestHelpers**
- executeWithTimeout
- **ScraperTestHelpers**
- mockChapterListHTML, mockChapterImagesHTML
- mockMangaInfoHTML
- mockChapterJSResult, mockImagesJSResult
- mockMangaInfoJSResult
- **AssertionHelpers**
- assertArraysEqual, assertArrayContains
- assertValidURL, assertValidManga, assertValidChapter
- **PerformanceTestHelpers**
- measureTime, measureAsyncTime, averageTime
### 6. XCTestSuiteExtensions.swift (~10 KB, 250+ líneas)
**Extensiones de XCTest:**
- **Async Extensions**
- wait(for duration:)
- **Operation Helpers**
- waitForOperation(timeout:operation:)
- **Error Assertions**
- assertThrowsError
- assertNoThrow
- **Custom Assertions**
- assertDatesEqual, assertCount, assertEmpty, assertNotEmpty
- **Memory Leak Detection**
- assertNoMemoryLeak
- **Test Logging**
- logTest(_:level:)
- **Cleanup Helpers**
- clearAllUserDefaults, clearTemporaryDirectory
- **Test Metrics**
- recordMetric, assertMetricImproved
- **Documentation**
- Guía de ejecución
- Estructura de tests
- Mejores prácticas
### 7. README.md (~12 KB, 400+ líneas)
**Documentación completa:**
- Descripción general de la suite
- Estructura de tests
- Cómo ejecutar tests (Xcode y CLI)
- Guía detallada de cada test
- Mejores prácticas de testing
- Troubleshooting
- Recursos adicionales
### 8. run_tests.sh (~6 KB, 200 líneas)
**Script para ejecutar tests:**
- Opciones de ejecución (--all, --unit, --integration)
- Soporte para cobertura de código
- Output con colores
- Limpieza de build
- Ayuda integrada
## Estadísticas Totales
**Cantidad de Tests:**
- ModelTests: ~35 tests
- StorageServiceTests: ~40 tests
- ManhwaWebScraperTests: ~25 tests
- IntegrationTests: ~20 tests
- **Total: ~120 tests**
**Líneas de Código:**
- Código de tests: ~1,850 líneas
- Helpers y utilities: ~650 líneas
- Documentación: ~400 líneas
- **Total: ~2,900 líneas**
**Cobertura:**
- Modelos: 95%+
- StorageService: 90%+
- ManhwaWebScraper: 85%+ (con mocks)
- Integración: 80%+
## Características Principales
### 1. Tests Independientes
- Cada test tiene su propio setup/teardown
- Los tests pueden ejecutarse en cualquier orden
- Limpieza automática de estado
### 2. Setup y Teardown
- `setUp()` ejecuta antes de cada test
- `tearDown()` limpia después de cada test
- Limpieza de UserDefaults, archivos, etc.
### 3. Mocks Apropiados
- Mock de WKWebView responses
- Mock de HTML/JavaScript
- TestDataFactory para objetos de prueba
### 4. Tests Asíncronos
- Uso de async/await
- Tests de concurrencia
- Timeouts apropiados
### 5. Performance Tests
- Medición de rendimiento
- Tests de gran escala
- Comparativas de métricas
### 6. Edge Cases
- Datos inválidos
- Arrays vacíos
- Valores nulos
- Caracteres especiales
- Operaciones concurrentes
### 7. Documentación Completa
- README detallado
- Comentarios en cada test
- Ejemplos de uso
- Troubleshooting
## Cómo Ejecutar
### En Xcode:
```bash
# Todos los tests
Cmd + U
# Test específico
Click derecho > Run
# Con cobertura
Product > Test > Gather coverage
```
### Con script:
```bash
# Todos los tests
./run_tests.sh --all
# Con cobertura
./run_tests.sh --all --coverage
# Solo unitarios
./run_tests.sh --unit
# Solo integración
./run_tests.sh --integration --verbose
```
### Con xcodebuild:
```bash
xcodebuild test -scheme MangaReader \
-destination 'platform=iOS Simulator,name=iPhone 15'
```
## Próximos Pasos
1. **Ejecutar los tests** para verificar que funcionan
2. **Agregar al proyecto Xcode** como target de tests
3. **Configurar CI/CD** para ejecutar tests automáticamente
4. **Ajustar cobertura** según necesidades
5. **Agregar tests adicionales** para nuevas features
## Notas
- Todos los tests usan XCTest framework
- Compatible con iOS 15+
- Requiere Xcode 14+
- Tests marcados con @MainActor donde es necesario
- Soporte completo para async/await