1import Foundation2import Combine3import CoreGraphics4import 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: Date14 15 var thumbnailURL: URL? {16 return url17 }18 19 static func == (lhs: LocalWallpaper, rhs: LocalWallpaper) -> Bool {20 return lhs.id == rhs.id21 }22}2324@MainActor25class LocalWallpapersManager: ObservableObject {26 static let shared = LocalWallpapersManager()27 28 @Published var wallpapers: [LocalWallpaper] = []29 @Published var isLoading: Bool = false30 @Published var errorMessage: String?31 32 private var folderMonitor: FolderMonitor?33 private let imageExtensions: Set<String> = ["jpg", "jpeg", "png", "webp", "bmp", "gif"]34 35 private init() {36 37 if let folderPath = UserDefaults.standard.string(forKey: "saveFolderPath") {38 startMonitoring(folderPath: folderPath)39 }40 }41 42 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 }52 53 func stopMonitoring() {54 folderMonitor?.stop()55 folderMonitor = nil56 }57 58 func scanFolder(path: String) {59 guard !isLoading else { return }60 isLoading = true61 errorMessage = nil62 63 Task.detached(priority: .userInitiated) { [weak self] in64 guard let self = self else { return }65 66 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: [.contentTypeKey, .fileSizeKey, .creationDateKey, .contentModificationDateKey],77 options: [.skipsHiddenFiles, .skipsPackageDescendants]78 )79 80 var wallpapers: [LocalWallpaper] = []81 for url in contents {82 let ext = url.pathExtension.lowercased()83 guard self.imageExtensions.contains(ext) else { continue }84 85 do {86 let resourceValues = try url.resourceValues(forKeys: [87 .fileSizeKey,88 .creationDateKey,89 .contentModificationDateKey90 ])9192 93 94 95 guard let resolution = Self.imageResolution(url: url) else { continue }9697 let filename = url.lastPathComponent98 let fileSize = Int64(resourceValues.fileSize ?? 0)99 let createdAt = resourceValues.creationDate ?? Date()100 let modifiedAt = resourceValues.contentModificationDate ?? Date()101102 wallpapers.append(LocalWallpaper(103 id: url.path,104 url: url,105 filename: filename,106 resolution: resolution,107 fileSize: fileSize,108 createdAt: createdAt,109 modifiedAt: modifiedAt110 ))111 } catch {112 continue113 }114 }115 116 wallpapers.sort { $0.modifiedAt > $1.modifiedAt }117 118 let finalWallpapers = wallpapers119 await MainActor.run {120 self.wallpapers = finalWallpapers121 self.isLoading = false122 }123 } catch {124 await MainActor.run {125 self.errorMessage = "Failed to scan folder: \(error.localizedDescription)"126 self.isLoading = false127 }128 }129 }130 }131 132 133 134 135 nonisolated private static func imageResolution(url: URL) -> String? {136 guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),137 let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],138 let width = properties[kCGImagePropertyPixelWidth as String] as? NSNumber,139 let height = properties[kCGImagePropertyPixelHeight as String] as? NSNumber,140 width.intValue > 0, height.intValue > 0 else {141 return nil142 }143 return "\(width.intValue)x\(height.intValue)"144 }145 146 func deleteWallpaper(_ wallpaper: LocalWallpaper) {147 do {148 try FileManager.default.removeItem(at: wallpaper.url)149 wallpapers.removeAll { $0.id == wallpaper.id }150 } catch {151 errorMessage = "Failed to delete: \(error.localizedDescription)"152 }153 }154}155156157158private class FolderMonitor {159 private let folderPath: String160 private let callback: () -> Void161 private var fileDescriptor: Int32 = -1162 private var source: DispatchSourceFileSystemObject?163 private let queue = DispatchQueue(label: "com.haven.folderMonitor", qos: .utility)164 165 init(folderPath: String, callback: @escaping () -> Void) {166 self.folderPath = folderPath167 self.callback = callback168 }169 170 func start() {171 fileDescriptor = open(folderPath, O_EVTONLY)172 guard fileDescriptor >= 0 else { return }173 174 source = DispatchSource.makeFileSystemObjectSource(175 fileDescriptor: fileDescriptor,176 eventMask: [.write, .rename, .delete, .link],177 queue: queue178 )179 180 source?.setEventHandler { [weak self] in181 DispatchQueue.main.async {182 self?.callback()183 }184 }185 186 source?.setCancelHandler { [weak self] in187 guard let self = self else { return }188 close(self.fileDescriptor)189 }190 191 source?.resume()192 }193 194 func stop() {195 source?.cancel()196 source = nil197 }198 199 deinit {200 stop()201 }202}