Some checks failed
CI / UI / Build (push) Failing after 11s
CI / Scraper / Lint (pull_request) Failing after 15s
CI / UI / Docker Push (push) Has been skipped
CI / Scraper / Test (pull_request) Successful in 20s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Build (pull_request) Failing after 17s
CI / UI / Docker Push (pull_request) Has been skipped
iOS CI / Build (push) Successful in 3m40s
iOS CI / Build (pull_request) Successful in 1m49s
iOS CI / Test (push) Successful in 4m24s
iOS CI / Test (pull_request) Successful in 4m46s
- Add UserProfileView and UserProfileViewModel for iOS - Implement user library API endpoint (/api/users/[username]/library) - Add DELETE /api/progress/[slug] endpoint for removing books from library - Integrate subscription feed in home API - Update Xcode project with new profile components
105 lines
3.6 KiB
Swift
105 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Navigation destination enum used across all tabs
|
|
|
|
enum NavDestination: Hashable {
|
|
case book(String) // slug
|
|
case chapter(String, Int) // slug + chapter number
|
|
case userProfile(String) // username
|
|
}
|
|
|
|
// MARK: - View extensions for shared navigation + error alert patterns
|
|
|
|
extension View {
|
|
/// Registers the app-wide navigation destinations for NavDestination values.
|
|
/// Apply once per NavigationStack instead of repeating the switch in every tab.
|
|
func appNavigationDestination() -> some View {
|
|
modifier(AppNavigationDestinationModifier())
|
|
}
|
|
|
|
/// Presents a standard "Error" alert driven by an optional String binding.
|
|
/// Dismissing the alert sets the binding back to nil.
|
|
func errorAlert(_ error: Binding<String?>) -> some View {
|
|
alert("Error", isPresented: Binding(
|
|
get: { error.wrappedValue != nil },
|
|
set: { if !$0 { error.wrappedValue = nil } }
|
|
)) {
|
|
Button("OK") { error.wrappedValue = nil }
|
|
} message: {
|
|
Text(error.wrappedValue ?? "")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Navigation destination modifier
|
|
|
|
private struct AppNavigationDestinationModifier: ViewModifier {
|
|
@Namespace private var zoomNamespace
|
|
|
|
func body(content: Content) -> some View {
|
|
if #available(iOS 18.0, *) {
|
|
content
|
|
.navigationDestination(for: NavDestination.self) { dest in
|
|
switch dest {
|
|
case .book(let slug):
|
|
BookDetailView(slug: slug)
|
|
.navigationTransition(.zoom(sourceID: slug, in: zoomNamespace))
|
|
case .chapter(let slug, let n):
|
|
ChapterReaderView(slug: slug, chapterNumber: n)
|
|
case .userProfile(let username):
|
|
UserProfileView(username: username)
|
|
}
|
|
}
|
|
// Expose namespace to child views via environment
|
|
.environment(\.bookZoomNamespace, zoomNamespace)
|
|
} else {
|
|
content
|
|
.navigationDestination(for: NavDestination.self) { dest in
|
|
switch dest {
|
|
case .book(let slug): BookDetailView(slug: slug)
|
|
case .chapter(let slug, let n): ChapterReaderView(slug: slug, chapterNumber: n)
|
|
case .userProfile(let username): UserProfileView(username: username)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Environment key for zoom namespace
|
|
|
|
struct BookZoomNamespaceKey: EnvironmentKey {
|
|
static var defaultValue: Namespace.ID? { nil }
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var bookZoomNamespace: Namespace.ID? {
|
|
get { self[BookZoomNamespaceKey.self] }
|
|
set { self[BookZoomNamespaceKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
// MARK: - Cover card zoom source modifier
|
|
|
|
/// Apply this to any cover image that should be a zoom source for book navigation.
|
|
/// Falls back to a no-op on iOS 17 or when no namespace is available.
|
|
struct BookCoverZoomSource: ViewModifier {
|
|
let slug: String
|
|
@Environment(\.bookZoomNamespace) private var namespace
|
|
|
|
func body(content: Content) -> some View {
|
|
if #available(iOS 18.0, *), let ns = namespace {
|
|
content.matchedTransitionSource(id: slug, in: ns)
|
|
} else {
|
|
content
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// Marks a cover image as the zoom source for a book's navigation transition.
|
|
func bookCoverZoomSource(slug: String) -> some View {
|
|
modifier(BookCoverZoomSource(slug: slug))
|
|
}
|
|
}
|
|
|