Atoll: deliver Spotify Canvas as lockscreen wallpaper without album-art flash

Two fixes for the right-click-artwork-to-wallpaper flow on Spotify
tracks that have a Canvas video.

1. Force AllSpacesAndDisplays.Type and SystemDefault.Type to
   "individual" while the artwork is active (was: keep existing
   value via nil-coalescing). On macOS Tahoe the user-level
   wallpaper rotation honors the parent Type field, so leaving it
   as "folder" caused WallpaperAgent to advance to the next image
   in a rotating wallpaper pack instead of honoring the patched
   single-image / aerial choice. The original Type is preserved in
   the plist backup at temp/atoll_wallpaper_backup.plist and is
   restored byte-for-byte on dismiss, so rotation resumes normally
   after the artwork is dismissed. The forced override is also
   applied to nested Displays / Spaces blocks but only when Type
   was already present, to avoid mutating block shapes that
   intentionally omit it.

2. When the Canvas is expected at right-click time
   (effectiveVideoURL != nil), defer applyArtworkToPlist and
   restartWallpaperAgent entirely. Previously the flow applied raw
   album art as the static wallpaper, restarted the agent, then
   prepared and applied the aerial in a second restart, causing a
   visible flash of the raw album cover. The deferred path stores
   the blurred fallback URL in pendingFallbackStaticURL and only
   touches the plist once: with the aerial on success, or with the
   blurred album cover via applyDeferredStaticFallback if the
   aerial asset prep or aerial-plist patch fails. The in-app
   videoWindow continues to mask the desktop wallpaper during prep
   on the unlocked screen; the lock screen simply transitions from
   the existing wallpaper straight to the Canvas video.

The earlier ?? nil-coalescing in patchWallpaperEntries also covered
per-Display, per-Space.Default, and per-Space.Displays Type fields,
so those are forced too where present.

Signed-off-by: Jis G Jacob <studiokeys@blissroms.org>
This commit is contained in:
Jis G Jacob 2026-05-12 16:10:17 +05:30
commit a8d7dff322

View file

@ -324,6 +324,8 @@ final class FullScreenArtworkWindowManager: ObservableObject {
private var isLiveWallpaperAllowed = false
private var activeLiveWallpaperFingerprint: String?
private var activeWallpaperKey: String?
private var pendingFallbackStaticURL: URL?
private var pendingFallbackWallpaperKey: String?
private var fallbackRightClickMonitor: Any?
private var artworkLayoutOverCanvasPreferenceCancellable: AnyCancellable?
private var lyricsTextCancellable: AnyCancellable?
@ -358,6 +360,8 @@ final class FullScreenArtworkWindowManager: ObservableObject {
isLiveWallpaperAllowed = false
activeLiveWallpaperFingerprint = nil
activeWallpaperKey = nil
pendingFallbackStaticURL = nil
pendingFallbackWallpaperKey = nil
let shouldRestoreStandardPanelLayout = isShowingSpotifyCanvasFallback
isShowingSpotifyCanvasFallback = false
removeFallbackRightClickMonitor()
@ -439,12 +443,14 @@ final class FullScreenArtworkWindowManager: ObservableObject {
let previousFallbackState = isShowingSpotifyCanvasFallback
guard let rawArtworkFileURL = persistedArtworkFileURL(for: artwork, fingerprint: artworkFingerprint) else { return }
let wallpaperURL = shouldUseStaticFallbackWallpaper
? blurredWallpaperFileURL(for: artwork, fingerprint: artworkFingerprint, screen: screen) ?? rawArtworkFileURL
let expectCanvas = effectiveVideoURL != nil
let blurredFallbackURL = blurredWallpaperFileURL(for: artwork, fingerprint: artworkFingerprint, screen: screen) ?? rawArtworkFileURL
let wallpaperURL = (shouldUseStaticFallbackWallpaper || expectCanvas)
? blurredFallbackURL
: rawArtworkFileURL
let wallpaperKey = wallpaperIdentityKey(
fingerprint: artworkFingerprint,
usesFallback: shouldUseStaticFallbackWallpaper,
usesFallback: shouldUseStaticFallbackWallpaper || expectCanvas,
screen: screen
)
let desiredLiveWallpaperFingerprint = effectiveVideoURL?.absoluteString
@ -459,17 +465,26 @@ final class FullScreenArtworkWindowManager: ObservableObject {
}
if requiresWallpaperRefresh {
showWallpaperTransition(on: screen, imageURL: wallpaperURL)
if expectCanvas {
pendingFallbackStaticURL = blurredFallbackURL
pendingFallbackWallpaperKey = wallpaperKey
activeWallpaperKey = nil
activeLiveWallpaperFingerprint = nil
} else {
showWallpaperTransition(on: screen, imageURL: wallpaperURL)
guard applyArtworkToPlist(imageURL: wallpaperURL) else {
print("[FullScreenArtworkWindowManager] Failed to patch plist")
hideWallpaperTransition()
return
guard applyArtworkToPlist(imageURL: wallpaperURL) else {
print("[FullScreenArtworkWindowManager] Failed to patch plist")
hideWallpaperTransition()
return
}
restartWallpaperAgent()
activeWallpaperKey = wallpaperKey
activeLiveWallpaperFingerprint = nil
pendingFallbackStaticURL = nil
pendingFallbackWallpaperKey = nil
}
restartWallpaperAgent()
activeWallpaperKey = wallpaperKey
activeLiveWallpaperFingerprint = nil
}
isShowing = true
@ -773,6 +788,27 @@ final class FullScreenArtworkWindowManager: ObservableObject {
return finalBitmap.representation(using: .png, properties: [:])
}
private func applyDeferredStaticFallback() {
guard let url = pendingFallbackStaticURL else { return }
let key = pendingFallbackWallpaperKey
pendingFallbackStaticURL = nil
pendingFallbackWallpaperKey = nil
if let screen = NSScreen.main {
showWallpaperTransition(on: screen, imageURL: url)
}
guard applyArtworkToPlist(imageURL: url) else {
print("[FullScreenArtworkWindowManager] Failed to apply deferred static fallback")
hideWallpaperTransition()
return
}
restartWallpaperAgent()
activeWallpaperKey = key
activeLiveWallpaperFingerprint = nil
hideVideoWindow()
scheduleWallpaperTransitionHide(after: .milliseconds(900))
}
private func scheduleLiveWallpaperPreparation(for videoURL: URL?, artwork: NSImage, identifier: String) {
liveWallpaperTask?.cancel()
liveWallpaperTask = nil
@ -837,10 +873,18 @@ final class FullScreenArtworkWindowManager: ObservableObject {
guard currentIdentifier == identifier else { return }
guard currentVideoURL?.absoluteString == nextFingerprint else { return }
guard !self.shouldUseSpotifyStaticFallbackWallpaper(videoURL: currentVideoURL) else { return }
guard prepared else { return }
guard self.applyAerialToPlist(assetID: assetID) else { return }
guard prepared else {
self.applyDeferredStaticFallback()
return
}
guard self.applyAerialToPlist(assetID: assetID) else {
self.applyDeferredStaticFallback()
return
}
self.activeLiveWallpaperFingerprint = nextFingerprint
self.pendingFallbackStaticURL = nil
self.pendingFallbackWallpaperKey = nil
self.restartWallpaperAgent()
self.scheduleHideVideoWindow(after: .milliseconds(950), expectedURL: videoURL)
print("[FullScreenArtworkWindowManager] Live wallpaper applied")
@ -1549,13 +1593,13 @@ final class FullScreenArtworkWindowManager: ObservableObject {
var allSpaces = (plist["AllSpacesAndDisplays"] as? [String: Any]) ?? [:]
allSpaces["Desktop"] = desktopBlock
allSpaces["Idle"] = idleBlock
allSpaces["Type"] = allSpaces["Type"] ?? "individual"
allSpaces["Type"] = "individual"
plist["AllSpacesAndDisplays"] = allSpaces
var systemDefault = (plist["SystemDefault"] as? [String: Any]) ?? [:]
systemDefault["Desktop"] = desktopBlock
systemDefault["Idle"] = idleBlock
systemDefault["Type"] = systemDefault["Type"] ?? "individual"
systemDefault["Type"] = "individual"
plist["SystemDefault"] = systemDefault
if var displays = plist["Displays"] as? [String: Any] {
@ -1563,6 +1607,9 @@ final class FullScreenArtworkWindowManager: ObservableObject {
if var display = displays[key] as? [String: Any] {
display["Desktop"] = desktopBlock
display["Idle"] = idleBlock
if display["Type"] != nil {
display["Type"] = "individual"
}
displays[key] = display
}
}
@ -1575,6 +1622,9 @@ final class FullScreenArtworkWindowManager: ObservableObject {
if var defaultEntry = space["Default"] as? [String: Any] {
defaultEntry["Desktop"] = desktopBlock
defaultEntry["Idle"] = idleBlock
if defaultEntry["Type"] != nil {
defaultEntry["Type"] = "individual"
}
space["Default"] = defaultEntry
}
if var spaceDisplays = space["Displays"] as? [String: Any] {
@ -1582,6 +1632,9 @@ final class FullScreenArtworkWindowManager: ObservableObject {
if var display = spaceDisplays[displayKey] as? [String: Any] {
display["Desktop"] = desktopBlock
display["Idle"] = idleBlock
if display["Type"] != nil {
display["Type"] = "individual"
}
spaceDisplays[displayKey] = display
}
}