Files
run/RunPlus/ContentView.swift

161 lines
5.1 KiB
Swift

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))
)
}
}