import SwiftUI struct ContentView: View { @StateObject private var viewModel = MangaListViewModel() var body: some View { NavigationView { Group { if viewModel.isLoading && viewModel.mangas.isEmpty { loadingView } else if viewModel.mangas.isEmpty { emptyStateView } else { mangaListView } } .navigationTitle("MangaReader") .searchable(text: $viewModel.searchText, prompt: "Buscar manga...") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu { Button { viewModel.filter = .all } label: { Label("Todos", systemImage: viewModel.filter == .all ? "checkmark" : "") } Button { viewModel.filter = .favorites } label: { Label("Favoritos", systemImage: viewModel.filter == .favorites ? "checkmark" : "") } Button { viewModel.filter = .downloaded } label: { Label("Descargados", systemImage: viewModel.filter == .downloaded ? "checkmark" : "") } } label: { Image(systemName: "line.3.horizontal.decrease.circle") } } } } } private var loadingView: some View { VStack(spacing: 20) { ProgressView() .scaleEffect(1.5) Text("Cargando mangas...") .foregroundColor(.secondary) } } private var emptyStateView: some View { VStack(spacing: 20) { Image(systemName: "book.closed") .font(.system(size: 60)) .foregroundColor(.secondary) Text("Agrega un manga manualmente") .font(.headline) Text("Ingresa el slug del manga (ej: one-piece_1695365223767)") .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) HStack { TextField("Slug del manga", text: $viewModel.newMangaSlug) .textFieldStyle(.roundedBorder) .autocapitalization(.none) Button("Agregar") { Task { await viewModel.addManga(viewModel.newMangaSlug) } } .buttonStyle(.borderedProminent) } .padding() } } private var mangaListView: some View { ScrollView { LazyVStack(spacing: 16) { ForEach(viewModel.filteredMangas) { manga in NavigationLink(destination: MangaDetailView(manga: manga)) { MangaRowView(manga: manga) } .buttonStyle(.plain) } } .padding() } .refreshable { await viewModel.loadMangas() } } } struct MangaRowView: View { let manga: Manga @StateObject private var storage = StorageService.shared var body: some View { HStack(spacing: 12) { // Cover image placeholder AsyncImage(url: URL(string: manga.coverImage ?? "")) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Rectangle() .fill(Color.gray.opacity(0.2)) .overlay( Image(systemName: "book.closed") .foregroundColor(.gray) ) } .frame(width: 60, height: 80) .cornerRadius(8) .clipped() // Manga info VStack(alignment: .leading, spacing: 4) { Text(manga.title) .font(.headline) .lineLimit(2) Text(manga.displayStatus) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 2) .background(statusColor.opacity(0.2)) .foregroundColor(statusColor) .cornerRadius(4) if !manga.genres.isEmpty { Text(manga.genres.prefix(3).joined(separator: ", ")) .font(.caption) .foregroundColor(.secondary) .lineLimit(1) } if storage.isFavorite(mangaSlug: manga.slug) { HStack(spacing: 4) { Image(systemName: "heart.fill") .foregroundColor(.red) .font(.caption) Text("Favorito") .font(.caption) .foregroundColor(.secondary) } } } Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.caption) } .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(Color(.systemBackground)) .shadow(color: .black.opacity(0.1), radius: 5, x: 0, y: 2) ) } private var statusColor: Color { switch manga.status { case "PUBLICANDOSE": return .green case "FINALIZADO": return .blue case "EN_PAUSA", "EN_ESPERA": return .orange default: return .gray } } } // MARK: - ViewModel @MainActor class MangaListViewModel: ObservableObject { @Published var mangas: [Manga] = [] @Published var isLoading = false @Published var searchText = "" @Published var filter: MangaFilter = .all @Published var newMangaSlug = "" private let storage = StorageService.shared private let scraper = ManhwaWebScraper.shared var filteredMangas: [Manga] { var result = mangas // Apply search filter if !searchText.isEmpty { result = result.filter { manga in manga.title.localizedCaseInsensitiveContains(searchText) } } // Apply category filter switch filter { case .favorites: result = result.filter { storage.isFavorite(mangaSlug: $0.slug) } case .downloaded: result = result.filter { manga in storage.getDownloadedChapters().contains { $0.mangaSlug == manga.slug } } case .all: break } return result } func loadMangas() async { isLoading = true // Cargar mangas guardados let favorites = storage.getFavorites() // Para demo, agregar One Piece por defecto if mangas.isEmpty { await addManga("one-piece_1695365223767") } isLoading = false } func addManga(_ slug: String) async { guard !slug.isEmpty else { return } do { let manga = try await scraper.scrapeMangaInfo(mangaSlug: slug) if !mangas.contains(where: { $0.slug == manga.slug }) { mangas.append(manga) } newMangaSlug = "" } catch { print("Error adding manga: \(error)") } } } enum MangaFilter { case all case favorites case downloaded } #Preview { ContentView() }