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:
47
Core/Models/GalleryItem.swift
Normal file
47
Core/Models/GalleryItem.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
|
||||
struct GalleryItem: Identifiable, Sendable {
|
||||
let id: String
|
||||
let filename: String
|
||||
let creationDate: Date?
|
||||
let duration: TimeInterval? // non-nil for video
|
||||
let fileSize: Int64?
|
||||
let mediaType: MediaType
|
||||
let source: ItemSource
|
||||
|
||||
enum MediaType: Sendable { case photo, video, unknown }
|
||||
|
||||
enum ItemSource: Sendable {
|
||||
case phone(localIdentifier: String)
|
||||
case nas(path: String)
|
||||
case synced(localIdentifier: String, nasPath: String)
|
||||
|
||||
var localIdentifier: String? {
|
||||
switch self { case .phone(let id), .synced(let id, _): id; case .nas: nil }
|
||||
}
|
||||
var nasPath: String? {
|
||||
switch self { case .nas(let p), .synced(_, let p): p; case .phone: nil }
|
||||
}
|
||||
}
|
||||
|
||||
enum SyncState: Sendable {
|
||||
case phoneOnly // orange — backed up soon
|
||||
case nasOnly // blue — on NAS, not this device
|
||||
case synced // green — confirmed both places
|
||||
}
|
||||
|
||||
var syncState: SyncState {
|
||||
switch source {
|
||||
case .phone: .phoneOnly
|
||||
case .nas: .nasOnly
|
||||
case .synced: .synced
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GallerySection: Identifiable {
|
||||
var id: String // "2026-05-17" or "unknown"
|
||||
var date: Date
|
||||
var title: String
|
||||
var items: [GalleryItem]
|
||||
}
|
||||
Reference in New Issue
Block a user