From ca829dde4cb582d3e9d452f0916722712b50a2c1 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 11 Jun 2025 13:00:09 -0700 Subject: [PATCH] Implements server selection UI --- QueueCube/Backend/API.swift | 4 ++ QueueCube/Views/ContentView.swift | 27 ++++++++- QueueCube/Views/MainView.swift | 97 ++++++++++++++++++++++++------ QueueCube/Views/PlaylistView.swift | 8 ++- 4 files changed, 115 insertions(+), 21 deletions(-) diff --git a/QueueCube/Backend/API.swift b/QueueCube/Backend/API.swift index fe370c3..fc483e1 100644 --- a/QueueCube/Backend/API.swift +++ b/QueueCube/Backend/API.swift @@ -17,6 +17,10 @@ struct MediaItem: Codable let playing: Bool? let metadata: Metadata? + var displayTitle: String { + metadata?.title ?? title ?? filename ?? "item \(id)" + } + // MARK: - Types struct Metadata: Codable diff --git a/QueueCube/Views/ContentView.swift b/QueueCube/Views/ContentView.swift index caf524a..9d1e02b 100644 --- a/QueueCube/Views/ContentView.swift +++ b/QueueCube/Views/ContentView.swift @@ -78,6 +78,7 @@ struct ContentView: View MainView(model: model) .task { await watchWebsocket() } .task { await refresh([.nowPlaying, .playlist, .favorites]) } + .task { await watchForSettingsChanges() } } // MARK: - Types @@ -118,7 +119,7 @@ extension ContentView model.playlistModel.items = playlist.enumerated().map { (idx, mediaItem) in MediaListItem( id: String(mediaItem.id), - title: mediaItem.title ?? mediaItem.filename ?? "", + title: mediaItem.displayTitle, filename: mediaItem.filename ?? "", index: idx, isCurrent: mediaItem.current ?? false @@ -131,7 +132,7 @@ extension ContentView model.favoritesModel.items = favorites.map { mediaItem in MediaListItem( id: String(mediaItem.id), - title: mediaItem.title ?? mediaItem.filename ?? "", + title: mediaItem.displayTitle, filename: mediaItem.filename ?? "" ) } @@ -186,4 +187,26 @@ extension ContentView } } } + + private func watchForSettingsChanges() async { + let settingsChangedNotifications = NotificationCenter.default.notifications(named: .settingsChanged) + .map({ _ in Optional.none }) + + for await _ in settingsChangedNotifications { + let newSelectedServer = Settings.fromDefaults().selectedServer + if newSelectedServer != model.selectedServer { + model.selectedServer = newSelectedServer + + // Reset view model to defaults + model.playlistModel = MediaListViewModel(mode: .playlist) + model.favoritesModel = MediaListViewModel(mode: .favorites) + model.nowPlayingViewModel = NowPlayingViewModel() + + await refresh([.playlist, .nowPlaying, .favorites]) + } + + // Always reset this + model.serverSelectionViewModel = ServerSelectionToolbarModifier.ViewModel() + } + } } diff --git a/QueueCube/Views/MainView.swift b/QueueCube/Views/MainView.swift index 4e19272..0ad314d 100644 --- a/QueueCube/Views/MainView.swift +++ b/QueueCube/Views/MainView.swift @@ -10,15 +10,16 @@ import SwiftUI @Observable class MainViewModel { - var selectedServer: Server? { Settings.fromDefaults().selectedServer } + var selectedServer: Server? = Settings.fromDefaults().selectedServer var connectionError: Error? = nil var selectedTab: Tab = .playlist - var playlistModel = MediaListViewModel(mode: .playlist) - var favoritesModel = MediaListViewModel(mode: .favorites) - var nowPlayingViewModel = NowPlayingViewModel() - var addMediaViewModel = AddMediaBarViewModel() + var playlistModel = MediaListViewModel(mode: .playlist) + var favoritesModel = MediaListViewModel(mode: .favorites) + var nowPlayingViewModel = NowPlayingViewModel() + var addMediaViewModel = AddMediaBarViewModel() + var serverSelectionViewModel = ServerSelectionToolbarModifier.ViewModel() enum Tab: String, CaseIterable { @@ -40,16 +41,6 @@ struct MainView: View if !Settings.fromDefaults().isConfigured { model.selectedTab = .settings } - - Task { - let settingsChangedNotifications = NotificationCenter.default.notifications(named: .settingsChanged) - .map({ _ in Optional.none }) - - for await _ in settingsChangedNotifications { - // TODO - // model.api = API.fromSettings() - } - } } var body: some View { @@ -57,11 +48,19 @@ struct MainView: View TabView(selection: $model.selectedTab) { Tab(.playlist, systemImage: "list.bullet", value: .playlist) { - MediaListView(model: model.playlistModel) + NavigationStack { + MediaListView(model: model.playlistModel) + .displayingServerSelectionToolbar(model: $model.serverSelectionViewModel) + .navigationTitle(.playlist) + } } Tab(.favorites, systemImage: "heart.fill", value: .favorites) { - MediaListView(model: model.favoritesModel) + NavigationStack { + MediaListView(model: model.favoritesModel) + .displayingServerSelectionToolbar(model: $model.serverSelectionViewModel) + .navigationTitle(.favorites) + } } Tab(.settings, systemImage: "gear", value: .settings) { @@ -107,3 +106,67 @@ struct MainView: View } } +struct ServerSelectionToolbarModifier: ViewModifier +{ + @Binding var model: ViewModel + + func body(content: Content) -> some View { + content + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + Section { + ForEach(model.selectableServers) { server in + Button { + model.selectedServer = server + } label: { + Text(server.displayName) + if model.selectedServer == server { + Image(systemName: "checkmark") + } + } + } + } + + #if false + // TODO + Section { + Button(.addServer) { + + } + } + #endif + } label: { + Label(model.selectedServer?.displayName ?? "Servers", systemImage: "chevron.down") + .labelStyle(.titleAndIcon) + } + .buttonBorderShape(.capsule) + .buttonStyle(.bordered) + .menuStyle(.button) + } + } + } + + // MARK: - Types + + @Observable + class ViewModel + { + var selectableServers: [Server] = Settings.fromDefaults().configuredServers + var selectedServer: Server? = Settings.fromDefaults().selectedServer { + didSet { + Settings + .fromDefaults() + .selectedServer(selectedServer) + .save() + } + } + } +} + +extension View { + func displayingServerSelectionToolbar(model: Binding) -> some View { + modifier(ServerSelectionToolbarModifier(model: model)) + } +} + diff --git a/QueueCube/Views/PlaylistView.swift b/QueueCube/Views/PlaylistView.swift index afffc94..3aecc9e 100644 --- a/QueueCube/Views/PlaylistView.swift +++ b/QueueCube/Views/PlaylistView.swift @@ -9,14 +9,18 @@ import SwiftUI struct MediaListItem: Identifiable { - let id: String + let _id: String let title: String let filename: String let index: Int? let isCurrent: Bool + var id: String { + _id + filename // temporary: we get duplicate ids from the server sometimes... + } + init(id: String, title: String, filename: String, index: Int? = nil, isCurrent: Bool = false) { - self.id = id + self._id = id self.title = title self.filename = filename self.index = index