From 7b88b180fd15501809d22f1a6bb462c0aac2b3ce Mon Sep 17 00:00:00 2001 From: Laurent Date: Sat, 30 May 2026 12:29:39 +0200 Subject: [PATCH] feat: wire SmartPlaylistBuilderSheet into menu and context menu Co-Authored-By: Claude Sonnet 4.6 --- Music/ContentView.swift | 118 +++++++++++++++++++----------- Music/MusicApp.swift | 8 ++ Music/Views/PlaylistBarView.swift | 7 +- 3 files changed, 91 insertions(+), 42 deletions(-) diff --git a/Music/ContentView.swift b/Music/ContentView.swift index 3a48a7f..f1d3cc4 100644 --- a/Music/ContentView.swift +++ b/Music/ContentView.swift @@ -9,9 +9,11 @@ struct ContentView: View { var shazam: ShazamService var db: DatabaseService @Binding var showNewPlaylistAlert: Bool + @Binding var showSmartPlaylistBuilder: Bool var networkStatus: NetworkStatus? @State private var showRenameAlert = false @State private var showEditQueryAlert = false + @State private var smartPlaylistBuilderEditing: SmartPlaylist? @State private var playlistNameInput = "" @State private var editQueryInput = "" @State private var itemToRename: (any PlaylistRepresentable)? @@ -24,52 +26,60 @@ struct ContentView: View { @State private var totalDuration: Double = 0 @State private var monthlyAdditions: [MonthlyCount] = [] - var body: some View { - VStack(spacing: 0) { - if let status = networkStatus { - switch status.mode { - case .remote(let hostName): - HStack(spacing: 8) { - Image(systemName: "antenna.radiowaves.left.and.right") - .font(.system(size: 10)).foregroundStyle(.blue) - Text("Connected to \(hostName)") - .font(.system(size: 11, weight: .medium)).foregroundStyle(.blue) - Spacer() + /// The remote/streaming connection status banner. Extracted from `body` so the + /// type-checker doesn't have to solve the whole (very large) view in one expression + /// — without this, a clean build fails with "unable to type-check in reasonable time". + @ViewBuilder + private var networkBanner: some View { + if let status = networkStatus { + switch status.mode { + case .remote(let hostName): + HStack(spacing: 8) { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.system(size: 10)).foregroundStyle(.blue) + Text("Connected to \(hostName)") + .font(.system(size: 11, weight: .medium)).foregroundStyle(.blue) + Spacer() + Button("Refresh") { status.onRefreshLibrary?() } + .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.secondary) + Button("Disconnect") { status.onDisconnect?() } + .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.red) + } + .padding(.horizontal, 12).padding(.vertical, 4) + .background(Color.blue.opacity(0.08)) + case .hosting(let remoteName): + HStack(spacing: 8) { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.system(size: 10)).foregroundStyle(.green) + Text(remoteName != nil ? "Hosting · \(remoteName!) connected" : "Hosting") + .font(.system(size: 11, weight: .medium)).foregroundStyle(.green) + Spacer() + } + .padding(.horizontal, 12).padding(.vertical, 4) + .background(Color.green.opacity(0.08)) + case .streamHosting, .streamClient: + HStack(spacing: 8) { + Image(systemName: "cloud") + .font(.system(size: 10)).foregroundStyle(.purple) + Text(status.statusMessage) + .font(.system(size: 11, weight: .medium)).foregroundStyle(.purple) + Spacer() + if case .streamClient = status.mode { Button("Refresh") { status.onRefreshLibrary?() } .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.secondary) - Button("Disconnect") { status.onDisconnect?() } - .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.red) - } - .padding(.horizontal, 12).padding(.vertical, 4) - .background(Color.blue.opacity(0.08)) - case .hosting(let remoteName): - HStack(spacing: 8) { - Image(systemName: "antenna.radiowaves.left.and.right") - .font(.system(size: 10)).foregroundStyle(.green) - Text(remoteName != nil ? "Hosting · \(remoteName!) connected" : "Hosting") - .font(.system(size: 11, weight: .medium)).foregroundStyle(.green) - Spacer() - } - .padding(.horizontal, 12).padding(.vertical, 4) - .background(Color.green.opacity(0.08)) - case .streamHosting, .streamClient: - HStack(spacing: 8) { - Image(systemName: "cloud") - .font(.system(size: 10)).foregroundStyle(.purple) - Text(status.statusMessage) - .font(.system(size: 11, weight: .medium)).foregroundStyle(.purple) - Spacer() - if case .streamClient = status.mode { - Button("Refresh") { status.onRefreshLibrary?() } - .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.secondary) - } - Button("Disconnect") { status.onDisconnect?() } - .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.red) } - .padding(.horizontal, 12).padding(.vertical, 4) - .background(Color.purple.opacity(0.08)) + Button("Disconnect") { status.onDisconnect?() } + .font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.red) } + .padding(.horizontal, 12).padding(.vertical, 4) + .background(Color.purple.opacity(0.08)) } + } + } + + var body: some View { + VStack(spacing: 0) { + networkBanner SearchBarView( searchText: $searchText, @@ -239,6 +249,9 @@ struct ContentView: View { smartPlaylistToEdit = smart editQueryInput = smart.searchQuery showEditQueryAlert = true + }, + onEditConditions: { smart in + smartPlaylistBuilderEditing = smart } ) @@ -327,6 +340,29 @@ struct ContentView: View { Text(error) } } + .sheet(isPresented: $showSmartPlaylistBuilder) { + SmartPlaylistBuilderSheet( + editingPlaylist: nil, + onSave: { name, conditions in + try? playlist.createSmartPlaylist(name: name, conditions: conditions) + showSmartPlaylistBuilder = false + }, + onCancel: { showSmartPlaylistBuilder = false } + ) + } + .sheet(item: $smartPlaylistBuilderEditing) { smart in + SmartPlaylistBuilderSheet( + editingPlaylist: smart, + onSave: { name, conditions in + if name != smart.name { + try? playlist.renameSmartPlaylist(smart, to: name) + } + try? playlist.updateSmartPlaylistConditions(smart, to: conditions) + smartPlaylistBuilderEditing = nil + }, + onCancel: { smartPlaylistBuilderEditing = nil } + ) + } } private var playerControls: some View { diff --git a/Music/MusicApp.swift b/Music/MusicApp.swift index 3269287..e254fc2 100644 --- a/Music/MusicApp.swift +++ b/Music/MusicApp.swift @@ -11,6 +11,7 @@ struct MusicApp: App { @State private var shazamService = ShazamService() @State private var playlistVM: PlaylistViewModel? @State private var showNewPlaylistAlert = false + @State private var showSmartPlaylistBuilder = false @State private var initError: String? @State private var hostServer: HostServer? @State private var remoteClient = RemoteClient() @@ -38,6 +39,7 @@ struct MusicApp: App { shazam: shazamService, db: db, showNewPlaylistAlert: $showNewPlaylistAlert, + showSmartPlaylistBuilder: $showSmartPlaylistBuilder, networkStatus: computeNetworkStatus() ) } else if let error = initError { @@ -83,6 +85,12 @@ struct MusicApp: App { .keyboardShortcut("n") .disabled(remoteClient.connectionState.isConnected) + Button("New Smart Playlist...") { + showSmartPlaylistBuilder = true + } + .keyboardShortcut("n", modifiers: [.command, .shift]) + .disabled(remoteClient.connectionState.isConnected) + Divider() Toggle("Enable Host Mode", isOn: Binding( diff --git a/Music/Views/PlaylistBarView.swift b/Music/Views/PlaylistBarView.swift index e87ddb2..e3572ff 100644 --- a/Music/Views/PlaylistBarView.swift +++ b/Music/Views/PlaylistBarView.swift @@ -11,6 +11,7 @@ struct PlaylistBarView: View { var onRename: (any PlaylistRepresentable) -> Void var onDelete: (any PlaylistRepresentable) -> Void var onEditQuery: (SmartPlaylist) -> Void + var onEditConditions: (SmartPlaylist) -> Void var body: some View { FlowLayout(spacing: 6) { @@ -39,7 +40,11 @@ struct PlaylistBarView: View { if !isRemoteMode { Button("Rename...") { onRename(item) } if let smart = item as? SmartPlaylist { - Button("Edit Search Query...") { onEditQuery(smart) } + if smart.conditions != nil { + Button("Edit...") { onEditConditions(smart) } + } else { + Button("Edit Search Query...") { onEditQuery(smart) } + } } Button("Delete") { onDelete(item) } }