161 lines
5.1 KiB
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))
|
|
)
|
|
}
|
|
}
|