51067e23fd
BookLibraryView is itself pushed from PracticeView's NavigationStack, so the .navigationDestination(for: Book.self) it declared was a non-root registration. Combined with NavigationLink(value: book), that resolved the push to *both* the destination handler and the closure that produced BookLibraryView originally — pushing the chapter list underneath, then re-pushing the library on top. Hitting back popped the library and revealed the chapter list, in the wrong order. Switched both Library→ChapterList and ChapterList→Reader to closure- based NavigationLinks. Destinations attach directly to the link, no type-keyed registry involved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.1 KiB
Swift
100 lines
3.1 KiB
Swift
import SwiftUI
|
|
import SharedModels
|
|
import SwiftData
|
|
|
|
struct BookLibraryView: View {
|
|
@Query(sort: \Book.title) private var books: [Book]
|
|
|
|
var body: some View {
|
|
Group {
|
|
if books.isEmpty {
|
|
ContentUnavailableView(
|
|
"No Books",
|
|
systemImage: "books.vertical",
|
|
description: Text("Books bundled with the app will appear here.")
|
|
)
|
|
} else {
|
|
ScrollView {
|
|
LazyVStack(spacing: 12) {
|
|
ForEach(books) { book in
|
|
NavigationLink {
|
|
BookChapterListView(book: book)
|
|
} label: {
|
|
BookCard(book: book)
|
|
}
|
|
.tint(.primary)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Books")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
}
|
|
|
|
private struct BookCard: View {
|
|
let book: Book
|
|
|
|
private var accentColor: Color {
|
|
Color(hex: book.accentColorHex) ?? .indigo
|
|
}
|
|
|
|
private var shortTitle: String {
|
|
// Trim "Volume X" subtitle if present — most book titles are way too long.
|
|
if let colon = book.title.firstIndex(of: ":") {
|
|
return String(book.title[..<colon])
|
|
}
|
|
return book.title
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 14) {
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(accentColor.gradient)
|
|
.frame(width: 48, height: 64)
|
|
.overlay {
|
|
Image(systemName: "book.closed.fill")
|
|
.font(.title3)
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(shortTitle)
|
|
.font(.subheadline.weight(.semibold))
|
|
.multilineTextAlignment(.leading)
|
|
if !book.author.isEmpty {
|
|
Text(book.author)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text("\(book.chapterCount) chapter\(book.chapterCount == 1 ? "" : "s")")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
.glassEffect(in: RoundedRectangle(cornerRadius: 14))
|
|
}
|
|
}
|
|
|
|
private extension Color {
|
|
init?(hex: String) {
|
|
var s = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if s.hasPrefix("#") { s.removeFirst() }
|
|
guard s.count == 6, let v = UInt32(s, radix: 16) else { return nil }
|
|
let r = Double((v >> 16) & 0xFF) / 255.0
|
|
let g = Double((v >> 8) & 0xFF) / 255.0
|
|
let b = Double(v & 0xFF) / 255.0
|
|
self = Color(red: r, green: g, blue: b)
|
|
}
|
|
}
|