diff --git a/iOS/Views/Companion/CompanionSettingsView.swift b/iOS/Views/Companion/CompanionSettingsView.swift index 2fb25dc..b9f753d 100644 --- a/iOS/Views/Companion/CompanionSettingsView.swift +++ b/iOS/Views/Companion/CompanionSettingsView.swift @@ -12,6 +12,8 @@ struct CompanionSettingsView: View { @State private var fixLibraryMessage: String? @State private var cleanTagsStatus: AnalyzeStatus = .idle @State private var cleanTagsMessage: String? + @State private var djFetchStatus: AnalyzeStatus = .idle + @State private var djFetchMessage: String? private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) @@ -169,6 +171,36 @@ struct CompanionSettingsView: View { cleanTagsMessage = "✓ Tag cleaning complete" } + Section { + Button(action: fetchAllDJProfiles) { + HStack { + Image(systemName: djFetchStatus == .analyzing ? "arrow.down.circle" : "arrow.down.circle.fill") + .foregroundColor(accentPink) + .symbolEffect(.pulse, isActive: djFetchStatus == .analyzing) + VStack(alignment: .leading, spacing: 2) { + Text("Fetch All DJ Profiles").foregroundColor(.white) + Text("Download crossfade + volume data for your entire library") + .font(.caption).foregroundColor(.gray) + } + } + } + .disabled(djFetchStatus == .analyzing) + + if let msg = djFetchMessage { + Text(msg).font(.caption) + .foregroundColor(djFetchStatus == .done ? .green : djFetchStatus == .failed ? .red : .gray) + } + + HStack { + Text("Cached DJ Profiles") + Spacer() + Text("\(SmartDJCache.shared.cachedCount)") + .foregroundColor(.gray) + } + } header: { Text("Cache Management") } footer: { + Text("Fetches all Smart DJ profiles from the server in one request. Speeds up crossfade — no per-song API calls needed.") + } + Section { Button(action: { triggerPreAnalyze(force: false) }) { HStack { @@ -271,6 +303,29 @@ struct CompanionSettingsView: View { } } + // MARK: - DJ Profile Bulk Fetch + + private func fetchAllDJProfiles() { + djFetchStatus = .analyzing + djFetchMessage = "Fetching profiles from server..." + Task { + do { + let api = CompanionAPIService.shared + let profiles = try await api.fetchAllProfiles() + SmartDJCache.shared.bulkImport(profiles) + await MainActor.run { + djFetchStatus = .done + djFetchMessage = "✓ \(profiles.count) profiles cached" + } + } catch { + await MainActor.run { + djFetchStatus = .failed + djFetchMessage = "✗ \(error.localizedDescription)" + } + } + } + } + // MARK: - Server Visualizer Precompute private func triggerPreAnalyze(force: Bool) { diff --git a/iOS/Views/Library/DownloadsSettingsView.swift b/iOS/Views/Library/DownloadsSettingsView.swift index 4cb9d55..f0c5d25 100644 --- a/iOS/Views/Library/DownloadsSettingsView.swift +++ b/iOS/Views/Library/DownloadsSettingsView.swift @@ -743,6 +743,11 @@ struct SettingsView: View { serverManager.disconnect() } } + + // Extra space so the last items aren't hidden behind the mini player + Section {} footer: { + Spacer().frame(height: 60) + } } .navigationTitle("Settings") .sheet(isPresented: $showVisualizerSettings) { diff --git a/iOS/Views/Settings/BackupRestoreView.swift b/iOS/Views/Settings/BackupRestoreView.swift index 1e8b886..ed3202c 100644 --- a/iOS/Views/Settings/BackupRestoreView.swift +++ b/iOS/Views/Settings/BackupRestoreView.swift @@ -89,7 +89,10 @@ struct BackupRestoreSection: View { } message: { Text(errorMessage) } - .confettiCannon(trigger: $confettiTrigger, num: 50, radius: 400) + .overlay { + // ConfettiCannon as overlay prevents it from interfering with List row layout + EmptyView().confettiCannon(trigger: $confettiTrigger, num: 50, radius: 400) + } } private func exportBackup() {