feat(remote): wire HostServer and RemoteClient into MusicApp with menu items and DB swap

feat/music-streaming
Laurent 1 month ago
parent 4ee0325d2f
commit 4b256f7811
  1. 1
      Music/ContentView.swift
  2. 97
      Music/MusicApp.swift

@ -9,6 +9,7 @@ struct ContentView: View {
var shazam: ShazamService var shazam: ShazamService
var db: DatabaseService var db: DatabaseService
@Binding var showNewPlaylistAlert: Bool @Binding var showNewPlaylistAlert: Bool
var networkStatus: NetworkStatus?
@State private var showRenameAlert = false @State private var showRenameAlert = false
@State private var showEditQueryAlert = false @State private var showEditQueryAlert = false
@State private var playlistNameInput = "" @State private var playlistNameInput = ""

@ -11,6 +11,9 @@ struct MusicApp: App {
@State private var playlistVM: PlaylistViewModel? @State private var playlistVM: PlaylistViewModel?
@State private var showNewPlaylistAlert = false @State private var showNewPlaylistAlert = false
@State private var initError: String? @State private var initError: String?
@State private var hostServer: HostServer?
@State private var remoteClient = RemoteClient()
@State private var showConnectionSheet = false
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
@ -27,7 +30,8 @@ struct MusicApp: App {
playlist: playlist, playlist: playlist,
shazam: shazamService, shazam: shazamService,
db: db, db: db,
showNewPlaylistAlert: $showNewPlaylistAlert showNewPlaylistAlert: $showNewPlaylistAlert,
networkStatus: computeNetworkStatus()
) )
} else if let error = initError { } else if let error = initError {
Text("Failed to initialize database: \(error)") Text("Failed to initialize database: \(error)")
@ -38,6 +42,16 @@ struct MusicApp: App {
} }
} }
.frame(minWidth: 800, minHeight: 500) .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 { .commands {
CommandGroup(after: .newItem) { CommandGroup(after: .newItem) {
@ -45,11 +59,27 @@ struct MusicApp: App {
pickFolder() pickFolder()
} }
.keyboardShortcut("o") .keyboardShortcut("o")
.disabled(remoteClient.connectionState.isConnected)
Button("New Playlist...") { Button("New Playlist...") {
showNewPlaylistAlert = true showNewPlaylistAlert = true
} }
.keyboardShortcut("n") .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 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
}
} }

Loading…
Cancel
Save