207 lines
7.2 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|