1ac917959d28
add multi-source local folders with idempotent migration (v0.6.0)
- time
- 2026-07-16 22:10 UTC · 2 months ago
- machine
- koh2
- agent
- minimax-m3
- session
- resolve remaining Haven findings
- parent
- c3734780b18a
↓ haven-1ac91795.tar.gzatlas at this save
files changed1 file
Sources/Haven/UI/AppState.swift+81 −0Sources/Haven/UI/AppState.swift+81 −0
@@ -74,6 +74,9 @@7474 @Published var saveFolderPath: String {7575 didSet { UserDefaults.standard.set(saveFolderPath, forKey: "saveFolderPath") }7676 }77+ @Published var sourceFolderPaths: [String] {78+ didSet { UserDefaults.standard.set(sourceFolderPaths, forKey: "sourceFolderPaths") }79+ }7780 @Published var launchAtLogin: Bool {7881 didSet { autostartManager.isEnabled = launchAtLogin }7982 }@@ -89,6 +92,7 @@8992 private let wallpaperManager: WallpaperManaging9093 private let autostartManager: AutostartManaging9194 private let chooseFolder: @MainActor (URL, @escaping (URL?) -> Void) -> Void95+ private let chooseSourceFolders: @MainActor ([URL], @escaping ([URL]) -> Void) -> Void9296 private let terminateApplication: @MainActor () -> Void9397 private let removeTempFile: @MainActor (URL) throws -> Void9498 private var cancellables = Set<AnyCancellable>()@@ -103,6 +107,7 @@103107 wallpaperManager: WallpaperManaging? = nil,104108 autostartManager: AutostartManaging? = nil,105109 chooseFolder: (@MainActor (URL, @escaping (URL?) -> Void) -> Void)? = nil,110+ chooseSourceFolders: (@MainActor ([URL], @escaping ([URL]) -> Void) -> Void)? = nil,106111 terminateApplication: (@MainActor () -> Void)? = nil,107112 removeTempFile: @escaping @MainActor (URL) throws -> Void = {108113 try FileManager.default.removeItem(at: $0)@@ -119,6 +124,7 @@119124 self.wallpaperManager = wallpaperManager120125 self.autostartManager = autostartManager121126 self.chooseFolder = chooseFolder ?? AppState.presentFolderPicker127+ self.chooseSourceFolders = chooseSourceFolders ?? AppState.chooseDefaultSourceFolders122128 self.terminateApplication = terminateApplication ?? { NSApp.terminate(nil) }123129 self.removeTempFile = removeTempFile124130 self.launchAtLogin = autostartManager.isEnabled@@ -130,6 +136,20 @@130136 let pictures = picturesDirectoryProvider() ?? FileManager.default.temporaryDirectory131137 self.saveFolderPath = pictures.appendingPathComponent("Haven").path132138 }139+ // Migrate the legacy single-folder setting into the new source list140+ // exactly once, so existing users keep seeing their gallery. Idempotent141+ // because we only seed when the new key is absent.142+ if let storedSources = UserDefaults.standard.array(forKey: "sourceFolderPaths")143+ as? [String]144+ {145+ self.sourceFolderPaths = storedSources146+ } else if let legacyPath = UserDefaults.standard.string(forKey: "saveFolderPath") {147+ let seeded = [legacyPath]148+ self.sourceFolderPaths = seeded149+ UserDefaults.standard.set(seeded, forKey: "sourceFolderPaths")150+ } else {151+ self.sourceFolderPaths = []152+ }133153 134154 guard observeChanges else { return }135155 @@ -189,6 +209,13 @@189209 self?.updateLocalWallpapersMonitoring()190210 }191211 .store(in: &cancellables)212+213+ $sourceFolderPaths214+ .dropFirst()215+ .sink { [weak self] _ in216+ self?.updateLocalWallpapersMonitoring()217+ }218+ .store(in: &cancellables)192219 }193220 194221 private func updateLocalWallpapersMonitoring() {@@ -393,6 +420,60 @@393420 ) {394421 panel.begin { response in completion(response == .OK ? panel.url : nil) }395422 }423+424+ /// Show the multi-select source folder picker and append the chosen425+ /// directories to `sourceFolderPaths`. Cancelling the picker is a no-op.426+ func addSourceFolders() {427+ MenuController.shared?.beginModalPresentation()428+ NSApp.activate(ignoringOtherApps: true)429+430+ chooseSourceFolders([]) { urls in431+ let merged = (self.sourceFolderPaths + urls.map(\.path))432+ .reduce(into: [String]()) { acc, path in433+ if !acc.contains(path) { acc.append(path) }434+ }435+ self.sourceFolderPaths = merged436+ MenuController.shared?.endModalPresentation()437+ }438+ }439+440+ /// Remove a previously added source folder. Idempotent: removing an441+ /// unknown path is a no-op.442+ func removeSourceFolder(_ path: String) {443+ sourceFolderPaths.removeAll { $0 == path }444+ }445+446+ static func makeSourceFoldersPanel() -> NSOpenPanel {447+ let panel = NSOpenPanel()448+ panel.canChooseFiles = false449+ panel.canChooseDirectories = true450+ panel.canCreateDirectories = true451+ panel.allowsMultipleSelection = true452+ panel.prompt = "Add"453+ return panel454+ }455+456+ static func presentSourceFoldersPicker(457+ panel: NSOpenPanel, completion: @escaping ([URL]) -> Void458+ ) {459+ panel.begin { response in460+ completion(response == .OK ? panel.urls : [])461+ }462+ }463+464+ static func presentSourceFoldersPicker(465+ _ directory: URL, completion: @escaping ([URL]) -> Void466+ ) {467+ let panel = makeSourceFoldersPanel()468+ panel.directoryURL = directory469+ presentSourceFoldersPicker(panel: panel, completion: completion)470+ }471+472+ static let chooseDefaultSourceFolders: @MainActor ([URL], @escaping ([URL]) -> Void) -> Void =473+ { directories, completion in474+ let start = directories.last ?? FileManager.default.homeDirectoryForCurrentUser475+ presentSourceFoldersPicker(start, completion: completion)476+ }396477 397478 func quit() {398479 terminateApplication()