1import Combine2import CoreGraphics3import Foundation4import ImageIO56struct LocalWallpaper: Identifiable, Equatable {7 let id: String8 let url: URL9 let filename: String10 let resolution: String11 let fileSize: Int6412 let createdAt: Date13 let modifiedAt: Date1415 var thumbnailURL: URL? {16 return url17 }1819 static func == (lhs: LocalWallpaper, rhs: LocalWallpaper) -> Bool {20 return lhs.id == rhs.id21 }22}2324@MainActor25class LocalWallpapersManager: ObservableObject {26 static let shared = LocalWallpapersManager()2728 @Published var wallpapers: [LocalWallpaper] = []29 @Published var isLoading: Bool = false30 @Published var errorMessage: String?3132 private var folderMonitor: FolderMonitor?33 private let imageExtensions: Set<String> = ["jpg", "jpeg", "png", "webp", "bmp", "gif"]3435 init() {36 37 if let folderPath = UserDefaults.standard.string(forKey: "saveFolderPath") {38 startMonitoring(folderPath: folderPath)39 }40 }4142 func startMonitoring(folderPath: String) {43 folderMonitor?.stop()44 folderMonitor = FolderMonitor(folderPath: folderPath) { [weak self] in45 Task { @MainActor in46 self?.scanFolder(path: folderPath)47 }48 }49 folderMonitor?.start()50 scanFolder(path: folderPath)51 }5253 func stopMonitoring() {54 folderMonitor?.stop()55 folderMonitor = nil56 }5758 func scanFolder(path: String) {59 guard !isLoading else { return }60 isLoading = true61 errorMessage = nil6263 Task.detached(priority: .userInitiated) { [weak self] in64 guard let self = self else { return }6566 let folderURL = URL(fileURLWithPath: path)67 let fileManager = FileManager.default6869 70 71 try? fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true)7273 do {74 let contents = try fileManager.contentsOfDirectory(75 at: folderURL,76 includingPropertiesForKeys: [77 .contentTypeKey, .fileSizeKey, .creationDateKey, .contentModificationDateKey,78 ],79 options: [.skipsHiddenFiles, .skipsPackageDescendants]80 )8182 var wallpapers: [LocalWallpaper] = []83 for url in contents {84 let ext = url.pathExtension.lowercased()85 guard self.imageExtensions.contains(ext) else { continue }86 if let wallpaper = Self.wallpaper(at: url) { wallpapers.append(wallpaper) }87 }8889 wallpapers.sort { $0.modifiedAt > $1.modifiedAt }9091 let finalWallpapers = wallpapers92 await MainActor.run {93 self.wallpapers = finalWallpapers94 self.isLoading = false95 }96 } catch {97 await MainActor.run {98 self.errorMessage = "Failed to scan folder: \(error.localizedDescription)"99 self.isLoading = false100 }101 }102 }103 }104105 106 107 108 nonisolated private static func imageResolution(url: URL) -> String? {109 guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),110 let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],111 let width = properties[kCGImagePropertyPixelWidth as String] as? NSNumber,112 let height = properties[kCGImagePropertyPixelHeight as String] as? NSNumber,113 width.intValue > 0, height.intValue > 0114 else {115 return nil116 }117 return "\(width.intValue)x\(height.intValue)"118 }119120 nonisolated static func wallpaper(at url: URL) -> LocalWallpaper? {121 guard122 let resourceValues = try? url.resourceValues(forKeys: [123 .fileSizeKey, .creationDateKey, .contentModificationDateKey,124 ]),125 let resolution = imageResolution(url: url)126 else { return nil }127 return LocalWallpaper(128 id: url.path, url: url, filename: url.lastPathComponent, resolution: resolution,129 fileSize: Int64(resourceValues.fileSize ?? 0),130 createdAt: resourceValues.creationDate ?? Date(),131 modifiedAt: resourceValues.contentModificationDate ?? Date())132 }133134 func deleteWallpaper(_ wallpaper: LocalWallpaper) {135 do {136 try FileManager.default.removeItem(at: wallpaper.url)137 wallpapers.removeAll { $0.id == wallpaper.id }138 } catch {139 errorMessage = "Failed to delete: \(error.localizedDescription)"140 }141 }142}143144145146class FolderMonitor {147 private let folderPath: String148 private let callback: () -> Void149 private var fileDescriptor: Int32 = -1150 private var source: DispatchSourceFileSystemObject?151 private let queue = DispatchQueue(label: "com.haven.folderMonitor", qos: .utility)152153 init(folderPath: String, fileDescriptor: Int32 = -1, callback: @escaping () -> Void) {154 self.folderPath = folderPath155 self.fileDescriptor = fileDescriptor156 self.callback = callback157 }158159 func start() {160 fileDescriptor = open(folderPath, O_EVTONLY)161 guard fileDescriptor >= 0 else { return }162163 source = DispatchSource.makeFileSystemObjectSource(164 fileDescriptor: fileDescriptor,165 eventMask: [.write, .rename, .delete, .link],166 queue: queue167 )168169 source?.setEventHandler { [weak self] in170 DispatchQueue.main.async {171 self?.callback()172 }173 }174175 source?.setCancelHandler { [weak self] in self?.closeFileDescriptor() }176177 source?.resume()178 }179180 func stop() {181 source?.cancel()182 source = nil183 }184185 func closeFileDescriptor() {186 guard fileDescriptor >= 0 else { return }187 close(fileDescriptor)188 fileDescriptor = -1189 }190191 deinit {192 stop()193 }194}