feat(ios): tab tabanlı navigasyon ve okuma durumu takibi ekle
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user