178 lines
6.4 KiB
Swift
178 lines
6.4 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
struct StationDetailView: View {
|
|
@Environment(AppModel.self) private var model
|
|
@Environment(\.openURL) private var openURL
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let station = model.library.selectedStation {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
header(station)
|
|
actions(station)
|
|
if model.playback.currentStation?.id == station.id {
|
|
nowPlayingSection
|
|
}
|
|
technicalSection(station)
|
|
linksSection(station)
|
|
}
|
|
.padding(16)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
} else {
|
|
ContentUnavailableView("No station selected", systemImage: "radio")
|
|
}
|
|
}
|
|
.background(.regularMaterial)
|
|
}
|
|
|
|
private func header(_ station: RadioStation) -> some View {
|
|
let artworkImage: NSImage
|
|
if model.playback.currentStation?.id == station.id {
|
|
artworkImage = model.playback.currentArtwork
|
|
} else {
|
|
artworkImage = model.artwork.image(for: station) ?? NSImage()
|
|
}
|
|
|
|
return VStack(alignment: .leading, spacing: 10) {
|
|
Image(nsImage: artworkImage)
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(width: 72, height: 72)
|
|
.background(.quaternary)
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(station.name)
|
|
.font(.title3.weight(.semibold))
|
|
.lineLimit(2)
|
|
if !station.country.isEmpty {
|
|
Text(station.country)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func actions(_ station: RadioStation) -> some View {
|
|
HStack(spacing: 8) {
|
|
Button {
|
|
model.play(station)
|
|
} label: {
|
|
Label("Play", systemImage: "play.fill")
|
|
}
|
|
|
|
Button {
|
|
model.library.toggleFavorite(station)
|
|
} label: {
|
|
Label(model.library.isFavorite(station) ? "Remove Favorite" : "Add Favorite", systemImage: model.library.isFavorite(station) ? "star.fill" : "star")
|
|
}
|
|
|
|
if !model.library.groups.isEmpty {
|
|
Menu {
|
|
ForEach(model.library.groups) { group in
|
|
Button {
|
|
if model.library.isStationInGroup(station, group: group) {
|
|
model.library.removeStationFromGroup(station, group: group)
|
|
} else {
|
|
model.library.addStationToGroup(station, group: group)
|
|
}
|
|
} label: {
|
|
Label(
|
|
model.library.isStationInGroup(station, group: group) ? "Remove from \(group.name)" : "Add to \(group.name)",
|
|
systemImage: model.library.isStationInGroup(station, group: group) ? "checkmark" : "plus"
|
|
)
|
|
}
|
|
}
|
|
} label: {
|
|
Label("Folders", systemImage: "folder.badge.plus")
|
|
}
|
|
}
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.small)
|
|
}
|
|
|
|
private var nowPlayingSection: some View {
|
|
DetailSection(title: "Now Playing") {
|
|
LabeledContent("Status", value: model.playback.state.title)
|
|
if case let .failed(error) = model.playback.state {
|
|
Text(error.detail)
|
|
.foregroundStyle(.red)
|
|
.font(.caption)
|
|
}
|
|
if !model.playback.currentTrackArtist.isEmpty {
|
|
LabeledContent("Artist", value: model.playback.currentTrackArtist)
|
|
}
|
|
if !model.playback.currentTrackTitle.isEmpty {
|
|
LabeledContent("Title", value: model.playback.currentTrackTitle)
|
|
}
|
|
if model.playback.currentTrackArtist.isEmpty && model.playback.currentTrackTitle.isEmpty, !model.playback.state.isPlaying, model.playback.state != .stopped {
|
|
Text("No stream metadata yet")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func technicalSection(_ station: RadioStation) -> some View {
|
|
DetailSection(title: "Details") {
|
|
if !station.codec.isEmpty {
|
|
LabeledContent("Codec", value: station.codec.uppercased())
|
|
}
|
|
if station.bitrate > 0 {
|
|
LabeledContent("Bitrate", value: "\(station.bitrate) kbps")
|
|
}
|
|
if station.votes > 0 {
|
|
LabeledContent("Votes", value: station.votes.formatted())
|
|
}
|
|
if !station.tags.isEmpty {
|
|
LabeledContent("Tags") {
|
|
Text(station.tags)
|
|
.lineLimit(4)
|
|
.multilineTextAlignment(.trailing)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func linksSection(_ station: RadioStation) -> some View {
|
|
DetailSection(title: "Links") {
|
|
if let homepage = URL(string: station.homepage), !station.homepage.isEmpty {
|
|
Button {
|
|
openURL(homepage)
|
|
} label: {
|
|
Label("Open Homepage", systemImage: "safari")
|
|
}
|
|
}
|
|
Button {
|
|
NSPasteboard.general.clearContents()
|
|
NSPasteboard.general.setString(station.playbackURLString, forType: .string)
|
|
} label: {
|
|
Label("Copy Stream URL", systemImage: "doc.on.doc")
|
|
}
|
|
.disabled(station.playbackURLString.isEmpty)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.controlSize(.small)
|
|
}
|
|
}
|
|
|
|
private struct DetailSection<Content: View>: View {
|
|
let title: LocalizedStringKey
|
|
@ViewBuilder let content: Content
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(title)
|
|
.font(.headline)
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
content
|
|
}
|
|
.font(.subheadline)
|
|
}
|
|
}
|
|
}
|