import SwiftUI struct ConnectView: View { @StateObject private var vm = ConnectViewModel() @State private var navigateToDashboard = false @FocusState private var focused: ConnectField? var body: some View { NavigationStack { ZStack { Color.white.ignoresSafeArea() ScrollView { VStack(spacing: 0) { // Logo Image("nas_logo_clean") .resizable() .scaledToFit() .frame(width: 44, height: 44) .clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous)) .padding(.top, 44) .padding(.bottom, 24) // Protocol picker HStack(spacing: 0) { ForEach(NASProtocol.allCases, id: \.self) { proto in Button { withAnimation(.spring(response: 0.25, dampingFraction: 0.8)) { vm.selectedProtocol = proto } } label: { Text(proto.rawValue) .font(.system(size: 13, weight: .medium)) .foregroundStyle(vm.selectedProtocol == proto ? AppTheme.ink : AppTheme.inkTertiary) .frame(maxWidth: .infinity) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(vm.selectedProtocol == proto ? Color.white : Color.clear) .shadow( color: vm.selectedProtocol == proto ? .black.opacity(0.07) : .clear, radius: 4, x: 0, y: 1 ) ) } .buttonStyle(PlainButtonStyle()) } } .padding(4) .background(AppTheme.surfaceSunken) .clipShape(RoundedRectangle(cornerRadius: 11, style: .continuous)) .padding(.horizontal, AppTheme.hPad) .padding(.bottom, 16) // Form card VStack(spacing: 0) { formField( icon: "network", placeholder: "IP address or hostname", text: $vm.host, field: .host, keyboard: .URL ) hairline formField( icon: "person", placeholder: "Username", text: $vm.username, field: .username, keyboard: .emailAddress ) hairline secureField( icon: "lock", placeholder: "Password", text: $vm.password, field: .password ) hairline folderRow } .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: AppTheme.radius, style: .continuous) .stroke( vm.isConnected ? AppTheme.positive.opacity(0.4) : vm.verifyError != nil ? AppTheme.destructive.opacity(0.4) : AppTheme.cardBorderColor, lineWidth: vm.isConnected || vm.verifyError != nil ? 1 : 0.5 ) ) .shadow(color: AppTheme.cardShadow, radius: 12, x: 0, y: 2) .padding(.horizontal, AppTheme.hPad) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.isConnected) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.verifyError != nil) // Feedback Group { if let error = vm.verifyError { HStack(spacing: 6) { Image(systemName: "exclamationmark.circle") .font(.system(size: 13)) Text(error) .font(AppTheme.caption()) } .foregroundStyle(AppTheme.destructive) .padding(.top, 10) .padding(.horizontal, AppTheme.hPad) .transition(.opacity.combined(with: .move(edge: .top))) } else if vm.isConnected { HStack(spacing: 6) { Image(systemName: "checkmark.circle") .font(.system(size: 13)) Text("Connected successfully") .font(AppTheme.caption()) } .foregroundStyle(AppTheme.positive) .padding(.top, 10) .padding(.horizontal, AppTheme.hPad) .transition(.opacity.combined(with: .move(edge: .top))) } } .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.verifyError) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.isConnected) // Button — below the card with deliberate gap Spacer().frame(height: 20) Button(action: { if vm.isConnected { navigateToDashboard = true } else { Task { await vm.verify() } } }) { Group { if vm.isVerifying { ProgressView().tint(.white) } else { Text(vm.isConnected ? "Continue" : "Connect") } } } .buttonStyle(PrimaryButtonStyle( color: vm.isConnected ? AppTheme.positive : AppTheme.ink )) .padding(.horizontal, AppTheme.hPad) .disabled(!vm.canConnect) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: vm.isConnected) Spacer().frame(height: 40) } } } .navigationTitle("Connect to NAS") .navigationBarTitleDisplayMode(.inline) .navigationDestination(isPresented: $navigateToDashboard) { MainTabView() } .sheet(isPresented: $vm.showFolderBrowser) { if let conn = ConnectionStore.shared.savedConnection { BrowseView(connection: conn, selectedPath: $vm.remotePath) } } } } // MARK: — Sub-views private var hairline: some View { Rectangle() .fill(AppTheme.separator) .frame(height: 0.5) .padding(.leading, 44) } private var folderRow: some View { Button(action: { if vm.isConnected { vm.showFolderBrowser = true } }) { HStack(spacing: 12) { Image(systemName: "folder") .font(.system(size: 14, weight: .regular)) .foregroundStyle(vm.isConnected ? AppTheme.inkSecondary : AppTheme.inkQuaternary) .frame(width: 20) Text(vm.remotePath == "/" ? "Destination folder" : vm.remotePath) .font(AppTheme.body()) .foregroundStyle(vm.remotePath == "/" ? AppTheme.inkTertiary : AppTheme.ink) .lineLimit(1) Spacer() Image(systemName: "chevron.right") .font(.system(size: 12, weight: .medium)) .foregroundStyle(AppTheme.inkQuaternary) } .padding(.horizontal, 16) .padding(.vertical, 14) } .buttonStyle(ScaleButtonStyle()) } private func formField( icon: String, placeholder: String, text: Binding, field: ConnectField, keyboard: UIKeyboardType ) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 14, weight: .regular)) .foregroundStyle(AppTheme.inkSecondary) .frame(width: 20) TextField(placeholder, text: text) .font(AppTheme.body()) .foregroundStyle(AppTheme.ink) .keyboardType(keyboard) .autocorrectionDisabled() .textInputAutocapitalization(.never) .focused($focused, equals: field) } .padding(.horizontal, 16) .padding(.vertical, 14) } private func secureField( icon: String, placeholder: String, text: Binding, field: ConnectField ) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.system(size: 14, weight: .regular)) .foregroundStyle(AppTheme.inkSecondary) .frame(width: 20) SecureField(placeholder, text: text) .font(AppTheme.body()) .foregroundStyle(AppTheme.ink) .focused($focused, equals: field) } .padding(.horizontal, 16) .padding(.vertical, 14) } }