Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
import Foundation
|
|
|
|
|
import Combine
|
|
|
|
|
|
|
|
|
|
enum BackupStatus: Equatable {
|
|
|
|
|
case idle
|
|
|
|
|
case preparing
|
|
|
|
|
case running
|
|
|
|
|
case paused
|
|
|
|
|
case completed
|
|
|
|
|
case failed(String)
|
|
|
|
|
case cancelled
|
|
|
|
|
|
|
|
|
|
var isActive: Bool {
|
|
|
|
|
switch self {
|
|
|
|
|
case .running, .paused, .preparing: return true
|
|
|
|
|
default: return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
final class BackupJob: ObservableObject {
|
|
|
|
|
@Published private(set) var status: BackupStatus = .idle
|
|
|
|
|
@Published private(set) var totalFiles: Int = 0
|
|
|
|
|
@Published private(set) var uploadedFiles: Int = 0
|
|
|
|
|
@Published private(set) var skippedFiles: Int = 0
|
|
|
|
|
@Published private(set) var failedFiles: Int = 0
|
|
|
|
|
@Published private(set) var currentFileName: String = ""
|
|
|
|
|
@Published private(set) var currentFileSizeBytes: Int64 = 0
|
|
|
|
|
@Published private(set) var bytesTransferred: Int64 = 0
|
|
|
|
|
@Published private(set) var totalBytes: Int64 = 0
|
|
|
|
|
@Published private(set) var speedBytesPerSecond: Double = 0
|
|
|
|
|
@Published private(set) var startDate: Date?
|
|
|
|
|
@Published private(set) var endDate: Date?
|
|
|
|
|
|
|
|
|
|
var progress: Double {
|
|
|
|
|
guard totalFiles > 0 else { return 0 }
|
fix(signing): remove Access Wi-Fi entitlement (unsupported on free teams)
- Drop com.apple.developer.networking.wifi-info from project.yml entitlements
and NSLocationWhenInUseUsageDescription from Info.plist
- SSID detection gracefully returns nil without the capability; all UI handles
nil SSID already via optional chaining
feat: branding, ring polish, sign-out UX, version/support
- LoginView: replace title with "kisani." monospaced wordmark + UGREEN logo,
show username then NAS display name after login
- BackupView: "kisani." + UGREEN logo chip in toolbar; ring color is orange
(in-progress), green (completed), destructive (failed), dim (idle)
- BackupJob: cap progress at min(1.0, ...) to prevent >100% display
- SettingsView sign-out dialog: two actions — "Sign Out" (lock only, keeps NAS)
and "Sign Out & Forget NAS" (full clear); clearer message
- SettingsView ACCOUNT section: App Version v1.0 + Support mailto rows
- AppMenuView: About section with kisani. wordmark, version, support link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 18:39:35 +03:00
|
|
|
return min(1.0, Double(uploadedFiles + skippedFiles + failedFiles) / Double(totalFiles))
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var eta: TimeInterval? {
|
|
|
|
|
guard speedBytesPerSecond > 0, totalBytes > bytesTransferred else { return nil }
|
|
|
|
|
return Double(totalBytes - bytesTransferred) / speedBytesPerSecond
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func prepare() { status = .preparing }
|
|
|
|
|
|
|
|
|
|
func start(totalFiles: Int, totalBytes: Int64) {
|
|
|
|
|
self.totalFiles = totalFiles
|
|
|
|
|
self.totalBytes = totalBytes
|
|
|
|
|
self.uploadedFiles = 0
|
|
|
|
|
self.skippedFiles = 0
|
|
|
|
|
self.failedFiles = 0
|
|
|
|
|
self.bytesTransferred = 0
|
|
|
|
|
self.startDate = Date()
|
|
|
|
|
self.endDate = nil
|
|
|
|
|
self.status = .running
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateProgress(fileName: String, fileSize: Int64, bytesTransferred: Int64, speed: Double) {
|
|
|
|
|
self.currentFileName = fileName
|
|
|
|
|
self.currentFileSizeBytes = fileSize
|
|
|
|
|
self.bytesTransferred = bytesTransferred
|
|
|
|
|
self.speedBytesPerSecond = speed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fileCompleted(skipped: Bool = false) {
|
|
|
|
|
if skipped { skippedFiles += 1 } else { uploadedFiles += 1 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fileFailed() { failedFiles += 1 }
|
|
|
|
|
|
|
|
|
|
func finish(result: BackupResult) {
|
|
|
|
|
endDate = Date()
|
2026-05-17 15:30:52 +03:00
|
|
|
status = result.failedCount == 0
|
|
|
|
|
? .completed
|
|
|
|
|
: .failed("Completed with \(result.failedCount) error\(result.failedCount == 1 ? "" : "s")")
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-17 15:58:54 +03:00
|
|
|
/// Call after reconciliation proves needBackup == 0 — upgrades stale .failed to .completed.
|
|
|
|
|
func resolveSuccess() {
|
|
|
|
|
if case .failed = status { status = .completed }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Used when the pending queue is empty at backup start — nothing to do.
|
|
|
|
|
func allAlreadySafe() {
|
|
|
|
|
totalFiles = 0; uploadedFiles = 0; skippedFiles = 0; failedFiles = 0
|
|
|
|
|
startDate = Date(); endDate = Date()
|
|
|
|
|
status = .completed
|
|
|
|
|
}
|
|
|
|
|
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
func fail(error: BackupError) {
|
|
|
|
|
endDate = Date()
|
|
|
|
|
status = .failed(error.errorDescription ?? error.localizedDescription)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cancel() {
|
|
|
|
|
endDate = Date()
|
|
|
|
|
status = .cancelled
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func pause() { if case .running = status { status = .paused } }
|
|
|
|
|
func resume() { if case .paused = status { status = .running } }
|
|
|
|
|
}
|