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:
@@ -175,22 +175,31 @@ final class GalleryViewModel: ObservableObject {
|
|||||||
) -> [GalleryItem] {
|
) -> [GalleryItem] {
|
||||||
var byID = [String: ManifestEntry](minimumCapacity: manifest.count)
|
var byID = [String: ManifestEntry](minimumCapacity: manifest.count)
|
||||||
var byName = [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 {
|
for entry in manifest {
|
||||||
if !entry.localIdentifier.isEmpty { byID[entry.localIdentifier] = entry }
|
if !entry.localIdentifier.isEmpty { byID[entry.localIdentifier] = entry }
|
||||||
let key = entry.filename.lowercased()
|
let key = entry.filename.lowercased()
|
||||||
if byName[key] == nil { byName[key] = entry }
|
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] = []
|
var result: [GalleryItem] = []
|
||||||
result.reserveCapacity(phone.count + manifest.count + nasDirectory.count / 4)
|
result.reserveCapacity(phone.count + manifest.count + nasDirectory.count / 4)
|
||||||
var coveredFilenames = Set<String>()
|
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 {
|
for (localID, filename, date, duration, isVideo) in phone {
|
||||||
let key = filename.lowercased()
|
let key = filename.lowercased()
|
||||||
if let entry = byID[localID] ?? byName[key] {
|
if let entry = byID[localID] ?? byName[key] {
|
||||||
|
// Kisani manifest match → green dot (.synced)
|
||||||
coveredFilenames.insert(entry.filename.lowercased())
|
coveredFilenames.insert(entry.filename.lowercased())
|
||||||
result.append(GalleryItem(
|
result.append(GalleryItem(
|
||||||
id: "synced_\(localID)",
|
id: "synced_\(localID)",
|
||||||
@@ -201,7 +210,20 @@ final class GalleryViewModel: ObservableObject {
|
|||||||
mediaType: isVideo ? .video : .photo,
|
mediaType: isVideo ? .video : .photo,
|
||||||
source: .synced(localIdentifier: localID, nasPath: entry.remotePath)
|
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 {
|
} else {
|
||||||
|
// Not on NAS → orange dot (.phone)
|
||||||
result.append(GalleryItem(
|
result.append(GalleryItem(
|
||||||
id: "phone_\(localID)",
|
id: "phone_\(localID)",
|
||||||
filename: filename,
|
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 {
|
for item in nasDirectory {
|
||||||
let lower = item.name.lowercased()
|
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 ext = (item.name as NSString).pathExtension.lowercased()
|
||||||
let mediaType: GalleryItem.MediaType =
|
let mediaType: GalleryItem.MediaType =
|
||||||
videoExts.contains(ext) ? .video : imageExts.contains(ext) ? .photo : .unknown
|
videoExts.contains(ext) ? .video : imageExts.contains(ext) ? .photo : .unknown
|
||||||
guard mediaType != .unknown else { continue }
|
guard mediaType != .unknown else { continue }
|
||||||
|
allNASFilenames.insert(lower)
|
||||||
result.append(GalleryItem(
|
result.append(GalleryItem(
|
||||||
id: "nas_dir_\(item.path)",
|
id: "nas_dir_\(item.path)",
|
||||||
filename: item.name,
|
filename: item.name,
|
||||||
|
|||||||
Reference in New Issue
Block a user