chore: clean unnecessary markdown files for CV sharing
This commit is contained in:
@@ -1,266 +0,0 @@
|
||||
# Checklist de Implementación - Sistema de Descarga
|
||||
|
||||
## ✅ Componentes Core
|
||||
|
||||
### DownloadManager
|
||||
- [x] Crear clase `DownloadManager` con patrón Singleton
|
||||
- [x] Implementar `downloadChapter()` con async/await
|
||||
- [x] Implementar `downloadChapters()` para múltiples capítulos
|
||||
- [x] Implementar `downloadImages()` con concurrencia limitada
|
||||
- [x] Implementar `cancelDownload(taskId:)` para cancelación individual
|
||||
- [x] Implementar `cancelAllDownloads()` para cancelación masiva
|
||||
- [x] Crear `DownloadTask` con propiedades @Published
|
||||
- [x] Crear enum `DownloadState` con todos los estados
|
||||
- [x] Crear enum `DownloadError` con tipos de error
|
||||
- [x] Crear `CancellationChecker` para cancelación asíncrona
|
||||
- [x] Integrar con `StorageService` para guardar imágenes
|
||||
- [x] Integrar con `ManhwaWebScraper` para obtener URLs
|
||||
- [x] Implementar manejo robusto de errores
|
||||
- [x] Implementar actualización de progreso en tiempo real
|
||||
- [x] Mantener historial de descargas (completadas y fallidas)
|
||||
- [x] Verificar duplicados antes de descargar
|
||||
|
||||
### MangaDetailView
|
||||
- [x] Añadir botón de descarga en toolbar
|
||||
- [x] Crear alert para seleccionar cantidad de capítulos
|
||||
- [x] Actualizar `ChapterRowView` con botón de descarga
|
||||
- [x] Mostrar progreso de descarga en cada fila
|
||||
- [x] Añadir indicador visual de capítulo descargado
|
||||
- [x] Actualizar `MangaDetailViewModel`:
|
||||
- [x] Integrar `DownloadManager`
|
||||
- [x] Implementar `downloadChapter()` async
|
||||
- [x] Implementar `downloadAllChapters()`
|
||||
- [x] Implementar `downloadLastChapters(count:)`
|
||||
- [x] Implementar `getDownloadProgress(for:)`
|
||||
- [x] Implementar `isDownloadingChapter(_:)`
|
||||
- [x] Implementar notificaciones de estado
|
||||
- [x] Crear overlay de notificaciones
|
||||
- [x] Manejar estados de error y éxito
|
||||
|
||||
### DownloadsView
|
||||
- [x] Crear `DownloadsView` con 3 tabs
|
||||
- [x] Tab "Activas"
|
||||
- [x] Tab "Completadas"
|
||||
- [x] Tab "Fallidas"
|
||||
- [x] Crear `ActiveDownloadCard` con progreso
|
||||
- [x] Crear `CompletedDownloadCard`
|
||||
- [x] Crear `FailedDownloadCard` con reintentar
|
||||
- [x] Implementar `DownloadsViewModel`
|
||||
- [x] Añadir botón "Cancelar todas"
|
||||
- [x] Añadir botones "Limpiar historial"
|
||||
- [x] Mostrar tamaño de almacenamiento
|
||||
- [x] Añadir botón "Limpiar todo" con alert
|
||||
- [x] Crear estados vacíos descriptivos
|
||||
- [x] Implementar picker segmentado para tabs
|
||||
|
||||
## ✅ Extensiones y Utilidades
|
||||
|
||||
### DownloadExtensions
|
||||
- [x] Extensión de `DownloadTask`
|
||||
- [x] `formattedSize` - tamaño estimado
|
||||
- [x] `estimatedTimeRemaining` - tiempo restante
|
||||
- [x] Extensión de `DownloadManager`
|
||||
- [x] `downloadStats` - estadísticas
|
||||
- [x] `hasActiveDownloads` - check de activas
|
||||
- [x] `totalDownloads` - contador total
|
||||
- [x] Extensión de `UIImage`
|
||||
- [x] `compressedData(quality:)` - compresión JPEG
|
||||
- [x] `resized(maxWidth:maxHeight:)` - redimensionado
|
||||
- [x] `optimizedForStorage()` - optimización completa
|
||||
- [x] Crear `DownloadStats` modelo
|
||||
- [x] Definir nombres de notificaciones
|
||||
- [x] Crear `URLSession.downloadSession()`
|
||||
|
||||
## ✅ Integración
|
||||
|
||||
### StorageService
|
||||
- [x] Verificar que `saveImage()` existe y funciona
|
||||
- [x] Verificar que `getImageURL()` existe y funciona
|
||||
- [x] Verificar que `isChapterDownloaded()` existe y funciona
|
||||
- [x] Verificar que `getChapterDirectory()` existe y funciona
|
||||
- [x] Verificar que `deleteDownloadedChapter()` existe y funciona
|
||||
- [x] Verificar que `getStorageSize()` existe y funciona
|
||||
- [x] Verificar que `formatFileSize()` existe y funciona
|
||||
|
||||
### Models
|
||||
- [x] Verificar que `DownloadedChapter` modelo existe
|
||||
- [x] Verificar que `MangaPage` modelo existe
|
||||
- [x] Verificar que `Chapter` modelo tiene propiedades necesarias
|
||||
|
||||
## ✅ UI/UX
|
||||
|
||||
### Notificaciones
|
||||
- [x] Toast notification al completar descarga
|
||||
- [x] Icono verde para éxito
|
||||
- [x] Icono rojo para error
|
||||
- [x] Auto-ocultado después de 3 segundos
|
||||
- [x] Animación desde abajo
|
||||
- [x] Overlay con blur shadow
|
||||
|
||||
### Progreso Visual
|
||||
- [x] ProgressView lineal
|
||||
- [x] Porcentaje numérico
|
||||
- [x] Páginas descargadas/total
|
||||
- [x] Barra animada
|
||||
- [x] Colores significativos (azul descargando, verde completado)
|
||||
|
||||
### Estados de Descarga
|
||||
- [x] Icono para pending (gris)
|
||||
- [x] Icono para downloading (azul animado)
|
||||
- [x] Icono para completed (checkmark verde)
|
||||
- [x] Icono para failed (X rojo)
|
||||
- [x] Icono para cancelled (gris)
|
||||
|
||||
### Estados Vacíos
|
||||
- [x] Icono grande y descriptivo
|
||||
- [x] Mensaje claro
|
||||
- [x] Llamada a la acción si aplica
|
||||
|
||||
## ✅ Manejo de Errores
|
||||
|
||||
### Tipos de Error
|
||||
- [x] `alreadyDownloaded` - Capítulo ya descargado
|
||||
- [x] `noImagesFound` - Scraper no encontró imágenes
|
||||
- [x] `invalidURL` - URL malformada
|
||||
- [x] `invalidResponse` - Respuesta HTTP inválida
|
||||
- [x] `httpError(statusCode)` - Error HTTP específico
|
||||
- [x] `invalidImageData` - Datos no son imagen válida
|
||||
- [x] `cancelled` - Usuario canceló
|
||||
- [x] `storageError(String)` - Error de almacenamiento
|
||||
|
||||
### Recuperación
|
||||
- [x] Limpieza de archivos parciales al cancelar
|
||||
- [x] Mensajes descriptivos al usuario
|
||||
- [x] Logging de errores para debugging
|
||||
- [x] Estado `failed` en FailedDownloadCard
|
||||
- [x] Opción de reintentar (preparado)
|
||||
|
||||
## ✅ Concurrencia y Performance
|
||||
|
||||
### Estrategia de Concurrencia
|
||||
- [x] Usar Swift Concurrency (async/await)
|
||||
- [x] Usar `@MainActor` para UI
|
||||
- [x] Usar `TaskGroup` para descargas en paralelo
|
||||
- [x] Limitar a 3 capítulos simultáneos
|
||||
- [x] Limitar a 5 imágenes simultáneas por capítulo
|
||||
- [x] Usar `CancellationChecker` para cancelación segura
|
||||
|
||||
### Optimizaciones
|
||||
- [x] Comprimir imágenes al 75-80% JPEG
|
||||
- [x] Redimensionar si > 2048px
|
||||
- [x] Concurrencia limitada para evitar picos
|
||||
- [x] Limpieza automática de historiales (50 completadas, 20 fallidas)
|
||||
|
||||
## ✅ Configuración
|
||||
|
||||
### Parámetros
|
||||
- [x] `maxConcurrentDownloads = 3`
|
||||
- [x] `maxConcurrentImagesPerChapter = 5`
|
||||
- [x] JPEG compression quality 0.8
|
||||
- [x] Optimized storage quality 0.75
|
||||
- [x] Max dimension 2048px
|
||||
|
||||
### Timeouts
|
||||
- [x] URLSession request: 30 segundos
|
||||
- [x] URLSession resource: 5 minutos
|
||||
- [x] Scraper page load: 3-5 segundos
|
||||
|
||||
## ✅ Documentación
|
||||
|
||||
### Archivos de Documentación
|
||||
- [x] `DOWNLOAD_SYSTEM_README.md` - Guía completa
|
||||
- [x] `IMPLEMENTATION_SUMMARY.md` - Resumen ejecutivo
|
||||
- [x] `DIAGRAMS.md` - Diagramas de flujo
|
||||
- [x] `IntegrationExample.swift` - Ejemplos de código
|
||||
|
||||
### Código
|
||||
- [x] Comentarios en código complejo
|
||||
- [x] Documentación de métodos públicos
|
||||
- [x] Ejemplos de uso en README
|
||||
|
||||
## ✅ Testing
|
||||
|
||||
### Testing Manual
|
||||
- [x] Verificar descarga de un capítulo
|
||||
- [x] Verificar descarga de múltiples capítulos
|
||||
- [x] Verificar cancelación de descarga
|
||||
- [x] Verificar manejo de errores
|
||||
- [x] Verificar progreso visual
|
||||
- [x] Verificar notificaciones
|
||||
- [x] Verificar limpieza de almacenamiento
|
||||
|
||||
### Casos de Prueba
|
||||
- [ ] Descargar capítulo sin internet
|
||||
- [ ] Descargar capítulo ya descargado
|
||||
- [ ] Cancelar descarga a mitad
|
||||
- [ ] Descargar capítulo con 0 imágenes
|
||||
- [ ] Llenar almacenamiento del dispositivo
|
||||
- [ ] Probar con diferentes tamaños de capítulo
|
||||
- [ ] Probar concurrentemente múltiples descargas
|
||||
|
||||
## 📋 Próximos Pasos (Opcionales)
|
||||
|
||||
### Mejoras Futuras
|
||||
- [ ] Background downloads con URLSession
|
||||
- [ ] Reanudar descargas pausadas
|
||||
- [ ] Priorización de descargas
|
||||
- [ ] Descarga automática de nuevos capítulos
|
||||
- [ ] Soporte para formato WebP
|
||||
- [ ] Batch operations en StorageService
|
||||
- [ ] Metrics y analytics
|
||||
|
||||
### Testing Automatizado
|
||||
- [ ] Unit tests para DownloadManager
|
||||
- [ ] Integration tests
|
||||
- [ ] UI tests para DownloadsView
|
||||
- [ ] Performance tests
|
||||
- [ ] Memory leak tests con XCTest
|
||||
|
||||
### UI Adicional
|
||||
- [ ] SettingsView con preferencias de descarga
|
||||
- [ ] ActiveDownloadsWidget para home
|
||||
- [ ] ActiveDownloadsBanner modifier
|
||||
- [ ] Badge en TabView
|
||||
- [ ] Sheet para descargas desde cualquier vista
|
||||
|
||||
---
|
||||
|
||||
## 📊 Estadísticas de Implementación
|
||||
|
||||
**Fecha**: 2026-02-04
|
||||
**Versión**: 1.0
|
||||
**Estado**: ✅ COMPLETO
|
||||
|
||||
### Archivos
|
||||
- **Nuevos**: 5 archivos principales
|
||||
- **Modificados**: 2 archivos existentes
|
||||
- **Total de líneas**: ~1,500 líneas
|
||||
|
||||
### Tiempos
|
||||
- **Desarrollo**: 4-6 horas
|
||||
- **Testing**: 1-2 horas
|
||||
- **Documentación**: 2-3 horas
|
||||
- **Total**: 7-11 horas
|
||||
|
||||
### Cobertura
|
||||
- **DownloadManager**: 100% completo
|
||||
- **MangaDetailView**: 100% completo
|
||||
- **DownloadsView**: 100% completo
|
||||
- **Extensiones**: 100% completo
|
||||
- **Integración**: 100% completo
|
||||
- **Documentación**: 100% completo
|
||||
|
||||
## 🎉 Checklist Final
|
||||
|
||||
- [x] Todos los componentes core implementados
|
||||
- [x] UI/UX pulida y funcional
|
||||
- [x] Manejo de errores robusto
|
||||
- [x] Concurrencia optimizada
|
||||
- [x] Integración completa con servicios existentes
|
||||
- [x] Documentación exhaustiva
|
||||
- [x] Ejemplos de integración
|
||||
- [x] Diagramas de flujo
|
||||
- [x] Testing manual completado
|
||||
- [x] Código limpio y mantenible
|
||||
|
||||
**ESTADO FINAL**: ✅ LISTO PARA PRODUCCIÓN
|
||||
@@ -1,352 +0,0 @@
|
||||
# API Configuration for MangaReader iOS App
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains the API configuration for connecting the iOS app to the VPS backend. The configuration is centralized in `APIConfig.swift` and includes all necessary settings for API communication.
|
||||
|
||||
## Files
|
||||
|
||||
- **APIConfig.swift**: Main configuration file with all API settings, endpoints, and helper methods
|
||||
- **APIConfigExample.swift**: Comprehensive usage examples and demonstrations
|
||||
- **README.md** (this file): Documentation and usage guide
|
||||
|
||||
## Current Configuration
|
||||
|
||||
### Server Connection
|
||||
- **Server URL**: `https://gitea.cbcren.online`
|
||||
- **Port**: `3001`
|
||||
- **Full Base URL**: `https://gitea.cbcren.online:3001`
|
||||
- **API Version**: `v1`
|
||||
- **API Base Path**: `https://gitea.cbcren.online:3001/api/v1`
|
||||
|
||||
### Timeouts
|
||||
- **Default Request Timeout**: `30.0` seconds (for regular API calls)
|
||||
- **Resource Download Timeout**: `300.0` seconds (5 minutes, for large downloads)
|
||||
|
||||
### Retry Policy
|
||||
- **Max Retries**: `3` attempts
|
||||
- **Base Retry Delay**: `1.0` second (with exponential backoff)
|
||||
|
||||
### Cache Configuration
|
||||
- **Max Memory Usage**: `100` cached responses
|
||||
- **Cache Expiry**: `300.0` seconds (5 minutes)
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic URL Construction
|
||||
|
||||
```swift
|
||||
// Method 1: Use the helper function
|
||||
let url = APIConfig.url(for: "manga/popular")
|
||||
// Result: "https://gitea.cbcren.online:3001/manga/popular"
|
||||
|
||||
// Method 2: Get a URL object
|
||||
if let urlObj = APIConfig.urlObject(for: "manga/popular") {
|
||||
var request = URLRequest(url: urlObj)
|
||||
// Make request...
|
||||
}
|
||||
|
||||
// Method 3: Use predefined endpoints
|
||||
let endpoint = APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1089)
|
||||
// Result: "https://gitea.cbcren.online:3001/api/v1/download/one-piece/1089"
|
||||
```
|
||||
|
||||
### URLSession Configuration
|
||||
|
||||
```swift
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = APIConfig.defaultTimeout
|
||||
configuration.timeoutIntervalForResource = APIConfig.downloadTimeout
|
||||
let session = URLSession(configuration: configuration)
|
||||
```
|
||||
|
||||
### URLRequest with Headers
|
||||
|
||||
```swift
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = APIConfig.defaultTimeout
|
||||
|
||||
// Add common headers
|
||||
for (key, value) in APIConfig.commonHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// Add authentication if needed
|
||||
if let token = authToken {
|
||||
let authHeaders = APIConfig.authHeader(token: token)
|
||||
for (key, value) in authHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Endpoints
|
||||
|
||||
### Download Endpoints
|
||||
|
||||
```swift
|
||||
// Request chapter download
|
||||
APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1089)
|
||||
|
||||
// Check if chapter is downloaded
|
||||
APIConfig.Endpoints.checkDownloaded(mangaSlug: "one-piece", chapterNumber: 1089)
|
||||
|
||||
// List all downloaded chapters for a manga
|
||||
APIConfig.Endpoints.listChapters(mangaSlug: "one-piece")
|
||||
|
||||
// Get specific image from chapter
|
||||
APIConfig.Endpoints.getImage(mangaSlug: "one-piece", chapterNumber: 1089, pageIndex: 0)
|
||||
|
||||
// Delete a chapter
|
||||
APIConfig.Endpoints.deleteChapter(mangaSlug: "one-piece", chapterNumber: 1089)
|
||||
```
|
||||
|
||||
### Server Endpoints
|
||||
|
||||
```swift
|
||||
// Get storage statistics
|
||||
APIConfig.Endpoints.storageStats()
|
||||
|
||||
// Health check
|
||||
APIConfig.Endpoints.health()
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
The configuration includes presets for different environments:
|
||||
|
||||
### Development
|
||||
```swift
|
||||
APIConfig.development
|
||||
// - serverURL: "http://192.168.1.100"
|
||||
// - port: 3001
|
||||
// - timeout: 60.0s
|
||||
// - logging: true
|
||||
```
|
||||
|
||||
### Staging
|
||||
```swift
|
||||
APIConfig.staging
|
||||
// - serverURL: "https://staging.cbcren.online"
|
||||
// - port: 3001
|
||||
// - timeout: 30.0s
|
||||
// - logging: true
|
||||
```
|
||||
|
||||
### Production (Current)
|
||||
```swift
|
||||
APIConfig.production
|
||||
// - serverURL: "https://gitea.cbcren.online"
|
||||
// - port: 3001
|
||||
// - timeout: 30.0s
|
||||
// - logging: false
|
||||
```
|
||||
|
||||
### Testing (Debug Only)
|
||||
```swift
|
||||
#if DEBUG
|
||||
APIConfig.testing
|
||||
// - serverURL: "http://localhost:3001"
|
||||
// - port: 3001
|
||||
// - timeout: 5.0s
|
||||
// - logging: true
|
||||
#endif
|
||||
```
|
||||
|
||||
## Changing the Server URL
|
||||
|
||||
To change the API server URL, modify the `serverURL` property in `APIConfig.swift`:
|
||||
|
||||
```swift
|
||||
// In APIConfig.swift, line 37
|
||||
static let serverURL = "https://gitea.cbcren.online" // Change this
|
||||
```
|
||||
|
||||
For environment-specific URLs, use compile-time conditionals:
|
||||
|
||||
```swift
|
||||
#if DEBUG
|
||||
static let serverURL = "http://192.168.1.100" // Local development
|
||||
#else
|
||||
static let serverURL = "https://gitea.cbcren.online" // Production
|
||||
#endif
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
The API defines specific error codes for different scenarios:
|
||||
|
||||
```swift
|
||||
APIConfig.ErrorCodes.chapterNotFound // 40401
|
||||
APIConfig.ErrorCodes.chapterAlreadyDownloaded // 40901
|
||||
APIConfig.ErrorCodes.storageLimitExceeded // 50701
|
||||
APIConfig.ErrorCodes.invalidImageFormat // 42201
|
||||
APIConfig.ErrorCodes.downloadFailed // 50001
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
The configuration includes a validation method:
|
||||
|
||||
```swift
|
||||
if APIConfig.isValid {
|
||||
print("Configuration is valid")
|
||||
} else {
|
||||
print("Configuration is invalid")
|
||||
}
|
||||
```
|
||||
|
||||
This checks:
|
||||
- Server URL is not empty
|
||||
- Port is in valid range (1-65535)
|
||||
- Timeout values are positive
|
||||
- Retry count is non-negative
|
||||
|
||||
## Debug Support
|
||||
|
||||
In debug builds, you can print the current configuration:
|
||||
|
||||
```swift
|
||||
#if DEBUG
|
||||
APIConfig.printConfiguration()
|
||||
#endif
|
||||
```
|
||||
|
||||
This outputs:
|
||||
```
|
||||
=== API Configuration ===
|
||||
Server URL: https://gitea.cbcren.online
|
||||
Port: 3001
|
||||
Base URL: https://gitea.cbcren.online:3001
|
||||
API Version: v1
|
||||
Default Timeout: 30.0s
|
||||
Download Timeout: 300.0s
|
||||
Max Retries: 3
|
||||
Logging Enabled: false
|
||||
Cache Enabled: true
|
||||
=========================
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use predefined endpoints** when available instead of manually constructing URLs
|
||||
2. **Use appropriate timeouts** - `defaultTimeout` for regular calls, `downloadTimeout` for large downloads
|
||||
3. **Validate configuration** on app startup
|
||||
4. **Use the helper methods** (`url()`, `urlObject()`) for URL construction
|
||||
5. **Include common headers** in all requests
|
||||
6. **Handle specific error codes** defined in `APIConfig.ErrorCodes`
|
||||
7. **Enable logging only in debug builds** for security
|
||||
|
||||
## Example: Making an API Call
|
||||
|
||||
```swift
|
||||
func fetchPopularManga() async throws -> [Manga] {
|
||||
// Construct URL
|
||||
guard let url = APIConfig.urlObject(for: "manga/popular") else {
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
// Create request
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = APIConfig.defaultTimeout
|
||||
|
||||
// Add headers
|
||||
for (key, value) in APIConfig.commonHeaders {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
// Make request
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// Validate response
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
httpResponse.statusCode == 200 else {
|
||||
throw APIError.requestFailed
|
||||
}
|
||||
|
||||
// Decode response
|
||||
let mangas = try JSONDecoder().decode([Manga].self, from: data)
|
||||
return mangas
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Downloading with Retry
|
||||
|
||||
```swift
|
||||
func downloadChapterWithRetry(
|
||||
mangaSlug: String,
|
||||
chapterNumber: Int
|
||||
) async throws -> Data {
|
||||
let endpoint = APIConfig.Endpoints.download(
|
||||
mangaSlug: mangaSlug,
|
||||
chapterNumber: chapterNumber
|
||||
)
|
||||
|
||||
return try await fetchWithRetry(endpoint: endpoint, retryCount: 0)
|
||||
}
|
||||
|
||||
func fetchWithRetry(endpoint: String, retryCount: Int) async throws -> Data {
|
||||
guard let url = URL(string: endpoint),
|
||||
retryCount < APIConfig.maxRetries else {
|
||||
throw APIError.retryLimitExceeded
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.timeoutInterval = APIConfig.downloadTimeout
|
||||
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse,
|
||||
httpResponse.statusCode == 200 {
|
||||
return data
|
||||
} else {
|
||||
throw APIError.requestFailed
|
||||
}
|
||||
} catch {
|
||||
// Calculate exponential backoff delay
|
||||
let delay = APIConfig.baseRetryDelay * pow(2.0, Double(retryCount))
|
||||
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
||||
|
||||
return try await fetchWithRetry(endpoint: endpoint, retryCount: retryCount + 1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
1. **Verify server URL**: Check that `serverURL` is correct and accessible
|
||||
2. **Check port**: Ensure `port` matches the backend server configuration
|
||||
3. **Test connectivity**: Use the health endpoint: `APIConfig.Endpoints.health()`
|
||||
4. **Enable logging**: Set `loggingEnabled = true` to see request details
|
||||
|
||||
### Timeout Issues
|
||||
|
||||
1. **For regular API calls**: Use `APIConfig.defaultTimeout` (30 seconds)
|
||||
2. **For large downloads**: Use `APIConfig.downloadTimeout` (300 seconds)
|
||||
3. **Slow networks**: Increase timeout values if needed
|
||||
|
||||
### SSL Certificate Issues
|
||||
|
||||
If using HTTPS with a self-signed certificate:
|
||||
1. Add the certificate to the app's bundle
|
||||
2. Configure URLSession to trust the certificate
|
||||
3. Or use HTTP for development (not recommended for production)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
When migrating from the old configuration:
|
||||
|
||||
1. Replace hardcoded URLs with `APIConfig.url(for:)` or predefined endpoints
|
||||
2. Use `APIConfig.commonHeaders` instead of manually setting headers
|
||||
3. Replace hardcoded timeouts with `APIConfig.defaultTimeout` or `APIConfig.downloadTimeout`
|
||||
4. Add validation on app startup with `APIConfig.isValid`
|
||||
5. Use specific error codes from `APIConfig.ErrorCodes`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- See `APIConfigExample.swift` for more comprehensive examples
|
||||
- Check the backend API documentation for available endpoints
|
||||
- Review the iOS app's Services directory for integration examples
|
||||
@@ -1,412 +0,0 @@
|
||||
# Diagramas del Sistema de Descarga
|
||||
|
||||
## 1. Arquitectura General
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ UI Layer │
|
||||
├──────────────────────┬──────────────────┬───────────────────┤
|
||||
│ MangaDetailView │ DownloadsView │ ReaderView │
|
||||
│ - Download buttons │ - Active tab │ - Read offline │
|
||||
│ - Progress bars │ - Completed tab │ - Use local URLs │
|
||||
│ - Notifications │ - Failed tab │ │
|
||||
└──────────┬───────────┴──────────┬───────┴─────────┬─────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
├──────────────────────┬──────────────────┬───────────────────┤
|
||||
│ MangaDetailViewModel│ DownloadsViewModel│ │
|
||||
│ - downloadChapter() │ - clearAllStorage│ │
|
||||
│ - downloadChapters()│ - showClearAlert│ │
|
||||
└──────────┬──────────────────────────────┴───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Business Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ DownloadManager │
|
||||
│ - downloadChapter() │
|
||||
│ - downloadChapters() │
|
||||
│ - cancelDownload() │
|
||||
│ - cancelAllDownloads() │
|
||||
│ - downloadImages() │
|
||||
└──────────┬──────────────────────┬───────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────────────┐ ┌────────────────────────────────┐
|
||||
│ Scraper Layer │ │ Storage Layer │
|
||||
├────────────────────────┤ ├────────────────────────────────┤
|
||||
│ ManhwaWebScraper │ │ StorageService │
|
||||
│ - scrapeChapters() │ │ - saveImage() │
|
||||
│ - scrapeChapterImages()│ │ - getImageURL() │
|
||||
│ - scrapeMangaInfo() │ │ - isChapterDownloaded() │
|
||||
│ │ │ - getChapterDirectory() │
|
||||
│ │ │ - deleteDownloadedChapter() │
|
||||
└────────────────────────┘ └────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Network Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ URLSession │
|
||||
│ - downloadImage(from: URL) │
|
||||
│ - data(from: URL) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Flujo de Descarga Detallado
|
||||
|
||||
```
|
||||
USUARIO TOCA "DESCARGAR CAPÍTULO"
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ MangaDetailView │
|
||||
│ Button tapped → downloadChapter() │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ MangaDetailViewModel │
|
||||
│ 1. Verificar si ya está descargado │
|
||||
│ 2. Llamar downloadManager.downloadChapter()│
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ DownloadManager │
|
||||
│ 1. Verificar duplicados │
|
||||
│ 2. Crear DownloadTask (state: .pending) │
|
||||
│ 3. Agregar a activeDownloads[] │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ ManhwaWebScraper │
|
||||
│ scrapeChapterImages(chapterSlug) │
|
||||
│ → Retorna [String] URLs de imágenes │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ DownloadManager │
|
||||
│ 4. Actualizar task.imageURLs │
|
||||
│ 5. Iniciar downloadImages() │
|
||||
│ Task 1: state = .downloading(0.0) │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ downloadImages() - CONCURRENCIA │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ TaskGroup (max 5 concurrent) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
|
||||
│ │ │Img 0│ │Img 1│ │Img 2│ │Img 3│... │ │
|
||||
│ │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ▼ ▼ ▼ ▼ │ │
|
||||
│ │ ┌───────────────────────────────┐ │ │
|
||||
│ │ │ downloadImage(from: URL) │ │ │
|
||||
│ │ │ 1. URLSession.data(from:) │ │ │
|
||||
│ │ │ 2. Validar HTTP 200 │ │ │
|
||||
│ │ │ 3. UIImage(data:) │ │ │
|
||||
│ │ │ 4. optimizedForStorage() │ │ │
|
||||
│ │ └───────────────┬───────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌───────────────────────────────┐ │ │
|
||||
│ │ │ StorageService.saveImage() │ │ │
|
||||
│ │ │ → Documents/Chapters/... │ │ │
|
||||
│ │ └───────────────┬───────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌───────────────────────────────┐ │ │
|
||||
│ │ │ task.updateProgress() │ │ │
|
||||
│ │ │ downloadedPages += 1 │ │ │
|
||||
│ │ │ progress = new value │ │ │
|
||||
│ │ │ @Published → UI updates │ │ │
|
||||
│ │ └───────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Repetir para todas las imágenes... │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ DownloadManager │
|
||||
│ 6. Todas las imágenes descargadas │
|
||||
│ 7. Crear DownloadedChapter metadata │
|
||||
│ 8. storage.saveDownloadedChapter() │
|
||||
│ 9. task.complete() → state = .completed │
|
||||
│ 10. Mover de activeDownloads[] a │
|
||||
│ completedDownloads[] │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ MangaDetailViewModel │
|
||||
│ 11. showDownloadCompletionNotification() │
|
||||
│ 12. "1 capítulo(s) descargado(s)" │
|
||||
│ 13. loadChapters() para actualizar UI │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ UI ACTUALIZADA │
|
||||
│ - ChapterRow muestra checkmark verde │
|
||||
│ - Toast notification aparece │
|
||||
│ - DownloadsView actualiza │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. Estados de una Descarga
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ESTADOS DE DESCARGA │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
PENDING
|
||||
┌──────────────────────┐
|
||||
│ state: .pending │
|
||||
│ downloadedPages: 0 │
|
||||
│ progress: 0.0 │
|
||||
│ UI: Icono gris │
|
||||
└──────────┬───────────┘
|
||||
│ Usuario inicia descarga
|
||||
▼
|
||||
DOWNLOADING
|
||||
┌──────────────────────┐
|
||||
│ state: .downloading │
|
||||
│ downloadedPages: N │ ← Incrementando
|
||||
│ progress: N/Total │ ← 0.0 a 1.0
|
||||
│ UI: Barra azul │ ← Animando
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
├──────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
COMPLETADO CANCELADO/ERROR
|
||||
┌──────────────────┐ ┌──────────────────────┐
|
||||
│ state: .completed│ │ state: .cancelled │
|
||||
│ downloadedPages: │ │ state: .failed(error)│
|
||||
│ Total │ │ downloadedPages: N │
|
||||
│ progress: 1.0 │ │ progress: N/Total │
|
||||
│ UI: Checkmark │ │ UI: X rojo / Icono │
|
||||
└──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## 4. Cancelación de Descarga
|
||||
|
||||
```
|
||||
USUARIO TOCA "CANCELAR"
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ DownloadManager.cancelDownload(taskId) │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ 1. Encontrar task en activeDownloads[] │
|
||||
│ 2. task.cancel() │
|
||||
│ → cancellationToken.isCancelled = true│
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ TaskGroup detecta cancelación │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ if task.isCancelled { │ │
|
||||
│ │ throw DownloadError.cancelled │ │
|
||||
│ │ } │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ downloadImages() lanza error │
|
||||
│ → Catch block ejecuta cleanup │
|
||||
└───────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ LIMPIEZA │
|
||||
│ 1. Remover de activeDownloads[] │
|
||||
│ 2. storage.deleteDownloadedChapter() │
|
||||
│ → Eliminar imágenes parciales │
|
||||
│ 3. NO agregar a completed[] │
|
||||
└───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────┐
|
||||
│ UI ACTUALIZADA │
|
||||
│ - Progress bar desaparece │
|
||||
│ - Icono de descarga restaurado │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 5. Concurrencia de Descargas
|
||||
|
||||
```
|
||||
NIVEL 1: Descarga de Capítulos (max 3 simultáneos)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ downloadManager.downloadChapters([ch1, ch2, ch3, ch4...]) │
|
||||
└───────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TaskGroup (limitado a 3 tasks) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Chapter 1 │ │ Chapter 2 │ │ Chapter 3 │ ← Active │
|
||||
│ │ downloading│ │ downloading│ │ downloading│ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
|
||||
│ │ Chapter 4 │ │ Chapter 5 │ │ Chapter 6 │ ← Waiting │
|
||||
│ │ waiting │ │ waiting │ │ waiting │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
│ Cuando Chapter 1 completa → Chapter 4 inicia │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
NIVEL 2: Descarga de Imágenes (max 5 simultáneas por capítulo)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Chapter 1: downloadImages([img0, img1, ... img50]) │
|
||||
└───────────────┬─────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TaskGroup (limitado a 5 tasks) │
|
||||
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
|
||||
│ │img0│ │img1│ │img2│ │img3│ │img4│ ← Descargando │
|
||||
│ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ │
|
||||
│ │ │ │ │ │ │
|
||||
│ ┌─▼──────▼──────▼──────▼──────▼──┐ │
|
||||
│ │ img5, img6, img7, img8, img9...│ ← Waiting │
|
||||
│ └─────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Cuando img0-4 completan → img5-9 inician │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
RESULTADO: Máximo 15 imágenes descargando simultáneamente
|
||||
(3 capítulos × 5 imágenes)
|
||||
```
|
||||
|
||||
## 6. Gestión de Errores
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIPOS DE ERROR │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
NETWORK ERRORS
|
||||
┌──────────────────────┐
|
||||
│ - Timeout (30s) │ → Reintentar automáticamente
|
||||
│ - No internet │ → Error al usuario
|
||||
│ - HTTP 4xx, 5xx │ → Error específico
|
||||
└──────────────────────┘
|
||||
|
||||
SCRAPER ERRORS
|
||||
┌──────────────────────┐
|
||||
│ - No images found │ → Error: "No se encontraron imágenes"
|
||||
│ - Page load failed │ → Error: "Error al cargar página"
|
||||
│ - Parsing error │ → Error: "Error al procesar"
|
||||
└──────────────────────┘
|
||||
|
||||
STORAGE ERRORS
|
||||
┌──────────────────────┐
|
||||
│ - No space left │ → Error: "Espacio insuficiente"
|
||||
│ - Permission denied │ → Error: "Sin permisos"
|
||||
│ - Disk write error │ → Error: "Error de escritura"
|
||||
└──────────────────────┘
|
||||
|
||||
VALIDATION ERRORS
|
||||
┌──────────────────────┐
|
||||
│ - Already downloaded │ → Skip o sobrescribir
|
||||
│ - Invalid URL │ → Error: "URL inválida"
|
||||
│ - Invalid image data │ → Error: "Imagen inválida"
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## 7. Sincronización de UI
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ @Published PROPERTIES │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
DownloadManager
|
||||
┌───────────────────────────────────┐
|
||||
│ @Published var activeDownloads │ → Vista observa
|
||||
│ @Published var completedDownloads │ → Vista observa
|
||||
│ @Published var failedDownloads │ → Vista observa
|
||||
│ @Published var totalProgress │ → Vista observa
|
||||
└───────────────────────────────────┘
|
||||
│
|
||||
│ @Published cambia
|
||||
▼
|
||||
┌───────────────────────────────────┐
|
||||
│ SwiftUI View se re-renderiza │
|
||||
│ automáticamente │
|
||||
└───────────────────────────────────┘
|
||||
|
||||
DownloadTask
|
||||
┌───────────────────────────────────┐
|
||||
│ @Published var state │ → Card observa
|
||||
│ @Published var downloadedPages │ → ProgressView observa
|
||||
│ @Published var progress │ → ProgressView observa
|
||||
└───────────────────────────────────┘
|
||||
│
|
||||
│ @Published cambia
|
||||
▼
|
||||
┌───────────────────────────────────┐
|
||||
│ ActiveDownloadCard se actualiza │
|
||||
│ automáticamente │
|
||||
└───────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 8. Estructura de Archivos
|
||||
|
||||
```
|
||||
Documents/
|
||||
└── Chapters/
|
||||
└── {mangaSlug}/
|
||||
└── Chapter{chapterNumber}/
|
||||
├── page_0.jpg
|
||||
├── page_1.jpg
|
||||
├── page_2.jpg
|
||||
├── ...
|
||||
└── page_N.jpg
|
||||
|
||||
Ejemplo:
|
||||
Documents/
|
||||
└── Chapters/
|
||||
└── one-piece_1695365223767/
|
||||
└── Chapter1/
|
||||
├── page_0.jpg (150 KB)
|
||||
├── page_1.jpg (180 KB)
|
||||
├── page_2.jpg (165 KB)
|
||||
└── ...
|
||||
└── Chapter2/
|
||||
├── page_0.jpg
|
||||
├── page_1.jpg
|
||||
└── ...
|
||||
|
||||
metadata.json
|
||||
{
|
||||
"downloadedChapters": [
|
||||
{
|
||||
"id": "one-piece_1695365223767-chapter1",
|
||||
"mangaSlug": "one-piece_1695365223767",
|
||||
"mangaTitle": "One Piece",
|
||||
"chapterNumber": 1,
|
||||
"pages": [...],
|
||||
"downloadedAt": "2026-02-04T10:30:00Z",
|
||||
"totalSize": 5242880
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1,355 +0,0 @@
|
||||
# Sistema de Descarga de Capítulos - Resumen de Implementación
|
||||
|
||||
## Archivos Creados/Modificados
|
||||
|
||||
### Archivos Nuevos Creados
|
||||
|
||||
1. **`/Sources/Services/DownloadManager.swift`** (470 líneas)
|
||||
- Clase principal `DownloadManager` con patrón Singleton
|
||||
- `DownloadTask`: Representa una tarea de descarga individual
|
||||
- `DownloadState`: Enum con estados de descarga
|
||||
- `DownloadProgress`: Modelo de progreso
|
||||
- `CancellationChecker`: Sistema de cancelación asíncrona
|
||||
- `DownloadError`: Tipos de errores específicos
|
||||
|
||||
2. **`/Sources/Views/DownloadsView.swift`** (350 líneas)
|
||||
- Vista principal de gestión de descargas
|
||||
- 3 tabs: Activas, Completadas, Fallidas
|
||||
- `ActiveDownloadCard`: Card con progreso en tiempo real
|
||||
- `CompletedDownloadCard`: Card de descargas exitosas
|
||||
- `FailedDownloadCard`: Card con opción de reintentar
|
||||
- `DownloadsViewModel`: ViewModel para la vista
|
||||
|
||||
3. **`/Sources/Extensions/DownloadExtensions.swift`** (180 líneas)
|
||||
- Extensiones de `DownloadTask` para formateo
|
||||
- Extensiones de `DownloadManager` para estadísticas
|
||||
- Extensiones de `UIImage` para compresión y optimización
|
||||
- Constantes de notificaciones
|
||||
- `DownloadStats` modelo
|
||||
|
||||
4. **`/Sources/Examples/IntegrationExample.swift`** (250 líneas)
|
||||
- Ejemplos de integración con TabView
|
||||
- Ejemplo de navegación desde MangaDetailView
|
||||
- Badge en TabView para descargas activas
|
||||
- Sheet para descargas
|
||||
- Vista de configuración
|
||||
- Widget de descargas activas
|
||||
- Modificador de banner
|
||||
|
||||
5. **`/Sources/Services/DOWNLOAD_SYSTEM_README.md`** (400 líneas)
|
||||
- Documentación completa del sistema
|
||||
- Guía de uso de todos los componentes
|
||||
- Ejemplos de código
|
||||
- Configuración y parámetros
|
||||
- Best practices
|
||||
- Troubleshooting
|
||||
|
||||
### Archivos Modificados
|
||||
|
||||
1. **`/Sources/Views/MangaDetailView.swift`**
|
||||
- Actualizado `ChapterRowView` para mostrar progreso de descarga
|
||||
- Añadido botón de descarga individual por capítulo
|
||||
- Actualizado `MangaDetailViewModel`:
|
||||
- Integración con `DownloadManager`
|
||||
- Métodos para descargar capítulos
|
||||
- Notificaciones de completado/error
|
||||
- Seguimiento de progreso
|
||||
- Añadido overlay de notificaciones
|
||||
|
||||
2. **`/Sources/Models/Manga.swift`** (sin cambios)
|
||||
- Ya contiene los modelos necesarios:
|
||||
- `DownloadedChapter`
|
||||
- `ReadingProgress`
|
||||
- `MangaPage`
|
||||
|
||||
3. **`/Sources/Services/StorageService.swift`** (sin cambios)
|
||||
- Ya contiene métodos necesarios:
|
||||
- `saveImage()`
|
||||
- `getImageURL()`
|
||||
- `isChapterDownloaded()`
|
||||
- `getChapterDirectory()`
|
||||
- `getStorageSize()`
|
||||
|
||||
## Características Implementadas
|
||||
|
||||
### 1. DownloadManager (Gerente de Descargas)
|
||||
- ✅ Descarga asíncrona de imágenes con async/await
|
||||
- ✅ Concurrencia controlada (3 capítulos, 5 imágenes simultáneas)
|
||||
- ✅ Cancelación de descargas (individual o masiva)
|
||||
- ✅ Progreso en tiempo real
|
||||
- ✅ Manejo robusto de errores
|
||||
- ✅ Historial de descargas (completadas y fallidas)
|
||||
- ✅ Integración con StorageService
|
||||
- ✅ Verificación de duplicados
|
||||
|
||||
### 2. MangaDetailView Actualizado
|
||||
- ✅ Botón de descarga en toolbar
|
||||
- ✅ Descarga individual por capítulo
|
||||
- ✅ Progreso visible en cada fila
|
||||
- ✅ Notificaciones de estado
|
||||
- ✅ Alert para descargar múltiples capítulos
|
||||
- ✅ Indicador visual de capítulos descargados
|
||||
|
||||
### 3. DownloadsView (Vista de Descargas)
|
||||
- ✅ Tabs: Activas, Completadas, Fallidas
|
||||
- ✅ Cards con información detallada
|
||||
- ✅ Cancelación de descargas
|
||||
- ✅ Limpieza de historiales
|
||||
- ✅ Información de almacenamiento usado
|
||||
- ✅ Alert para limpiar todo
|
||||
- ✅ Estados vacíos descriptivos
|
||||
|
||||
### 4. Extensiones y Utilidades
|
||||
- ✅ Formateo de tamaños de archivo
|
||||
- ✅ Estimación de tiempo restante
|
||||
- ✅ Optimización de imágenes
|
||||
- ✅ Compresión JPEG configurable
|
||||
- ✅ Notificaciones del sistema
|
||||
- ✅ URLSession configurada
|
||||
|
||||
## Flujo de Descarga Completo
|
||||
|
||||
```
|
||||
1. Usuario toca botón de descarga
|
||||
↓
|
||||
2. DownloadManager.downloadChapter()
|
||||
↓
|
||||
3. ManhwaWebScraper.scrapeChapterImages()
|
||||
↓
|
||||
4. Se crea DownloadTask con estado .pending
|
||||
↓
|
||||
5. downloadImages() inicia con TaskGroup
|
||||
↓
|
||||
6. Por cada imagen:
|
||||
- downloadImage() desde URL
|
||||
- UIImage.optimizedForStorage()
|
||||
- StorageService.saveImage()
|
||||
- Actualizar progreso
|
||||
↓
|
||||
7. Al completar todas:
|
||||
- StorageService.saveDownloadedChapter()
|
||||
- Mover tarea a completadas
|
||||
- Notificar usuario
|
||||
↓
|
||||
8. Capítulo marcado como descargado
|
||||
```
|
||||
|
||||
## Concurrencia y Performance
|
||||
|
||||
### Estrategia de Concurrencia
|
||||
```swift
|
||||
// Nivel 1: Descarga de capítulos (máximo 3 en paralelo)
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for chapter in chapters {
|
||||
group.addTask {
|
||||
try await downloadChapter(chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nivel 2: Descarga de imágenes por capítulo (máximo 5 en paralelo)
|
||||
try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
|
||||
for (index, imageURL) in imageURLs.enumerated() {
|
||||
group.addTask {
|
||||
return (index, try await downloadImage(from: imageURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optimizaciones de Memoria
|
||||
- Imágenes comprimidas al 75-80% JPEG
|
||||
- Redimensionado si > 2048px
|
||||
- Concurrencia limitada para evitar picos
|
||||
- Limpieza automática de historiales
|
||||
|
||||
## Manejo de Errores
|
||||
|
||||
### Tipos de Errores
|
||||
```swift
|
||||
enum DownloadError {
|
||||
case alreadyDownloaded // Ya existe
|
||||
case noImagesFound // Scraper falló
|
||||
case invalidURL // URL malformada
|
||||
case invalidResponse // Error HTTP
|
||||
case httpError(statusCode) // 4xx, 5xx
|
||||
case invalidImageData // No es imagen
|
||||
case cancelled // Usuario canceló
|
||||
case storageError(String) // Error disco
|
||||
}
|
||||
```
|
||||
|
||||
### Recuperación
|
||||
- Reintentos automáticos en errores de red
|
||||
- Limpieza de archivos parciales
|
||||
- Logging de errores para debugging
|
||||
- Mensajes descriptivos al usuario
|
||||
|
||||
## Integración con StorageService
|
||||
|
||||
### Guardado de Imágenes
|
||||
```swift
|
||||
try await storage.saveImage(
|
||||
image, // UIImage optimizada
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
// Guarda en: Documents/Chapters/manga-slug/Chapter1/page_0.jpg
|
||||
```
|
||||
|
||||
### Verificación de Descarga
|
||||
```swift
|
||||
if storage.isChapterDownloaded(
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1
|
||||
) {
|
||||
// Ya está descargado
|
||||
}
|
||||
```
|
||||
|
||||
### Lectura de Imágenes
|
||||
```swift
|
||||
if let imageURL = storage.getImageURL(
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
) {
|
||||
// Usar URL local
|
||||
AsyncImage(url: imageURL) { image in
|
||||
image.resizable()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## UI/UX Implementada
|
||||
|
||||
### Notificaciones
|
||||
- Toast notification al completar
|
||||
- Icono verde (éxito) o rojo (error)
|
||||
- Auto-ocultado después de 3 segundos
|
||||
- Animación desde abajo
|
||||
|
||||
### Progreso Visual
|
||||
- Barra de progreso lineal
|
||||
- Porcentaje numérico
|
||||
- Páginas descargadas/total
|
||||
- Tiempo estimado restante
|
||||
|
||||
### Estados Vacíos
|
||||
- Iconos grandes y descriptivos
|
||||
- Mensajes claros
|
||||
- Llamadas a la acción
|
||||
|
||||
### Estados de Descarga
|
||||
- ⏳ Pending: Gris
|
||||
- 🔄 Downloading: Azul con progreso
|
||||
- ✅ Completed: Verde
|
||||
- ❌ Failed: Rojo con mensaje
|
||||
- ❌ Cancelled: Gris
|
||||
|
||||
## Testing y Debugging
|
||||
|
||||
### Logs Implementados
|
||||
```swift
|
||||
print("Downloading chapter \(chapter.number)")
|
||||
print("Error downloading chapter: \(error.localizedDescription)")
|
||||
```
|
||||
|
||||
### Puntos de Verificación
|
||||
- ¿El capítulo ya está descargado?
|
||||
- ¿Se encontraron imágenes?
|
||||
- ¿Las URLs son válidas?
|
||||
- ¿Las imágenes son válidas?
|
||||
- ¿Hay espacio disponible?
|
||||
|
||||
### Métricas Disponibles
|
||||
- Número de descargas activas
|
||||
- Progreso general
|
||||
- Tiempo restante estimado
|
||||
- Tamaño de almacenamiento
|
||||
- Tasa de éxito
|
||||
|
||||
## Configuración
|
||||
|
||||
### Parámetros Ajustables
|
||||
```swift
|
||||
// En DownloadManager
|
||||
private let maxConcurrentDownloads = 3
|
||||
private let maxConcurrentImagesPerChapter = 5
|
||||
|
||||
// En StorageService.saveImage()
|
||||
image.jpegData(compressionQuality: 0.8)
|
||||
|
||||
// En DownloadExtensions
|
||||
let maxDimension: CGFloat = 2048
|
||||
return resized.compressedData(quality: 0.75)
|
||||
```
|
||||
|
||||
### Timeouts
|
||||
- URLSession request: 30 segundos
|
||||
- URLSession resource: 5 minutos
|
||||
- Espera carga de página scraper: 3-5 segundos
|
||||
|
||||
## Uso Recomendado
|
||||
|
||||
### En Tu App Principal
|
||||
1. Agregar `DownloadsView` a tu TabView principal
|
||||
2. Opcional: Añadir badge con count de descargas activas
|
||||
3. Usar `ActiveDownloadsWidget` en home
|
||||
4. Implementar navegación desde `MangaDetailView`
|
||||
|
||||
### En ReaderView
|
||||
1. Verificar si capítulo está descargado
|
||||
2. Usar `storage.getImageURL()` para imágenes locales
|
||||
3. Fallback a URLs remotas si no existe
|
||||
|
||||
### En SettingsView
|
||||
1. Mostrar tamaño de almacenamiento usado
|
||||
2. Botón para limpiar descargas
|
||||
3. Estadísticas de descargas
|
||||
4. Preferencias (solo Wi-Fi, etc.)
|
||||
|
||||
## Archivos de Configuración No Necesarios
|
||||
|
||||
El sistema no requiere:
|
||||
- ❌ Info.plist modifications (permisos estándar)
|
||||
- ❌ Entitlements especiales
|
||||
- ❌ Background modes (opcional para futuro)
|
||||
- ❌ Network configurations (usa URLSession por defecto)
|
||||
|
||||
## Next Steps Opcionales
|
||||
|
||||
### Mejoras Futuras
|
||||
- [ ] Background downloads con URLSession
|
||||
- [ ] Reanudar descargas pausadas
|
||||
- [ ] Priorización de descargas
|
||||
- [ ] Descarga automática de nuevos capítulos
|
||||
- [ ] Compresión adicional (WebP)
|
||||
- [ ] Batch operations
|
||||
- [ ] Metrics y analytics
|
||||
|
||||
### Testing
|
||||
- [ ] Unit tests para DownloadManager
|
||||
- [ ] Integration tests
|
||||
- [ ] UI tests para DownloadsView
|
||||
- [ ] Performance tests
|
||||
- [ ] Memory leak tests
|
||||
|
||||
### Documentación
|
||||
- [ ] Vídeo demostrativo
|
||||
- [ ] Screenshots en README
|
||||
- [ ] Diagramas de secuencia
|
||||
- [ ] API documentation
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
**Tiempo de Desarrollo**: ~4-6 horas
|
||||
**Líneas de Código**: ~1,500 líneas
|
||||
**Archivos Creados**: 5 nuevos
|
||||
**Archivos Modificados**: 2 existentes
|
||||
**Complejidad**: Media-Alta
|
||||
**Robustez**: Alta
|
||||
**UX**: Excelente
|
||||
|
||||
**Estado**: ✅ COMPLETO Y FUNCIONAL
|
||||
@@ -1,300 +0,0 @@
|
||||
# Quick Start - Sistema de Descarga
|
||||
|
||||
## Integración Rápida (5 minutos)
|
||||
|
||||
### Paso 1: Verificar Archivos
|
||||
|
||||
Los siguientes archivos ya están creados en tu proyecto:
|
||||
|
||||
```
|
||||
ios-app/Sources/
|
||||
├── Services/
|
||||
│ ├── DownloadManager.swift ✅ 13KB
|
||||
│ └── DOWNLOAD_SYSTEM_README.md ✅ Documentación completa
|
||||
├── Views/
|
||||
│ ├── DownloadsView.swift ✅ 13KB
|
||||
│ └── MangaDetailView.swift ✅ Actualizado
|
||||
├── Extensions/
|
||||
│ └── DownloadExtensions.swift ✅ 4.7KB
|
||||
├── Examples/
|
||||
│ └── IntegrationExample.swift ✅ Ejemplos de integración
|
||||
└── Tests/
|
||||
└── DownloadManagerTests.swift ✅ Tests unitarios
|
||||
```
|
||||
|
||||
### Paso 2: Agregar DownloadsView a Tu App
|
||||
|
||||
Si tienes un TabView, simplemente agrega:
|
||||
|
||||
```swift
|
||||
// En tu ContentView o App principal
|
||||
TabView {
|
||||
ContentView() // Tu vista actual
|
||||
.tabItem {
|
||||
Label("Biblioteca", systemImage: "books.vertical")
|
||||
}
|
||||
|
||||
DownloadsView() // ✅ NUEVA VISTA
|
||||
.tabItem {
|
||||
Label("Descargas", systemImage: "arrow.down.circle")
|
||||
}
|
||||
.badge(downloadManager.activeDownloads.count) // Opcional: badge
|
||||
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Ajustes", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Paso 3: Probar la Descarga
|
||||
|
||||
1. Abre `MangaDetailView` (ya está actualizado)
|
||||
2. Toca el botón de descarga (icono de flecha hacia abajo) en la toolbar
|
||||
3. Selecciona "Descargar últimos 10" o "Descargar todos"
|
||||
4. Observa el progreso en cada fila de capítulo
|
||||
5. Ve a la tab "Descargas" para ver el progreso detallado
|
||||
|
||||
¡Eso es todo! El sistema está completamente integrado.
|
||||
|
||||
## Características Incluidas
|
||||
|
||||
### ✅ Ya Funciona
|
||||
- Descarga de capítulos individuales
|
||||
- Descarga masiva (todos o últimos N)
|
||||
- Progreso en tiempo real
|
||||
- Cancelación de descargas
|
||||
- Historial de descargas
|
||||
- Notificaciones de estado
|
||||
- Gestión de almacenamiento
|
||||
- Manejo de errores
|
||||
|
||||
### 📱 UI Components
|
||||
- `DownloadsView` - Vista completa con tabs
|
||||
- `ActiveDownloadCard` - Card con progreso
|
||||
- `CompletedDownloadCard` - Card de completados
|
||||
- `FailedDownloadCard` - Card con reintentar
|
||||
- Toast notifications
|
||||
- Progress bars
|
||||
|
||||
### 🔧 Services
|
||||
- `DownloadManager` - Singleton gerente de descargas
|
||||
- `DownloadTask` - Modelo de tarea individual
|
||||
- `DownloadState` - Estados de descarga
|
||||
- `DownloadError` - Tipos de error
|
||||
|
||||
## Uso Básico
|
||||
|
||||
### Desde MangaDetailView
|
||||
|
||||
```swift
|
||||
// Ya está implementado en MangaDetailView
|
||||
// El usuario solo necesita tocar el botón de descarga
|
||||
```
|
||||
|
||||
### Programáticamente
|
||||
|
||||
```swift
|
||||
let downloadManager = DownloadManager.shared
|
||||
|
||||
// Descargar un capítulo
|
||||
try await downloadManager.downloadChapter(
|
||||
mangaSlug: manga.slug,
|
||||
mangaTitle: manga.title,
|
||||
chapter: chapter
|
||||
)
|
||||
|
||||
// Descargar múltiples
|
||||
await downloadManager.downloadChapters(
|
||||
mangaSlug: manga.slug,
|
||||
mangaTitle: manga.title,
|
||||
chapters: chapters
|
||||
)
|
||||
|
||||
// Cancelar descarga
|
||||
downloadManager.cancelDownload(taskId: taskId)
|
||||
|
||||
// Cancelar todas
|
||||
downloadManager.cancelAllDownloads()
|
||||
```
|
||||
|
||||
### Verificar Descargas
|
||||
|
||||
```swift
|
||||
let storage = StorageService.shared
|
||||
|
||||
// ¿Está descargado?
|
||||
if storage.isChapterDownloaded(
|
||||
mangaSlug: manga.slug,
|
||||
chapterNumber: 1
|
||||
) {
|
||||
// Usar imagen local
|
||||
let imageURL = storage.getImageURL(
|
||||
mangaSlug: manga.slug,
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Personalización Opcional
|
||||
|
||||
### Ajustar Concurrencia
|
||||
|
||||
En `DownloadManager.swift`:
|
||||
|
||||
```swift
|
||||
private let maxConcurrentDownloads = 3 // Capítulos simultáneos
|
||||
private let maxConcurrentImagesPerChapter = 5 // Imágenes simultáneas
|
||||
```
|
||||
|
||||
### Ajustar Calidad de Imagen
|
||||
|
||||
En `StorageService.swift`:
|
||||
|
||||
```swift
|
||||
image.jpegData(compressionQuality: 0.8) // 80% de calidad
|
||||
```
|
||||
|
||||
En `DownloadExtensions.swift`:
|
||||
|
||||
```swift
|
||||
let maxDimension: CGFloat = 2048 // Redimensionar si es mayor
|
||||
return resized.compressedData(quality: 0.75) // 75% de calidad
|
||||
```
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### Las descargas no inician
|
||||
1. Verificar conexión a internet
|
||||
2. Verificar que ManhwaWebScraper funciona
|
||||
3. Verificar logs en consola
|
||||
|
||||
### El progreso no se actualiza
|
||||
1. Asegurar que estás en @MainActor
|
||||
2. Verificar que las propiedades son @Published
|
||||
3. Verificar que observas DownloadManager
|
||||
|
||||
### Error "Already downloaded"
|
||||
1. Es normal - el capítulo ya existe
|
||||
2. Usa `storage.deleteDownloadedChapter()` para eliminar
|
||||
3. O permite sobrescribir
|
||||
|
||||
### Las imágenes no se guardan
|
||||
1. Verificar permisos de la app
|
||||
2. Verificar espacio disponible
|
||||
3. Verificar que directorios existen
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
### Opcional: Badge en TabView
|
||||
|
||||
```swift
|
||||
struct MainTabView: View {
|
||||
@StateObject private var downloadManager = DownloadManager.shared
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
// ...
|
||||
DownloadsView()
|
||||
.tabItem {
|
||||
Label("Descargas", systemImage: "arrow.down.circle")
|
||||
}
|
||||
.badge(downloadManager.activeDownloads.count) // ✅ Badge
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Opcional: Widget en Home
|
||||
|
||||
```swift
|
||||
struct ContentView: View {
|
||||
@ObservedObject var downloadManager = DownloadManager.shared
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
// Tu contenido actual
|
||||
|
||||
if downloadManager.hasActiveDownloads {
|
||||
ActiveDownloadsWidget()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Opcional: Banner de Descargas
|
||||
|
||||
```swift
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
MangaDetailView(manga: manga)
|
||||
.activeDownloadsBanner() // ✅ Modificador personalizado
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual
|
||||
1. Descargar un capítulo
|
||||
2. Cancelar una descarga
|
||||
3. Descargar múltiples capítulos
|
||||
4. Probar sin internet
|
||||
5. Limpiar almacenamiento
|
||||
|
||||
### Automatizado
|
||||
Los tests están en `/Sources/Tests/DownloadManagerTests.swift`
|
||||
|
||||
Para ejecutar en Xcode:
|
||||
1. Cmd + U
|
||||
2. O Product → Test
|
||||
|
||||
## Archivos de Referencia
|
||||
|
||||
### Documentación
|
||||
- `DOWNLOAD_SYSTEM_README.md` - Guía completa (400 líneas)
|
||||
- `IMPLEMENTATION_SUMMARY.md` - Resumen ejecutivo
|
||||
- `DIAGRAMS.md` - Diagramas de flujo
|
||||
- `CHECKLIST.md` - Checklist de implementación
|
||||
|
||||
### Código
|
||||
- `DownloadManager.swift` - Core del sistema
|
||||
- `DownloadsView.swift` - Vista principal
|
||||
- `DownloadExtensions.swift` - Extensiones útiles
|
||||
- `IntegrationExample.swift` - Ejemplos de integración
|
||||
|
||||
## Soporte
|
||||
|
||||
### Problemas Comunes
|
||||
|
||||
**"No se compila"**
|
||||
- Asegúrate de tener iOS 15+
|
||||
- Verificar que todos los archivos están en el target
|
||||
- Limpiar carpeta de builds (Cmd + Shift + K)
|
||||
|
||||
**"Las descargas fallan"**
|
||||
- Verificar que ManhwaWebScraper funciona correctamente
|
||||
- Probar con diferentes capítulos
|
||||
- Verificar logs en consola
|
||||
|
||||
**"No se guardan las imágenes"**
|
||||
- Verificar permisos en Info.plist
|
||||
- Probar en dispositivo real (no simulador)
|
||||
- Verificar espacio disponible
|
||||
|
||||
### Contacto
|
||||
|
||||
Para más ayuda, consulta:
|
||||
1. `DOWNLOAD_SYSTEM_README.md` - Documentación completa
|
||||
2. `DIAGRAMS.md` - Diagramas de flujo
|
||||
3. `IntegrationExample.swift` - Ejemplos de código
|
||||
|
||||
---
|
||||
|
||||
**Tiempo de integración**: 5 minutos
|
||||
**Dificultad**: Fácil
|
||||
**Estado**: ✅ COMPLETO
|
||||
|
||||
¡Happy coding! 🚀
|
||||
@@ -1,343 +0,0 @@
|
||||
# Sistema de Descarga de Capítulos - MangaReader iOS
|
||||
|
||||
## Overview
|
||||
|
||||
El sistema de descarga de capítulos permite a los usuarios descargar capítulos completos de manga para lectura offline. El sistema está diseñado con arquitectura asíncrona moderna usando Swift async/await.
|
||||
|
||||
## Componentes Principales
|
||||
|
||||
### 1. DownloadManager (`/Sources/Services/DownloadManager.swift`)
|
||||
|
||||
Gerente centralizado que maneja todas las operaciones de descarga.
|
||||
|
||||
**Características:**
|
||||
- Descarga asíncrona de imágenes con concurrencia controlada
|
||||
- Máximo 3 descargas simultáneas de capítulos
|
||||
- Máximo 5 imágenes simultáneas por capítulo
|
||||
- Cancelación de descargas individuales o masivas
|
||||
- Seguimiento de progreso en tiempo real
|
||||
- Manejo robusto de errores
|
||||
- Historial de descargas completadas y fallidas
|
||||
|
||||
**Uso básico:**
|
||||
```swift
|
||||
let downloadManager = DownloadManager.shared
|
||||
|
||||
// Descargar un capítulo
|
||||
try await downloadManager.downloadChapter(
|
||||
mangaSlug: "one-piece",
|
||||
mangaTitle: "One Piece",
|
||||
chapter: chapter
|
||||
)
|
||||
|
||||
// Descargar múltiples capítulos
|
||||
await downloadManager.downloadChapters(
|
||||
mangaSlug: "one-piece",
|
||||
mangaTitle: "One Piece",
|
||||
chapters: chapters
|
||||
)
|
||||
|
||||
// Cancelar descarga
|
||||
downloadManager.cancelDownload(taskId: "taskId")
|
||||
|
||||
// Cancelar todas
|
||||
downloadManager.cancelAllDownloads()
|
||||
```
|
||||
|
||||
### 2. MangaDetailView (`/Sources/Views/MangaDetailView.swift`)
|
||||
|
||||
Vista de detalles del manga con funcionalidad de descarga integrada.
|
||||
|
||||
**Características añadidas:**
|
||||
- Botón de descarga en la toolbar
|
||||
- Descarga individual por capítulo
|
||||
- Progreso de descarga visible en cada fila de capítulo
|
||||
- Notificaciones de completado/error
|
||||
- Alert para descargar últimos 10 o todos los capítulos
|
||||
|
||||
**Flujo de descarga:**
|
||||
1. Usuario toca botón de descarga en toolbar → muestra alert
|
||||
2. Selecciona cantidad de capítulos a descargar
|
||||
3. Cada capítulo muestra progreso de descarga en tiempo real
|
||||
4. Notificación aparece al completar todas las descargas
|
||||
5. Capítulos descargados muestran checkmark verde
|
||||
|
||||
### 3. DownloadsView (`/Sources/Views/DownloadsView.swift`)
|
||||
|
||||
Vista dedicada para gestionar todas las descargas.
|
||||
|
||||
**Tabs:**
|
||||
- **Activas**: Descargas en progreso con barra de progreso
|
||||
- **Completadas**: Historial de descargas exitosas
|
||||
- **Fallidas**: Descargas con errores, permite reintentar
|
||||
|
||||
**Funcionalidades:**
|
||||
- Cancelar descargas individuales
|
||||
- Cancelar todas las descargas activas
|
||||
- Limpiar historiales (completadas/fallidas)
|
||||
- Ver tamaño de almacenamiento usado
|
||||
- Limpiar todo el almacenamiento descargado
|
||||
|
||||
### 4. StorageService (`/Sources/Services/StorageService.swift`)
|
||||
|
||||
Servicio de almacenamiento ya existente, ahora con soporte para descargas.
|
||||
|
||||
**Métodos utilizados:**
|
||||
```swift
|
||||
// Guardar imagen descargada
|
||||
try await storage.saveImage(
|
||||
image,
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
)
|
||||
|
||||
// Verificar si capítulo está descargado
|
||||
storage.isChapterDownloaded(mangaSlug: "manga-slug", chapterNumber: 1)
|
||||
|
||||
// Obtener directorio del capítulo
|
||||
let chapterDir = storage.getChapterDirectory(
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1
|
||||
)
|
||||
|
||||
// Obtener URL de imagen local
|
||||
if let imageURL = storage.getImageURL(
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1,
|
||||
pageIndex: 0
|
||||
) {
|
||||
// Usar imagen local
|
||||
}
|
||||
|
||||
// Eliminar capítulo descargado
|
||||
storage.deleteDownloadedChapter(
|
||||
mangaSlug: "manga-slug",
|
||||
chapterNumber: 1
|
||||
)
|
||||
|
||||
// Obtener tamaño de almacenamiento
|
||||
let size = storage.getStorageSize()
|
||||
let formatted = storage.formatFileSize(size)
|
||||
```
|
||||
|
||||
## Modelos de Datos
|
||||
|
||||
### DownloadTask
|
||||
Representa una tarea de descarga individual:
|
||||
```swift
|
||||
class DownloadTask: ObservableObject {
|
||||
let id: String
|
||||
let mangaSlug: String
|
||||
let mangaTitle: String
|
||||
let chapterNumber: Int
|
||||
let imageURLs: [String]
|
||||
|
||||
@Published var state: DownloadState
|
||||
@Published var downloadedPages: Int
|
||||
@Published var progress: Double
|
||||
}
|
||||
```
|
||||
|
||||
### DownloadState
|
||||
Estados posibles de una descarga:
|
||||
```swift
|
||||
enum DownloadState {
|
||||
case pending
|
||||
case downloading(progress: Double)
|
||||
case completed
|
||||
case failed(error: String)
|
||||
case cancelled
|
||||
}
|
||||
```
|
||||
|
||||
### DownloadError
|
||||
Tipos de errores de descarga:
|
||||
```swift
|
||||
enum DownloadError: LocalizedError {
|
||||
case alreadyDownloaded
|
||||
case noImagesFound
|
||||
case invalidURL
|
||||
case invalidResponse
|
||||
case httpError(statusCode: Int)
|
||||
case invalidImageData
|
||||
case cancelled
|
||||
case storageError(String)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuración
|
||||
|
||||
### Parámetros de Descarga
|
||||
En `DownloadManager`:
|
||||
```swift
|
||||
private let maxConcurrentDownloads = 3 // Máximo de capítulos simultáneos
|
||||
private let maxConcurrentImagesPerChapter = 5 // Máximo de imágenes simultáneas por capítulo
|
||||
```
|
||||
|
||||
### Calidad de Imagen
|
||||
En `StorageService.saveImage()`:
|
||||
```swift
|
||||
image.jpegData(compressionQuality: 0.8) // 80% de calidad JPEG
|
||||
```
|
||||
|
||||
En `DownloadExtensions`:
|
||||
```swift
|
||||
func optimizedForStorage() -> Data? {
|
||||
// Redimensiona si > 2048px
|
||||
// Comprime a 75% de calidad
|
||||
}
|
||||
```
|
||||
|
||||
## Integración con ReaderView
|
||||
|
||||
Para leer capítulos descargados:
|
||||
|
||||
```swift
|
||||
struct ReaderView: View {
|
||||
let chapter: Chapter
|
||||
let mangaSlug: String
|
||||
|
||||
@StateObject private var storage = StorageService.shared
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(pageIndices, id: \.self) { index in
|
||||
if let imageURL = storage.getImageURL(
|
||||
mangaSlug: mangaSlug,
|
||||
chapterNumber: chapter.number,
|
||||
pageIndex: index
|
||||
) {
|
||||
// Usar imagen local
|
||||
AsyncImage(url: imageURL) { image in
|
||||
image.resizable()
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
} else {
|
||||
// Fallback a URL remota
|
||||
RemoteChapterPage(url: remoteURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notificaciones
|
||||
|
||||
El sistema emite notificaciones para seguimiento:
|
||||
```swift
|
||||
extension Notification.Name {
|
||||
static let downloadDidStart = Notification.Name("downloadDidStart")
|
||||
static let downloadDidUpdate = Notification.Name("downloadDidUpdate")
|
||||
static let downloadDidComplete = Notification.Name("downloadDidComplete")
|
||||
static let downloadDidFail = Notification.Name("downloadDidFail")
|
||||
static let downloadDidCancel = Notification.Name("downloadDidCancel")
|
||||
}
|
||||
```
|
||||
|
||||
## Manejo de Errores
|
||||
|
||||
### Errores de Red
|
||||
- Timeout: 30 segundos por imagen
|
||||
- Reintentos: Manejados por URLSession
|
||||
- HTTP errors: Capturados y reportados en UI
|
||||
|
||||
### Errores de Almacenamiento
|
||||
- Espacio insuficiente: Error con mensaje descriptivo
|
||||
- Permisos: Manejados por FileManager
|
||||
- Corrupción de archivos: Archivos eliminados y descarga reiniciada
|
||||
|
||||
### Errores de Scraping
|
||||
- No se encontraron imágenes: Error `noImagesFound`
|
||||
- Página no carga: Error del scraper propagado
|
||||
- Cambios en la web: Requieren actualización del scraper
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Concurrencia
|
||||
El sistema usa Swift Concurrency:
|
||||
- `async/await` para operaciones asíncronas
|
||||
- `Task` para crear contextos de concurrencia
|
||||
- `@MainActor` para actualizaciones de UI
|
||||
- `TaskGroup` para descargas en paralelo
|
||||
|
||||
### 2. Memoria
|
||||
- Imágenes comprimidas antes de guardar
|
||||
- Descarga limitada a 5 imágenes simultáneas
|
||||
- Limpieza automática de historiales (50 completadas, 20 fallidas)
|
||||
|
||||
### 3. UX
|
||||
- Progreso visible en tiempo real
|
||||
- Cancelación en cualquier punto
|
||||
- Notificaciones de estado
|
||||
- Estados vacíos descriptivos
|
||||
- Feedback inmediato de acciones
|
||||
|
||||
### 4. Robustez
|
||||
- Validación de estados antes de descargar
|
||||
- Limpieza de archivos parciales al cancelar
|
||||
- Verificación de archivos existentes
|
||||
- Manejo exhaustivo de errores
|
||||
|
||||
## Testing
|
||||
|
||||
### Pruebas Unitarias
|
||||
```swift
|
||||
func testDownloadManager() async throws {
|
||||
let manager = DownloadManager.shared
|
||||
|
||||
// Probar descarga individual
|
||||
try await manager.downloadChapter(
|
||||
mangaSlug: "test",
|
||||
mangaTitle: "Test Manga",
|
||||
chapter: testChapter
|
||||
)
|
||||
|
||||
XCTAssertTrue(manager.activeDownloads.isEmpty)
|
||||
XCTAssertEqual(manager.completedDownloads.count, 1)
|
||||
}
|
||||
```
|
||||
|
||||
### Pruebas de Integración
|
||||
- Descargar capítulo completo
|
||||
- Cancelar descarga a mitad
|
||||
- Descargar múltiples capítulos
|
||||
- Probar con y sin conexión
|
||||
- Verificar persistencia de archivos
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Descargas no inician
|
||||
- Verificar conexión a internet
|
||||
- Verificar que el scraper puede acceder a la web
|
||||
- Revisar logs del scraper
|
||||
|
||||
### Progreso no actualiza
|
||||
- Asegurar que las vistas están en @MainActor
|
||||
- Verificar que DownloadTask es @ObservedObject
|
||||
- Chequear que las propiedades son @Published
|
||||
|
||||
### Archivos no se guardan
|
||||
- Verificar permisos de la app
|
||||
- Chequear espacio disponible
|
||||
- Revisar que directorios existen
|
||||
|
||||
### Imágenes corruptas
|
||||
- Verificar calidad de compresión
|
||||
- Chequear que URLs sean válidas
|
||||
- Probar redimensionado de imágenes
|
||||
|
||||
## Futuras Mejoras
|
||||
|
||||
- [ ] Soporte para reanudar descargas pausadas
|
||||
- [ ] Priorización de descargas
|
||||
- [ ] Descarga automática de nuevos capítulos
|
||||
- [ ] Compresión adicional de imágenes
|
||||
- [ ] Soporte para formatos WebP
|
||||
- [ ] Batch operations en StorageService
|
||||
- [ ] Background downloads con URLSession
|
||||
- [ ] Metrics y analytics de descargas
|
||||
Reference in New Issue
Block a user