import SwiftUI /// Saved internet radio stations. Tap to play, swipe to delete, long press to edit. struct WebRadioScreen: View { @Environment(\.palette) private var p @State private var editing: WebStation? private var g: AppGraph { .shared } var body: some View { let s = g.settings let radio = g.radio let list = s.stations List { if let on = radio.station { Section { HStack(spacing: 14) { SettingsIcon(symbol: "antenna.radiowaves.left.and.right", bg: 0xFF2D55) VStack(alignment: .leading, spacing: 1) { Text(on.name).foregroundStyle(p.label).lineLimit(1) Text(radio.playing ? "Live" : "Paused").font(.footnote).foregroundStyle(p.label2) } Spacer() Button { radio.toggle() } label: { Image(systemName: radio.playing ? "pause.fill" : "play.fill").font(.system(size: 20)).frame(width: 40, height: 40) } .buttonStyle(.borderless) Button { radio.stop() } label: { Image(systemName: "stop.fill").font(.system(size: 18)).frame(width: 40, height: 40) } .buttonStyle(.borderless) } .themedRow() } header: { Text("Now Playing").foregroundStyle(p.label2) } } Section { ForEach(list) { st in Button { radio.play(st) } label: { HStack(spacing: 14) { SettingsIcon(symbol: "dot.radiowaves.left.and.right", bg: 0x5856D6) VStack(alignment: .leading, spacing: 1) { Text(st.name).foregroundStyle(p.label).lineLimit(1) Text(st.url).font(.footnote).foregroundStyle(p.label2).lineLimit(1) } Spacer() if radio.station?.id == st.id { PlayingBars(playing: radio.playing) } } } .themedRow() .swipeActions(edge: .leading) { Button("Edit") { editing = st }.tint(.orange) } .contextMenu { Button { editing = st } label: { Label("Edit", systemImage: "pencil") } Button(role: .destructive) { remove(st) } label: { Label("Delete", systemImage: "trash") } } } .onDelete { rows in rows.map { list[$0] }.forEach(remove) } Button { editing = WebStation(name: "", url: "") } label: { Label("Add Station", systemImage: "plus.circle.fill") } .themedRow() } header: { Text("Stations").foregroundStyle(p.label2) } footer: { Text("Any direct stream URL works (MP3, AAC or HLS). Playing a station pauses your queue, and playing a song stops the station.") .foregroundStyle(p.label2) } } .listStyle(.insetGrouped) .themedList() .navigationTitle("Web Radio") .sheet(item: $editing) { st in StationSheet(station: st) } } private func remove(_ st: WebStation) { if g.radio.station?.id == st.id { g.radio.stop() } g.settings.stations.removeAll { $0.id == st.id } } } /// Name and stream URL for a new or existing station. private struct StationSheet: View { let station: WebStation @Environment(\.dismiss) private var dismiss @State private var name = "" @State private var url = "" var body: some View { let n = name.trimmingCharacters(in: .whitespaces) let u = url.trimmingCharacters(in: .whitespaces) let valid = !n.isEmpty && URL(string: u).map { ["http", "https"].contains($0.scheme?.lowercased() ?? "") } == true NavigationStack { List { Section { TextField("Name", text: $name).themedRow() TextField("Stream URL", text: $url) .keyboardType(.URL).textInputAutocapitalization(.never).autocorrectionDisabled() .themedRow() } } .listStyle(.insetGrouped) .themedList() .navigationTitle(station.name.isEmpty ? "New Station" : "Edit Station") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { let s = AppGraph.shared.settings var list = s.stations let st = WebStation(id: station.id, name: n, url: u) if let i = list.firstIndex(where: { $0.id == station.id }) { list[i] = st } else { list.append(st) } s.stations = list dismiss() } .disabled(!valid) } } } .onAppear { name = station.name; url = station.url } } }