import SwiftUI /// Reusable download state indicator for cloud tracks. /// Shows four states: not downloaded, downloading (progress), downloaded, error. /// 20pt frame, designed to sit between Spacer() and duration in track rows. struct DownloadIndicator: View { let track: Track @Environment(\.accessibilityReduceMotion) private var reduceMotion @State private var downloadManager = DownloadManager.shared @State private var showBounce = false var body: some View { Button { handleTap() } label: { ZStack { switch track.downloadState { case .none: Image(systemName: "arrow.down.circle") .foregroundStyle(.tertiary) case .downloading: let progress = downloadManager.progress(for: track) CircularProgressView(progress: progress) .foregroundStyle(Color.accentColor) case .downloaded: Image(systemName: "arrow.down.circle.fill") .foregroundStyle(.green) .scaleEffect(showBounce ? 1.15 : 1.0) case .error: Image(systemName: "exclamationmark.circle.fill") .foregroundStyle(.red) } } .font(.system(size: 14)) .frame(width: 20, height: 20) .contentShape(Rectangle()) } .buttonStyle(.plain) .help(helpText) .onChange(of: track.downloadState) { oldValue, newValue in if oldValue == .downloading && newValue == .downloaded { triggerBounce() } } } private var helpText: String { switch track.downloadState { case .none: "Download for offline playback" case .downloading: "Cancel download" case .downloaded: "Downloaded — available offline" case .error: "Download failed — tap to retry" } } private func handleTap() { switch track.downloadState { case .none: downloadManager.download(track: track, apiClient: ChadMusicAPIClient.shared) case .downloading: downloadManager.cancel(track: track) case .downloaded: break // No action on tap when downloaded — use context menu for remove case .error: downloadManager.download(track: track, apiClient: ChadMusicAPIClient.shared) } } private func triggerBounce() { if reduceMotion { return } withAnimation(.easeInOut(duration: 0.2)) { showBounce = true } DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { withAnimation(.easeInOut(duration: 0.2)) { showBounce = false } } } } /// Circular progress ring for download indicator. private struct CircularProgressView: View { let progress: Double var body: some View { ZStack { Circle() .stroke(lineWidth: 2) .opacity(0.2) Circle() .trim(from: 0, to: CGFloat(max(0.05, progress))) .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round)) .rotationEffect(.degrees(-90)) } .padding(2) } }