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