✨ 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>
155 lines
4.7 KiB
Swift
155 lines
4.7 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
// MARK: - Download Extensions
|
|
|
|
extension DownloadTask {
|
|
/// Formatea el tamaño total de la descarga
|
|
var formattedSize: String {
|
|
let formatter = ByteCountFormatter()
|
|
formatter.allowedUnits = [.useBytes, .useKB, .useMB]
|
|
formatter.countStyle = .file
|
|
return formatter.string(fromByteCount: Int64(imageURLs.count * 500_000)) // Estimación de 500KB por imagen
|
|
}
|
|
|
|
/// Retorna el tiempo estimado restante
|
|
var estimatedTimeRemaining: String? {
|
|
guard progress > 0 && progress < 1 else { return nil }
|
|
|
|
let downloadedPages = Double(imageURLs.count) * progress
|
|
let remainingPages = Double(imageURLs.count) - downloadedPages
|
|
|
|
// Estimación: 2 segundos por página
|
|
let estimatedSeconds = remainingPages * 2
|
|
|
|
if estimatedSeconds < 60 {
|
|
return "\(Int(estimatedSeconds))s restantes"
|
|
} else {
|
|
let minutes = Int(estimatedSeconds / 60)
|
|
return "\(min)m restantes"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DownloadManager {
|
|
/// Obtiene estadísticas de descarga
|
|
var downloadStats: DownloadStats {
|
|
let activeCount = activeDownloads.count
|
|
let completedCount = completedDownloads.count
|
|
let failedCount = failedDownloads.count
|
|
|
|
return DownloadStats(
|
|
activeDownloads: activeCount,
|
|
completedDownloads: completedCount,
|
|
failedDownloads: failedCount,
|
|
totalProgress: totalProgress
|
|
)
|
|
}
|
|
|
|
/// Verifica si hay descargas activas
|
|
var hasActiveDownloads: Bool {
|
|
!activeDownloads.isEmpty
|
|
}
|
|
|
|
/// Obtiene el número total de descargas
|
|
var totalDownloads: Int {
|
|
activeDownloads.count + completedDownloads.count + failedDownloads.count
|
|
}
|
|
}
|
|
|
|
// MARK: - Download Stats Model
|
|
|
|
struct DownloadStats {
|
|
let activeDownloads: Int
|
|
let completedDownloads: Int
|
|
let failedDownloads: Int
|
|
let totalProgress: Double
|
|
|
|
var totalDownloads: Int {
|
|
activeDownloads + completedDownloads + failedDownloads
|
|
}
|
|
|
|
var successRate: Double {
|
|
guard totalDownloads > 0 else { return 0 }
|
|
return Double(completedDownloads) / Double(totalDownloads)
|
|
}
|
|
}
|
|
|
|
// MARK: - UIImage Extension for Compression
|
|
|
|
extension UIImage {
|
|
/// Comprime la imagen con una calidad específica
|
|
func compressedData(quality: CGFloat = 0.8) -> Data? {
|
|
return jpegData(compressionQuality: quality)
|
|
}
|
|
|
|
/// Redimensiona la imagen a un tamaño máximo
|
|
func resized(maxWidth: CGFloat, maxHeight: CGFloat) -> UIImage? {
|
|
let size = size
|
|
|
|
let widthRatio = maxWidth / size.width
|
|
let heightRatio = maxHeight / size.height
|
|
let ratio = min(widthRatio, heightRatio)
|
|
|
|
let newSize = CGSize(
|
|
width: size.width * ratio,
|
|
height: size.height * ratio
|
|
)
|
|
|
|
UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
|
|
draw(in: CGRect(origin: .zero, size: newSize))
|
|
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
|
|
return resizedImage
|
|
}
|
|
|
|
/// Optimiza la imagen para almacenamiento
|
|
func optimizedForStorage() -> Data? {
|
|
// Redimensionar si es muy grande
|
|
let maxDimension: CGFloat = 2048
|
|
let resized: UIImage
|
|
|
|
if size.width > maxDimension || size.height > maxDimension {
|
|
resized = self.resized(maxWidth: maxDimension, maxHeight: maxDimension) ?? self
|
|
} else {
|
|
resized = self
|
|
}
|
|
|
|
// Comprimir con calidad balanceada
|
|
return resized.compressedData(quality: 0.75)
|
|
}
|
|
}
|
|
|
|
// MARK: - Notification Names
|
|
|
|
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")
|
|
}
|
|
|
|
// MARK: - Download Progress Notification
|
|
|
|
struct DownloadProgressNotification {
|
|
let taskId: String
|
|
let progress: Double
|
|
let downloadedPages: Int
|
|
let totalPages: Int
|
|
}
|
|
|
|
// MARK: - URLSession Extension for Download Tracking
|
|
|
|
extension URLSession {
|
|
/// Configura una URLSession para descargas con timeout
|
|
static func downloadSession() -> URLSession {
|
|
let configuration = URLSessionConfiguration.default
|
|
configuration.timeoutIntervalForRequest = 30
|
|
configuration.timeoutIntervalForResource = 300
|
|
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
return URLSession(configuration: configuration)
|
|
}
|
|
}
|