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

207 lines
7.2 KiB
Swift

import SwiftUI
public struct ContentView: View {
@Environment(AppModel.self) private var model
@Environment(\.openWindow) private var openWindow
@State private var searchText = ""
@State private var lastSubmittedSearch = ""
@State private var pendingSearchTask: Task<Void, Never>?
@State private var showingManualSheet = false
@State private var showingNewFolderSheet = false
@Binding var isMiniPlayer: Bool
public init(isMiniPlayer: Binding<Bool> = .constant(false)) {
_isMiniPlayer = isMiniPlayer
}
public var body: some View {
Group {
if isMiniPlayer {
MiniPlayerView(isMiniPlayer: $isMiniPlayer)
} else {
fullView
}
}
.navigationTitle("BearWave")
.onChange(of: searchText) { _, newValue in
scheduleSearch(newValue)
}
}
private var fullView: some View {
NavigationSplitView {
SidebarView()
.frame(minWidth: 220, idealWidth: 220, maxWidth: 280)
} detail: {
VStack(spacing: 0) {
VStack(spacing: 0) {
toolbar
Divider()
HSplitView {
StationListView()
.frame(minWidth: 520)
StationDetailView()
.frame(minWidth: 280, idealWidth: 320, maxWidth: 380)
}
}
Divider()
PlayerBarView()
}
}
.navigationSplitViewStyle(.automatic)
.toolbar {
ToolbarItemGroup {
Button {
showingManualSheet = true
} label: {
Label("Add Station", systemImage: "plus")
}
Button {
model.resumeLastStation()
} label: {
Label("Resume", systemImage: "arrow.clockwise")
}
.disabled(!model.library.canResume)
Button {
showingNewFolderSheet = true
} label: {
Label("New Folder", systemImage: "folder.badge.plus")
}
Button {
isMiniPlayer = true
} label: {
Label("Mini Player", systemImage: "arrow.down.left.and.arrow.up.right")
}
.keyboardShortcut("m", modifiers: .command)
}
}
.sheet(isPresented: $showingManualSheet) {
ManualStationView()
.environment(model)
}
.sheet(isPresented: $showingNewFolderSheet) {
NewGroupView()
.environment(model)
}
}
private var toolbar: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center, spacing: 12) {
HStack(spacing: 9) {
Image(nsImage: BearWaveAssets.image(named: "bearwave_line") ?? NSImage())
.resizable()
.scaledToFit()
.frame(width: 112, height: 32)
VStack(alignment: .leading, spacing: 1) {
Text("BearWave")
.font(.headline.weight(.semibold))
Text("Internet Radio")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.frame(width: 220, alignment: .leading)
Divider()
.frame(height: 32)
TextField("Search stations, genre, country", text: Binding(
get: { searchText },
set: { newValue in
searchText = newValue
}
))
.textFieldStyle(.roundedBorder)
.onSubmit {
Task { await submitSearch(searchText) }
}
.frame(minWidth: 280, idealWidth: 420, maxWidth: .infinity)
Button {
Task { await submitSearch(searchText) }
} label: {
Label("Search", systemImage: "magnifyingglass")
}
.disabled(searchText.trimmingCharacters(in: .whitespacesAndNewlines).count < 2)
.keyboardShortcut(.defaultAction)
Button {
searchText = ""
lastSubmittedSearch = ""
pendingSearchTask?.cancel()
model.library.filterQuery = ""
Task { await model.library.loadSelectedPage() }
} label: {
Label("Clear", systemImage: "xmark.circle")
}
.disabled(searchText.isEmpty)
if model.library.isLoading {
ProgressView()
.controlSize(.small)
}
}
HStack(spacing: 8) {
Text("Quick")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
ForEach(["rock", "news", "jazz"], id: \.self) { tag in
Button(tag.capitalized) {
Task { await model.library.loadTag(tag) }
}
}
Divider().frame(height: 20)
ForEach(["US", "GB", "FR"], id: \.self) { country in
Button(country) {
Task { await model.library.loadCountry(country) }
}
}
Spacer()
Text("Sort")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
Picker("", selection: Binding(
get: { model.library.sortMode },
set: { model.library.sortMode = $0 }
)) {
ForEach(StationSortMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
.pickerStyle(.segmented)
.frame(width: 230)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(.regularMaterial)
}
private func submitSearch(_ rawQuery: String) async {
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
guard query.count >= 2 else { return }
if query == lastSubmittedSearch, !model.library.visibleStations.isEmpty {
return
}
lastSubmittedSearch = query
await model.library.search(query)
}
private func scheduleSearch(_ rawQuery: String) {
pendingSearchTask?.cancel()
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
guard query.count >= 3 else { return }
pendingSearchTask = Task {
try? await Task.sleep(for: .milliseconds(500))
guard !Task.isCancelled else { return }
await submitSearch(query)
}
}
}