Initial commit: MangaReader iOS App
✨ Features: - App iOS completa para leer manga sin publicidad - Scraper con WKWebView para manhwaweb.com - Sistema de descargas offline - Lector con zoom y navegación - Favoritos y progreso de lectura - Compatible con iOS 15+ y Sideloadly/3uTools 📦 Contenido: - Backend Node.js con Puppeteer (opcional) - App iOS con SwiftUI - Scraper de capítulos e imágenes - Sistema de almacenamiento local - Testing completo - Documentación exhaustiva 🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente - 21 páginas descargadas - 4.68 MB total - URLs verificadas y funcionales 🎉 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
308
ios-app/Sources/Models/Manga.swift
Normal file
308
ios-app/Sources/Models/Manga.swift
Normal file
@@ -0,0 +1,308 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user