Some checks failed
CI / UI / Build (pull_request) Failing after 7s
CI / Scraper / Test (pull_request) Failing after 10s
CI / Scraper / Lint (pull_request) Failing after 10s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
- Remove swift-markdown-ui dependency (project.yml, pbxproj, Package.resolved) - Remove all debug print() statements (APIClient, BrowseViewModel, ChapterReaderViewModel, ChapterReaderView) - Remove dead UI stubs: isFavorited/isLiked state, heart/star buttons, empty Share/Add to Library menu items in FullPlayerView - Remove unused audioToolbarButton toolbar builder in ChapterReaderView - Remove unused model types: NovelListing, BookDetailData, ChapterContent - Remove amberLight color (never referenced) - Remove mini player interactive seek (horizontal drag/tap to seek) — progress bar is now display-only - Fix AccentColor asset to amber #f59e0b (was orange-pink mismatch) - Fix Discover tab icon: globe → globe.americas.fill - Fix speed slider max: 3.0 → 2.0 in ProfileView - Fix yearText: use static DateFormatter instead of allocating on every access - Fix ChangePasswordView.save(): DispatchQueue.main.asyncAfter → Task.sleep - Deduplicate navigationDestination blocks via .appNavigationDestination() View extension - Deduplicate .alert(Error) pattern via .errorAlert() View extension
140 lines
5.2 KiB
Swift
140 lines
5.2 KiB
Swift
import SwiftUI
|
|
import Kingfisher
|
|
|
|
struct HomeView: View {
|
|
@StateObject private var vm = HomeViewModel()
|
|
@EnvironmentObject var authStore: AuthStore
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 28) {
|
|
|
|
// Stats bar
|
|
if let stats = vm.stats {
|
|
HStack(spacing: 0) {
|
|
StatCell(value: "\(stats.totalBooks)", label: "Books")
|
|
Divider().frame(height: 32)
|
|
StatCell(value: "\(stats.totalChapters)", label: "Chapters")
|
|
Divider().frame(height: 32)
|
|
StatCell(value: "\(stats.booksInProgress)", label: "In Progress")
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 16)
|
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14))
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// Continue reading
|
|
if !vm.continueReading.isEmpty {
|
|
SectionHeader(title: "Continue Reading")
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
ForEach(vm.continueReading) { item in
|
|
NavigationLink(value: NavDestination.book(item.book.slug)) {
|
|
ContinueReadingCard(item: item)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
|
|
// Recently updated
|
|
if !vm.recentlyUpdated.isEmpty {
|
|
SectionHeader(title: "Recently Updated")
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150), spacing: 12)], spacing: 16) {
|
|
ForEach(vm.recentlyUpdated) { book in
|
|
NavigationLink(value: NavDestination.book(book.slug)) {
|
|
BookCard(book: book)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
// Empty state
|
|
if vm.continueReading.isEmpty && vm.recentlyUpdated.isEmpty && !vm.isLoading {
|
|
EmptyStateView(
|
|
icon: "books.vertical",
|
|
title: "Your library is empty",
|
|
message: "Head to Discover to find novels to read."
|
|
)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 60)
|
|
}
|
|
|
|
if vm.isLoading {
|
|
ProgressView()
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.top, 60)
|
|
}
|
|
}
|
|
.padding(.vertical)
|
|
}
|
|
.navigationTitle("Home")
|
|
.appNavigationDestination()
|
|
.refreshable { await vm.load() }
|
|
.task { await vm.load() }
|
|
.errorAlert($vm.error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Supporting components
|
|
|
|
private struct StatCell: View {
|
|
let value: String
|
|
let label: String
|
|
var body: some View {
|
|
VStack(spacing: 2) {
|
|
Text(value).font(.title2.bold()).foregroundStyle(.primary)
|
|
Text(label).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
}
|
|
|
|
private struct SectionHeader: View {
|
|
let title: String
|
|
var body: some View {
|
|
Text(title)
|
|
.font(.title3.bold())
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
|
|
private struct ContinueReadingCard: View {
|
|
let item: ContinueReadingItem
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
KFImage(URL(string: item.book.cover))
|
|
.resizable()
|
|
.placeholder { coverPlaceholder }
|
|
.scaledToFill()
|
|
.frame(width: 120, height: 170)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
.overlay(alignment: .bottomTrailing) {
|
|
Text("Ch.\(item.chapter)")
|
|
.font(.caption2.bold())
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 3)
|
|
.background(.ultraThinMaterial, in: Capsule())
|
|
.padding(6)
|
|
}
|
|
Text(item.book.title)
|
|
.font(.caption.bold())
|
|
.lineLimit(2)
|
|
.frame(width: 120, alignment: .leading)
|
|
}
|
|
}
|
|
private var coverPlaceholder: some View {
|
|
RoundedRectangle(cornerRadius: 10)
|
|
.fill(Color(.systemGray5))
|
|
.frame(width: 120, height: 170)
|
|
.overlay(Image(systemName: "book.closed").foregroundStyle(.secondary))
|
|
}
|
|
}
|