import SwiftUI struct ReaderView: View { let manga: Manga let chapter: Chapter @StateObject private var viewModel: ReaderViewModel @Environment(\.dismiss) var dismiss init(manga: Manga, chapter: Chapter) { self.manga = manga self.chapter = chapter _viewModel = StateObject(wrappedValue: ReaderViewModel(manga: manga, chapter: chapter)) } var body: some View { ZStack { // Color de fondo configurable (viewModel.backgroundColor) .ignoresSafeArea() VStack(spacing: 0) { // Header readerHeader .background(viewModel.backgroundColor) // Content if viewModel.isLoading { loadingView } else if viewModel.pages.isEmpty { errorView } else { readerContent } // Footer/Toolbar readerFooter .background(viewModel.backgroundColor) } // Gestures para mostrar/ocultar controles if viewModel.showControls { Color.black.opacity(0.3) .ignoresSafeArea() .onTapGesture { withAnimation { viewModel.showControls = false } } } } .navigationBarHidden(true) .statusBar(hidden: viewModel.showControls ? false : true) .task { await viewModel.loadPages() } .alert("Error", isPresented: $viewModel.showError) { Button("OK") { dismiss() } } message: { Text(viewModel.errorMessage) } } private var readerHeader: View { HStack { Button { dismiss() } label: { Image(systemName: "chevron.left") .foregroundColor(.primary) .padding() } VStack(alignment: .leading, spacing: 2) { Text(manga.title) .font(.caption) .foregroundColor(.secondary) Text("\(chapter.displayNumber)") .font(.headline) } Spacer() Button { viewModel.toggleFavorite() } label: { Image(systemName: viewModel.isFavorite ? "heart.fill" : "heart") .foregroundColor(viewModel.isFavorite ? .red : .primary) .padding() } Button { viewModel.showingSettings = true } label: { Image(systemName: "textformat") .foregroundColor(.primary) .padding() } } .opacity(viewModel.showControls ? 1 : 0) .animation(.easeInOut(duration: 0.2), value: viewModel.showControls) } private var loadingView: some View { VStack(spacing: 20) { ProgressView() .scaleEffect(1.5) Text("Cargando capítulo...") .foregroundColor(.secondary) if let progress = viewModel.downloadProgress { Text("\(Int(progress * 100))%") .font(.caption) .foregroundColor(.secondary) } } } private var errorView: some View { VStack(spacing: 20) { Image(systemName: "exclamationmark.triangle") .font(.system(size: 50)) .foregroundColor(.orange) Text("No se pudieron cargar las páginas") .foregroundColor(.secondary) Button("Reintentar") { Task { await viewModel.loadPages() } } .buttonStyle(.borderedProminent) } } private var readerContent: some View { GeometryReader { geometry in TabView(selection: $viewModel.currentPage) { ForEach(viewModel.pages) { page in PageView( page: page, mangaSlug: manga.slug, chapterNumber: chapter.number, viewModel: viewModel ) .tag(page.index) } } .tabViewStyle(.page(indexDisplayMode: .never)) .onTapGesture(count: 2) { withAnimation { viewModel.showControls.toggle() } } .onTapGesture { // Tap simple para avanzar/retroceder let tapLocation = geometry.frame(in: .local).midX // Implementar lógica de navegación por tap } } } private var readerFooter: some View { VStack(spacing: 8) { // Page indicator HStack { Text("Página \(viewModel.currentPageIndex + 1)") .font(.caption) .foregroundColor(.secondary) Text("de") .font(.caption) .foregroundColor(.secondary) Text("\(viewModel.totalPages)") .font(.caption) .foregroundColor(.secondary) Spacer() if viewModel.isDownloaded { Label("Descargado", systemImage: "checkmark.circle.fill") .font(.caption) .foregroundColor(.green) } } .padding(.horizontal) // Progress bar ProgressView(value: Double(viewModel.currentPageIndex + 1), total: Double(viewModel.totalPages)) .progressViewStyle(.linear) .padding(.horizontal) // Controls HStack(spacing: 20) { Button { viewModel.showingPageSlider = true } label: { Image(systemName: "slider.horizontal.3") .foregroundColor(.primary) } Button { viewModel.readingMode = viewModel.readingMode == .vertical ? .horizontal : .vertical } label: { Image(systemName: viewModel.readingMode == .vertical ? "rectangle.grid.1x2" : "rectangle.grid.2x1") .foregroundColor(.primary) } Button { viewModel.cycleBackgroundColor() } label: { Image(systemName: "circle.fill") .foregroundColor(viewModel.backgroundColor == .white ? .black : .white) .padding(4) .background(viewModel.backgroundColor == .white ? .black : .white) .clipShape(Circle()) } Spacer() // First/Last buttons Button { withAnimation { viewModel.currentPage = 0 } } label: { Image(systemName: "chevron.left.2") .foregroundColor(.primary) } Button { withAnimation { viewModel.currentPage = viewModel.totalPages - 1 } } label: { Image(systemName: "chevron.right.2") .foregroundColor(.primary) } } .padding() } .opacity(viewModel.showControls ? 1 : 0) .animation(.easeInOut(duration: 0.2), value: viewModel.showControls) .sheet(isPresented: $viewModel.showingPageSlider) { pageSliderSheet } .sheet(isPresented: $viewModel.showingSettings) { readerSettingsSheet } } private var pageSliderSheet: some View { NavigationView { VStack(spacing: 20) { Text("Ir a página") .font(.headline) VStack(alignment: .leading, spacing: 8) { Text("\(viewModel.currentPageIndex + 1) / \(viewModel.totalPages)") .font(.title) .bold() Slider( value: Binding( get: { Double(viewModel.currentPageIndex + 1) }, set: { viewModel.currentPage = Int($0) - 1 } ), in: 1...Double(viewModel.totalPages), step: 1 ) } .padding() Spacer() } .navigationTitle("Navegación") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Cerrar") { viewModel.showingPageSlider = false } } } } } private var readerSettingsSheet: some View { NavigationView { Form { Section("Fondo de pantalla") { Picker("Color", selection: $viewModel.backgroundColor) { Text("Blanco").tag(Color.white) Text("Negro").tag(Color.black) Text("Sepia").tag(Color(red: 0.76, green: 0.70, blue: 0.50)) } .pickerStyle(.segmented) } Section("Lectura") { Picker("Modo de lectura", selection: $viewModel.readingMode) { Text("Vertical").tag(ReadingMode.vertical) Text("Horizontal").tag(ReadingMode.horizontal) } } } .navigationTitle("Configuración") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Cerrar") { viewModel.showingSettings = false } } } } } } // MARK: - Page View struct PageView: View { let page: MangaPage let mangaSlug: String let chapterNumber: Int @ObservedObject var viewModel: ReaderViewModel @State private var scale: CGFloat = 1.0 @State private var lastScale: CGFloat = 1.0 @State private var offset: CGSize = .zero @State private var lastOffset: CGSize = .zero var body: some View { GeometryReader { geometry in Group { if let localURL = StorageService.shared.getImageURL( mangaSlug: mangaSlug, chapterNumber: chapterNumber, pageIndex: page.index ) { // Load from local cache Image(uiImage: UIImage(contentsOfFile: localURL.path)!) .resizable() .aspectRatio(contentMode: .fit) } else { // Load from URL AsyncImage(url: URL(string: page.url)) { phase in switch phase { case .empty: ProgressView() case .success(let image): image .resizable() .aspectRatio(contentMode: .fit) .onAppear { // Cache image for offline reading Task { await viewModel.cachePage(page, image: image) } } case .failure: Image(systemName: "photo") .foregroundColor(.gray) @unknown default: EmptyView() } } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .scaleEffect(scale) .offset(offset) .gesture( SimultaneousGesture( MagnificationGesture() .onChanged { value in let delta = value / lastScale lastScale = value scale = max(1, min(scale * delta, 10)) } .onEnded { _ in lastScale = 1.0 if scale < 1.2 { withAnimation { scale = 1.0 offset = .zero } } }, DragGesture() .onChanged { value in offset = CGSize( width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height ) } .onEnded { _ in if scale == 1.0 { withAnimation { offset = .zero } } lastOffset = offset } ) ) } } } // MARK: - ViewModel @MainActor class ReaderViewModel: ObservableObject { @Published var pages: [MangaPage] = [] @Published var currentPage: Int = 0 @Published var isLoading = true @Published var showError = false @Published var errorMessage = "" @Published var showControls = true @Published var isFavorite = false @Published var isDownloaded = false @Published var downloadProgress: Double? @Published var showingPageSlider = false @Published var showingSettings = false @Published var backgroundColor: Color = .white @Published var readingMode: ReadingMode = .vertical var currentPageIndex: Int { currentPage } var totalPages: Int { pages.count } private let manga: Manga private let chapter: Chapter private let scraper = ManhwaWebScraper.shared private let storage = StorageService.shared init(manga: Manga, chapter: Chapter) { self.manga = manga self.chapter = chapter self.isFavorite = storage.isFavorite(mangaSlug: manga.slug) self.isDownloaded = storage.isChapterDownloaded(mangaSlug: manga.slug, chapterNumber: chapter.number) } func loadPages() async { isLoading = true do { // Intentar cargar desde descarga local if let downloadedChapter = storage.getDownloadedChapter(mangaSlug: manga.slug, chapterNumber: chapter.number) { pages = downloadedChapter.pages isDownloaded = true // Cargar progreso guardado if let progress = storage.getReadingProgress(mangaSlug: manga.slug, chapterNumber: chapter.number) { currentPage = progress.pageNumber } } else { // Scrapear imágenes let imageUrls = try await scraper.scrapeChapterImages(chapterSlug: chapter.slug) pages = imageUrls.enumerated().map { index, url in MangaPage(url: url, index: index) } } } catch { errorMessage = error.localizedDescription showError = true } isLoading = false saveProgress() } func cachePage(_ page: MangaPage, image: Image) async { // Implementar cache de imagen // TODO: Guardar imagen localmente } func toggleFavorite() { if storage.isFavorite(mangaSlug: manga.slug) { storage.removeFavorite(mangaSlug: manga.slug) } else { storage.saveFavorite(mangaSlug: manga.slug) } isFavorite.toggle() } func cycleBackgroundColor() { switch backgroundColor { case .white: backgroundColor = .black case .black: backgroundColor = Color(red: 0.76, green: 0.70, blue: 0.50) // Sepia default: backgroundColor = .white } } private func saveProgress() { let progress = ReadingProgress( mangaSlug: manga.slug, chapterNumber: chapter.number, pageNumber: currentPage, timestamp: Date() ) storage.saveReadingProgress(progress) } } enum ReadingMode { case vertical case horizontal } #Preview { ReaderView( manga: Manga( slug: "one-piece_1695365223767", title: "One Piece", description: "", genres: [], status: "PUBLICANDOSE", url: "", coverImage: nil ), chapter: Chapter(number: 1, title: "El inicio", url: "", slug: "") ) }