Create BearWave macOS radio-first app
This commit is contained in:
177
Sources/BearWaveCore/Views/StationDetailView.swift
Normal file
177
Sources/BearWaveCore/Views/StationDetailView.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user