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