# 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