Initial commit: MangaReader iOS App

 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>
This commit is contained in:
2026-02-04 15:34:18 +01:00
commit b474182dd9
6394 changed files with 1063909 additions and 0 deletions

View File

@@ -0,0 +1,264 @@
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()
}