Home screen, library-wide search, splash with orbital particles, macOS download fix

- Add Netflix-style Home with recent audiobooks/podcasts, list/grid toggle shared with Library
- Add searchable libraries — audiobooks filter on title/author; podcasts also index and search episodes across the whole library
- Add unified context menu (download / progress remove) across Home, Library, and PodcastDetail
- Rework cover badges: listened checkmark top-right, download indicator bottom-right; hide progress bar when finished
- Add AirPlay picker in PlayerBar
- Replace splash with four-phase animation: orbital particles → attract → flash → fade; cross-platform iOS+macOS, runtime AppIcon lookup
- Fix macOS parallel downloads: align connection settings with iOS, raise timeoutIntervalForRequest to 300s
- Fix progress deletion using server-side progress UUID (DELETE /api/me/progress/:id)
- Bump CFBundleVersion to 8

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-23 08:54:43 +02:00
parent 1dbd133b75
commit a17a61ad9a
20 changed files with 1423 additions and 267 deletions

View File

@@ -3,20 +3,21 @@ import SwiftUI
struct ContentView: View {
@Environment(AppState.self) private var app
@Environment(\.scenePhase) private var scenePhase
#if os(iOS)
@State private var splashVisible = true
#endif
/// Bootstrap fertig Signal an den Splash, Phase 2 zu starten.
@State private var isReadyToDismiss = false
/// Splash hat seine Fade-Phase ausgespielt und kann aus dem View-Tree raus.
@State private var splashFinished = false
var body: some View {
ZStack {
mainContent
#if os(iOS)
if splashVisible {
SplashView()
.zIndex(10)
.transition(.opacity.animation(.easeOut(duration: 0.55)))
if !splashFinished {
SplashScreenView(isReady: isReadyToDismiss) {
splashFinished = true
}
.zIndex(10)
}
#endif
}
.task { await boot() }
.onChange(of: scenePhase) { _, newPhase in
@@ -42,17 +43,11 @@ struct ContentView: View {
}
private func boot() async {
#if os(iOS)
// Run bootstrap and minimum splash time in parallel;
// dismiss splash only after BOTH complete.
await withTaskGroup(of: Void.self) { group in
group.addTask { await app.bootstrap() }
group.addTask { try? await Task.sleep(for: .seconds(1.2)) }
await group.waitForAll()
}
withAnimation { splashVisible = false }
#else
await app.bootstrap()
#endif
// SplashScreenView wartet selbst auf seine Mindest-Loop-Zeit, daher
// kein künstlicher Sleep mehr hier nötig. Sobald isReadyToDismiss
// gesetzt wird, fängt der Splash mit Phase 2 an und ruft am Ende
// seinen onComplete-Callback (splashFinished = true).
isReadyToDismiss = true
}
}