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