/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import AppKit import AVFoundation import Combine import CoreImage import Defaults import SkyLightWindow import SwiftUI // MARK: - Click Receiver Window private class ClickReceiverWindow: NSWindow { var onClick: (() -> Void)? override func mouseDown(with event: NSEvent) { // Keep left click inert so the fallback only dismisses on right click. } override func rightMouseDown(with event: NSEvent) { onClick?() } } private final class LoopingVideoView: NSView { private var playerLayer: AVPlayerLayer? private var queuePlayer: AVQueuePlayer? private var playerLooper: AVPlayerLooper? override init(frame frameRect: NSRect) { super.init(frame: frameRect) wantsLayer = true layer?.backgroundColor = NSColor.clear.cgColor } required init?(coder: NSCoder) { super.init(coder: coder) wantsLayer = true layer?.backgroundColor = NSColor.clear.cgColor } override func layout() { super.layout() playerLayer?.frame = bounds } func play(url: URL) { stop() let playerItem = AVPlayerItem(url: url) let player = AVQueuePlayer() player.isMuted = true player.actionAtItemEnd = .none let layer = AVPlayerLayer(player: player) layer.videoGravity = .resizeAspectFill layer.frame = bounds layer.backgroundColor = NSColor.clear.cgColor self.layer?.sublayers?.forEach { $0.removeFromSuperlayer() } self.layer?.addSublayer(layer) playerLayer = layer queuePlayer = player playerLooper = AVPlayerLooper(player: player, templateItem: playerItem) player.play() } func stop() { queuePlayer?.pause() playerLooper = nil queuePlayer = nil playerLayer?.removeFromSuperlayer() playerLayer = nil } } private final class WallpaperTransitionImageView: NSView { private let imageLayer = CALayer() override init(frame frameRect: NSRect) { super.init(frame: frameRect) wantsLayer = true layer?.backgroundColor = NSColor.clear.cgColor imageLayer.contentsGravity = .resizeAspectFill imageLayer.masksToBounds = true imageLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2 layer?.addSublayer(imageLayer) } required init?(coder: NSCoder) { super.init(coder: coder) wantsLayer = true layer?.backgroundColor = NSColor.clear.cgColor imageLayer.contentsGravity = .resizeAspectFill imageLayer.masksToBounds = true imageLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2 layer?.addSublayer(imageLayer) } override func layout() { super.layout() imageLayer.frame = bounds } func updateImage(_ image: NSImage) { imageLayer.contents = image.cgImage(forProposedRect: nil, context: nil, hints: nil) } } private final class SpotifyCanvasFallbackArtworkOverlayView: NSView { var onDismiss: (() -> Void)? private let imageLayer = CALayer() private let borderLayer = CAShapeLayer() private var artwork: NSImage init(artwork: NSImage) { self.artwork = artwork super.init(frame: .zero) wantsLayer = true layer?.backgroundColor = NSColor.clear.cgColor layer?.shadowColor = NSColor.black.withAlphaComponent(0.34).cgColor layer?.shadowOpacity = 1 layer?.shadowRadius = 26 layer?.shadowOffset = CGSize(width: 0, height: -22) imageLayer.contentsGravity = .resizeAspectFill imageLayer.masksToBounds = true imageLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2 imageLayer.contents = artwork.cgImage(forProposedRect: nil, context: nil, hints: nil) borderLayer.fillColor = NSColor.clear.cgColor borderLayer.strokeColor = NSColor.white.withAlphaComponent(0.18).cgColor borderLayer.lineWidth = 1.1 layer?.addSublayer(imageLayer) layer?.addSublayer(borderLayer) } required init?(coder: NSCoder) { nil } override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true } override func layout() { super.layout() let radius = cornerRadius imageLayer.frame = bounds imageLayer.cornerRadius = radius borderLayer.frame = bounds borderLayer.path = CGPath( roundedRect: bounds.insetBy(dx: 0.55, dy: 0.55), cornerWidth: radius, cornerHeight: radius, transform: nil ) layer?.shadowPath = CGPath( roundedRect: bounds, cornerWidth: radius, cornerHeight: radius, transform: nil ) } override func mouseDown(with event: NSEvent) { if event.clickCount >= 2 { onDismiss?() } } override func rightMouseDown(with event: NSEvent) { onDismiss?() } func updateArtwork(_ artwork: NSImage) { self.artwork = artwork imageLayer.contents = artwork.cgImage(forProposedRect: nil, context: nil, hints: nil) needsLayout = true } private var aspectRatio: CGFloat { let height = max(artwork.size.height, 1) return artwork.size.width / height } private var cornerRadius: CGFloat { aspectRatio > 1 ? 16 : 34 } } private struct FullScreenLyricsOverlayContent: View { let text: String let fontSize: CGFloat private var isPlaceholder: Bool { text == "Loading lyrics..." || text == "No lyrics found" } var body: some View { VStack { Text(text) .font(.system(size: fontSize, weight: .semibold, design: .rounded)) .foregroundColor(.white.opacity(isPlaceholder ? 0.72 : 0.94)) .multilineTextAlignment(.center) .lineLimit(4) .minimumScaleFactor(0.62) .allowsTightening(true) .shadow(color: .black.opacity(0.42), radius: 18, x: 0, y: 6) .frame(maxWidth: .infinity, alignment: .center) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding(.horizontal, 18) .padding(.vertical, 8) .background(Color.clear) } } private struct SpotifyCanvasFallbackLayoutFrames { let artworkFrame: NSRect let panelFrame: NSRect let groupFrame: NSRect let lyricsFrame: NSRect? } // MARK: - Window Manager @MainActor final class FullScreenArtworkWindowManager: ObservableObject { private enum ArtworkOverlayMode { case none case spotifyFallback } static let shared = FullScreenArtworkWindowManager() @Published private(set) var isShowing = false @Published private(set) var isShowingSpotifyCanvasFallback = false var onDismiss: (() -> Void)? private let wallpaperPlistURL: URL = { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/com.apple.wallpaper/Store/Index.plist") }() private let backupPlistURL: URL = { FileManager.default.temporaryDirectory .appendingPathComponent("atoll_wallpaper_backup.plist") }() private let aerialManifestURL: URL = { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/com.apple.wallpaper/aerials/manifest/entries.json") }() private let aerialVideosDirectoryURL: URL = { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/com.apple.wallpaper/aerials/videos", isDirectory: true) }() private let aerialThumbnailsDirectoryURL: URL = { FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/com.apple.wallpaper/aerials/thumbnails", isDirectory: true) }() private let customLiveWallpaperAssetID = "6D6834A4-2F0F-479A-B053-7D4DC5CB8EB7" private let liveWallpaperManifestBackupURL: URL = { FileManager.default.temporaryDirectory .appendingPathComponent("atoll_aerial_manifest_backup.json") }() private let liveWallpaperVideoBackupURL: URL = { FileManager.default.temporaryDirectory .appendingPathComponent("atoll_aerial_video_backup.mov") }() private let liveWallpaperThumbnailBackupURL: URL = { FileManager.default.temporaryDirectory .appendingPathComponent("atoll_aerial_thumbnail_backup.png") }() private var artworkFileURL: URL? private var cachedArtworkPNG: URL? private var cachedArtworkFingerprint: String? private var cachedBlurredArtworkPNG: URL? private var cachedBlurredArtworkFingerprint: String? private var cachedBlurredArtworkPixelSize: CGSize = .zero private var activeSongTitle: String? private var activeArtist: String? private var trackChangeCancellable: AnyCancellable? private var artworkCacheCancellable: AnyCancellable? private var videoArtworkCancellable: AnyCancellable? private var clickWindow: ClickReceiverWindow? private var clickWindowDelegated = false private var videoWindow: NSWindow? private var videoView: LoopingVideoView? private var activeVideoWindowURL: URL? private var videoWindowHideTask: Task? private var artworkOverlayWindow: NSWindow? private var artworkOverlayWindowDelegated = false private var lyricsOverlayWindow: NSWindow? private var lyricsOverlayWindowDelegated = false private var currentArtworkOverlayMode: ArtworkOverlayMode = .none private var wallpaperTransitionWindow: NSWindow? private var wallpaperTransitionView: WallpaperTransitionImageView? private var wallpaperTransitionHideTask: Task? private var panelFrameChangeObserver: NSObjectProtocol? private var liveWallpaperTask: Task? private var deferredTrackRefreshTask: Task? 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? private var lyricsPreferenceCancellable: AnyCancellable? private let spotifyCanvasFallbackHorizontalMargin: CGFloat = 48 private init() { observeArtworkChanges() observeVideoArtworkChanges() observePanelFrameChanges() observeArtworkLayoutOverCanvasPreference() observeLyricsChanges() observeLyricsPreference() } func show(artwork: NSImage, videoURL: URL? = nil, allowLiveWallpaper: Bool = false) { guard !isShowing else { return } guard let screen = NSScreen.main else { return } applyPresentation( artwork: artwork, videoURL: videoURL, allowLiveWallpaper: allowLiveWallpaper, on: screen, backupWallpaperConfiguration: true ) observeTrackChanges() } func hide() { guard isShowing else { return } isShowing = false isLiveWallpaperAllowed = false activeLiveWallpaperFingerprint = nil activeWallpaperKey = nil pendingFallbackStaticURL = nil pendingFallbackWallpaperKey = nil let shouldRestoreStandardPanelLayout = isShowingSpotifyCanvasFallback isShowingSpotifyCanvasFallback = false removeFallbackRightClickMonitor() trackChangeCancellable?.cancel() trackChangeCancellable = nil liveWallpaperTask?.cancel() liveWallpaperTask = nil deferredTrackRefreshTask?.cancel() deferredTrackRefreshTask = nil hideVideoWindow() hideArtworkOverlay() hideLyricsOverlay() hideWallpaperTransition() hideClickReceiver() if shouldRestoreStandardPanelLayout, LockScreenManager.shared.isLocked { LockScreenPanelManager.shared.applyOffsetAdjustment(animated: true) } restoreWallpaper() activeSongTitle = nil activeArtist = nil let callback = onDismiss onDismiss = nil callback?() print("[FullScreenArtworkWindowManager] Original wallpaper restored") } // MARK: - Artwork Pre-Cache private func observeArtworkChanges() { artworkCacheCancellable = MusicManager.shared.$albumArt .debounce(for: .milliseconds(180), scheduler: DispatchQueue.main) .sink { [weak self] newArt in guard let self else { return } guard let fingerprint = self.imageFingerprint(for: newArt) else { return } guard fingerprint != self.cachedArtworkFingerprint else { return } let url = self.artworkCacheFileURL(for: fingerprint) Task.detached(priority: .utility) { guard let pngData = self.encodeToPNG(newArt) else { return } try? pngData.write(to: url, options: .atomic) await MainActor.run { self.cachedArtworkPNG = url self.cachedArtworkFingerprint = fingerprint if self.isShowing { self.refreshPresentationForCurrentTrack() } } } } } private func observeVideoArtworkChanges() { videoArtworkCancellable = MusicManager.shared.$videoArtworkURL .debounce(for: .milliseconds(120), scheduler: DispatchQueue.main) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self, self.isShowing, self.isLiveWallpaperAllowed else { return } self.refreshPresentationForCurrentTrack() } } private func applyPresentation( artwork: NSImage, videoURL: URL?, allowLiveWallpaper: Bool, on screen: NSScreen, backupWallpaperConfiguration: Bool ) { let artworkID = "\(MusicManager.shared.songTitle)|\(MusicManager.shared.artistName)" guard let artworkFingerprint = imageFingerprint(for: artwork) else { return } isLiveWallpaperAllowed = allowLiveWallpaper let shouldUseStaticFallbackWallpaper = shouldUseSpotifyStaticFallbackWallpaper(videoURL: videoURL) let shouldUseFallbackLayout = shouldUseSpotifyFallbackLayout(videoURL: videoURL) let effectiveVideoURL = resolvedVideoURL(videoURL: videoURL) let overlayMode = artworkOverlayMode(shouldUseFallbackLayout: shouldUseFallbackLayout) let previousFallbackState = isShowingSpotifyCanvasFallback guard let rawArtworkFileURL = persistedArtworkFileURL(for: artwork, fingerprint: artworkFingerprint) else { return } 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 || expectCanvas, screen: screen ) let desiredLiveWallpaperFingerprint = effectiveVideoURL?.absoluteString let requiresWallpaperRefresh = backupWallpaperConfiguration || activeWallpaperKey != wallpaperKey || activeLiveWallpaperFingerprint != desiredLiveWallpaperFingerprint artworkFileURL = wallpaperURL if backupWallpaperConfiguration { backupWallpaperConfig() } if requiresWallpaperRefresh { 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 } restartWallpaperAgent() activeWallpaperKey = wallpaperKey activeLiveWallpaperFingerprint = nil pendingFallbackStaticURL = nil pendingFallbackWallpaperKey = nil } } isShowing = true isShowingSpotifyCanvasFallback = shouldUseFallbackLayout activeSongTitle = MusicManager.shared.songTitle activeArtist = MusicManager.shared.artistName if overlayMode == .spotifyFallback { installFallbackRightClickMonitorIfNeeded() } else { removeFallbackRightClickMonitor() } if overlayMode == .none { hideArtworkOverlay() } else { showArtworkOverlay(on: screen, artwork: artwork, mode: overlayMode) } updateLyricsOverlayIfNeeded(on: screen) if previousFallbackState != shouldUseFallbackLayout, LockScreenManager.shared.isLocked { LockScreenPanelManager.shared.applyOffsetAdjustment(animated: true) } showClickReceiver(on: screen) scheduleLiveWallpaperPreparation(for: effectiveVideoURL, artwork: artwork, identifier: artworkID) if effectiveVideoURL == nil { hideVideoWindow() scheduleWallpaperTransitionHide(after: .milliseconds(900)) } print("[FullScreenArtworkWindowManager] Artwork applied as wallpaper") } private func refreshPresentationForCurrentTrack() { guard isShowing else { return } guard MusicManager.shared.hasActiveSession else { return } guard let screen = NSScreen.main else { return } applyPresentation( artwork: MusicManager.shared.albumArt, videoURL: isLiveWallpaperAllowed ? MusicManager.shared.videoArtworkURL : nil, allowLiveWallpaper: isLiveWallpaperAllowed, on: screen, backupWallpaperConfiguration: false ) } private func observePanelFrameChanges() { panelFrameChangeObserver = NotificationCenter.default.addObserver( forName: .atollLockScreenPanelFrameDidChange, object: nil, queue: .main ) { [weak self] _ in Task { @MainActor [weak self] in self?.updateArtworkOverlayFrameIfNeeded() self?.updateLyricsOverlayIfNeeded() } } } private func observeArtworkLayoutOverCanvasPreference() { artworkLayoutOverCanvasPreferenceCancellable = Defaults.publisher(.lockScreenUseArtworkLayoutOverFullscreenCanvas) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self, self.isShowing else { return } self.refreshPresentationForCurrentTrack() } } private func observeLyricsChanges() { lyricsTextCancellable = MusicManager.shared.$currentLyrics .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self, self.isShowing else { return } self.updateLyricsOverlayIfNeeded() } } private func observeLyricsPreference() { lyricsPreferenceCancellable = Defaults.publisher(.enableLyrics) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self, self.isShowing else { return } self.updateLyricsOverlayIfNeeded() } } private func shouldUseSpotifyStaticFallbackWallpaper(videoURL: URL?) -> Bool { guard MusicManager.shared.bundleIdentifier == SpotifyController.bundleIdentifier else { return false } return videoURL == nil } private func shouldUseSpotifyFallbackLayout(videoURL: URL?) -> Bool { guard MusicManager.shared.bundleIdentifier == SpotifyController.bundleIdentifier else { return false } if videoURL == nil { return true } return Defaults[.lockScreenUseArtworkLayoutOverFullscreenCanvas] } private func resolvedVideoURL(videoURL: URL?) -> URL? { guard isLiveWallpaperAllowed else { return nil } return videoURL } private func artworkOverlayMode(shouldUseFallbackLayout: Bool) -> ArtworkOverlayMode { if shouldUseFallbackLayout { return .spotifyFallback } return .none } func spotifyCanvasFallbackInterItemSpacing(screenFrame: NSRect) -> CGFloat { min(max(screenFrame.width * 0.018, 28), 42) } func spotifyCanvasFallbackArtworkSideLength(screenFrame: NSRect, panelSize: CGSize) -> CGFloat { let preferred = min(max(screenFrame.height * 0.28, 250), 330) let spacing = spotifyCanvasFallbackInterItemSpacing(screenFrame: screenFrame) let maxByWidth = max( min(screenFrame.width - (spotifyCanvasFallbackHorizontalMargin * 2) - spacing - panelSize.width, 360), 180 ) let maxByHeight = max(min(screenFrame.height * 0.42, 360), 180) return min(preferred, maxByWidth, maxByHeight) } private func persistedArtworkFileURL(for artwork: NSImage, fingerprint: String) -> URL? { if let cached = cachedArtworkPNG, cachedArtworkFingerprint == fingerprint, FileManager.default.fileExists(atPath: cached.path) { return cached } guard let encoded = encodeToPNG(artwork) else { return nil } let tempURL = artworkCacheFileURL(for: fingerprint) do { try encoded.write(to: tempURL, options: .atomic) cachedArtworkPNG = tempURL cachedArtworkFingerprint = fingerprint return tempURL } catch { return nil } } private func blurredWallpaperFileURL(for artwork: NSImage, fingerprint: String, screen: NSScreen) -> URL? { let pixelSize = wallpaperPixelSize(for: screen) if let cached = cachedBlurredArtworkPNG, cachedBlurredArtworkFingerprint == fingerprint, cachedBlurredArtworkPixelSize == pixelSize, FileManager.default.fileExists(atPath: cached.path) { return cached } guard let encoded = encodeBlurredWallpaperPNG(from: artwork, targetPixelSize: pixelSize) else { return nil } let tempURL = blurredArtworkCacheFileURL(for: fingerprint, pixelSize: pixelSize) do { try encoded.write(to: tempURL, options: .atomic) cachedBlurredArtworkPNG = tempURL cachedBlurredArtworkFingerprint = fingerprint cachedBlurredArtworkPixelSize = pixelSize return tempURL } catch { return nil } } private func wallpaperPixelSize(for screen: NSScreen) -> CGSize { let scale = max(screen.backingScaleFactor, 1) let frame = screen.frame return CGSize( width: max(frame.width * scale, 1), height: max(frame.height * scale, 1) ) } private func artworkCacheFileURL(for fingerprint: String) -> URL { let normalizedFingerprint = normalizedFingerprintComponent(fingerprint) return FileManager.default.temporaryDirectory .appendingPathComponent("atoll_artwork_wallpaper_\(normalizedFingerprint).png") } private func blurredArtworkCacheFileURL(for fingerprint: String, pixelSize: CGSize) -> URL { let normalizedFingerprint = normalizedFingerprintComponent(fingerprint) let width = Int(pixelSize.width.rounded()) let height = Int(pixelSize.height.rounded()) return FileManager.default.temporaryDirectory .appendingPathComponent("atoll_artwork_wallpaper_blurred_\(normalizedFingerprint)_\(width)x\(height).png") } private func normalizedFingerprintComponent(_ fingerprint: String) -> String { fingerprint.replacingOccurrences(of: "-", with: "_") } private func wallpaperIdentityKey(fingerprint: String, usesFallback: Bool, screen: NSScreen) -> String { if usesFallback { let pixelSize = wallpaperPixelSize(for: screen) let width = Int(pixelSize.width.rounded()) let height = Int(pixelSize.height.rounded()) return "fallback:\(fingerprint):\(width)x\(height)" } return "artwork:\(fingerprint)" } private nonisolated func encodeToPNG(_ image: NSImage) -> Data? { guard let tiffData = image.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiffData) else { return nil } return bitmap.representation(using: .png, properties: [:]) } private nonisolated func imageFingerprint(for image: NSImage) -> String? { guard let pngData = encodeToPNG(image) else { return nil } var hasher = Hasher() hasher.combine(pngData) hasher.combine(Int(image.size.width.rounded())) hasher.combine(Int(image.size.height.rounded())) return String(hasher.finalize()) } private func encodeBlurredWallpaperPNG(from artwork: NSImage, targetPixelSize: CGSize) -> Data? { guard let tiffData = artwork.tiffRepresentation, let sourceImage = CIImage(data: tiffData) else { return nil } let targetRect = CGRect(origin: .zero, size: targetPixelSize) let sourceExtent = sourceImage.extent guard sourceExtent.width > 0, sourceExtent.height > 0 else { return nil } let scale = max( targetRect.width / sourceExtent.width, targetRect.height / sourceExtent.height ) let scaledWidth = sourceExtent.width * scale let scaledHeight = sourceExtent.height * scale let offsetX = (targetRect.width - scaledWidth) / 2 let offsetY = (targetRect.height - scaledHeight) / 2 let scaledImage = sourceImage .transformed(by: CGAffineTransform(scaleX: scale, y: scale)) .transformed(by: CGAffineTransform(translationX: offsetX, y: offsetY)) .cropped(to: targetRect) let blurredImage = scaledImage .clampedToExtent() .applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 28]) .cropped(to: targetRect) .applyingFilter( "CIColorControls", parameters: [ kCIInputSaturationKey: 0.92, kCIInputBrightnessKey: -0.03, kCIInputContrastKey: 0.98 ] ) let context = CIContext(options: nil) guard let cgImage = context.createCGImage(blurredImage, from: targetRect) else { return nil } let renderedImage = NSImage(cgImage: cgImage, size: NSSize(width: targetPixelSize.width, height: targetPixelSize.height)) let composedImage = NSImage(size: renderedImage.size) composedImage.lockFocus() renderedImage.draw(in: NSRect(origin: .zero, size: renderedImage.size)) NSColor(calibratedWhite: 0.02, alpha: 0.08).setFill() NSBezierPath(rect: NSRect(origin: .zero, size: renderedImage.size)).fill() let gradient = NSGradient(colors: [ NSColor.white.withAlphaComponent(0.05), NSColor.clear, NSColor.black.withAlphaComponent(0.16) ]) gradient?.draw(in: NSRect(origin: .zero, size: renderedImage.size), angle: -90) composedImage.unlockFocus() guard let finalTIFF = composedImage.tiffRepresentation, let finalBitmap = NSBitmapImageRep(data: finalTIFF) else { return nil } 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 guard isLiveWallpaperAllowed else { scheduleWallpaperTransitionHide(after: .milliseconds(260)) return } guard let videoURL else { scheduleWallpaperTransitionHide(after: .milliseconds(260)) return } let nextFingerprint = videoURL.absoluteString if activeLiveWallpaperFingerprint == nextFingerprint { scheduleWallpaperTransitionHide(after: .milliseconds(220)) scheduleHideVideoWindow(after: .milliseconds(220), expectedURL: videoURL) return } if let screen = NSScreen.main { showVideoWindow(on: screen, videoURL: videoURL) } scheduleWallpaperTransitionHide(after: .milliseconds(420)) let assetID = customLiveWallpaperAssetID let manifestURL = aerialManifestURL let videosDirectoryURL = aerialVideosDirectoryURL let thumbnailsDirectoryURL = aerialThumbnailsDirectoryURL let manifestBackupURL = liveWallpaperManifestBackupURL let videoBackupURL = liveWallpaperVideoBackupURL let thumbnailBackupURL = liveWallpaperThumbnailBackupURL let thumbnailData = encodeToPNG(artwork) let title = activeSongTitle ?? MusicManager.shared.songTitle let artist = activeArtist ?? MusicManager.shared.artistName let displayName = [title, artist] .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } .joined(separator: " - ") liveWallpaperTask = Task(priority: .utility) { [weak self] in let prepared = await Self.prepareCustomLiveWallpaperAsset( from: videoURL, assetID: assetID, displayName: displayName.isEmpty ? "Atoll Canvas" : displayName, manifestURL: manifestURL, videosDirectoryURL: videosDirectoryURL, thumbnailsDirectoryURL: thumbnailsDirectoryURL, thumbnailData: thumbnailData, manifestBackupURL: manifestBackupURL, videoBackupURL: videoBackupURL, thumbnailBackupURL: thumbnailBackupURL ) guard !Task.isCancelled else { return } await MainActor.run { guard let self, self.isShowing, self.isLiveWallpaperAllowed else { return } let currentIdentifier = "\(MusicManager.shared.songTitle)|\(MusicManager.shared.artistName)" let currentVideoURL = self.isLiveWallpaperAllowed ? MusicManager.shared.videoArtworkURL : nil guard currentIdentifier == identifier else { return } guard currentVideoURL?.absoluteString == nextFingerprint else { return } guard !self.shouldUseSpotifyStaticFallbackWallpaper(videoURL: currentVideoURL) 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") } } } private nonisolated static func prepareCustomLiveWallpaperAsset( from sourceURL: URL, assetID: String, displayName: String, manifestURL: URL, videosDirectoryURL: URL, thumbnailsDirectoryURL: URL, thumbnailData: Data?, manifestBackupURL: URL, videoBackupURL: URL, thumbnailBackupURL: URL ) async -> Bool { let fm = FileManager.default let videoDestinationURL = videosDirectoryURL.appendingPathComponent("\(assetID).mov") let thumbnailDestinationURL = thumbnailsDirectoryURL.appendingPathComponent("\(assetID).png") do { try fm.createDirectory(at: videosDirectoryURL, withIntermediateDirectories: true) try fm.createDirectory(at: thumbnailsDirectoryURL, withIntermediateDirectories: true) } catch { return false } backupItemIfNeeded(at: manifestURL, backupURL: manifestBackupURL) backupItemIfNeeded(at: videoDestinationURL, backupURL: videoBackupURL) backupItemIfNeeded(at: thumbnailDestinationURL, backupURL: thumbnailBackupURL) guard await materializeVideo(from: sourceURL, to: videoDestinationURL) else { return false } if let thumbnailData { try? thumbnailData.write(to: thumbnailDestinationURL, options: .atomic) } return updateAerialManifest( manifestURL: manifestURL, assetID: assetID, videoURL: videoDestinationURL, thumbnailURL: fm.fileExists(atPath: thumbnailDestinationURL.path) ? thumbnailDestinationURL : nil, displayName: displayName ) } private nonisolated static func materializeVideo(from sourceURL: URL, to destinationURL: URL) async -> Bool { let fm = FileManager.default try? fm.removeItem(at: destinationURL) if !sourceURL.isFileURL { guard let downloadedURL = await downloadRemoteVideo(from: sourceURL) else { return false } defer { try? fm.removeItem(at: downloadedURL) } return await materializeVideo(from: downloadedURL, to: destinationURL) } if sourceURL.isFileURL, sourceURL.pathExtension.lowercased() == "mov" { do { try fm.copyItem(at: sourceURL, to: destinationURL) return validateVideo(at: destinationURL) } catch { return false } } let asset = AVURLAsset(url: sourceURL) let presets = [AVAssetExportPresetPassthrough, AVAssetExportPresetHighestQuality] for preset in presets { try? fm.removeItem(at: destinationURL) guard let exportSession = AVAssetExportSession(asset: asset, presetName: preset) else { continue } exportSession.outputURL = destinationURL exportSession.outputFileType = .mov exportSession.shouldOptimizeForNetworkUse = true let exported = await export(exportSession) if exported, validateVideo(at: destinationURL) { return true } } return false } private nonisolated static func downloadRemoteVideo(from sourceURL: URL) async -> URL? { do { let (temporaryURL, response) = try await URLSession.shared.download(from: sourceURL) guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) else { return nil } let destinationURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension(sourceURL.pathExtension.isEmpty ? "mp4" : sourceURL.pathExtension) try? FileManager.default.removeItem(at: destinationURL) try FileManager.default.moveItem(at: temporaryURL, to: destinationURL) return destinationURL } catch { return nil } } private nonisolated static func export(_ exportSession: AVAssetExportSession) async -> Bool { await withCheckedContinuation { continuation in exportSession.exportAsynchronously { continuation.resume(returning: exportSession.status == .completed) } } } private nonisolated static func validateVideo(at url: URL) -> Bool { guard FileManager.default.fileExists(atPath: url.path) else { return false } let asset = AVURLAsset(url: url) return !asset.tracks(withMediaType: .video).isEmpty } private nonisolated static func updateAerialManifest( manifestURL: URL, assetID: String, videoURL: URL, thumbnailURL: URL?, displayName: String ) -> Bool { guard let manifestData = try? Data(contentsOf: manifestURL), var root = try? JSONSerialization.jsonObject(with: manifestData) as? [String: Any], var assets = root["assets"] as? [[String: Any]] else { return false } guard let existingIndex = assets.firstIndex(where: { ($0["id"] as? String) == assetID }) else { return false } var customAsset = assets[existingIndex] customAsset["accessibilityLabel"] = displayName customAsset["previewImage"] = thumbnailURL?.absoluteString ?? "" customAsset["url-4K-SDR-240FPS"] = videoURL.absoluteString customAsset["pointsOfInterest"] = [:] assets[existingIndex] = customAsset root["assets"] = assets guard let encoded = try? JSONSerialization.data(withJSONObject: root, options: []) else { return false } do { try encoded.write(to: manifestURL, options: .atomic) return true } catch { return false } } private nonisolated static func backupItemIfNeeded(at sourceURL: URL, backupURL: URL) { let fm = FileManager.default guard fm.fileExists(atPath: sourceURL.path) else { return } guard !fm.fileExists(atPath: backupURL.path) else { return } try? fm.copyItem(at: sourceURL, to: backupURL) } private func restoreLiveWallpaperResources() { let fm = FileManager.default let assetID = customLiveWallpaperAssetID let videoDestinationURL = aerialVideosDirectoryURL.appendingPathComponent("\(assetID).mov") let thumbnailDestinationURL = aerialThumbnailsDirectoryURL.appendingPathComponent("\(assetID).png") if fm.fileExists(atPath: liveWallpaperManifestBackupURL.path) { try? fm.removeItem(at: aerialManifestURL) try? fm.copyItem(at: liveWallpaperManifestBackupURL, to: aerialManifestURL) try? fm.removeItem(at: liveWallpaperManifestBackupURL) } if fm.fileExists(atPath: liveWallpaperVideoBackupURL.path) { try? fm.removeItem(at: videoDestinationURL) try? fm.copyItem(at: liveWallpaperVideoBackupURL, to: videoDestinationURL) try? fm.removeItem(at: liveWallpaperVideoBackupURL) } if fm.fileExists(atPath: liveWallpaperThumbnailBackupURL.path) { try? fm.removeItem(at: thumbnailDestinationURL) try? fm.copyItem(at: liveWallpaperThumbnailBackupURL, to: thumbnailDestinationURL) try? fm.removeItem(at: liveWallpaperThumbnailBackupURL) } } // MARK: - Click Receiver private func installFallbackRightClickMonitorIfNeeded() { guard fallbackRightClickMonitor == nil else { return } fallbackRightClickMonitor = NSEvent.addLocalMonitorForEvents(matching: [.rightMouseDown]) { [weak self] event in guard let self, self.isShowing, self.isShowingSpotifyCanvasFallback else { return event } self.hide() return nil } } private func removeFallbackRightClickMonitor() { guard let fallbackRightClickMonitor else { return } NSEvent.removeMonitor(fallbackRightClickMonitor) self.fallbackRightClickMonitor = nil } private func showClickReceiver(on screen: NSScreen) { let screenFrame = screen.frame let window: ClickReceiverWindow if let existing = clickWindow { window = existing } else { let newWindow = ClickReceiverWindow( contentRect: screenFrame, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) newWindow.isReleasedWhenClosed = false newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] newWindow.isMovable = false newWindow.hasShadow = false ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) clickWindow = newWindow window = newWindow clickWindowDelegated = false } window.setFrame(screenFrame, display: true) window.onClick = { [weak self] in self?.hide() } if !clickWindowDelegated { SkyLightOperator.shared.delegateWindow(window) clickWindowDelegated = true } window.orderFrontRegardless() for other in NSApp.windows where other !== window && other.level.rawValue >= Int(CGShieldingWindowLevel()) && other.isVisible { window.order(.below, relativeTo: other.windowNumber) } } private func hideClickReceiver() { clickWindow?.orderOut(nil) clickWindow?.onClick = nil } private func showArtworkOverlay(on screen: NSScreen, artwork: NSImage, mode: ArtworkOverlayMode) { currentArtworkOverlayMode = mode guard let targetFrame = artworkOverlayFrame(on: screen) else { return } let window: NSWindow if let existing = artworkOverlayWindow { window = existing } else { let newWindow = NSWindow( contentRect: targetFrame, styleMask: [.borderless], backing: .buffered, defer: false ) newWindow.isReleasedWhenClosed = false newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.ignoresMouseEvents = false newWindow.hasShadow = false newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) artworkOverlayWindow = newWindow window = newWindow artworkOverlayWindowDelegated = false } window.setFrame(targetFrame, display: true) let contentView: SpotifyCanvasFallbackArtworkOverlayView if let existing = window.contentView as? SpotifyCanvasFallbackArtworkOverlayView { contentView = existing contentView.frame = NSRect(origin: .zero, size: targetFrame.size) contentView.updateArtwork(artwork) } else { let newContentView = SpotifyCanvasFallbackArtworkOverlayView(artwork: artwork) newContentView.frame = NSRect(origin: .zero, size: targetFrame.size) window.contentView = newContentView contentView = newContentView } contentView.onDismiss = { [weak self] in self?.hide() } if !artworkOverlayWindowDelegated { SkyLightOperator.shared.delegateWindow(window) artworkOverlayWindowDelegated = true } window.orderFrontRegardless() } private func hideArtworkOverlay() { currentArtworkOverlayMode = .none if let contentView = artworkOverlayWindow?.contentView as? SpotifyCanvasFallbackArtworkOverlayView { contentView.onDismiss = nil } artworkOverlayWindow?.orderOut(nil) artworkOverlayWindow?.contentView = nil } private func updateArtworkOverlayFrameIfNeeded() { guard isShowing, currentArtworkOverlayMode != .none else { return } guard let window = artworkOverlayWindow else { return } guard let screen = NSScreen.main, let frame = artworkOverlayFrame(on: screen) else { return } window.setFrame(frame, display: true) } private func artworkOverlayFrame(on screen: NSScreen) -> NSRect? { switch currentArtworkOverlayMode { case .none: return nil case .spotifyFallback: return spotifyFallbackArtworkOverlayFrame(on: screen) } } private func spotifyFallbackArtworkOverlayFrame(on screen: NSScreen) -> NSRect? { guard let frames = spotifyCanvasFallbackLayoutFrames(on: screen) else { return nil } return frames.artworkFrame } private func updateLyricsOverlayIfNeeded(on screen: NSScreen? = NSScreen.main) { guard isShowing, isShowingSpotifyCanvasFallback, Defaults[.enableLyrics] else { hideLyricsOverlay() return } let line = MusicManager.shared.currentLyrics.trimmingCharacters(in: .whitespacesAndNewlines) guard !line.isEmpty else { hideLyricsOverlay() return } guard let screen, let frame = spotifyCanvasFallbackLayoutFrames(on: screen)?.lyricsFrame else { hideLyricsOverlay() return } let fontSize = min(max(frame.height * 0.24, 24), 40) let content = FullScreenLyricsOverlayContent(text: line, fontSize: fontSize) let window: NSWindow let hostingView: NSHostingView if let existingWindow = lyricsOverlayWindow, let existingView = existingWindow.contentView as? NSHostingView { window = existingWindow hostingView = existingView } else { let newWindow = NSWindow( contentRect: frame, styleMask: [.borderless], backing: .buffered, defer: false ) newWindow.isReleasedWhenClosed = false newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.ignoresMouseEvents = true newWindow.hasShadow = false newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] let newHostingView = NSHostingView(rootView: content) newHostingView.frame = NSRect(origin: .zero, size: frame.size) newHostingView.wantsLayer = true newHostingView.layer?.backgroundColor = NSColor.clear.cgColor newWindow.contentView = newHostingView ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) lyricsOverlayWindow = newWindow window = newWindow hostingView = newHostingView lyricsOverlayWindowDelegated = false } window.setFrame(frame, display: true) hostingView.frame = NSRect(origin: .zero, size: frame.size) hostingView.rootView = content if !lyricsOverlayWindowDelegated { SkyLightOperator.shared.delegateWindow(window) lyricsOverlayWindowDelegated = true } window.orderFrontRegardless() } private func hideLyricsOverlay() { lyricsOverlayWindow?.orderOut(nil) } private func spotifyCanvasFallbackLayoutFrames(on screen: NSScreen) -> SpotifyCanvasFallbackLayoutFrames? { let screenFrame = screen.frame guard let panelFrame = LockScreenPanelManager.shared.latestFrame else { return nil } let overlaySide = spotifyCanvasFallbackArtworkSideLength( screenFrame: screenFrame, panelSize: panelFrame.size ) let spacing = spotifyCanvasFallbackInterItemSpacing(screenFrame: screenFrame) let targetX = panelFrame.minX - spacing - overlaySide let targetY = panelFrame.midY - (overlaySide / 2) let artworkFrame = NSRect(x: targetX, y: targetY, width: overlaySide, height: overlaySide) let groupFrame = artworkFrame.union(panelFrame) let lyricsGap = min(max(screenFrame.height * 0.022, 18), 28) let bottomMargin = min(max(screenFrame.height * 0.16, 140), 220) let availableHeight = groupFrame.minY - lyricsGap - (screenFrame.minY + bottomMargin) let desiredHeight = min(max(screenFrame.height * 0.13, 108), 156) let lyricsHeight = min(desiredHeight, max(availableHeight, 0)) let lyricsFrame: NSRect? if lyricsHeight >= 56 { lyricsFrame = NSRect( x: groupFrame.minX, y: groupFrame.minY - lyricsGap - lyricsHeight, width: groupFrame.width, height: lyricsHeight ) } else { lyricsFrame = nil } return SpotifyCanvasFallbackLayoutFrames( artworkFrame: artworkFrame, panelFrame: panelFrame, groupFrame: groupFrame, lyricsFrame: lyricsFrame ) } private func showWallpaperTransition(on screen: NSScreen, imageURL: URL) { guard let image = NSImage(contentsOf: imageURL) else { return } wallpaperTransitionHideTask?.cancel() wallpaperTransitionHideTask = nil let screenFrame = screen.frame let window: NSWindow let view: WallpaperTransitionImageView if let existingWindow = wallpaperTransitionWindow, let existingView = wallpaperTransitionView { window = existingWindow view = existingView } else { let newWindow = NSWindow( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) newWindow.isReleasedWhenClosed = false newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.ignoresMouseEvents = true newWindow.hasShadow = false newWindow.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.desktopWindow)) + 1) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] let newView = WallpaperTransitionImageView(frame: screenFrame) newWindow.contentView = newView wallpaperTransitionWindow = newWindow wallpaperTransitionView = newView window = newWindow view = newView } window.setFrame(screenFrame, display: true) view.frame = NSRect(origin: .zero, size: screenFrame.size) view.updateImage(image) window.orderFrontRegardless() } private func scheduleWallpaperTransitionHide(after delay: Duration) { wallpaperTransitionHideTask?.cancel() wallpaperTransitionHideTask = Task { @MainActor [weak self] in try? await Task.sleep(for: delay) self?.hideWallpaperTransition() } } private func hideWallpaperTransition() { wallpaperTransitionHideTask?.cancel() wallpaperTransitionHideTask = nil wallpaperTransitionWindow?.orderOut(nil) } private func showVideoWindow(on screen: NSScreen, videoURL: URL) { videoWindowHideTask?.cancel() videoWindowHideTask = nil let screenFrame = screen.frame let window: NSWindow let view: LoopingVideoView if let existingWindow = videoWindow, let existingView = videoView { window = existingWindow view = existingView } else { let newWindow = NSWindow( contentRect: screenFrame, styleMask: [.borderless], backing: .buffered, defer: false ) newWindow.isReleasedWhenClosed = false newWindow.isOpaque = false newWindow.backgroundColor = .clear newWindow.ignoresMouseEvents = true newWindow.hasShadow = false newWindow.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.desktopWindow)) + 1) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] let newView = LoopingVideoView(frame: screenFrame) newWindow.contentView = newView videoWindow = newWindow videoView = newView window = newWindow view = newView } window.setFrame(screenFrame, display: true) view.frame = NSRect(origin: .zero, size: screenFrame.size) if activeVideoWindowURL != videoURL { view.play(url: videoURL) activeVideoWindowURL = videoURL } window.orderFrontRegardless() } private func scheduleHideVideoWindow(after delay: Duration, expectedURL: URL?) { videoWindowHideTask?.cancel() let expectedURLString = expectedURL?.absoluteString videoWindowHideTask = Task { @MainActor [weak self] in try? await Task.sleep(for: delay) guard let self else { return } if let expectedURLString, self.activeVideoWindowURL?.absoluteString != expectedURLString { return } self.hideVideoWindow() } } private func hideVideoWindow() { videoWindowHideTask?.cancel() videoWindowHideTask = nil activeVideoWindowURL = nil videoView?.stop() videoWindow?.orderOut(nil) } // MARK: - Backup / Restore private var hasValidBackup: Bool { FileManager.default.fileExists(atPath: backupPlistURL.path) } private func backupWallpaperConfig() { guard !hasValidBackup else { return } try? FileManager.default.copyItem(at: wallpaperPlistURL, to: backupPlistURL) } private func applyArtworkToPlist(imageURL: URL) -> Bool { guard let plistData = try? Data(contentsOf: wallpaperPlistURL), var plist = try? PropertyListSerialization.propertyList(from: plistData, format: nil) as? [String: Any] else { return false } let desktopBlock = makeImageWallpaperBlock(imageURL: imageURL) let idleBlock = makeImageWallpaperBlock(imageURL: imageURL) patchWallpaperEntries(&plist, desktopBlock: desktopBlock, idleBlock: idleBlock) guard let newData = try? PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0) else { return false } return (try? newData.write(to: wallpaperPlistURL, options: .atomic)) != nil } private func applyAerialToPlist(assetID: String) -> Bool { guard let plistData = try? Data(contentsOf: wallpaperPlistURL), var plist = try? PropertyListSerialization.propertyList(from: plistData, format: nil) as? [String: Any] else { return false } let desktopTemplate = wallpaperBlock( named: "Desktop", in: plist["AllSpacesAndDisplays"] as? [String: Any], fallback: plist["SystemDefault"] as? [String: Any] ) let idleTemplate = wallpaperBlock( named: "Idle", in: plist["AllSpacesAndDisplays"] as? [String: Any], fallback: plist["SystemDefault"] as? [String: Any] ) let desktopBlock = makeAerialWallpaperBlock(assetID: assetID, existingBlock: desktopTemplate) let idleBlock = makeAerialWallpaperBlock(assetID: assetID, existingBlock: idleTemplate) patchWallpaperEntries(&plist, desktopBlock: desktopBlock, idleBlock: idleBlock) guard let newData = try? PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0) else { return false } return (try? newData.write(to: wallpaperPlistURL, options: .atomic)) != nil } private func wallpaperBlock(named name: String, in primary: [String: Any]?, fallback: [String: Any]?) -> [String: Any]? { if let primaryBlock = primary?[name] as? [String: Any] { return primaryBlock } return fallback?[name] as? [String: Any] } private func makeImageWallpaperBlock(imageURL: URL) -> [String: Any] { let config: [String: Any] = [ "type": "imageFile", "url": ["relative": imageURL.absoluteString] ] let configData = (try? PropertyListSerialization.data(fromPropertyList: config, format: .binary, options: 0)) ?? Data() let imageChoice: [String: Any] = [ "Provider": "com.apple.wallpaper.choice.image", "Files": [] as [Any], "Configuration": configData ] let contentBlock: [String: Any] = [ "Choices": [imageChoice], "Shuffle": "$null" ] return [ "Content": contentBlock, "LastSet": Date(), "LastUse": Date() ] } private func makeAerialWallpaperBlock(assetID: String, existingBlock: [String: Any]?) -> [String: Any] { let config: [String: Any] = [ "assetID": assetID ] let configData = (try? PropertyListSerialization.data(fromPropertyList: config, format: .binary, options: 0)) ?? Data() let aerialChoice: [String: Any] = [ "Provider": "com.apple.wallpaper.choice.aerials", "Files": [] as [Any], "Configuration": configData ] var contentBlock: [String: Any] = [ "Choices": [aerialChoice], "Shuffle": "$null" ] if let existingContent = existingBlock?["Content"] as? [String: Any], let encodedOptionValues = existingContent["EncodedOptionValues"] { contentBlock["EncodedOptionValues"] = encodedOptionValues } return [ "Content": contentBlock, "LastSet": Date(), "LastUse": Date() ] } private func patchWallpaperEntries(_ plist: inout [String: Any], desktopBlock: [String: Any], idleBlock: [String: Any]) { var allSpaces = (plist["AllSpacesAndDisplays"] as? [String: Any]) ?? [:] allSpaces["Desktop"] = desktopBlock allSpaces["Idle"] = idleBlock allSpaces["Type"] = "individual" plist["AllSpacesAndDisplays"] = allSpaces var systemDefault = (plist["SystemDefault"] as? [String: Any]) ?? [:] systemDefault["Desktop"] = desktopBlock systemDefault["Idle"] = idleBlock systemDefault["Type"] = "individual" plist["SystemDefault"] = systemDefault if var displays = plist["Displays"] as? [String: Any] { for key in displays.keys { if var display = displays[key] as? [String: Any] { display["Desktop"] = desktopBlock display["Idle"] = idleBlock if display["Type"] != nil { display["Type"] = "individual" } displays[key] = display } } plist["Displays"] = displays } if var spaces = plist["Spaces"] as? [String: Any] { for spaceKey in spaces.keys { if var space = spaces[spaceKey] as? [String: Any] { 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] { for displayKey in spaceDisplays.keys { if var display = spaceDisplays[displayKey] as? [String: Any] { display["Desktop"] = desktopBlock display["Idle"] = idleBlock if display["Type"] != nil { display["Type"] = "individual" } spaceDisplays[displayKey] = display } } space["Displays"] = spaceDisplays } spaces[spaceKey] = space } } plist["Spaces"] = spaces } } private func restartWallpaperAgent() { let task = Process() task.executableURL = URL(fileURLWithPath: "/usr/bin/killall") task.arguments = ["WallpaperAgent"] try? task.run() } private func restoreWallpaper() { let fm = FileManager.default guard hasValidBackup else { return } do { try fm.removeItem(at: wallpaperPlistURL) try fm.copyItem(at: backupPlistURL, to: wallpaperPlistURL) try fm.removeItem(at: backupPlistURL) } catch { print("[FullScreenArtworkWindowManager] Failed to restore plist: \(error)") } restoreLiveWallpaperResources() restartWallpaperAgent() } // MARK: - Track Change Observer private func observeTrackChanges() { trackChangeCancellable?.cancel() trackChangeCancellable = MusicManager.shared.$songTitle .combineLatest(MusicManager.shared.$artistName) .dropFirst() .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .receive(on: DispatchQueue.main) .sink { [weak self] title, artist in guard let self, self.isShowing else { return } if title != self.activeSongTitle || artist != self.activeArtist { self.refreshPresentationForCurrentTrack() self.scheduleDeferredTrackRefresh(expectedTitle: title, expectedArtist: artist) } } } private func scheduleDeferredTrackRefresh(expectedTitle: String, expectedArtist: String) { deferredTrackRefreshTask?.cancel() deferredTrackRefreshTask = Task { @MainActor [weak self] in try? await Task.sleep(for: .milliseconds(700)) guard let self, self.isShowing else { return } guard MusicManager.shared.songTitle == expectedTitle, MusicManager.shared.artistName == expectedArtist else { return } self.refreshPresentationForCurrentTrack() } } }