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? @State private var showingManualSheet = false @State private var showingNewFolderSheet = false @Binding var isMiniPlayer: Bool public init(isMiniPlayer: Binding = .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) } } }