snapshot current state before gitea sync
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject private var appModel: AppModel
|
||||
@State private var isImporting = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [Color.blue.opacity(0.25), Color.cyan.opacity(0.18), Color.mint.opacity(0.12)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
header
|
||||
card(roleSection)
|
||||
card(librarySection)
|
||||
card(playbackSection)
|
||||
card(connectionSection)
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isImporting,
|
||||
allowedContentTypes: [.mp3, .mpeg4Audio, .audio],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
do {
|
||||
let url = try result.get().first
|
||||
guard let url else { return }
|
||||
try appModel.importTrack(url: url)
|
||||
} catch {
|
||||
appModel.statusText = "Import failed: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
VStack(spacing: 6) {
|
||||
Text("Run+")
|
||||
.font(.title2).bold()
|
||||
Text(appModel.statusText)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var roleSection: some View {
|
||||
HStack(spacing: 12) {
|
||||
Button("Host") { appModel.host() }
|
||||
.buttonStyle(.borderedProminent)
|
||||
Button("Join") { appModel.join() }
|
||||
.buttonStyle(.bordered)
|
||||
Button("Stop") { appModel.stop() }
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
}
|
||||
|
||||
private var librarySection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Library")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button("Import MP3") {
|
||||
isImporting = true
|
||||
}
|
||||
}
|
||||
|
||||
if appModel.library.isEmpty {
|
||||
Text("No local tracks yet.")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Picker("Track", selection: Binding(get: {
|
||||
appModel.selectedTrack
|
||||
}, set: { newValue in
|
||||
if let track = newValue { appModel.select(track: track) }
|
||||
})) {
|
||||
ForEach(appModel.library) { track in
|
||||
Text(track.displayName).tag(Optional(track))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var playbackSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Playback")
|
||||
.font(.headline)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
Button(appModel.isPlaying ? "Pause" : "Play") {
|
||||
appModel.togglePlay()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(appModel.selectedTrack == nil || appModel.role != .host)
|
||||
|
||||
Button("Send Track") {
|
||||
appModel.sendTrackToPeers()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(appModel.selectedTrack == nil || appModel.role != .host || !appModel.isConnected)
|
||||
|
||||
Button("Sync Now") {
|
||||
appModel.syncNow()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(appModel.role != .host)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Position")
|
||||
Slider(value: Binding(get: {
|
||||
appModel.playbackPosition
|
||||
}, set: { newValue in
|
||||
appModel.seek(to: newValue)
|
||||
}), in: 0...max(appModel.selectedTrack?.duration ?? 1, 1))
|
||||
}
|
||||
.disabled(appModel.selectedTrack == nil || appModel.role != .host)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var connectionSection: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Peers")
|
||||
.font(.headline)
|
||||
|
||||
if appModel.peers.isEmpty {
|
||||
Text("No peers yet")
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(appModel.peers) { peer in
|
||||
Text(peer.name)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private func card<V: View>(_ content: V) -> some View {
|
||||
content
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.15))
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user