From f2a6d682c66bee977acb0dbf7f153e6e1d9c1ba2 Mon Sep 17 00:00:00 2001 From: Renato97 Date: Tue, 31 Mar 2026 01:16:14 -0300 Subject: [PATCH] chore: clean unnecessary markdown files for CV sharing --- BEFORE_AFTER_COMPARISON.md | 1154 ----------------- CHANGES.md | 381 ------ IMPLEMENTATION_GUIDE.md | 587 --------- OPTIMIZATION_SUMMARY.md | 865 ------------ README_OPTIMIZATIONS.md | 620 --------- SERVER_SETUP.md | 230 ---- VPS_INTEGRATION_SUMMARY.md | 221 ---- backend/TEST_QUICK_START.md | 197 --- backend/TEST_README.md | 246 ---- backend/TEST_SUMMARY.md | 316 ----- docs/API.md | 992 -------------- docs/ARCHITECTURE.md | 642 --------- docs/CONTRIBUTING.md | 735 ----------- docs/README.md | 277 ---- ios-app/Sources/CHECKLIST.md | 266 ---- ios-app/Sources/Config/README.md | 352 ----- ios-app/Sources/DIAGRAMS.md | 412 ------ ios-app/Sources/IMPLEMENTATION_SUMMARY.md | 355 ----- ios-app/Sources/QUICK_START.md | 300 ----- .../Services/DOWNLOAD_SYSTEM_README.md | 343 ----- ios-app/Tests/EXECUTIVE_SUMMARY.md | 224 ---- ios-app/Tests/README.md | 426 ------ ios-app/Tests/TEST_SUMMARY.md | 372 ------ 23 files changed, 10513 deletions(-) delete mode 100644 BEFORE_AFTER_COMPARISON.md delete mode 100644 CHANGES.md delete mode 100644 IMPLEMENTATION_GUIDE.md delete mode 100644 OPTIMIZATION_SUMMARY.md delete mode 100644 README_OPTIMIZATIONS.md delete mode 100644 SERVER_SETUP.md delete mode 100644 VPS_INTEGRATION_SUMMARY.md delete mode 100644 backend/TEST_QUICK_START.md delete mode 100644 backend/TEST_README.md delete mode 100644 backend/TEST_SUMMARY.md delete mode 100644 docs/API.md delete mode 100644 docs/ARCHITECTURE.md delete mode 100644 docs/CONTRIBUTING.md delete mode 100644 docs/README.md delete mode 100644 ios-app/Sources/CHECKLIST.md delete mode 100644 ios-app/Sources/Config/README.md delete mode 100644 ios-app/Sources/DIAGRAMS.md delete mode 100644 ios-app/Sources/IMPLEMENTATION_SUMMARY.md delete mode 100644 ios-app/Sources/QUICK_START.md delete mode 100644 ios-app/Sources/Services/DOWNLOAD_SYSTEM_README.md delete mode 100644 ios-app/Tests/EXECUTIVE_SUMMARY.md delete mode 100644 ios-app/Tests/README.md delete mode 100644 ios-app/Tests/TEST_SUMMARY.md diff --git a/BEFORE_AFTER_COMPARISON.md b/BEFORE_AFTER_COMPARISON.md deleted file mode 100644 index 20548dd..0000000 --- a/BEFORE_AFTER_COMPARISON.md +++ /dev/null @@ -1,1154 +0,0 @@ -# Before/After Code Comparison - MangaReader Optimizations - -## Table of Contents -1. [Scraper Optimizations](#scraper-optimizations) -2. [Storage Optimizations](#storage-optimizations) -3. [ReaderView Optimizations](#readerview-optimizations) -4. [Cache System](#cache-system) - ---- - -## 1. Scraper Optimizations - -### 1.1 WKWebView Reuse - -#### ❌ BEFORE -```swift -class ManhwaWebScraper: NSObject { - private var webView: WKWebView? - - // Crea nueva instancia cada vez - func scrapeChapters(mangaSlug: String) async throws -> [Chapter] { - let configuration = WKWebViewConfiguration() - webView = WKWebView(frame: .zero, configuration: configuration) - webView?.navigationDelegate = self - - // Siempre inicializa nuevo - // Siempre espera 3 segundos fijos - try await loadURLAndWait(url) - // ... - } -} -``` - -**Problemas:** -- Creaba múltiples instancias de WKWebView (memory leak) -- Timeout fijo de 3 segundos (muy lento en conexiones buenas) -- Sin reutilización de recursos - -#### ✅ AFTER -```swift -class ManhwaWebScraperOptimized: NSObject { - // Singleton con WKWebView reutilizable - static let shared = ManhwaWebScraperOptimized() - private var webView: WKWebView? - - private override init() { - super.init() - setupWebView() // Se crea solo una vez - } - - func scrapeChapters(mangaSlug: String) async throws -> [Chapter] { - // Reutiliza WKWebView existente - guard let webView = webView else { - throw ScrapingError.webViewNotInitialized - } - - // Timeout adaptativo basado en historial - let timeout = getAdaptiveTimeout() - try await loadURLAndWait(url, timeout: timeout) - // ... - } -} -``` - -**Mejoras:** -- ✅ Una sola instancia de WKWebView -- ✅ Timeout adaptativo: 2-8 segundos según red -- ✅ 70-80% más rápido en scraping subsiguiente - ---- - -### 1.2 HTML Cache System - -#### ❌ BEFORE -```swift -func scrapeChapters(mangaSlug: String) async throws -> [Chapter] { - // Siempre hace scraping - let url = URL(string: "https://manhwaweb.com/manga/\(mangaSlug)")! - - try await loadURLAndWait(url) // 3-5 segundos - - let chapters = try await webView.evaluateJavaScript(""" - // JavaScript inline - """) - - return parse(chapters) -} - -// Cada llamada toma 3-5 segundos -// Sin cache, siempre descarga y parsea HTML -``` - -**Problemas:** -- Siempre descarga HTML (uso innecesario de red) -- Siempre parsea JavaScript (CPU) -- Mismo manga → mismo tiempo de espera - -#### ✅ AFTER -```swift -// Cache inteligente con expiración -private var htmlCache: NSCache -private var cacheTimestamps: [String: Date] = [:] -private let cacheValidDuration: TimeInterval = 1800 // 30 minutos - -func scrapeChapters(mangaSlug: String) async throws -> [Chapter] { - let cacheKey = "chapters_\(mangaSlug)" - - // ✅ Verificar cache primero - if let cachedResult = getCachedResult(for: cacheKey) { - print("✅ Cache HIT") - return try parseChapters(from: cachedResult) - } - - print("🌐 Cache MISS - Scraping...") - - // Solo scraping si no hay cache válido - try await loadURLAndWait(url, timeout: adaptiveTimeout) - - let result = try await webView.evaluateJavaScript( - JavaScriptScripts.extractChapters.rawValue - ) - - // ✅ Guardar en cache para futuras consultas - cacheResult(result, for: cacheKey) - - return parse(result) -} -``` - -**Mejoras:** -- ✅ 80-90% de requests sirven desde cache (0.1-0.5s) -- ✅ Drástica reducción de uso de red -- ✅ Tiempo de respuesta: 3-5s → 0.1-0.5s (con cache) - ---- - -### 1.3 Precompiled JavaScript - -#### ❌ BEFORE -```swift -// JavaScript como string literal en cada llamada -func scrapeChapters(mangaSlug: String) async throws -> [Chapter] { - let chapters = try await webView.evaluateJavaScript(""" - (function() { - const chapters = []; - const links = document.querySelectorAll('a[href*="/leer/"]'); - - links.forEach(link => { - // ... 30 líneas de código - }); - - const unique = chapters.filter((chapter, index, self) => - index === self.findIndex((c) => c.number === chapter.number) - ); - - return unique.sort((a, b) => b.number - a.number); - })(); - """) as! [[String: Any]] - - // El string se crea y parsea en cada llamada -} -``` - -**Problemas:** -- String se recrea en cada scraping (memoria) -- Parsing del JavaScript cada vez (CPU) -- Código difícil de mantener y testear - -#### ✅ AFTER -```swift -// Scripts precompilados (enum) -private enum JavaScriptScripts: String { - case extractChapters = """ - (function() { - const chapters = []; - const links = document.querySelectorAll('a[href*="/leer/"]'); - links.forEach(link => { - const href = link.getAttribute('href'); - const text = link.textContent?.trim(); - if (href && text && href.includes('/leer/')) { - const match = href.match(/(\\d+)(?:\\/|\\?|\\s*$)/); - const chapterNumber = match ? parseInt(match[1]) : null; - if (chapterNumber && !isNaN(chapterNumber)) { - chapters.push({ - number: chapterNumber, - title: text, - url: href.startsWith('http') ? href : 'https://manhwaweb.com' + href, - slug: href.replace('/leer/', '').replace(/^\\//, '') - }); - } - } - }); - const unique = chapters.filter((chapter, index, self) => - index === self.findIndex((c) => c.number === chapter.number) - ); - return unique.sort((a, b) => b.number - a.number); - })(); - """ - - case extractImages = "/* script optimizado */" - case extractMangaInfo = "/* script optimizado */" -} - -// Uso eficiente -func scrapeChapters(mangaSlug: String) async throws -> [Chapter] { - // Referencia directa al enum (sin recrear strings) - let chapters = try await webView.evaluateJavaScript( - JavaScriptScripts.extractChapters.rawValue - ) as! [[String: Any]] -} -``` - -**Mejoras:** -- ✅ Strings precompilados (no se recrean) -- ✅ 10-15% más rápido en ejecución -- ✅ Código más mantenible y testeable -- ✅ Menor uso de memoria - ---- - -### 1.4 Adaptive Timeout - -#### ❌ BEFORE -```swift -private func loadURLAndWait(_ url: URL) async throws { - webView.load(URLRequest(url: url)) - - // Siempre 3 segundos fijos - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - continuation.resume() - } - - // Problemas: - // - Muy lento en conexiones buenas (espera innecesaria) - // - Muy rápido en conexiones malas (puede fallar) -} -``` - -**Problemas:** -- Timeout fijo ineficiente -- No se adapta a condiciones de red -- Experiencia de usuario inconsistente - -#### ✅ AFTER -```swift -// Sistema adaptativo con historial -private var loadTimeHistory: [TimeInterval] = [] -private var averageLoadTime: TimeInterval = 3.0 - -private func loadURLAndWait(_ url: URL, timeout: TimeInterval) async throws { - let startTime = Date() - - webView.load(URLRequest(url: url)) - - // ✅ Timeout adaptativo basado en historial - DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { - let loadTime = Date().timeIntervalSince(startTime) - self.updateLoadTimeHistory(loadTime) - continuation.resume() - } -} - -// Aprende del rendimiento -private func updateLoadTimeHistory(_ loadTime: TimeInterval) { - loadTimeHistory.append(loadTime) - - // Mantener solo últimos 10 tiempos - if loadTimeHistory.count > 10 { - loadTimeHistory.removeFirst() - } - - // Calcular promedio móvil - averageLoadTime = loadTimeHistory.reduce(0, +) / Double(loadTimeHistory.count) - - // ✅ Límites inteligentes: 2-8 segundos - averageLoadTime = max(2.0, min(averageLoadTime, 8.0)) -} - -private func getAdaptiveTimeout() -> TimeInterval { - return averageLoadTime + 1.0 // Margen de seguridad -} -``` - -**Mejoras:** -- ✅ 20-30% más rápido en conexiones buenas -- ✅ Más robusto en conexiones lentas -- ✅ Timeout óptimo: 2-8 segundos (adaptativo) -- ✅ Mejor experiencia de usuario - ---- - -## 2. Storage Optimizations - -### 2.1 Adaptive Image Compression - -#### ❌ BEFORE -```swift -func saveImage(_ image: UIImage, mangaSlug: String, chapterNumber: Int, pageIndex: Int) throws -> URL { - let fileURL = getChapterDirectory(...).appendingPathComponent("page_\(pageIndex).jpg") - - // Calidad fija de 0.8 para todas las imágenes - if let data = image.jpegData(compressionQuality: 0.8) { - try data.write(to: fileURL) - return fileURL - } - - throw NSError(...) -} - -// Problemas: -// - Imágenes pequeñas se sobrecomprimen (pérdida innecesaria de calidad) -// - Imágenes grandes no se comprimen lo suficiente (mucho espacio) -// - No considera el contenido de la imagen -``` - -**Problemas:** -- Compresión ineficiente -- Desperdicio de espacio en imágenes grandes -- Pérdida innecesaria de calidad en pequeñas - -#### ✅ AFTER -```swift -private enum ImageCompression { - static let highQuality: CGFloat = 0.9 - static let mediumQuality: CGFloat = 0.75 - static let lowQuality: CGFloat = 0.6 - - // ✅ Calidad adaptativa basada en tamaño - static func quality(for imageSize: Int) -> CGFloat { - let sizeMB = Double(imageSize) / (1024 * 1024) - - if sizeMB > 3.0 { - return lowQuality // Imágenes muy grandes > 3MB - } else if sizeMB > 1.5 { - return mediumQuality // Imágenes medianas 1.5-3MB - } else { - return highQuality // Imágenes pequeñas < 1.5MB - } - } -} - -func saveImage(_ image: UIImage, ...) async throws -> URL { - // Obtener tamaño original - guard let imageData = image.jpegData(compressionQuality: 1.0) else { - throw NSError(...) - } - - // ✅ Calidad adaptativa según tamaño - let quality = ImageCompression.quality(for: imageData.count) - - if let compressedData = image.jpegData(compressionQuality: quality) { - try compressedData.write(to: fileURL) - - // Crear thumbnail automáticamente - Task { - await createThumbnail(for: fileURL, ...) - } - - return fileURL - } -} -``` - -**Mejoras:** -- ✅ 30-40% reducción en espacio de almacenamiento -- ✅ Calidad visual imperceptible -- ✅ Ahorro significativo en ancho de banda -- ✅ Thumbnails automáticos para navegación rápida - ---- - -### 2.2 Thumbnail System - -#### ❌ BEFORE -```swift -// Sin sistema de thumbnails -struct MangaPage { - let url: String - let index: Int - - var thumbnailURL: String { - return url // No había thumbnails - } -} - -// En listas, cargaba imágenes completas -AsyncImage(url: URL(string: page.url)) { phase in - case .success(let image): - image - .resizable() - .frame(width: 100, height: 150) // Thumbnail on-the-fly (lento) -} -``` - -**Problemas:** -- Carga imágenes completas para thumbnails (lento) -- Alto uso de memoria en listas -- Navegación lenta - -#### ✅ AFTER -```swift -// Generación automática de thumbnails -private enum ThumbnailSize { - static let small = CGSize(width: 150, height: 200) // Para listas - static let medium = CGSize(width: 300, height: 400) // Para previews -} - -func saveImage(_ image: UIImage, ...) async throws -> URL { - // Guardar imagen completa - try data.write(to: fileURL) - - // ✅ Crear thumbnail automáticamente en background - Task(priority: .utility) { - await createThumbnail(for: fileURL, ...) - } - - return fileURL -} - -private func createThumbnail(for imageURL: URL, ...) async { - let thumbnail = await resizeImage(image, to: ThumbnailSize.small) - - // ✅ Guardar thumbnail con baja calidad (más pequeño) - if let thumbData = thumbnail.jpegData(compressionQuality: 0.5) { - try? thumbData.write(to: thumbnailURL) - } -} - -// Cargar thumbnail cuando sea apropiado -func loadImage(useThumbnail: Bool = false) -> UIImage? { - if useThumbnail { - return UIImage(contentsOfFile: thumbnailURL.path) - } else { - return UIImage(contentsOfFile: fullImageURL.path) - } -} - -// En listas - usar thumbnail -AsyncImage(url: thumbnailURL) { phase in - // 90-95% más rápido -} -``` - -**Mejoras:** -- ✅ Navegación 10x más rápida en listas -- ✅ 90-95% menos memoria en previews -- ✅ Generación automática y transparente -- ✅ Mejor experiencia de usuario - ---- - -### 2.3 Lazy Loading - -#### ❌ BEFORE -```swift -func getDownloadedChapters() -> [DownloadedChapter] { - // Carga todos los capítulos en memoria - guard let data = try? Data(contentsOf: metadataURL), - let downloaded = try? JSONDecoder().decode([DownloadedChapter].self, from: data) else { - return [] - } - - return downloaded // Todo en memoria -} - -// Problemas: -// - Carga 100+ capítulos en memoria (incluso si no se usan) -// - Startup lento de la app -// - UI no responde mientras carga -``` - -**Problemas:** -- Carga todo en memoria al inicio -- Startup muy lento -- UI bloqueada durante carga - -#### ✅ AFTER -```swift -// ✅ Paginación y filtros eficientes -func getDownloadedChapters(offset: Int = 0, limit: Int = 20) -> [DownloadedChapter] { - let all = getAllDownloadedChapters() - - // ✅ Solo cargar la página solicitada - let start = min(offset, all.count) - let end = min(offset + limit, all.count) - - return Array(all[start.. [DownloadedChapter] { - return getAllDownloadedChapters() - .filter { $0.mangaSlug == mangaSlug } // Solo del manga específico -} - -// ✅ Cache en memoria con invalidación inteligente -private var metadataCache: [String: [DownloadedChapter]] = [:] -private var cacheInvalidationTime: Date = Date.distantPast -private let metadataCacheDuration: TimeInterval = 300 // 5 minutos - -func getAllDownloadedChapters() -> [DownloadedChapter] { - // Verificar si cache es válido - if Date().timeIntervalSince(cacheInvalidationTime) < metadataCacheDuration, - let cached = metadataCache[key] { - return cached // ✅ Retorna cache si es válido - } - - // Cache inválido, cargar del disco - let downloaded = loadFromDisk() - - // Actualizar cache - metadataCache[key] = downloaded - cacheInvalidationTime = Date() - - return downloaded -} -``` - -**Mejoras:** -- ✅ Inicio de app 50-70% más rápido -- ✅ Menor uso de memoria en startup -- ✅ UI más fluida (no bloquea) -- ✅ Cache inteligente con expiración - ---- - -### 2.4 Automatic Cache Cleanup - -#### ❌ BEFORE -```swift -// Sin sistema de limpieza automática -// El cache crecía indefinidamente - -func clearAllDownloads() { - // Solo limpieza manual - try? fileManager.removeItem(at: chaptersDirectory) -} - -// Problemas: -// - El cache nunca se limpia automáticamente -// - El usuario debe limpiar manualmente -// - Puede llenar el dispositivo -``` - -**Problemas:** -- Crecimiento ilimitado de cache -- Riesgo de llenar el dispositivo -- Requiere intervención del usuario - -#### ✅ AFTER -```swift -// ✅ Sistema automático de limpieza -private let maxCacheAge: TimeInterval = 30 * 24 * 3600 // 30 días -private let maxCacheSize: Int64 = 2 * 1024 * 1024 * 1024 // 2 GB - -private func setupAutomaticCleanup() { - // Ejecutar cleanup cada 24 horas - Timer.scheduledTimer(withTimeInterval: 86400, repeats: true) { [weak self] _ in - self?.performCleanupIfNeeded() - } - - // Primer cleanup al iniciar - performCleanupIfNeeded() -} - -private func performCleanupIfNeeded() { - let currentSize = getStorageSize() - - // ✅ Si excede el tamaño máximo, limpiar - if currentSize > maxCacheSize { - print("⚠️ Cache size limit exceeded") - cleanupOldFiles() - } -} - -private func cleanupOldFiles() { - let now = Date() - - // ✅ Eliminar archivos más viejos que maxCacheAge - for fileURL in allFiles { - if let modificationDate = fileURL.modificationDate { - if modificationDate < now.addingTimeInterval(-maxCacheAge) { - try? fileManager.removeItem(at: fileURL) - print("🗑️ Removed old file: \(fileURL.lastPathComponent)") - } - } - } -} - -// ✅ Respuesta automática a storage warnings -func hasAvailableSpace(_ requiredBytes: Int64) -> Bool { - updateStorageInfo() - - if availableStorage < CacheLimits.minFreeSpace { - print("⚠️ Low free space") - performEmergencyCleanup() // ✅ Limpieza agresiva - } - - return availableStorage > requiredBytes -} -``` - -**Mejoras:** -- ✅ Almacenamiento controlado automáticamente -- ✅ No requiere intervención del usuario -- ✅ Previene problemas de espacio -- ✅ Limpieza inteligente por edad y tamaño - ---- - -## 3. ReaderView Optimizations - -### 3.1 Image Caching with NSCache - -#### ❌ BEFORE -```swift -struct PageView: View { - let page: MangaPage - - var body: some View { - AsyncImage(url: URL(string: page.url)) { phase in - switch phase { - case .empty: - ProgressView() - case .success(let image): - image - .resizable() - // ❌ No guarda en cache - case .failure: - Image(systemName: "photo") - } - } - // ❌ Cada vez recarga la imagen - } -} - -// Problemas: -// - Sin cache en memoria -// - Navegación lenta (recarga constantemente) -// - Alto uso de red -``` - -**Problemas:** -- Sin cache en memoria -- Navegación muy lenta -- Alto consumo de datos - -#### ✅ AFTER -```swift -// ✅ Sistema completo de cache -final class ImageCache { - static let shared = ImageCache() - - // Cache en memoria con NSCache - private let cache: NSCache - - // Cache en disco para persistencia - private let diskCacheDirectory: URL - - func image(for url: String) -> UIImage? { - // 1. ✅ Verificar memoria cache primero (más rápido) - if let cachedImage = getCachedImage(for: url) { - cacheHits += 1 - return cachedImage - } - - cacheMisses += 1 - - // 2. ✅ Verificar disco cache - if let diskImage = loadImageFromDisk(for: url) { - // Guardar en memoria cache - setImage(diskImage, for: url) - return diskImage - } - - // 3. No está en cache, necesita descarga - return nil - } - - func setImage(_ image: UIImage, for url: String) { - // Guardar en memoria - let cost = estimateImageCost(image) - cache.setObject(image, forKey: url as NSString, cost: cost) - - // Guardar en disco (async) - Task { - await saveImageToDisk(image, for: url) - } - } -} - -// Uso en PageView -struct PageView: View { - var body: some View { - Group { - if let cachedImage = ImageCache.shared.image(for: page.url) { - // ✅ Carga instantánea desde cache - Image(uiImage: cachedImage) - .resizable() - } else { - // Cargar desde URL - AsyncImage(url: URL(string: page.url)) { phase in - // ... - } - } - } - } -} -``` - -**Mejoras:** -- ✅ 80-90% de páginas cargan instantáneamente -- ✅ Hit rate de cache: 85-95% -- ✅ Navegación fluida sin recargas -- ✅ Menor consumo de datos - ---- - -### 3.2 Preloading System - -#### ❌ BEFORE -```swift -TabView(selection: $currentPage) { - ForEach(pages) { page in - PageView(page: page) - .tag(page.index) - .onAppear { - // ❌ Solo carga página actual - } - } -} - -// Problemas: -// - Sin preloading -// - Navegación con lag entre páginas -// - Mala experiencia de usuario -``` - -**Problemas:** -- Sin preloading de páginas adyacentes -- Navegación con delays -- Experiencia discontinua - -#### ✅ AFTER -```swift -// ✅ Sistema de preloading inteligente -func preloadAdjacentPages(currentIndex: Int, total: Int) { - guard enablePreloading else { return } - - // Precargar 2 páginas antes y 2 después - let startIndex = max(0, currentIndex - 2) - let endIndex = min(total - 1, currentIndex + 2) - - for index in startIndex...endIndex { - guard index != currentIndex else { continue } - - let page = pages[index] - - // ✅ Precargar en background con prioridad utility - Task(priority: .utility) { - // Si no está en cache, cargar - if ImageCache.shared.image(for: page.url) == nil { - await loadImage(pageIndex: index) - } - } - } -} - -TabView(selection: $currentPage) { - ForEach(pages) { page in - PageView(page: page) - .tag(page.index) - .onAppear { - // ✅ Precargar cuando aparece - viewModel.preloadAdjacentPages( - currentIndex: page.index, - total: pages.count - ) - } - } -} - -// ✅ También precargar al cambiar de página -.onChange(of: currentPage) { oldValue, newValue in - viewModel.preloadAdjacentPages( - currentIndex: newValue, - total: pages.count - ) -} -``` - -**Mejoras:** -- ✅ Navegación instantánea en 80% de casos -- ✅ Precarga inteligente de 4 páginas (2 antes, 2 después) -- ✅ No afecta significativamente el rendimiento -- ✅ Experiencia de lectura fluida - ---- - -### 3.3 Memory Management - -#### ❌ BEFORE -```swift -struct PageView: View { - var body: some View { - AsyncImage(url: URL(string: page.url)) { phase in - case .success(let image): - image - .resizable() - // ❌ Carga imagen a resolución completa - // ❌ Sin gestión de memoria - // ❌ Sin cleanup - } - } -} - -// Problemas: -// - Imágenes muy grandes consumen mucha memoria -// - Sin respuesta a memory warnings -// - Possible crashes por memory pressure -``` - -**Problemas:** -- Sin límite de tamaño de imagen -- Sin gestión de memoria -- Riesgo de crashes - -#### ✅ AFTER -```swift -// ✅ Redimensiona imágenes muy grandes -private func optimizeImageSize(_ image: UIImage) -> UIImage { - let maxDimension: CGFloat = 2048 - - guard let cgImage = image.cgImage else { return image } - - let width = CGFloat(cgImage.width) - let height = CGFloat(cgImage.height) - - // ✅ Si ya es pequeña, no cambiar - if width <= maxDimension && height <= maxDimension { - return image - } - - // Redimensionar manteniendo aspect ratio - let aspectRatio = width / height - let newWidth: CGFloat - let newHeight: CGFloat - - if width > height { - newWidth = maxDimension - newHeight = maxDimension / aspectRatio - } else { - newHeight = maxDimension - newWidth = maxDimension * aspectRatio - } - - let newSize = CGSize(width: newWidth, height: newHeight) - let renderer = UIGraphicsImageRenderer(size: newSize) - return renderer.image { _ in - image.draw(in: CGRect(origin: .zero, size: newSize)) - } -} - -// ✅ Respuesta a memory warnings -@objc private func handleMemoryWarning() { - print("⚠️ Memory warning - Clearing cache") - - // Limpiar cache de memoria - cache.removeAllObjects() - - // Cancelar preloading pendiente - preloadQueue.removeAll() - - // Permitir que el sistema libere memoria -} - -// ✅ Cleanup explícito -.onDisappear { - // Liberar imagen cuando la página no está visible - cleanupImageIfNeeded() -} -``` - -**Mejoras:** -- ✅ 50-70% menos memoria en imágenes -- ✅ Sin crashes por memory pressure -- ✅ Rendering más rápido -- ✅ Respuesta automática a warnings - ---- - -### 3.4 Debounced Progress Save - -#### ❌ BEFORE -```swift -.onChange(of: currentPage) { oldValue, newValue in - // ❌ Guarda progreso en cada cambio - let progress = ReadingProgress( - mangaSlug: manga.slug, - chapterNumber: chapter.number, - pageNumber: newValue, - timestamp: Date() - ) - storage.saveReadingProgress(progress) // I/O en cada cambio -} - -// Problemas: -// - I/O excesivo (cada cambio de página) -// - Navegación puede ser lenta -// - Desgaste de almacenamiento -``` - -**Problemas:** -- I/O excesivo -- Navegación lenta -- Desgaste de almacenamiento - -#### ✅ AFTER -```swift -// ✅ Debouncing de 2 segundos -private var progressSaveTimer: Timer? - -func currentPageChanged(from oldValue: Int, to newValue: Int) { - saveProgressDebounced() - - // Precargar nuevas páginas adyacentes - preloadAdjacentPages(currentIndex: to, total: pages.count) -} - -private func saveProgressDebounced() { - // Cancelar timer anterior - progressSaveTimer?.invalidate() - - // ✅ Crear nuevo timer (espera 2 segundos de inactividad) - progressSaveTimer = Timer.scheduledTimer( - withTimeInterval: 2.0, - repeats: false - ) { [weak self] _ in - self?.saveProgress() - } -} - -private func saveProgress() { - let progress = ReadingProgress( - mangaSlug: manga.slug, - chapterNumber: chapter.number, - pageNumber: currentPage, - timestamp: Date() - ) - storage.saveReadingProgress(progress) -} - -// ✅ Guardar progreso final al salir -deinit { - progressSaveTimer?.invalidate() - saveProgress() // Guardar estado final -} -``` - -**Mejoras:** -- ✅ 95% menos escrituras a disco -- ✅ Mejor rendimiento de navegación -- ✅ No se pierde el progreso -- ✅ Ahorro de almacenamiento - ---- - -## 4. Cache System - -### 4.1 LRU Policy Implementation - -#### ❌ BEFORE -```swift -// Sin política clara de eliminación -// FIFO simple sin considerar uso - -func performCleanup() { - // Elimina items viejos sin considerar su uso - for item in oldItems { - if item.age > maxAge { - remove(item) - } - } -} - -// Problemas: -// - Elimina items usados frecuentemente -// - No considera patrones de uso -// - Cache ineficiente -``` - -**Problemas:** -- Elimina items populares -- Baja eficiencia de cache -- Mal hit rate - -#### ✅ AFTER -```swift -// ✅ Tracking completo de uso -private struct CacheItem { - let key: String - let size: Int64 - var lastAccess: Date - var accessCount: Int - let created: Date -} - -func trackAccess(key: String, type: CacheType, size: Int64) { - if let existingItem = cacheItems[key] { - // Actualizar item existente - cacheItems[key] = CacheItem( - key: key, - size: size, - lastAccess: Date(), // ✅ Actualizar último acceso - accessCount: existingItem.accessCount + 1, // ✅ Incrementar contador - created: existingItem.created - ) - } else { - // Nuevo item - cacheItems[key] = CacheItem(...) - } -} - -// ✅ LRU con prioridades -func performCleanup() { - let sortedItems = cacheItems.sorted { item1, item2 in - // Primero por prioridad de tipo - if item1.type.priority != item2.type.priority { - return item1.type.priority < item2.type.priority - } - // Luego por recencia de acceso (LRU) - return item1.lastAccess < item2.lastAccess - } - - // Eliminar items con menor prioridad y más viejos primero - for (key, item) in sortedItems { - if removedSpace >= excessSpace { break } - removeCacheItem(key: key, type: item.type) - } -} -``` - -**Mejoras:** -- ✅ Preserva items usados frecuentemente -- ✅ Mayor hit rate de cache -- ✅ Eliminación más inteligente -- ✅ Mejor uso de espacio disponible - ---- - -### 4.2 Priority-Based Cleanup - -#### ❌ BEFORE -```swift -// Sin diferenciación por tipo de contenido -func cleanup() { - // Trata todo igual - for item in cacheItems { - if item.isOld { - remove(item) - } - } -} -``` - -**Problemas:** -- Elimina indistintamente -- Puede eliminar contenido importante -- No refleja prioridades de usuario - -#### ✅ AFTER -```swift -// ✅ Diferenciación por tipo con prioridades -enum CacheType: String { - case images // Alta prioridad - lo que más importa - case thumbnails // Media prioridad - útil pero regenerable - case html // Baja prioridad - fácil de obtener de nuevo - case metadata // Baja prioridad - pequeño y regenerable - - var priority: CachePriority { - switch self { - case .images: return .high - case .thumbnails: return .medium - case .html, .metadata: return .low - } - } -} - -// ✅ Limpieza por prioridad -func performEmergencyCleanup() { - print("🚨 EMERGENCY CLEANUP") - - // ✅ Primero eliminar 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) - } - } -} -``` - -**Mejoras:** -- ✅ Preserva contenido importante -- ✅ Limpieza graduada por fases -- ✅ Mejor experiencia de usuario -- ✅ Decisiones más inteligentes - ---- - -## Summary of Improvements - -### Performance Metrics - -| Aspect | Before | After | Improvement | -|--------|--------|-------|-------------| -| **Scraper Speed** | -| First scrape | 5-8s | 3-5s | **40%** | -| Cached scrape | 5-8s | 0.1-0.5s | **90%** | -| **Storage** | -| Image size | 15-25 MB/ch | 8-15 MB/ch | **40%** | -| Load time | 0.5-1s | 0.05-0.1s | **80%** | -| **Reader** | -| Page load (no cache) | 2-4s | 1-2s | **50%** | -| Page load (cached) | 2-4s | 0.05-0.1s | **95%** | -| Memory usage | 150-300 MB | 50-100 MB | **60%** | -| **General** | -| App launch | 2-3s | 0.5-1s | **70%** | -| App size | ~45 MB | ~30-35 MB | **25%** | - -### Key Takeaways - -1. **Caching is king**: Implement multi-layer caching (memory + disk) -2. **Adaptivity beats static**: Use adaptive timeouts and compression -3. **Preloading improves UX**: Load adjacent pages before user needs them -4. **Memory management matters**: Respond to warnings and clean up properly -5. **Prioritize intelligently**: Not all content is equally important - ---- - -**End of Comparison** diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index 8576cd0..0000000 --- a/CHANGES.md +++ /dev/null @@ -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))%") -``` - diff --git a/IMPLEMENTATION_GUIDE.md b/IMPLEMENTATION_GUIDE.md deleted file mode 100644 index 4ab1d7f..0000000 --- a/IMPLEMENTATION_GUIDE.md +++ /dev/null @@ -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.. UInt64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.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! 🚀** diff --git a/OPTIMIZATION_SUMMARY.md b/OPTIMIZATION_SUMMARY.md deleted file mode 100644 index 9908fe9..0000000 --- a/OPTIMIZATION_SUMMARY.md +++ /dev/null @@ -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 -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.. [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 - - 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. diff --git a/README_OPTIMIZATIONS.md b/README_OPTIMIZATIONS.md deleted file mode 100644 index 0eed255..0000000 --- a/README_OPTIMIZATIONS.md +++ /dev/null @@ -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. diff --git a/SERVER_SETUP.md b/SERVER_SETUP.md deleted file mode 100644 index 2ec68ae..0000000 --- a/SERVER_SETUP.md +++ /dev/null @@ -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 -``` - -## 📝 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) diff --git a/VPS_INTEGRATION_SUMMARY.md b/VPS_INTEGRATION_SUMMARY.md deleted file mode 100644 index 84cb322..0000000 --- a/VPS_INTEGRATION_SUMMARY.md +++ /dev/null @@ -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 diff --git a/backend/TEST_QUICK_START.md b/backend/TEST_QUICK_START.md deleted file mode 100644 index aebec9c..0000000 --- a/backend/TEST_QUICK_START.md +++ /dev/null @@ -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 diff --git a/backend/TEST_README.md b/backend/TEST_README.md deleted file mode 100644 index f9e3a82..0000000 --- a/backend/TEST_README.md +++ /dev/null @@ -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. diff --git a/backend/TEST_SUMMARY.md b/backend/TEST_SUMMARY.md deleted file mode 100644 index f5011ed..0000000 --- a/backend/TEST_SUMMARY.md +++ /dev/null @@ -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! diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index 2826f3e..0000000 --- a/docs/API.md +++ /dev/null @@ -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 `

` o ``) - - 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 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 336d8e1..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -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 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index bdea627..0000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -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 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 50a5dc9..0000000 --- a/docs/README.md +++ /dev/null @@ -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 diff --git a/ios-app/Sources/CHECKLIST.md b/ios-app/Sources/CHECKLIST.md deleted file mode 100644 index 4f7d71d..0000000 --- a/ios-app/Sources/CHECKLIST.md +++ /dev/null @@ -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 diff --git a/ios-app/Sources/Config/README.md b/ios-app/Sources/Config/README.md deleted file mode 100644 index abb901e..0000000 --- a/ios-app/Sources/Config/README.md +++ /dev/null @@ -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 diff --git a/ios-app/Sources/DIAGRAMS.md b/ios-app/Sources/DIAGRAMS.md deleted file mode 100644 index ff0381a..0000000 --- a/ios-app/Sources/DIAGRAMS.md +++ /dev/null @@ -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 - } - ] -} -``` diff --git a/ios-app/Sources/IMPLEMENTATION_SUMMARY.md b/ios-app/Sources/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 1e59ace..0000000 --- a/ios-app/Sources/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -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 diff --git a/ios-app/Sources/QUICK_START.md b/ios-app/Sources/QUICK_START.md deleted file mode 100644 index 2f4daf3..0000000 --- a/ios-app/Sources/QUICK_START.md +++ /dev/null @@ -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! 🚀 diff --git a/ios-app/Sources/Services/DOWNLOAD_SYSTEM_README.md b/ios-app/Sources/Services/DOWNLOAD_SYSTEM_README.md deleted file mode 100644 index 0cd95c8..0000000 --- a/ios-app/Sources/Services/DOWNLOAD_SYSTEM_README.md +++ /dev/null @@ -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 diff --git a/ios-app/Tests/EXECUTIVE_SUMMARY.md b/ios-app/Tests/EXECUTIVE_SUMMARY.md deleted file mode 100644 index 53a876a..0000000 --- a/ios-app/Tests/EXECUTIVE_SUMMARY.md +++ /dev/null @@ -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+ diff --git a/ios-app/Tests/README.md b/ios-app/Tests/README.md deleted file mode 100644 index 12b5618..0000000 --- a/ios-app/Tests/README.md +++ /dev/null @@ -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. diff --git a/ios-app/Tests/TEST_SUMMARY.md b/ios-app/Tests/TEST_SUMMARY.md deleted file mode 100644 index d7fb206..0000000 --- a/ios-app/Tests/TEST_SUMMARY.md +++ /dev/null @@ -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