Files
MangaReader/docs/CONTRIBUTING.md
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

19 KiB

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

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:

    git clone https://github.com/tu-usuario/MangaReader.git
    cd MangaReader/ios-app
    
  3. Abre el proyecto en Xcode:

    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:

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

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:

/// 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:

// 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:

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

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:

// 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:

    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:

    webView.evaluateJavaScript("console.log('Debug info')")
    
  4. Usa try? en vez de try! temporalmente para evitar crashes:

    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

class ManhwaWebScraper { }
struct Manga { }

Propiedades y Métodos: camelCase

var mangaTitle: String
func scrapeMangaInfo() { }

Constantes Privadas: camelCase con prefijo si es necesario

private let favoritesKey = "favoriteMangas"

2. Organización de Código

Usa MARK comments para organizar:

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:

/// 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:

// 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:

// 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:

// 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:

// 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:

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:

    git checkout main
    git pull upstream main
    git checkout tu-rama
    git rebase main
    
  2. Resuelve conflicts si los hay

  3. Ejecuta los tests:

    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:

## 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

Herramientas Útiles

  • SwiftLint: Linter para Swift
  • SwiftFormat: Formateador de código
  • SwiftGen: Generador de código para recursos

Comunidad


Última actualización: Febrero 2026 Versión: 1.0.0