its whats on the tin; culls raw photos
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: interleave loading front and behind

+25 -8
+8 -2
cull/Services/ThumbnailCache.swift
··· 7 7 private let memoryCache = NSCache<NSString, NSImage>() 8 8 private let previewCache = NSCache<NSString, NSImage>() 9 9 private var previewKeys = Set<String>() 10 + private var preloadTask: Task<Void, Never>? 10 11 private let diskCacheURL: URL 11 12 private let maxPixelSize: Int 12 13 ··· 206 207 } 207 208 208 209 /// Fire-and-forget: preload previews in background. Used during navigation. 210 + /// Cancels any previous preload so stale work doesn't compete. 209 211 func preloadPreviews(photos: [Photo]) { 212 + preloadTask?.cancel() 213 + 210 214 let work: [(String, URL)] = photos.compactMap { photo in 211 215 let key = photo.url.absoluteString 212 216 guard previewCache.object(forKey: key as NSString) == nil else { return nil } ··· 216 220 217 221 let pc = previewCache 218 222 219 - Task.detached(priority: .utility) { 223 + preloadTask = Task.detached(priority: .utility) { 220 224 for batchStart in stride(from: 0, to: work.count, by: 4) { 225 + guard !Task.isCancelled else { return } 221 226 let batch = Array(work[batchStart..<min(batchStart + 4, work.count)]) 222 227 await withTaskGroup(of: (String, NSImage?).self) { group in 223 228 for (key, url) in batch { 224 229 group.addTask { 225 - (key, Self.loadFullPreviewSync(from: url)) 230 + guard !Task.isCancelled else { return (key, nil) } 231 + return (key, Self.loadFullPreviewSync(from: url)) 226 232 } 227 233 } 228 234 for await (key, image) in group {
+17 -6
cull/Views/PhotoViewer.swift
··· 72 72 guard let photo = session.selectedPhoto else { return } 73 73 let photoID = photo.id 74 74 75 + // If full-res is already cached, show it immediately 76 + if let cached = cache.cachedPreview(for: photo) { 77 + displayImage = cached 78 + } 79 + 80 + // Wait for user to stop navigating before doing any loading 81 + try? await Task.sleep(for: .milliseconds(100)) 82 + guard !Task.isCancelled, displayedPhotoID == photoID else { return } 83 + 75 84 // Load current photo's full-res preview 76 85 if cache.cachedPreview(for: photo) == nil { 77 86 if let full = await cache.previewImage(for: photo) { ··· 80 89 } 81 90 } 82 91 83 - // Debounce: wait briefly before preloading window 84 - // If user is holding arrow keys, this task gets cancelled before preload fires 85 - guard displayedPhotoID == photoID else { return } 86 - try? await Task.sleep(for: .milliseconds(150)) 92 + // Preload window fanning out from current position (closest first) 87 93 guard !Task.isCancelled, displayedPhotoID == photoID else { return } 88 - 89 94 let ahead = session.photosAhead(lookaheadCount) 90 95 let behind = session.photosBehind(lookbehindCount) 91 - let window = behind + [photo] + ahead 96 + var fanOut: [Photo] = [] 97 + let maxLen = max(ahead.count, behind.count) 98 + for i in 0..<maxLen { 99 + if i < ahead.count { fanOut.append(ahead[i]) } 100 + if i < behind.count { fanOut.append(behind[i]) } 101 + } 102 + let window = [photo] + fanOut 92 103 cache.preloadPreviews(photos: window) 93 104 cache.evictPreviews(keeping: window) 94 105 }