import Foundation import SwiftUI import VelodyDomain struct MacLibraryView: View { @State private var viewModel = MacLibraryViewModel() var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Private Library Foundation") .font(.largeTitle) Text("Backend Connection") .font(.title2) TextField("http://localhost:3000", text: $viewModel.serverURLString) .textFieldStyle(.roundedBorder) .onSubmit { viewModel.persistServerURLSelection() } VStack(alignment: .leading, spacing: 8) { statusRow(title: "Server URL", value: viewModel.serverURLString) statusRow(title: "Registration", value: viewModel.deviceRegistrationStatus) statusRow( title: "Device ID", value: viewModel.registeredDeviceId ?? "Not registered yet." ) statusRow(title: "Last heartbeat", value: viewModel.lastHeartbeatStatus) statusRow(title: "Last bootstrap", value: viewModel.lastSyncBootstrapStatus) if let lastBootstrapTrackCount = viewModel.lastBootstrapTrackCount { statusRow(title: "Bootstrap tracks", value: "\(lastBootstrapTrackCount)") } if let lastBootstrapCursor = viewModel.lastBootstrapCursor { statusRow(title: "Next cursor", value: lastBootstrapCursor) } } HStack(spacing: 12) { Button("Register this Mac") { Task { await viewModel.registerThisMac() } } .disabled(viewModel.isRegisteringDevice) Button("Send Heartbeat") { Task { await viewModel.sendHeartbeat() } } .disabled(viewModel.registeredDeviceId == nil || viewModel.isSendingHeartbeat) Button("Sync Bootstrap") { Task { await viewModel.syncBootstrap() } } .disabled(viewModel.isRunningSyncBootstrap) } Text("Selected folder") .font(.headline) Text(viewModel.selectedFolderPath) .textSelection(.enabled) .foregroundStyle(.secondary) HStack(spacing: 12) { Button("Choose Music Folder") { viewModel.chooseFolder() } Button("Scan MP3 Files") { Task { await viewModel.scanMP3Files() } } .disabled(viewModel.selectedFolderPath == "No folder selected" || viewModel.isScanning) } HStack(spacing: 12) { Text("Discovered tracks: \(viewModel.discoveredTrackCount)") .font(.headline) if viewModel.isScanning { ProgressView() .controlSize(.small) } } Text(viewModel.scanStatus) .foregroundStyle(.secondary) List(viewModel.tracks) { track in VStack(alignment: .leading, spacing: 4) { Text(track.title) .font(.headline) Text(track.artist) .foregroundStyle(.secondary) if let album = track.album { Text(album) .font(.caption) .foregroundStyle(.secondary) } if let durationSeconds = track.durationSeconds { Text("Duration: \(format(durationSeconds: durationSeconds))") .font(.caption2) .foregroundStyle(.tertiary) } Text(track.localFilePath) .font(.caption2) .foregroundStyle(.tertiary) .lineLimit(1) } .padding(.vertical, 4) } .overlay { if viewModel.tracks.isEmpty && !viewModel.isScanning { ContentUnavailableView( "No MP3 Files Discovered", systemImage: "music.note.list", description: Text("Choose a folder, then run a manual scan.") ) } } Spacer(minLength: 0) } .padding(24) .task { await viewModel.loadIfNeeded() } } private func format(durationSeconds: Double) -> String { let totalSeconds = Int(durationSeconds.rounded()) let minutes = totalSeconds / 60 let seconds = totalSeconds % 60 return String(format: "%d:%02d", minutes, seconds) } @ViewBuilder private func statusRow(title: String, value: String) -> some View { VStack(alignment: .leading, spacing: 2) { Text(title) .font(.headline) Text(value) .textSelection(.enabled) .foregroundStyle(.secondary) } } }