Cached indexes: - NASManifestCache: actor-based disk cache for the NAS manifest (kisani_manifest_cache.json) Avoids NAS network round-trip when cache < 5 min old. Updated after every manifest download. - LocalPhotoIndex: actor-based disk cache of phone asset metadata (kisani_local_index.json) Built once on first launch, then updated incrementally via PHPhotoLibraryChangeObserver. O(1) countSafe via in-memory dict (vs PHFetchResult batch enumeration + CoreData calls). Fast path reconcile: When both caches are warm, reconcile() skips NAS connection + PHFetchResult enumeration entirely and returns in microseconds. Falls through to full NAS path only when stale. AutoBackupCoordinator.onActive(): Changed force: true → force: false so the 30-second debounce prevents expensive rescans on rapid app foreground/background cycles. Dashboard appears instantly from cached snapshot. BackupQueueItem + queue tracking: BackupEngine publishes queueItems: [BackupQueueItem] with per-file status, progress, speed, error details. Queue is persisted to disk and reloaded on next launch so SyncView always has data to show even after restart. NotificationService: Centralised local notification sender replacing the inline UNMutableNotificationContent blocks. Sends for: backup started, completed, failed, paused, NAS offline. Uses category identifiers so same-type notifications replace each other. SyncView redesign: - Removed pill/chip around NAS IP — replaced with plain "Online/Offline/Checking" dot+text - No live NAS directory listing (was expensive NAS connection on every tab open) - Shows engine.queueItems grouped into Active / Failed / Completed sections - Per-file rows with inline progress bar + bytes/speed for uploading items, error text for failed - Idle state shows archive count from statusService snapshot GalleryViewModel: NAS tab and All tab now use NASManifestCache as fallback when statusService.lastManifest is nil (offline launch). Gallery shows NAS items even when NAS is unreachable. AppDelegate: Bootstraps LocalPhotoIndex.loadOrBuild() in background on launch (non-blocking). Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
66 lines
2.1 KiB
Swift
66 lines
2.1 KiB
Swift
import UserNotifications
|
|
import Foundation
|
|
|
|
/// Centralised local-notification sender.
|
|
///
|
|
/// Uses notification identifiers as categories — posting the same id replaces
|
|
/// any previous notification in that category, preventing stale banners from
|
|
/// stacking up.
|
|
enum NotificationService {
|
|
static func notifyBackupStarted(count: Int) {
|
|
guard count > 0 else { return }
|
|
post(
|
|
id: "kisani.backup.started",
|
|
title: "Backup started",
|
|
body: "Kisani is backing up \(count) new \(count == 1 ? "item" : "items")."
|
|
)
|
|
}
|
|
|
|
static func notifyBackupCompleted(uploaded: Int, total: Int) {
|
|
post(
|
|
id: "kisani.backup.completed",
|
|
title: "Backup complete",
|
|
body: total > 0
|
|
? "All \(total) photos are safe."
|
|
: "\(uploaded) \(uploaded == 1 ? "item" : "items") backed up."
|
|
)
|
|
}
|
|
|
|
static func notifyBackupPaused(pendingCount: Int) {
|
|
post(
|
|
id: "kisani.backup.paused",
|
|
title: "Backup paused",
|
|
body: pendingCount > 0
|
|
? "Kisani will continue \(pendingCount) \(pendingCount == 1 ? "item" : "items") when your NAS is reachable."
|
|
: "Kisani will continue when your NAS is reachable."
|
|
)
|
|
}
|
|
|
|
static func notifyBackupFailed(count: Int) {
|
|
post(
|
|
id: "kisani.backup.failed",
|
|
title: "Backup failed",
|
|
body: "\(count) \(count == 1 ? "item" : "items") could not be backed up. Tap to review."
|
|
)
|
|
}
|
|
|
|
static func notifyNASOffline() {
|
|
post(
|
|
id: "kisani.nas.offline",
|
|
title: "NAS unreachable",
|
|
body: "Kisani will resume backup when your NAS is back online."
|
|
)
|
|
}
|
|
|
|
// MARK: — Private
|
|
|
|
private static func post(id: String, title: String, body: String) {
|
|
let content = UNMutableNotificationContent()
|
|
content.title = title
|
|
content.body = body
|
|
content.sound = .default
|
|
let request = UNNotificationRequest(identifier: id, content: content, trigger: nil)
|
|
UNUserNotificationCenter.current().add(request) { _ in }
|
|
}
|
|
}
|