feat(ios): tab tabanlı navigasyon ve okuma durumu takibi ekle

This commit is contained in:
2026-02-11 18:26:17 +03:00
parent 52212f015b
commit 362b9b7d1b
16 changed files with 976 additions and 442 deletions

View File

@@ -1,71 +1,111 @@
import SwiftUI
import SwiftData
import UIKit
struct CategoryListView: View {
@EnvironmentObject private var router: AppRouter
@Environment(\.modelContext) private var modelContext
@Query(sort: \LibraryBook.dateAdded, order: .reverse) private var allBooks: [LibraryBook]
@ObservedObject var viewModel: CategoryViewModel
var body: some View {
let books = viewModel.books(from: allBooks)
VStack {
HStack {
TextField("Search", text: $viewModel.searchText)
.textFieldStyle(.roundedBorder)
Menu {
ForEach(CategoryViewModel.SortOption.allCases, id: \.self) { option in
Button(option.rawValue) { viewModel.sortOption = option }
}
} label: {
Image(systemName: "arrow.up.arrow.down.circle")
.font(.title3)
}
}
.padding(.horizontal)
List {
controlRow
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 104), spacing: 12)], spacing: 16) {
ForEach(books, id: \.localId) { book in
let remote = BookRemote(
title: book.title,
authors: book.authors,
publishedYear: book.publishedYear,
isbn10: book.isbn10,
isbn13: book.isbn13,
coverImageUrl: book.coverUrlString.flatMap(URL.init(string:)),
language: book.language,
description: book.summary,
categories: book.categories
if books.isEmpty {
EmptyStateView(
symbol: "books.vertical",
title: "Bu kategoride kitap yok",
message: "Kitap ekleyerek bu rafı doldurabilirsin.",
buttonTitle: "Kitap Keşfet",
action: { router.selectedTab = .discover }
)
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
} else {
ForEach(books, id: \.localId) { book in
let remote = BookRemote(
title: book.title,
authors: book.authors,
publishedYear: book.publishedYear,
isbn10: book.isbn10,
isbn13: book.isbn13,
coverImageUrl: book.coverUrlString.flatMap(URL.init(string:)),
language: book.language,
description: book.summary,
categories: book.categories,
readingStatus: book.status,
readingProgress: book.readingProgressValue
)
Button {
router.path.append(.detail(remote))
} label: {
BookCardView(
title: remote.title,
author: remote.authors.first ?? "",
coverURL: remote.coverImageUrl,
status: remote.readingStatus ?? .wantToRead,
progress: remote.readingProgress
)
Button {
router.path.append(.detail(remote))
} label: {
VStack(alignment: .leading, spacing: 6) {
AsyncImage(url: remote.coverImageUrl) { phase in
if let image = phase.image {
image.resizable().scaledToFill()
} else {
RoundedRectangle(cornerRadius: 10).fill(Color.gray.opacity(0.2))
}
}
.frame(height: 154)
.clipShape(RoundedRectangle(cornerRadius: 10))
Text(book.title)
.font(.caption)
.lineLimit(2)
.foregroundStyle(.primary)
}
}
}
.buttonStyle(.plain)
.listRowInsets(EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16))
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
statusButton(.finished, for: book)
statusButton(.reading, for: book)
statusButton(.wantToRead, for: book)
}
}
.padding(.horizontal)
}
}
.scrollContentBackground(.hidden)
.background(Theme.background.ignoresSafeArea())
.navigationTitle(viewModel.categoryName)
.navigationBarTitleDisplayMode(.inline)
.background(Theme.background.ignoresSafeArea())
}
private var controlRow: some View {
HStack(spacing: 10) {
TextField("Kategori içinde ara", text: $viewModel.searchText)
.textFieldStyle(.roundedBorder)
Menu {
ForEach(CategoryViewModel.SortOption.allCases, id: \.self) { option in
Button(option.rawValue) { viewModel.sortOption = option }
}
} label: {
Label("Sırala", systemImage: "arrow.up.arrow.down.circle")
}
}
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
private func statusButton(_ status: ReadingStatus, for book: LibraryBook) -> some View {
Button {
withAnimation(.spring(duration: 0.3)) {
book.status = status
if status == .finished { book.readingProgressValue = 1 }
if status == .wantToRead { book.readingProgressValue = 0 }
try? modelContext.save()
}
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
} label: {
Label(status.title, systemImage: status.symbol)
}
.tint(color(for: status))
}
private func color(for status: ReadingStatus) -> Color {
switch status {
case .wantToRead: return .orange
case .reading: return .blue
case .finished: return .green
}
}
}