fix: always refresh NAS dir count, show connection status, fix screenshot filter reactivity
- buildManifestIndex now always scans the NAS directory on every full reconcile (removed the `totalCount <= 1` guard) so NAS Archive count updates accurately on every manual refresh, not just when the manifest is new/sparse - The dir > manifest check still switches to filename-based matching when the directory has more files than Kisani's manifest (uploaded by other tools) - checkStatusRow shows NAS connection status (orange dot + "NAS Connected" / red dot + "NAS Offline") below the timestamp line - BackupFilter now conforms to Equatable; BackupView re-reconciles immediately when the screenshot/video/RAW filter toggles change Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -20,7 +20,7 @@ protocol PhotoLibraryProtocol: AnyObject {
|
|||||||
func exportAsset(_ asset: PhotoAsset) async throws -> URL
|
func exportAsset(_ asset: PhotoAsset) async throws -> URL
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BackupFilter: Codable, Sendable {
|
struct BackupFilter: Codable, Sendable, Equatable {
|
||||||
var includePhotos: Bool = true
|
var includePhotos: Bool = true
|
||||||
var includeVideos: Bool = true
|
var includeVideos: Bool = true
|
||||||
var includeScreenshots: Bool = false
|
var includeScreenshots: Bool = false
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ struct BackupView: View {
|
|||||||
.onChange(of: store.savedConnection?.remotePath) { _ in
|
.onChange(of: store.savedConnection?.remotePath) { _ in
|
||||||
statusService.refresh(force: true)
|
statusService.refresh(force: true)
|
||||||
}
|
}
|
||||||
|
.onChange(of: store.backupFilter) { _ in
|
||||||
|
statusService.refresh(force: true)
|
||||||
|
}
|
||||||
.onChange(of: engine.job.status) { status in
|
.onChange(of: engine.job.status) { status in
|
||||||
if status == .completed {
|
if status == .completed {
|
||||||
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { ctaFlashGreen = true }
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { ctaFlashGreen = true }
|
||||||
@@ -492,37 +495,60 @@ struct BackupView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var checkStatusRow: some View {
|
private var checkStatusRow: some View {
|
||||||
HStack(spacing: 5) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
if statusService.isRefreshing {
|
HStack(spacing: 5) {
|
||||||
ProgressView()
|
if statusService.isRefreshing {
|
||||||
.scaleEffect(0.5)
|
ProgressView()
|
||||||
.tint(AppTheme.inkQuaternary)
|
.scaleEffect(0.5)
|
||||||
Text("Checking…")
|
.tint(AppTheme.inkQuaternary)
|
||||||
.font(.system(size: 10, weight: .regular))
|
Text("Checking…")
|
||||||
.foregroundStyle(AppTheme.inkQuaternary)
|
.font(.system(size: 10, weight: .regular))
|
||||||
} else if let checked = statusService.snapshot.lastCheckedAt {
|
.foregroundStyle(AppTheme.inkQuaternary)
|
||||||
Image(systemName: statusService.snapshot.connectionState == .offline
|
} else if let checked = statusService.snapshot.lastCheckedAt {
|
||||||
? "wifi.slash" : "checkmark.circle")
|
Image(systemName: statusService.snapshot.connectionState == .offline
|
||||||
.font(.system(size: 9, weight: .regular))
|
? "wifi.slash" : "checkmark.circle")
|
||||||
.foregroundStyle(statusService.snapshot.connectionState == .offline
|
.font(.system(size: 9, weight: .regular))
|
||||||
? .orange : AppTheme.inkQuaternary)
|
.foregroundStyle(statusService.snapshot.connectionState == .offline
|
||||||
Text(checked, style: .relative)
|
? .orange : AppTheme.inkQuaternary)
|
||||||
.font(.system(size: 10, weight: .regular))
|
Text(checked, style: .relative)
|
||||||
.foregroundColor(AppTheme.inkQuaternary)
|
.font(.system(size: 10, weight: .regular))
|
||||||
+ Text(" ago")
|
.foregroundColor(AppTheme.inkQuaternary)
|
||||||
.font(.system(size: 10, weight: .regular))
|
+ Text(" ago")
|
||||||
.foregroundColor(AppTheme.inkQuaternary)
|
.font(.system(size: 10, weight: .regular))
|
||||||
|
.foregroundColor(AppTheme.inkQuaternary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
statusService.refresh(force: true)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
.font(.system(size: 11, weight: .regular))
|
||||||
|
.foregroundStyle(AppTheme.inkQuaternary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
.frame(height: 16)
|
||||||
Button {
|
|
||||||
statusService.refresh(force: true)
|
if statusService.snapshot.connectionState != .unknown {
|
||||||
} label: {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: "arrow.clockwise")
|
Circle()
|
||||||
.font(.system(size: 11, weight: .regular))
|
.fill(nasConnectionStatusColor)
|
||||||
.foregroundStyle(AppTheme.inkQuaternary)
|
.frame(width: 5, height: 5)
|
||||||
|
Text(nasConnectionStatusLabel)
|
||||||
|
.font(.system(size: 9, weight: .regular))
|
||||||
|
.foregroundStyle(nasConnectionStatusColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 16)
|
}
|
||||||
|
|
||||||
|
private var nasConnectionStatusColor: Color {
|
||||||
|
statusService.snapshot.connectionState == .offline
|
||||||
|
? AppTheme.destructive
|
||||||
|
: Color(red: 1.0, green: 0.58, blue: 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nasConnectionStatusLabel: String {
|
||||||
|
statusService.snapshot.connectionState == .offline ? "NAS Offline" : "NAS Connected"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func compactStat(_ value: String, label: String, color: Color) -> some View {
|
private func compactStat(_ value: String, label: String, color: Color) -> some View {
|
||||||
|
|||||||
@@ -308,25 +308,26 @@ final class BackupStatusService: NSObject, ObservableObject {
|
|||||||
lastManifest = manifest
|
lastManifest = manifest
|
||||||
Task { await NASManifestCache.shared.update(manifest) }
|
Task { await NASManifestCache.shared.update(manifest) }
|
||||||
|
|
||||||
// Validity check: if the manifest has ≤ 1 entries it may be corrupt or newly
|
// Always scan directory so the NAS Archive count is accurate on every
|
||||||
// created. Scan the directory to get the real NAS contents. When the directory
|
// full reconcile (not just when the manifest is new/sparse).
|
||||||
// is larger than the manifest, return a filename-based index so countSafe
|
// When the directory has more files than the manifest (uploaded by other
|
||||||
// matches against the actual NAS files, not just the sparse manifest entries.
|
// tools), switch to filename-based matching so countSafe reflects real
|
||||||
if index.totalCount <= 1 {
|
// NAS coverage instead of only Kisani-tracked files.
|
||||||
let items = (try? await Self.withTimeout(12) { try await transfer.listDirectory(at: conn.remotePath) }) ?? []
|
let items = (try? await Self.withTimeout(12) { try await transfer.listDirectory(at: conn.remotePath) }) ?? []
|
||||||
let filteredItems = items.filter {
|
let filteredItems = items.filter {
|
||||||
!$0.isDirectory && !$0.name.hasPrefix(".") && $0.name != BackupManifest.remoteFilename
|
!$0.isDirectory && !$0.name.hasPrefix(".") && $0.name != BackupManifest.remoteFilename
|
||||||
}
|
}
|
||||||
let dirCount = filteredItems.count
|
let dirCount = filteredItems.count
|
||||||
log.info("buildManifestIndex: validity — manifest=\(index.totalCount) dir=\(dirCount) — filename matching")
|
log.info("buildManifestIndex: dir=\(dirCount) manifest=\(index.totalCount)")
|
||||||
if dirCount > index.totalCount {
|
if dirCount > index.totalCount {
|
||||||
await NASManifestCache.shared.setDirectoryFilenames(filteredItems.map { $0.name })
|
await NASManifestCache.shared.setDirectoryFilenames(filteredItems.map { $0.name })
|
||||||
return await Task.detached(priority: .utility) {
|
return await Task.detached(priority: .utility) {
|
||||||
ManifestIndex(nasListing: items)
|
ManifestIndex(nasListing: items)
|
||||||
}.value
|
}.value
|
||||||
}
|
} else {
|
||||||
|
await NASManifestCache.shared.setDirectoryCount(dirCount)
|
||||||
|
return index
|
||||||
}
|
}
|
||||||
return index
|
|
||||||
}
|
}
|
||||||
log.warning("buildManifestIndex: manifest downloaded but failed to decode (\(data.count) bytes)")
|
log.warning("buildManifestIndex: manifest downloaded but failed to decode (\(data.count) bytes)")
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user