Files
libnovel/ios/LibNovelV2/Extensions/NavDestination.swift
Admin 7413313100
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
fix: update integration_test.go to match server.New signature (version, commit args)
2026-03-14 14:25:46 +05:00

139 lines
4.8 KiB
Swift

import SwiftUI
// MARK: - Navigation destination enum
enum NavDestination: Hashable {
case book(String) // slug
case chapter(String, Int) // slug + chapter number
case userProfile(String) // username
case browseCategory(sort: String, genre: String, status: String, title: String)
}
// MARK: - View helpers
extension View {
/// Registers app-wide navigationDestination for NavDestination values.
/// Apply once per NavigationStack.
func appNavigationDestination() -> some View {
modifier(AppNavigationDestinationModifier())
}
/// Standard "Error" alert driven by an optional String binding.
/// Suppresses network errors silently when offline (banner handles them).
func errorAlert(_ error: Binding<String?>) -> some View {
modifier(ErrorAlertModifier(error: error))
}
/// Signal to the root overlay that the mini player should be hidden.
func hideMiniPlayer() -> some View {
preference(key: HideMiniPlayerKey.self, value: true)
}
/// Marks a cover image as the zoom source for a book navigation transition (iOS 18+).
func bookCoverZoomSource(slug: String) -> some View {
modifier(BookCoverZoomSource(slug: slug))
}
}
// MARK: - Error alert modifier
private struct ErrorAlertModifier: ViewModifier {
@Binding var error: String?
@EnvironmentObject var networkMonitor: NetworkMonitor
private var shouldShowAlert: Bool {
guard let msg = error else { return false }
if !networkMonitor.isConnected {
let keywords = ["internet", "offline", "network", "connection", "unreachable", "timed out", "no data"]
if keywords.contains(where: { msg.lowercased().contains($0) }) {
DispatchQueue.main.async { self.error = nil }
return false
}
}
return true
}
func body(content: Content) -> some View {
content.alert("Error", isPresented: Binding(
get: { shouldShowAlert },
set: { if !$0 { error = nil } }
)) {
Button("OK") { error = nil }
} message: {
Text(error ?? "")
}
}
}
// 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)
case .browseCategory(let sort, let genre, let status, let title):
BrowseCategoryView(sort: sort, genre: genre, status: status, title: title)
}
}
.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)
case .browseCategory(let sort, let genre, let status, let title):
BrowseCategoryView(sort: sort, genre: genre, status: status, title: title)
}
}
}
}
}
// MARK: - Environment key: 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: - Preference key: hide mini player
struct HideMiniPlayerKey: PreferenceKey {
static var defaultValue = false
static func reduce(value: inout Bool, nextValue: () -> Bool) { value = value || nextValue() }
}
// MARK: - Cover zoom source modifier
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
}
}
}