|
|
|
@ -9,9 +9,11 @@ struct ContentView: View { |
|
|
|
var shazam: ShazamService |
|
|
|
var shazam: ShazamService |
|
|
|
var db: DatabaseService |
|
|
|
var db: DatabaseService |
|
|
|
@Binding var showNewPlaylistAlert: Bool |
|
|
|
@Binding var showNewPlaylistAlert: Bool |
|
|
|
|
|
|
|
@Binding var showSmartPlaylistBuilder: Bool |
|
|
|
var networkStatus: NetworkStatus? |
|
|
|
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 smartPlaylistBuilderEditing: SmartPlaylist? |
|
|
|
@State private var playlistNameInput = "" |
|
|
|
@State private var playlistNameInput = "" |
|
|
|
@State private var editQueryInput = "" |
|
|
|
@State private var editQueryInput = "" |
|
|
|
@State private var itemToRename: (any PlaylistRepresentable)? |
|
|
|
@State private var itemToRename: (any PlaylistRepresentable)? |
|
|
|
@ -24,52 +26,60 @@ struct ContentView: View { |
|
|
|
@State private var totalDuration: Double = 0 |
|
|
|
@State private var totalDuration: Double = 0 |
|
|
|
@State private var monthlyAdditions: [MonthlyCount] = [] |
|
|
|
@State private var monthlyAdditions: [MonthlyCount] = [] |
|
|
|
|
|
|
|
|
|
|
|
var body: some View { |
|
|
|
/// The remote/streaming connection status banner. Extracted from `body` so the |
|
|
|
VStack(spacing: 0) { |
|
|
|
/// type-checker doesn't have to solve the whole (very large) view in one expression |
|
|
|
if let status = networkStatus { |
|
|
|
/// — without this, a clean build fails with "unable to type-check in reasonable time". |
|
|
|
switch status.mode { |
|
|
|
@ViewBuilder |
|
|
|
case .remote(let hostName): |
|
|
|
private var networkBanner: some View { |
|
|
|
HStack(spacing: 8) { |
|
|
|
if let status = networkStatus { |
|
|
|
Image(systemName: "antenna.radiowaves.left.and.right") |
|
|
|
switch status.mode { |
|
|
|
.font(.system(size: 10)).foregroundStyle(.blue) |
|
|
|
case .remote(let hostName): |
|
|
|
Text("Connected to \(hostName)") |
|
|
|
HStack(spacing: 8) { |
|
|
|
.font(.system(size: 11, weight: .medium)).foregroundStyle(.blue) |
|
|
|
Image(systemName: "antenna.radiowaves.left.and.right") |
|
|
|
Spacer() |
|
|
|
.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?() } |
|
|
|
Button("Refresh") { status.onRefreshLibrary?() } |
|
|
|
.font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(.secondary) |
|
|
|
.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) |
|
|
|
Button("Disconnect") { status.onDisconnect?() } |
|
|
|
.background(Color.purple.opacity(0.08)) |
|
|
|
.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( |
|
|
|
SearchBarView( |
|
|
|
searchText: $searchText, |
|
|
|
searchText: $searchText, |
|
|
|
@ -239,6 +249,9 @@ struct ContentView: View { |
|
|
|
smartPlaylistToEdit = smart |
|
|
|
smartPlaylistToEdit = smart |
|
|
|
editQueryInput = smart.searchQuery |
|
|
|
editQueryInput = smart.searchQuery |
|
|
|
showEditQueryAlert = true |
|
|
|
showEditQueryAlert = true |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onEditConditions: { smart in |
|
|
|
|
|
|
|
smartPlaylistBuilderEditing = smart |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
@ -327,6 +340,29 @@ struct ContentView: View { |
|
|
|
Text(error) |
|
|
|
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 { |
|
|
|
private var playerControls: some View { |
|
|
|
|