From 4b256f7811fe204e13b4118f600170b78f5cf9b0 Mon Sep 17 00:00:00 2001 From: Laurent Date: Tue, 26 May 2026 22:05:10 +0200 Subject: [PATCH] feat(remote): wire HostServer and RemoteClient into MusicApp with menu items and DB swap --- Music/ContentView.swift | 1 + Music/MusicApp.swift | 97 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/Music/ContentView.swift b/Music/ContentView.swift index 1b9a1ec..2299888 100644 --- a/Music/ContentView.swift +++ b/Music/ContentView.swift @@ -9,6 +9,7 @@ struct ContentView: View { var shazam: ShazamService var db: DatabaseService @Binding var showNewPlaylistAlert: Bool + var networkStatus: NetworkStatus? @State private var showRenameAlert = false @State private var showEditQueryAlert = false @State private var playlistNameInput = "" diff --git a/Music/MusicApp.swift b/Music/MusicApp.swift index ccb562f..d6d1af5 100644 --- a/Music/MusicApp.swift +++ b/Music/MusicApp.swift @@ -11,6 +11,9 @@ struct MusicApp: App { @State private var playlistVM: PlaylistViewModel? @State private var showNewPlaylistAlert = false @State private var initError: String? + @State private var hostServer: HostServer? + @State private var remoteClient = RemoteClient() + @State private var showConnectionSheet = false var body: some Scene { WindowGroup { @@ -27,7 +30,8 @@ struct MusicApp: App { playlist: playlist, shazam: shazamService, db: db, - showNewPlaylistAlert: $showNewPlaylistAlert + showNewPlaylistAlert: $showNewPlaylistAlert, + networkStatus: computeNetworkStatus() ) } else if let error = initError { Text("Failed to initialize database: \(error)") @@ -38,6 +42,16 @@ struct MusicApp: App { } } .frame(minWidth: 800, minHeight: 500) + .onChange(of: remoteClient.connectionState) { _, newState in + if case .connected = newState { + enterRemoteMode() + } else if newState == .disconnected { + exitRemoteMode() + } + } + .sheet(isPresented: $showConnectionSheet) { + ConnectionSheet(remoteClient: remoteClient, isPresented: $showConnectionSheet) + } } .commands { CommandGroup(after: .newItem) { @@ -45,11 +59,27 @@ struct MusicApp: App { pickFolder() } .keyboardShortcut("o") + .disabled(remoteClient.connectionState.isConnected) Button("New Playlist...") { showNewPlaylistAlert = true } .keyboardShortcut("n") + .disabled(remoteClient.connectionState.isConnected) + + Divider() + + Toggle("Enable Host Mode", isOn: Binding( + get: { hostServer?.isHosting ?? false }, + set: { $0 ? startHosting() : hostServer?.stop() } + )) + .disabled(remoteClient.connectionState.isConnected) + + Button("Connect to Remote...") { + showConnectionSheet = true + remoteClient.startDiscovery() + } + .disabled(hostServer?.isHosting ?? false) } } } @@ -144,4 +174,69 @@ struct MusicApp: App { return nil } } + + // MARK: - Remote / Host + + private func startHosting() { + guard let db = dbService, let player = playerVM else { return } + let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, in: .userDomainMask + ).first!.appendingPathComponent("Music", isDirectory: true) + let dbPath = appSupport.appendingPathComponent("db.sqlite").path + + let server = HostServer(dbPath: dbPath) + server.configure(player: player, db: db) + do { + try server.start() + hostServer = server + } catch { + print("Failed to start host: \(error)") + } + } + + private func enterRemoteMode() { + guard let player = playerVM else { return } + do { + let remoteDb = try DatabaseService(path: RemoteClient.remoteDBPath) + self.libraryVM = LibraryViewModel(db: remoteDb) + self.playlistVM = PlaylistViewModel(db: remoteDb) + + player.enterRemoteMode(client: remoteClient) + player.trackResolver = { trackId in + self.libraryVM?.tracks.first(where: { $0.id == trackId }) + } + + remoteClient.onPlaybackState = { [weak player] state in + player?.applyRemoteState(state) + } + } catch { + print("Failed to load remote DB: \(error)") + remoteClient.disconnect() + } + } + + private func exitRemoteMode() { + playerVM?.exitRemoteMode() + remoteClient.onPlaybackState = nil + guard let db = dbService else { return } + self.libraryVM = LibraryViewModel(db: db) + self.playlistVM = PlaylistViewModel(db: db) + try? FileManager.default.removeItem(atPath: RemoteClient.remoteDBPath) + } + + private func computeNetworkStatus() -> NetworkStatus? { + if remoteClient.connectionState.isConnected { + let hostName: String + if case .connected(let name) = remoteClient.connectionState { hostName = name } else { hostName = "Unknown" } + return NetworkStatus( + mode: .remote(hostName: hostName), + onDisconnect: { [remoteClient] in remoteClient.disconnect() }, + onRefreshLibrary: { [remoteClient] in remoteClient.sendCommand(.refreshDB) } + ) + } + if let server = hostServer, server.isHosting { + return NetworkStatus(mode: .hosting(connectedRemote: server.connectedRemoteName)) + } + return nil + } }