4887ee5521b8
fix stale pagination flag and Swift 6 scan capture warning (v0.6.0)
- time
- 2026-07-16 20:58 UTC · 2 months ago
- machine
- koh2
- agent
- minimax-m3
- session
- resolve audited findings
- parent
- 8d1af4a87126
↓ haven-4887ee55.tar.gzatlas at this save
files changed5 files
Sources/Haven/Managers/LocalWallpapersManager.swift+14 −13Sources/Haven/UI/AppState.swift+1 −0Sources/Haven/UI/Views/LocalView.swift+2 −1Tests/HavenTests/AppStateTests.swift+14 −8Tests/HavenTests/ViewCoverageTests.swift+28 −28Sources/Haven/Managers/LocalWallpapersManager.swift+14 −13
@@ -77,20 +77,21 @@7777 generation: requestGeneration,7878 imageExtensions: imageExtensions)7979 80- Task.detached(priority: .userInitiated) { [weak self] in81- let result = LocalWallpapersManager.performScan(request: snapshot)82- await MainActor.run {83- guard let self = self else { return }84- guard self.scanGeneration == snapshot.generation else { return }85- switch result {86- case .success(let wallpapers):87- self.wallpapers = wallpapers88- self.errorMessage = nil89- case .failure(let message):90- self.errorMessage = message91- }92- self.isLoading = false80+ let scanTask = Task.detached(priority: .userInitiated) {81+ LocalWallpapersManager.performScan(request: snapshot)82+ }83+ Task { @MainActor [weak self] in84+ guard let self = self else { return }85+ let result = await scanTask.value86+ guard self.scanGeneration == snapshot.generation else { return }87+ switch result {88+ case .success(let wallpapers):89+ self.wallpapers = wallpapers90+ self.errorMessage = nil91+ case .failure(let message):92+ self.errorMessage = message9393 }94+ self.isLoading = false9495 }9596 }9697 Sources/Haven/UI/AppState.swift+1 −0
@@ -180,6 +180,7 @@180180 181181 func fetchWallpapers(reset: Bool = true, refreshing: Bool = false) async {182182 if reset {183+ isFetchingMore = false183184 searchGeneration &+= 1184185 }185186 let requestGeneration = searchGenerationSources/Haven/UI/Views/LocalView.swift+2 −1
@@ -60,7 +60,8 @@6060 }6161 .alert(6262 "Delete Wallpaper?", isPresented: $showingDeleteConfirmation, presenting: wallpaperToDelete,63- actions: deleteActions, message: deleteMessage)63+ actions: deleteActions, message: deleteMessage64+ )6465 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)6566 .onAppear {6667 if appState.saveToFolderEnabled {Tests/HavenTests/AppStateTests.swift+14 −8
@@ -369,10 +369,12 @@369369 // the first response lands AFTER the second has already committed. The370370 // generation guard should drop the stale first response.371371 search.resume(372- .success(([makeWallpaper(id: "newer")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),372+ .success(373+ ([makeWallpaper(id: "newer")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),373374 for: 2)374375 search.resume(375- .success(([makeWallpaper(id: "stale")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),376+ .success(377+ ([makeWallpaper(id: "stale")], Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),376378 for: 1)377379 378380 _ = await (firstTask.value, secondTask.value)@@ -391,9 +393,11 @@391393 let firstReset = Task { await state.fetchWallpapers() }392394 await search.waitForPending(1)393395 search.resume(394- .success((395- [makeWallpaper(id: "first-reset")],396- Meta(current_page: 1, last_page: 2, per_page: 24, total: 2))),396+ .success(397+ (398+ [makeWallpaper(id: "first-reset")],399+ Meta(current_page: 1, last_page: 2, per_page: 24, total: 2)400+ )),397401 for: 1)398402 399403 // Drain the first reset so isLoading is false, then trigger a pagination.@@ -409,9 +413,11 @@409413 410414 // Resolve new reset first → wallpapers replaced with [newer-reset].411415 search.resume(412- .success((413- [makeWallpaper(id: "newer-reset")],414- Meta(current_page: 1, last_page: 1, per_page: 24, total: 1))),416+ .success(417+ (418+ [makeWallpaper(id: "newer-reset")],419+ Meta(current_page: 1, last_page: 1, per_page: 24, total: 1)420+ )),415421 for: 3)416422 417423 // Then resolve the (now stale) pagination response. Its return valueTests/HavenTests/ViewCoverageTests.swift+28 −28
@@ -236,37 +236,37 @@236236 ))237237 }238238 239-@Suite("Local wallpaper thumbnail downsampling", .serialized)240-@MainActor241-struct LocalWallpaperItemThumbnailTests {242- private static let onePixelPNG = Data(243- base64Encoded:244- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="245- )!239+ @Suite("Local wallpaper thumbnail downsampling", .serialized)240+ @MainActor241+ struct LocalWallpaperItemThumbnailTests {242+ private static let onePixelPNG = Data(243+ base64Encoded:244+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="245+ )!246246 247- @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")248- func thumbnailDecodeAndDownsample() throws {249- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)250- defer { try? FileManager.default.removeItem(at: root) }251- try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)252- let png = root.appendingPathComponent("image.png")253- try Self.onePixelPNG.write(to: png)247+ @Test("makeThumbnail returns an NSImage and downsamples oversized PNGs")248+ func thumbnailDecodeAndDownsample() throws {249+ let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)250+ defer { try? FileManager.default.removeItem(at: root) }251+ try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)252+ let png = root.appendingPathComponent("image.png")253+ try Self.onePixelPNG.write(to: png)254254 255- let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)256- #expect(image != nil)257- // ImageIO preserves the original pixel dimensions for tiny sources, so258- // we only assert that decoding returned something usable.259- #expect(image?.size.width ?? 0 >= 1)260- }255+ let image = LocalWallpaperItem.makeThumbnail(url: png, maxPixel: 320)256+ #expect(image != nil)257+ // ImageIO preserves the original pixel dimensions for tiny sources, so258+ // we only assert that decoding returned something usable.259+ #expect(image?.size.width ?? 0 >= 1)260+ }261261 262- @Test("makeThumbnail returns nil for a missing file rather than trapping")263- func missingFileReturnsNil() {264- let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)265- defer { try? FileManager.default.removeItem(at: root) }266- try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)267- let missing = root.appendingPathComponent("missing.png")262+ @Test("makeThumbnail returns nil for a missing file rather than trapping")263+ func missingFileReturnsNil() {264+ let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)265+ defer { try? FileManager.default.removeItem(at: root) }266+ try? FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)267+ let missing = root.appendingPathComponent("missing.png")268268 269- #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)269+ #expect(LocalWallpaperItem.makeThumbnail(url: missing, maxPixel: 320) == nil)270+ }270271 }271-}272272 }