From 20b2b8eeb864529699f2acac8d2751185bb14ac7 Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Tue, 19 May 2026 00:58:17 +0300 Subject: [PATCH] =?UTF-8?q?Gallery:=20fix=20sync=20dot=20colors=20?= =?UTF-8?q?=E2=80=94=20match=20phone=20photos=20against=20NAS=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-Authored-By: Sentry --- Features/Browse/GalleryViewModel.swift | 33 ++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/Features/Browse/GalleryViewModel.swift b/Features/Browse/GalleryViewModel.swift index 6efc368..a719921 100644 --- a/Features/Browse/GalleryViewModel.swift +++ b/Features/Browse/GalleryViewModel.swift @@ -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(minimumCapacity: manifest.count) + // allNASFilenames tracks every filename already represented so Phase 3 skips duplicates + var allNASFilenames = Set(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() - // 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,