import Foundation import UIKit enum ConnectField { case host, username, password } enum ConnectionState: Equatable { case idle case authenticating case authenticated case destinationSelected case failed(String) var isAuthenticated: Bool { switch self { case .authenticated, .destinationSelected: return true; default: return false } } var errorMessage: String? { if case .failed(let msg) = self { return msg } return nil } } @MainActor final class ConnectViewModel: ObservableObject { @Published var host: String = "" @Published var username: String = "" @Published var password: String = "" @Published var selectedProtocol: NASProtocol = .smb @Published var selectedDestination: String = "" @Published var connectionState: ConnectionState = .idle @Published var showFolderBrowser = false init() { if let conn = ConnectionStore.shared.savedConnection { host = conn.host username = conn.username password = conn.password selectedProtocol = conn.nasProtocol selectedDestination = conn.remotePath == "/" ? "" : conn.remotePath connectionState = conn.remotePath == "/" ? .authenticated : .destinationSelected } } var canConnect: Bool { !host.trimmingCharacters(in: .whitespaces).isEmpty && !username.isEmpty && !password.isEmpty && connectionState != .authenticating } var canContinue: Bool { connectionState == .destinationSelected && !selectedDestination.isEmpty } var statusMessage: String { switch connectionState { case .idle: return "Connect to your NAS to choose a destination" case .authenticating: return "Connecting to NAS…" case .authenticated: return "Authenticated — select a destination folder" case .destinationSelected: return "Backup destination ready" case .failed(let msg): return msg } } func authenticate() async { let trimmedHost = host.trimmingCharacters(in: .whitespaces) connectionState = .authenticating let service: any NASTransferProtocol = selectedProtocol == .smb ? SMBService() : SFTPService() let connection = NASConnection( host: trimmedHost, nasProtocol: selectedProtocol, username: username, password: password, remotePath: selectedDestination.isEmpty ? "/" : selectedDestination ) do { try await service.connect( to: connection.host, port: connection.port, username: connection.username, password: connection.password ) service.disconnect() ConnectionStore.shared.savedConnection = connection connectionState = .authenticated UIImpactFeedbackGenerator(style: .medium).impactOccurred() } catch let e as BackupError { connectionState = .failed(e.errorDescription ?? e.localizedDescription) } catch { connectionState = .failed(error.localizedDescription) } } func confirmDestination(_ path: String) { guard !path.isEmpty, path != "/" else { return } selectedDestination = path connectionState = .destinationSelected guard var conn = ConnectionStore.shared.savedConnection else { return } conn.remotePath = path ConnectionStore.shared.savedConnection = conn UIImpactFeedbackGenerator(style: .light).impactOccurred() } func resetAuth() { connectionState = .idle selectedDestination = "" } }