NavidromeApp/iOS/App/NavidromePlayerApp.swift
Dallas Groot d8041c0019 NavidromePlayer: iOS + watchOS Navidrome/Subsonic music player
Features:
- Dual-AVPlayer Smart DJ crossfade with LUFS normalization
- Mitsuha-style FFT visualizer (real-time + offline pre-computed)
- Companion API integration (Smart DJ, tag editing, vis frames)
- Offline-first SyncEngine with delta sync and album detail pre-caching
- Audio pre-fetcher for gapless queue playback
- Optimistic action queue (star/unstar with background retry)
- ShazamKit recognition with MusicKit preview playback
- Radio streaming with HLS/PLS/M3U support and buffer seek
- Watch app with Crown Sequencer and Ultra speaker support
- Batch metadata editing with album_artist fix for split albums
- Cache-first UI pattern across all views
- NWPathMonitor offline detection with reactive song greying
2026-03-28 20:49:47 +00:00

79 lines
2.7 KiB
Swift

import SwiftUI
// MARK: - App Delegate (Background Upload Session)
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
if identifier == "com.navidromeplayer.batchupload" {
ZipImportManager.shared.setBackgroundCompletionHandler(completionHandler)
DebugLogger.shared.log("Background session woke app: \(identifier)", category: "Upload")
}
}
}
@main
struct NavidromePlayerApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var serverManager = ServerManager.shared
@StateObject private var audioPlayer = AudioPlayer.shared
@StateObject private var offlineManager = OfflineManager.shared
// Initialize WatchConnectivity so sync works immediately
private let watchConnectivity = WatchConnectivityManager.shared
init() {
// Dismiss keyboard when scrolling any scroll view
UIScrollView.appearance().keyboardDismissMode = .interactive
}
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(serverManager)
.environmentObject(audioPlayer)
.environmentObject(offlineManager)
.tint(Color(red: 1.0, green: 0.176, blue: 0.333)) // iOS 8 Music pink
}
}
}
struct RootView: View {
@EnvironmentObject var serverManager: ServerManager
var body: some View {
Group {
if serverManager.servers.isEmpty {
// No servers configured show login
LoginView()
} else {
// Servers exist show main UI immediately (connects in background)
MainTabView()
}
}
.dismissKeyboardOnTap()
.task {
ImageCache.shared.trimDiskCache()
AudioPreFetcher.shared.cleanOldPrefetches(keeping: AudioPlayer.shared.queue)
if serverManager.activeServer != nil {
await serverManager.connectToActive()
// Background sync fills LibraryCache so UI never waits for network
SyncEngine.shared.syncIfNeeded()
// Flush any pending optimistic actions (star/unstar that failed offline)
OptimisticActionQueue.shared.flush()
}
// Connect Companion push client if enabled
if CompanionSettings.shared.isEnabled {
CompanionPushClient.shared.connect()
}
}
}
}