NavidromeApp/Widget/WidgetIntents.swift
Dallas Groot 3c28413af8 bug fixes and improvements
Gap 1: Lock Screen seek bar drift — nowPlayingSyncTimer (5s timer that
pushes elapsed time to MPNowPlayingInfoCenter) was created in
playWithAVPlayer but never restarted after a background/foreground
cycle. Now invalidated in suspendVisTimers() and recreated in
resumeVisTimers(). The crossfade path benefits too — it never went
through playWithAVPlayer so the timer was never created at all for
crossfade sessions.
Gap 2: Prefetcher re-downloads after restructure — AudioPreFetcher
used isSongDownloaded(song.id) (exact ID match). After a Companion
restructure changes IDs, songs already downloaded were re-fetched.
Changed to isSongAvailableOffline(song) which falls back to
title/artist/duration matching.
Gap 3: Unbounded image disk cache — trimDiskCache() only ran on app
launch. A long browsing session could push well past the 200MB limit.
Now storeToDisk increments a write counter and triggers trim every 50
writes. The counter lives on ioQueue (serial) so no lock needed.
Gap 4: Custom cover art in widget — WidgetBridge cached blur keyed by
coverArtId. Custom covers don’t change the ID, so the bridge skipped
the update. Now pushWidgetState() passes "custom_\(id)" as the key
when AlbumCoverStore has a custom image. Same album’s songs still
share the key → blur is reused, not redone. When custom is removed,
key reverts to the bare ID → re-blurs with server art.
2026-04-12 16:16:32 -07:00

115 lines
4.3 KiB
Swift

import AppIntents
import WidgetKit
//
// WidgetIntents.swift
// TARGET: Widget extension only.
//
// iOS 17+ interactive widget controls via AppIntents.
// Each intent writes a command to App Group UserDefaults, optimistically
// updates the widget state, posts a Darwin notification to wake the app,
// then reloads timelines so the widget re-renders immediately.
//
// MARK: - Play / Pause
struct PlayPauseIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Playback"
static var description: IntentDescription = "Play or pause the current track."
func perform() async throws -> some IntentResult {
let state = WidgetSharedState.shared
// Optimistic toggle widget re-renders with new state instantly
state.togglePlayingOptimistic()
// Enqueue command for the main app
let wasPlaying = !state.isPlaying // we already toggled
state.enqueueCommand(wasPlaying ? .pause : .play)
postDarwinNotification(kWidgetCommandNotification)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
// MARK: - Next Track
struct NextTrackIntent: AppIntent {
static var title: LocalizedStringResource = "Next Track"
static var description: IntentDescription = "Skip to the next track."
func perform() async throws -> some IntentResult {
let state = WidgetSharedState.shared
state.enqueueCommand(.next)
postDarwinNotification(kWidgetCommandNotification)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
// MARK: - Previous Track
struct PreviousTrackIntent: AppIntent {
static var title: LocalizedStringResource = "Previous Track"
static var description: IntentDescription = "Go to the previous track."
func perform() async throws -> some IntentResult {
let state = WidgetSharedState.shared
state.enqueueCommand(.previous)
postDarwinNotification(kWidgetCommandNotification)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
// MARK: - Seek Forward (+15s)
struct SeekForwardIntent: AppIntent {
static var title: LocalizedStringResource = "Seek Forward"
static var description: IntentDescription = "Skip forward 15 seconds."
func perform() async throws -> some IntentResult {
let state = WidgetSharedState.shared
state.enqueueCommand(.seekForward15)
postDarwinNotification(kWidgetCommandNotification)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
// MARK: - Seek Backward (-15s)
struct SeekBackwardIntent: AppIntent {
static var title: LocalizedStringResource = "Seek Backward"
static var description: IntentDescription = "Skip backward 15 seconds."
func perform() async throws -> some IntentResult {
let state = WidgetSharedState.shared
state.enqueueCommand(.seekBackward15)
postDarwinNotification(kWidgetCommandNotification)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}
// MARK: - Seek To Position (tap-to-seek on progress bar)
struct SeekToIntent: AppIntent {
static var title: LocalizedStringResource = "Seek To Position"
static var description: IntentDescription = "Seek to a specific position in the track."
@Parameter(title: "Fraction")
var fraction: Double
init() { self.fraction = 0 }
init(fraction: Double) { self.fraction = fraction }
func perform() async throws -> some IntentResult {
let state = WidgetSharedState.shared
let targetTime = state.duration * min(max(fraction, 0), 1)
state.enqueueSeekTo(time: targetTime)
// Optimistically update position so widget shows new location instantly
state.pushPosition(currentTime: targetTime, isPlaying: state.isPlaying)
postDarwinNotification(kWidgetCommandNotification)
WidgetCenter.shared.reloadAllTimelines()
return .result()
}
}