222 lines
9.2 KiB
Swift
222 lines
9.2 KiB
Swift
import SwiftUI
|
|
import VelodyDomain
|
|
|
|
struct iPhoneLibraryView: View {
|
|
@State private var viewModel = iPhoneLibraryViewModel()
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
List {
|
|
if let currentTitle = viewModel.nowPlaying.title {
|
|
Section("Now Playing") {
|
|
HStack(alignment: .center) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(currentTitle)
|
|
.font(.headline)
|
|
if let artist = viewModel.nowPlaying.artist {
|
|
Text(artist)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
Text(viewModel.nowPlaying.isPlaying ? "Playing offline" : "Paused")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if let trackID = viewModel.nowPlaying.trackID {
|
|
Button(viewModel.nowPlaying.isPlaying ? "Pause" : "Play") {
|
|
viewModel.togglePlayback(trackID: trackID)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Remote Library: \(viewModel.remoteTracks.count)") {
|
|
ForEach(viewModel.remoteTracks) { track in
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(alignment: .top) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(track.title)
|
|
.font(.headline)
|
|
Text(track.artist)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(track.statusText)
|
|
.font(.caption.weight(.semibold))
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 4)
|
|
.background(statusColor(for: track.status), in: Capsule())
|
|
.foregroundStyle(.white)
|
|
}
|
|
Text("Duration: \(track.durationText)")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
Text("Remote track ID: \(track.remoteTrackID)")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.textSelection(.enabled)
|
|
Text("Asset ID: \(track.assetID)")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.textSelection(.enabled)
|
|
|
|
HStack {
|
|
Button(track.downloadButtonTitle) {
|
|
Task {
|
|
await viewModel.downloadTrack(trackID: track.id)
|
|
}
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.disabled(!track.canDownload)
|
|
|
|
if track.canPlay {
|
|
Button(track.playButtonTitle) {
|
|
viewModel.togglePlayback(trackID: track.id)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
}
|
|
|
|
if let error = track.lastDownloadError,
|
|
(track.status == .failed || track.status == .missing)
|
|
{
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
Section("Available Offline: \(viewModel.availableOfflineTracks.count)") {
|
|
if viewModel.availableOfflineTracks.isEmpty {
|
|
Text("Downloaded tracks with a verified local MP3 will appear here.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(viewModel.availableOfflineTracks) { track in
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(alignment: .top) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(track.title)
|
|
.font(.headline)
|
|
Text(track.artist)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button(track.playButtonTitle) {
|
|
viewModel.togglePlayback(trackID: track.id)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
|
|
Text("Duration: \(track.durationText)")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
Text("Remote track ID: \(track.remoteTrackID)")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.textSelection(.enabled)
|
|
Text("Asset ID: \(track.assetID)")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.textSelection(.enabled)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.overlay {
|
|
overlayView
|
|
}
|
|
.navigationTitle("Velody")
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button("Sync Remote Library") {
|
|
Task {
|
|
await viewModel.refreshSync()
|
|
}
|
|
}
|
|
.disabled(viewModel.state == .loading)
|
|
}
|
|
}
|
|
.safeAreaInset(edge: .bottom) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
if let playbackError = viewModel.nowPlaying.errorMessage {
|
|
Text(playbackError)
|
|
.font(.footnote)
|
|
.foregroundStyle(.red)
|
|
}
|
|
|
|
Text(viewModel.syncStatus)
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(.ultraThinMaterial)
|
|
}
|
|
}
|
|
.task {
|
|
await viewModel.loadIfNeeded()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var overlayView: some View {
|
|
switch viewModel.state {
|
|
case .idle:
|
|
if viewModel.remoteTracks.isEmpty {
|
|
ContentUnavailableView(
|
|
"No Remote Library Yet",
|
|
systemImage: "music.note.list",
|
|
description: Text("Tap Sync Remote Library to fetch metadata from the backend.")
|
|
)
|
|
}
|
|
case .loading:
|
|
ProgressView("Syncing remote library...")
|
|
case .success:
|
|
EmptyView()
|
|
case .empty:
|
|
ContentUnavailableView(
|
|
"Empty Remote Library",
|
|
systemImage: "music.note.list",
|
|
description: Text("The backend returned no remote tracks for this iPhone.")
|
|
)
|
|
case .networkError(let message):
|
|
if viewModel.remoteTracks.isEmpty {
|
|
ContentUnavailableView(
|
|
"Network Error",
|
|
systemImage: "wifi.exclamationmark",
|
|
description: Text(message)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func statusColor(for status: OfflineLibraryRemoteTrackStatus) -> Color {
|
|
switch status {
|
|
case .notDownloaded:
|
|
return .gray
|
|
case .downloading:
|
|
return .blue
|
|
case .downloaded:
|
|
return .green
|
|
case .missing:
|
|
return .orange
|
|
case .failed:
|
|
return .red
|
|
}
|
|
}
|
|
}
|