import AppKit @MainActor protocol WallpaperManaging { func download(from url: URL) async throws -> URL func setDesktopImage(from fileURL: URL, screen: NSScreen?) throws func saveToFolder(from sourceURL: URL, to folderURL: URL) throws } @MainActor protocol DesktopImageSetting { func setDesktopImageURL( _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any] ) throws } extension NSWorkspace: DesktopImageSetting {} class WallpaperManager: WallpaperManaging { static let shared = WallpaperManager() private let session: URLSession private let desktopSetter: @MainActor (URL, NSScreen) throws -> Void init( session: URLSession = .shared, desktopSetter: (@MainActor (URL, NSScreen) throws -> Void)? = nil, workspace: DesktopImageSetting = NSWorkspace.shared ) { self.session = session self.desktopSetter = desktopSetter ?? { url, screen in try workspace.setDesktopImageURL( url, for: screen, options: [:] ) } } /// Downloads the file to a temporary location and returns the local URL. /// /// Each call writes to a fresh, unique temp filename so that distinct /// wallpapers sharing a `lastPathComponent` cannot overwrite one another /// before they are consumed (e.g. before `setDesktopImage` or /// `saveToFolder` reads the temp file). The caller owns the returned file /// and is responsible for removing it when no longer needed. func download(from url: URL) async throws -> URL { let (data, response) = try await session.data(from: url) guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else { throw WallhavenError.apiError("Wallpaper download failed") } let fileURL = Self.makeTempURL(for: url) try data.write(to: fileURL) return fileURL } /// Returns a temp URL unique to this call. The original `url.pathExtension` /// is preserved when present so downstream consumers can identify the kind /// of file, but the stem is a fresh UUID — never derived from the input /// filename — to guarantee no two concurrent or sequential downloads of /// distinct wallpapers collide in the shared temp directory. static func makeTempURL(for url: URL) -> URL { let fileExtension = url.pathExtension.isEmpty ? "img" : url.pathExtension let stem = UUID().uuidString let filename = "haven-\(stem).\(fileExtension)" return FileManager.default.temporaryDirectory.appendingPathComponent(filename) } /// Sets the desktop image from a local file URL. @MainActor func setDesktopImage(from fileURL: URL, screen: NSScreen? = nil) throws { if let screen = screen { try desktopSetter(fileURL, screen) } else { // Set for all screens or just main? Let's default to main for safety, // but the UI will likely drive this. // If no screen specified, maybe set for the 'active' one. let targetScreen = NSScreen.main ?? NSScreen.screens.first! try desktopSetter(fileURL, targetScreen) } } /// Copies the file to the specified destination directory. /// /// Policy for an existing destination with the same name: /// - if its contents match the source, leave it untouched (idempotent /// re-save of the same logical wallpaper, preserves modification time); /// - if its contents differ, replace it with the source bytes. /// Either branch avoids the prior behavior of unconditionally deleting the /// destination before copying, which would destroy the source if the caller /// passed the same path for both arguments. func saveToFolder(from sourceURL: URL, to folderURL: URL) throws { // Ensure folder exists try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true) let destinationURL = folderURL.appendingPathComponent(sourceURL.lastPathComponent) if FileManager.default.fileExists(atPath: destinationURL.path) { let sourceData = try Data(contentsOf: sourceURL) let existingData = try Data(contentsOf: destinationURL) if sourceData == existingData { return } try FileManager.default.removeItem(at: destinationURL) } try FileManager.default.copyItem(at: sourceURL, to: destinationURL) } // Legacy support wrapper (optional, but cleaner to update call sites) @MainActor func setWallpaper(from url: URL, screen: NSScreen? = nil) async throws { let localURL = try await download(from: url) try setDesktopImage(from: localURL, screen: screen) } }