Files
bearwave-mac/Sources/BearWaveCore/Views/StationDetailView.swift
2026-06-25 09:21:42 +02:00

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)
}
}
}