Widget v2 glassmorphism, lyrics, backup, crossfade fixes

Widget v2:
- Glassmorphism glass panel, 40-bar waveform Canvas with SeekToIntent
- CIAreaAverage color extraction + secondary color for adaptive theming
- Small: inset glass. Medium/Large: flush edge-to-edge (contentMarginsDisabled)
- Large: 3-item queue with real cover art thumbnails, crossfade countdown
- Waveform sampled from offline vis buffer, seeded PRNG fallback

Live Lyrics:
- LyricsService: direct LRCLIB from iOS, no companion dependency
- Embedded lyrics read via AVAsset.load(.metadata) from downloads
- Karaoke word-by-word gradient fill, auto-scroll, fade edges
- Tap-to-sync timing editor with +/-0.1s offset adjust
- Companion API fallback only for server-side embedded tags

Backup System:
- .nvdbackup export/import via ZIPFoundation
- UTI registered for AirDrop, passwords stripped on export
- PendingOperationsQueue with retry + disk persistence

Crossfade fixes:
- Seek bar: reports from incoming player immediately, not at 50%
- songHandoff at midpoint: art/colors/text/lyrics transition mid-fade
- Scrobble fires before metadata swap (correct outgoing song)

Visualizer fixes:
- stopAll() zeros _audioLevels + clears offlineVisBuffer
- All 5 simulation start sites gated behind realAudioAnalysis check
- Bars stay flat between skips, only rise when real vis data loads

Smart DJ bulk prefetch, appendingPathComponent slash fix
This commit is contained in:
Dallas Groot 2026-04-14 00:44:38 -07:00
parent cea4e3868e
commit 842cb6353b

View file

@ -293,9 +293,10 @@ class AudioPlayer: NSObject, ObservableObject {
if isUsingOfflineVis {
startOfflineVisSync()
} else if !isUsingCrossfade {
} else if !isUsingCrossfade && !VisualizerSettings.shared.realAudioAnalysis {
startLevelSimulation()
}
// When realAudioAnalysis is on but vis data hasn't loaded yet: stay at 0.
alog("Foreground: session reactivated, observers + vis timers resumed (crossfade=\(isUsingCrossfade) offlineVis=\(isUsingOfflineVis))")
}
@ -475,7 +476,9 @@ class AudioPlayer: NSObject, ObservableObject {
updateNowPlayingInfo()
fetchAndSetArtwork(coverArtId: song.coverArt)
startLevelSimulation()
if !VisualizerSettings.shared.realAudioAnalysis {
startLevelSimulation()
}
// Load offline visualizer for crossfade path too
if VisualizerSettings.shared.realAudioAnalysis {
@ -867,9 +870,12 @@ class AudioPlayer: NSObject, ObservableObject {
#if os(iOS)
if isRadioStream {
startRadioSimulation()
} else {
} else if !VisualizerSettings.shared.realAudioAnalysis {
// Simulation mode only real analysis waits for offline vis data
startLevelSimulation()
}
// When realAudioAnalysis is on: levels stay at 0 until
// loadOfflineVisualizer startOfflineVisSync fills them.
#else
startLevelSimulation()
#endif
@ -1090,7 +1096,7 @@ class AudioPlayer: NSObject, ObservableObject {
guard let self = self else { return }
self.setLevels(self.rawFFTLevels)
}
} else {
} else if !VisualizerSettings.shared.realAudioAnalysis {
DebugLogger.shared.log("resume: simulation path — levelTimer nil=\(levelTimer == nil)", category: "VisDebug")
// Simulation path: seed internalLevels from a fresh random target
// so the wave has height immediately rather than starting from floor.
@ -1100,6 +1106,7 @@ class AudioPlayer: NSObject, ObservableObject {
setLevels(internalLevels)
if levelTimer == nil { startLevelSimulation() }
}
// else: realAudioAnalysis on, vis data loading stay at 0
pushWidgetState()
#endif
updateNowPlayingInfo()
@ -1626,7 +1633,7 @@ class AudioPlayer: NSObject, ObservableObject {
}
} catch {
alog("Offline vis: analysis failed: \(error.localizedDescription)")
await MainActor.run { self.startLevelSimulation() }
// Don't fall back to simulation levels stay at 0
}
} // end if !fetched
}