feat: redesign Gallery as photo timeline with sync state, deduplication, and detail view
- GalleryItem: unified model with phone/nas/synced source, sync state enum - GalleryViewModel: off-actor PHFetchResult + manifest merge, Phone/NAS/All tab filtering, date section grouping, Combine-driven reactivity - GalleryView: sticky date section headers, 3-col grid, shimmer placeholders, sync dots (green/orange/blue), video duration badge, search bar, context menu (Share, Save to Photos, Delete from Device) - GalleryDetailView: full-screen swipeable photo viewer (TabView page mode), pinch-to-zoom, double-tap zoom toggle, bottom metadata sheet with file info and actions - BackupStatusService: expose lastManifest for gallery deduplication without extra NAS connection - ThumbnailCache: add decodeOffThread static helper for off-actor JPEG decode - BackupManifest: Sendable conformance for safe Task.detached capture - ThumbnailCache: synced items load from PHImageManager (instant) instead of NAS Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -13,6 +13,7 @@ final class BackupStatusService: NSObject, ObservableObject {
|
||||
@Published private(set) var snapshot: BackupStatusSnapshot = .empty
|
||||
@Published private(set) var isRefreshing: Bool = false
|
||||
@Published private(set) var refreshError: String?
|
||||
@Published private(set) var lastManifest: BackupManifest?
|
||||
|
||||
private let photoService = PhotoLibraryService()
|
||||
private let cacheKey = "backupStatusSnapshot_v2"
|
||||
@@ -204,12 +205,15 @@ final class BackupStatusService: NSObject, ObservableObject {
|
||||
do {
|
||||
let data = try await transfer.downloadData(at: path)
|
||||
// Decode and build index off main actor — JSONDecoder is not cheap for large manifests.
|
||||
let index: ManifestIndex? = await Task.detached(priority: .utility) {
|
||||
let decoded: (ManifestIndex, BackupManifest)? = await Task.detached(priority: .utility) {
|
||||
guard let manifest = try? JSONDecoder().decode(BackupManifest.self, from: data)
|
||||
else { return nil }
|
||||
return ManifestIndex(manifest: manifest)
|
||||
return (ManifestIndex(manifest: manifest), manifest)
|
||||
}.value
|
||||
if let index { return index }
|
||||
if let (index, manifest) = decoded {
|
||||
lastManifest = manifest
|
||||
return index
|
||||
}
|
||||
} catch {
|
||||
// File not found — fall through to directory listing bootstrap
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user