/* * 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 SkyLightWindow import SwiftUI // MARK: - Click Receiver Window private class ClickReceiverWindow: NSWindow { var onClick: (() -> Void)? override func mouseDown(with event: NSEvent) { onClick?() } 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 } } // MARK: - Window Manager @MainActor final class FullScreenArtworkWindowManager { static let shared = FullScreenArtworkWindowManager() private(set) var isShowing = 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 cachedArtworkIdentifier: String? 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 liveWallpaperTask: Task? private var isLiveWallpaperAllowed = false private var activeLiveWallpaperFingerprint: String? private init() { observeArtworkChanges() observeVideoArtworkChanges() } func show(artwork: NSImage, videoURL: URL? = nil, allowLiveWallpaper: Bool = false) { guard !isShowing else { return } guard let screen = NSScreen.main else { return } let fileURL: URL let artworkID = "\(MusicManager.shared.songTitle)|\(MusicManager.shared.artistName)" if let cached = cachedArtworkPNG, cachedArtworkIdentifier == artworkID, FileManager.default.fileExists(atPath: cached.path) { fileURL = cached } else { guard let encoded = encodeToPNG(artwork) else { return } let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("atoll_artwork_wallpaper.png") do { try encoded.write(to: tempURL, options: .atomic) } catch { return } fileURL = tempURL cachedArtworkPNG = tempURL cachedArtworkIdentifier = artworkID } artworkFileURL = fileURL isLiveWallpaperAllowed = allowLiveWallpaper activeLiveWallpaperFingerprint = nil backupWallpaperConfig() guard applyArtworkToPlist(imageURL: fileURL) else { print("[FullScreenArtworkWindowManager] Failed to patch plist") return } restartWallpaperAgent() hideVideoWindow() showClickReceiver(on: screen) isShowing = true activeSongTitle = MusicManager.shared.songTitle activeArtist = MusicManager.shared.artistName observeTrackChanges() scheduleLiveWallpaperPreparation(for: videoURL, artwork: artwork, identifier: artworkID) print("[FullScreenArtworkWindowManager] Artwork applied as wallpaper") } func hide() { guard isShowing else { return } isShowing = false isLiveWallpaperAllowed = false activeLiveWallpaperFingerprint = nil trackChangeCancellable?.cancel() trackChangeCancellable = nil liveWallpaperTask?.cancel() liveWallpaperTask = nil hideVideoWindow() hideClickReceiver() 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(500), scheduler: DispatchQueue.main) .sink { [weak self] newArt in guard let self else { return } let artID = "\(MusicManager.shared.songTitle)|\(MusicManager.shared.artistName)" guard artID != self.cachedArtworkIdentifier else { return } Task.detached(priority: .utility) { guard let pngData = self.encodeToPNG(newArt) else { return } let url = FileManager.default.temporaryDirectory.appendingPathComponent("atoll_artwork_wallpaper.png") try? pngData.write(to: url, options: .atomic) await MainActor.run { self.cachedArtworkPNG = url self.cachedArtworkIdentifier = artID } } } } private func observeVideoArtworkChanges() { videoArtworkCancellable = MusicManager.shared.$videoArtworkURL .receive(on: DispatchQueue.main) .sink { [weak self] videoURL in guard let self, self.isShowing, self.isLiveWallpaperAllowed else { return } let artworkID = "\(MusicManager.shared.songTitle)|\(MusicManager.shared.artistName)" self.scheduleLiveWallpaperPreparation( for: videoURL, artwork: MusicManager.shared.albumArt, identifier: artworkID ) } } 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 func scheduleLiveWallpaperPreparation(for videoURL: URL?, artwork: NSImage, identifier: String) { liveWallpaperTask?.cancel() liveWallpaperTask = nil guard isLiveWallpaperAllowed else { return } guard let artworkFileURL else { return } let nextFingerprint = videoURL?.absoluteString if activeLiveWallpaperFingerprint != nil, activeLiveWallpaperFingerprint != nextFingerprint { activeLiveWallpaperFingerprint = nil if applyArtworkToPlist(imageURL: artworkFileURL) { restartWallpaperAgent() } } guard let videoURL else { return } if activeLiveWallpaperFingerprint == nextFingerprint { return } if let screen = NSScreen.main { showVideoWindow(on: screen, videoURL: videoURL) } 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)" guard currentIdentifier == identifier else { return } guard prepared else { return } guard self.applyAerialToPlist(assetID: assetID) else { return } self.activeLiveWallpaperFingerprint = nextFingerprint self.hideVideoWindow() self.restartWallpaperAgent() 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 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 showVideoWindow(on screen: NSScreen, videoURL: URL) { 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) view.play(url: videoURL) window.orderFrontRegardless() } private func hideVideoWindow() { 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"] = allSpaces["Type"] ?? "individual" plist["AllSpacesAndDisplays"] = allSpaces var systemDefault = (plist["SystemDefault"] as? [String: Any]) ?? [:] systemDefault["Desktop"] = desktopBlock systemDefault["Idle"] = idleBlock systemDefault["Type"] = 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 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 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 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() .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.hide() } } } }