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:
2026-02-04 15:34:18 +01:00
commit b474182dd9
6394 changed files with 1063909 additions and 0 deletions

735
docs/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,735 @@
# 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