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,