1import AppKit23@MainActor4protocol WallpaperManaging {5 func download(from url: URL) async throws -> URL6 func cachedLocalFile(for url: URL) async throws -> URL7 func setDesktopImage(from fileURL: URL, screen: NSScreen?) throws8 func saveToFolder(from sourceURL: URL, to folderURL: URL) throws9}1011@MainActor12protocol DesktopImageSetting {13 func setDesktopImageURL(14 _ url: URL, for screen: NSScreen, options: [NSWorkspace.DesktopImageOptionKey: Any]15 ) throws16}1718extension NSWorkspace: DesktopImageSetting {}1920class WallpaperManager: WallpaperManaging {21 static let shared = WallpaperManager()2223 private let session: URLSession24 private let desktopSetter: @MainActor (URL, NSScreen) throws -> Void25 private let mainScreen: () -> NSScreen?26 private let firstScreen: () -> NSScreen?27 private let cacheDirectoryProvider: @MainActor () -> URL?2829 init(30 session: URLSession = .shared,31 desktopSetter: (@MainActor (URL, NSScreen) throws -> Void)? = nil,32 workspace: DesktopImageSetting = NSWorkspace.shared,33 mainScreen: @escaping () -> NSScreen? = { NSScreen.main },34 firstScreen: @escaping () -> NSScreen? = { NSScreen.screens.first },35 cacheDirectoryProvider: @escaping @MainActor () -> URL? = WallpaperManager36 .defaultCacheDirectory37 ) {38 self.session = session39 self.desktopSetter =40 desktopSetter ?? { url, screen in41 try workspace.setDesktopImageURL(42 url, for: screen, options: [:]43 )44 }45 self.mainScreen = mainScreen46 self.firstScreen = firstScreen47 self.cacheDirectoryProvider = cacheDirectoryProvider48 }4950 private static func defaultCacheDirectory() -> URL? {51 guard52 let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first53 else { return nil }54 let bundleID = Bundle.main.bundleIdentifier ?? "Haven"55 return base.appendingPathComponent(bundleID).appendingPathComponent("WallpaperCache")56 }5758 59 60 61 62 63 64 65 func download(from url: URL) async throws -> URL {66 let (data, response) = try await session.data(from: url)67 guard let response = response as? HTTPURLResponse,68 (200...299).contains(response.statusCode)69 else {70 throw WallhavenError.apiError("Wallpaper download failed")71 }7273 let fileURL = Self.makeTempURL(for: url)74 try data.write(to: fileURL)75 return fileURL76 }7778 79 80 81 82 83 84 85 86 87 88 89 90 func cachedLocalFile(for url: URL) async throws -> URL {91 guard let cacheDirectory = cacheDirectoryProvider() else {92 return try await download(from: url)93 }94 let destination = cacheDirectory.appendingPathComponent(Self.cacheFilename(for: url))95 if FileManager.default.fileExists(atPath: destination.path) {96 return destination97 }9899 let downloadedURL = try await download(from: url)100 try FileManager.default.createDirectory(101 at: cacheDirectory, withIntermediateDirectories: true)102 if FileManager.default.fileExists(atPath: destination.path) {103 try? FileManager.default.removeItem(at: destination)104 }105 try FileManager.default.moveItem(at: downloadedURL, to: destination)106 return destination107 }108109 110 111 112 113 114 static func cacheFilename(for url: URL) -> String {115 let name = url.lastPathComponent116 if !name.isEmpty { return name }117 return url.absoluteString.addingPercentEncoding(withAllowedCharacters: .alphanumerics)118 ?? "wallpaper"119 }120121 122 123 124 125 126 static func makeTempURL(for url: URL) -> URL {127 let fileExtension = url.pathExtension.isEmpty ? "img" : url.pathExtension128 let stem = UUID().uuidString129 let filename = "haven-\(stem).\(fileExtension)"130 return FileManager.default.temporaryDirectory.appendingPathComponent(filename)131 }132133 134 @MainActor135 func setDesktopImage(from fileURL: URL, screen: NSScreen? = nil) throws {136 if let screen = screen {137 try desktopSetter(fileURL, screen)138 return139 }140 141 142 143 144 145 let target: NSScreen146 if let main = mainScreen() {147 target = main148 } else if let first = firstScreen() {149 target = first150 } else {151 throw WallhavenError.apiError("No screen is available to set the desktop image")152 }153 try desktopSetter(fileURL, target)154 }155156 157 158 159 160 161 162 163 164 165 func saveToFolder(from sourceURL: URL, to folderURL: URL) throws {166 167 try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true)168169 let destinationURL = folderURL.appendingPathComponent(sourceURL.lastPathComponent)170171 if FileManager.default.fileExists(atPath: destinationURL.path) {172 let sourceData = try Data(contentsOf: sourceURL)173 let existingData = try Data(contentsOf: destinationURL)174 if sourceData == existingData {175 return176 }177 try FileManager.default.removeItem(at: destinationURL)178 }179180 try FileManager.default.copyItem(at: sourceURL, to: destinationURL)181 }182183 184 @MainActor185 func setWallpaper(from url: URL, screen: NSScreen? = nil) async throws {186 let localURL = try await download(from: url)187 try setDesktopImage(from: localURL, screen: screen)188 }189}