Create BearWave macOS radio-first app

This commit is contained in:
Sebastian Palencsar
2026-06-25 09:21:42 +02:00
commit b47b591e03
42 changed files with 3814 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
import SwiftUI
struct MiniPlayerView: View {
@Environment(AppModel.self) private var model
@Binding var isMiniPlayer: Bool
var body: some View {
HStack(spacing: 12) {
Image(nsImage: model.playback.currentArtwork)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
.background(.quaternary)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 2) {
Text(model.playback.currentStation?.name ?? String(localized: "No station selected"))
.font(.headline)
.lineLimit(1)
Text(miniPlayerText)
.font(.caption)
.foregroundStyle(playerDetailColor)
.lineLimit(1)
}
Spacer()
Button {
model.playback.togglePlayPause()
} label: {
Image(systemName: model.playback.isPlaying ? "pause.fill" : "play.fill")
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(model.playback.currentStation == nil)
Button {
model.playback.stop()
} label: {
Image(systemName: "stop.fill")
}
.buttonStyle(.bordered)
.controlSize(.small)
HStack(spacing: 4) {
Image(systemName: "speaker.wave.1")
.font(.caption)
Slider(value: Binding(
get: { model.playback.volume },
set: { model.setVolume($0) }
), in: 0...1)
.frame(width: 80)
Image(systemName: "speaker.wave.3")
.font(.caption)
}
.foregroundStyle(.secondary)
Button {
isMiniPlayer = false
} label: {
Image(systemName: "arrow.up.left.and.arrow.down.right")
}
.buttonStyle(.bordered)
.controlSize(.small)
.help("Show full window")
}
.padding(12)
.frame(height: 110)
.background(.bar)
}
private var miniPlayerText: String {
if let detail = model.playback.state.detail {
return "\(model.playback.state.title): \(detail)"
}
switch model.playback.state {
case .connecting, .buffering, .failed:
return model.playback.state.title
default:
break
}
if !model.playback.currentTrackTitle.isEmpty && !model.playback.currentTrackArtist.isEmpty {
return "\(model.playback.currentTrackArtist) - \(model.playback.currentTrackTitle)"
}
if !model.playback.currentTrackTitle.isEmpty {
return model.playback.currentTrackTitle
}
if !model.playback.currentTrackArtist.isEmpty {
return model.playback.currentTrackArtist
}
if let station = model.playback.currentStation, !station.tags.isEmpty {
return station.tags
}
return String(localized: "Live radio")
}
private var playerDetailColor: Color {
if case .failed = model.playback.state {
return .red
}
if case .connecting = model.playback.state {
return .secondary
}
if case .buffering = model.playback.state {
return .orange
}
return .secondary
}
}