Stemify iOS 1.1 and WebUI updates, repo restructure into desktop/ios/server/web
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
894dd5a695
commit
df5d847169
157 files changed
+3935
-742
No files matched your search
@@ -0,0 +1,116 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user