141 lines
5.2 KiB
Swift
141 lines
5.2 KiB
Swift
import SwiftUI
|
|
|
|
struct StationListView: View {
|
|
@Environment(AppModel.self) private var model
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
if let error = model.library.lastError {
|
|
Text(error)
|
|
.foregroundStyle(.red)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
.background(.red.opacity(0.08))
|
|
}
|
|
|
|
if model.library.visibleStations.isEmpty && !model.library.isLoading {
|
|
ContentUnavailableView(
|
|
"No stations loaded yet",
|
|
systemImage: "radio",
|
|
description: Text("Load a station page or search worldwide.")
|
|
)
|
|
} else {
|
|
List(selection: Binding(
|
|
get: { model.library.selectedStationID },
|
|
set: { model.library.selectedStationID = $0 }
|
|
)) {
|
|
ForEach(model.library.visibleStations) { station in
|
|
StationRowView(station: station)
|
|
.tag(station.id)
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
model.library.select(station)
|
|
}
|
|
.contextMenu {
|
|
Button(model.library.isFavorite(station) ? "Remove Favorite" : "Add Favorite") {
|
|
model.library.toggleFavorite(station)
|
|
}
|
|
if !model.library.groups.isEmpty {
|
|
Menu("Add to Folder") {
|
|
ForEach(model.library.groups) { group in
|
|
Button(group.name) {
|
|
model.library.addStationToGroup(station, group: group)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Button("Play") {
|
|
model.play(station)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct StationRowView: View {
|
|
@Environment(AppModel.self) private var model
|
|
let station: RadioStation
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
Image(nsImage: model.artwork.image(for: station) ?? NSImage())
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(width: 34, height: 34)
|
|
.background(.quaternary)
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
.overlay(alignment: .bottomTrailing) {
|
|
Image(systemName: station.isOnline ? "dot.radiowaves.left.and.right" : "exclamationmark.triangle")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
.foregroundStyle(station.isOnline ? Color.secondary : Color.orange)
|
|
.padding(2)
|
|
.background(.regularMaterial, in: Circle())
|
|
.offset(x: 4, y: 4)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(station.name)
|
|
.font(.headline)
|
|
.lineLimit(1)
|
|
HStack(spacing: 8) {
|
|
if isCurrentStation {
|
|
Text(model.playback.state.title)
|
|
.foregroundStyle(statusColor)
|
|
}
|
|
if !station.country.isEmpty {
|
|
Text(station.country)
|
|
}
|
|
if !station.codec.isEmpty {
|
|
Text(station.codec.uppercased())
|
|
}
|
|
if station.bitrate > 0 {
|
|
Text("\(station.bitrate) kbps")
|
|
}
|
|
if station.votes > 0 {
|
|
Text("\(station.votes) votes")
|
|
}
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
model.library.toggleFavorite(station)
|
|
} label: {
|
|
Image(systemName: model.library.isFavorite(station) ? "star.fill" : "star")
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundStyle(model.library.isFavorite(station) ? .yellow : .secondary)
|
|
|
|
Button {
|
|
model.play(station)
|
|
} label: {
|
|
Image(systemName: "play.fill")
|
|
}
|
|
.buttonStyle(.bordered)
|
|
}
|
|
.padding(.vertical, 5)
|
|
}
|
|
|
|
private var isCurrentStation: Bool {
|
|
model.playback.currentStation?.id == station.id
|
|
}
|
|
|
|
private var statusColor: Color {
|
|
if case .failed = model.playback.state {
|
|
return .red
|
|
}
|
|
if case .buffering = model.playback.state {
|
|
return .orange
|
|
}
|
|
return .secondary
|
|
}
|
|
}
|