Gallery: fix sync dot colors — match phone photos against NAS directory

Phase 1 was only checking the Kisani manifest for sync status. Phone photos
uploaded by other tools had no manifest entry, showed as orange (phone-only)
even though they were already on the NAS.

Now Phase 1 falls back to a filename lookup against the full NAS directory
listing: manifest match → green (.synced), NAS directory match → green (.synced),
no match → orange (.phone). Phase 3 skips filenames already resolved in Phase 1.

Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-19 00:58:17 +03:00
parent fae13eba9f
commit 20b2b8eeb8

View File

@@ -175,22 +175,31 @@ final class GalleryViewModel: ObservableObject {
) -> [GalleryItem] {
var byID = [String: ManifestEntry](minimumCapacity: manifest.count)
var byName = [String: ManifestEntry](minimumCapacity: manifest.count)
var manifestFilenames = Set<String>(minimumCapacity: manifest.count)
// allNASFilenames tracks every filename already represented so Phase 3 skips duplicates
var allNASFilenames = Set<String>(minimumCapacity: manifest.count + nasDirectory.count)
for entry in manifest {
if !entry.localIdentifier.isEmpty { byID[entry.localIdentifier] = entry }
let key = entry.filename.lowercased()
if byName[key] == nil { byName[key] = entry }
manifestFilenames.insert(key)
allNASFilenames.insert(key)
}
// Build NAS directory lookup for filename-based sync matching in Phase 1
var nasByName = [String: NASItem](minimumCapacity: nasDirectory.count)
for item in nasDirectory {
let lower = item.name.lowercased()
if nasByName[lower] == nil { nasByName[lower] = item }
}
var result: [GalleryItem] = []
result.reserveCapacity(phone.count + manifest.count + nasDirectory.count / 4)
var coveredFilenames = Set<String>()
// Phase 1: Phone assets matched to manifest = synced, otherwise phone-only
// Phase 1: Phone assets
// Priority: manifest match (Kisani upload) NAS directory match (other tool) phone-only
for (localID, filename, date, duration, isVideo) in phone {
let key = filename.lowercased()
if let entry = byID[localID] ?? byName[key] {
// Kisani manifest match green dot (.synced)
coveredFilenames.insert(entry.filename.lowercased())
result.append(GalleryItem(
id: "synced_\(localID)",
@@ -201,7 +210,20 @@ final class GalleryViewModel: ObservableObject {
mediaType: isVideo ? .video : .photo,
source: .synced(localIdentifier: localID, nasPath: entry.remotePath)
))
} else if let nasItem = nasByName[key] {
// Filename match in NAS directory (uploaded by other tool) green dot (.synced)
allNASFilenames.insert(key) // mark covered so Phase 3 skips it
result.append(GalleryItem(
id: "synced_\(localID)",
filename: filename,
creationDate: date,
duration: duration,
fileSize: nasItem.size > 0 ? nasItem.size : nil,
mediaType: isVideo ? .video : .photo,
source: .synced(localIdentifier: localID, nasPath: nasItem.path)
))
} else {
// Not on NAS orange dot (.phone)
result.append(GalleryItem(
id: "phone_\(localID)",
filename: filename,
@@ -231,14 +253,15 @@ final class GalleryViewModel: ObservableObject {
))
}
// Phase 3: NAS directory items not tracked by the manifest (uploaded by other tools)
// Phase 3: NAS directory items not already shown (uploaded by other tools, not on phone)
for item in nasDirectory {
let lower = item.name.lowercased()
guard !manifestFilenames.contains(lower) else { continue }
guard !allNASFilenames.contains(lower) else { continue }
let ext = (item.name as NSString).pathExtension.lowercased()
let mediaType: GalleryItem.MediaType =
videoExts.contains(ext) ? .video : imageExts.contains(ext) ? .photo : .unknown
guard mediaType != .unknown else { continue }
allNASFilenames.insert(lower)
result.append(GalleryItem(
id: "nas_dir_\(item.path)",
filename: item.name,