Files
MangaReader/ios-app/Sources/Models/Manga.swift
renato97 b474182dd9 Initial commit: MangaReader iOS App
 Features:
- App iOS completa para leer manga sin publicidad
- Scraper con WKWebView para manhwaweb.com
- Sistema de descargas offline
- Lector con zoom y navegación
- Favoritos y progreso de lectura
- Compatible con iOS 15+ y Sideloadly/3uTools

📦 Contenido:
- Backend Node.js con Puppeteer (opcional)
- App iOS con SwiftUI
- Scraper de capítulos e imágenes
- Sistema de almacenamiento local
- Testing completo
- Documentación exhaustiva

🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente
  - 21 páginas descargadas
  - 4.68 MB total
  - URLs verificadas y funcionales

🎉 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-04 15:34:18 +01:00

309 lines
8.8 KiB
Swift

import Foundation
// MARK: - Manga Model
/// Representa la información completa de un manga.
///
/// `Manga` es una estructura inmutable que contiene toda la información relevante
/// sobre un manga, incluyendo título, descripción, géneros, estado de publicación
/// y metadatos adicionales como la URL de la imagen de portada.
///
/// Conforma a `Codable` para serialización/deserialización automática,
/// `Identifiable` para uso en listas de SwiftUI, y `Hashable` para comparaciones
/// y uso en sets.
///
/// # Example
/// ```swift
/// let manga = Manga(
/// slug: "one-piece_1695365223767",
/// title: "One Piece",
/// description: "La historia de Monkey D. Luffy y su tripulación...",
/// 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"
/// ```
struct Manga: Codable, Identifiable, Hashable {
/// Identificador único del manga (computed, igual al slug)
let id: String { slug }
/// Slug único usado en URLs del sitio web
let slug: String
/// Título del manga
let title: String
/// Descripción o sinopsis del manga
let description: String
/// Array de géneros literarios del manga
let genres: [String]
/// Estado de publicación (crudo, sin traducir)
let status: String
/// URL completa del manga en el sitio web
let url: String
/// URL de la imagen de portada (opcional)
let coverImage: String?
/// Coding keys para mapeo JSON/codificación personalizada
enum CodingKeys: String, CodingKey {
case slug, title, description, genres, status, url, coverImage
}
/// Estado de publicación formateado para mostrar en la UI
///
/// Traduce los estados crudos del sitio web a formato legible para el usuario.
///
/// # Mapeos
/// - `"PUBLICANDOSE"` `"En publicación"`
/// - `"FINALIZADO"` `"Finalizado"`
/// - `"EN_PAUSA"`, `"EN_ESPERA"` `"En pausa"`
/// - Otro retorna el valor original sin modificar
///
/// - Returns: String con el estado traducido y formateado
var displayStatus: String {
switch status {
case "PUBLICANDOSE":
return "En publicación"
case "FINALIZADO":
return "Finalizado"
case "EN_PAUSA", "EN_ESPERA":
return "En pausa"
default:
return status
}
}
}
// MARK: - Chapter Model
/// Representa un capítulo individual de un manga.
///
/// `Chapter` contiene información sobre un capítulo específico, incluyendo
/// su número, título, URL, y metadatos de lectura como si ha sido leído,
/// descargado, y la última página leída.
///
/// Las propiedades `isRead`, `isDownloaded`, y `lastReadPage` son mutables
/// para actualizar el estado de lectura del usuario.
///
/// # Example
/// ```swift
/// var chapter = Chapter(
/// number: 1,
/// title: "El inicio de la aventura",
/// url: "https://manhwaweb.com/leer/one-piece/1",
/// slug: "one-piece/1"
/// )
/// chapter.isRead = true
/// chapter.lastReadPage = 15
/// print(chapter.displayNumber) // "Capítulo 1"
/// ```
struct Chapter: Codable, Identifiable, Hashable {
/// Identificador único del capítulo (computed, igual al número)
let id: Int { number }
/// Número del capítulo
let number: Int
/// Título del capítulo
let title: String
/// URL completa del capítulo en el sitio web
let url: String
/// Slug para identificar el capítulo en URLs
let slug: String
/// Indica si el capítulo ha sido marcado como leído
var isRead: Bool = false
/// Indica si el capítulo ha sido descargado localmente
var isDownloaded: Bool = false
/// Última página leída por el usuario
var lastReadPage: Int = 0
/// Número de capítulo formateado para mostrar en la UI
///
/// - Returns: String con formato "Capítulo {número}"
var displayNumber: String {
return "Capítulo \(number)"
}
/// Progreso de lectura como Double para ProgressViews
///
/// - Returns: Double representando la última página leída
var progress: Double {
return Double(lastReadPage)
}
}
// MARK: - Manga Page (Image)
/// Representa una página individual (imagen) de un capítulo.
///
/// `MangaPage` contiene la URL de una imagen de manga y su posición
/// dentro del capítulo. Puede marcar si la imagen está cacheada localmente
/// para evitar descargas redundantes.
///
/// # Example
/// ```swift
/// let page = MangaPage(url: "https://example.com/page1.jpg", index: 0)
/// print(page.id) // URL completa
/// ```
struct MangaPage: Codable, Identifiable, Hashable {
/// Identificador único de la página (computed, igual a la URL)
let id: String { url }
/// URL completa de la imagen
let url: String
/// Índice de la página en el capítulo (0-based)
let index: Int
/// Indica si la imagen está cacheada en almacenamiento local
var isCached: Bool = false
/// URL de la versión thumbnail de la imagen
///
/// Actualmente retorna la misma URL. Futura implementación puede
/// retornar una versión optimizada/miniatura de la imagen.
///
/// - Returns: URL del thumbnail (o de la imagen completa)
var thumbnailURL: String {
// Para thumbnail podríamos usar una versión más pequeña
return url
}
}
// MARK: - Reading Progress
/// Almacena el progreso de lectura de un usuario.
///
/// `ReadingProgress` registra qué página de qué capítulo de qué manga
/// leyó el usuario, junto con un timestamp para sincronización.
///
/// # Example
/// ```swift
/// let progress = ReadingProgress(
/// mangaSlug: "one-piece",
/// chapterNumber: 1,
/// pageNumber: 15,
/// timestamp: Date()
/// )
/// if progress.isCompleted {
/// print("Capítulo completado")
/// }
/// ```
struct ReadingProgress: Codable {
/// Slug del manga que se está leyendo
let mangaSlug: String
/// Número del capítulo
let chapterNumber: Int
/// Número de página actual
let pageNumber: Int
/// Fecha y hora en que se guardó el progreso
let timestamp: Date
/// Indica si el capítulo se considera completado
///
/// Un capítulo se considera completado si el usuario ha leído
/// más de 5 páginas. Este umbral evita marcar como completados
/// capítulos que el usuario solo hojeó.
///
/// - Returns: `true` si `pageNumber > 5`, `false` en caso contrario
var isCompleted: Bool {
// Considerar completado si leyó más de 5 páginas
return pageNumber > 5
}
}
// MARK: - Downloaded Chapter
/// Representa un capítulo descargado localmente en el dispositivo.
///
/// `DownloadedChapter` contiene metadata sobre un capítulo que ha sido
/// descargado, incluyendo todas sus páginas, fecha de descarga, y tamaño
/// total en disco.
///
/// # Example
/// ```swift
/// let downloaded = DownloadedChapter(
/// mangaSlug: "one-piece",
/// mangaTitle: "One Piece",
/// chapterNumber: 1,
/// pages: [page1, page2, page3],
/// downloadedAt: Date()
/// )
/// print(downloaded.displayTitle) // "One Piece - Capítulo 1"
/// ```
struct DownloadedChapter: Codable, Identifiable {
/// Identificador único compuesto por manga-slug y número de capítulo
let id: String { "\(mangaSlug)-chapter\(chapterNumber)" }
/// Slug del manga
let mangaSlug: String
/// Título del manga
let mangaTitle: String
/// Número del capítulo
let chapterNumber: Int
/// Array de páginas del capítulo
let pages: [MangaPage]
/// Fecha y hora de descarga
let downloadedAt: Date
/// Tamaño total del capítulo en bytes
var totalSize: Int64 = 0
/// Título formateado para mostrar en la UI
///
/// - Returns: String con formato "{MangaTitle} - Capítulo {number}"
var displayTitle: String {
"\(mangaTitle) - Capítulo \(chapterNumber)"
}
}
// MARK: - API Response Models
/// Respuesta de API que contiene una lista de mangas.
///
/// Usado para respuestas paginadas o listas completas de mangas
/// desde un backend opcional.
struct MangaListResponse: Codable {
/// Array de mangas en la respuesta
let mangas: [Manga]
/// Número total de mangas (útil para paginación)
let total: Int
}
/// Respuesta de API que contiene la lista de capítulos de un manga.
struct ChapterListResponse: Codable {
/// Array de capítulos del manga
let chapters: [Chapter]
/// Slug del manga al que pertenecen los capítulos
let mangaSlug: String
}
/// Respuesta de API con las URLs de imágenes de un capítulo.
struct ChapterImagesResponse: Codable {
/// Array de URLs de imágenes en orden
let images: [String]
/// Slug del capítulo
let chapterSlug: String
}