// // UnlockView.swift // KeeVault // import SwiftUI import UniformTypeIdentifiers import KeeVaultCore struct UnlockView: View { @ObservedObject var vm: UnlockViewModel var body: some View { VStack(spacing: 24) { Spacer() Image("AppIconImage") .resizable() .frame(width: 88, height: 88) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) Text("KeeVault") .font(.largeTitle.bold()) if vm.hasVault { vaultUnlockSection } else { openFileSection } if let msg = vm.errorMessage { Text(msg) .foregroundStyle(.red) .font(.caption) .multilineTextAlignment(.center) } Spacer() } .padding(32) .fileImporter( isPresented: $vm.showFilePicker, allowedContentTypes: [.init(filenameExtension: "kdbx")!], onCompletion: { result in if let url = try? result.get() { vm.openFile(url: url) } } ) } @ViewBuilder private var vaultUnlockSection: some View { VStack(spacing: 16) { if vm.canUseBiometrics { Button(action: vm.unlockWithBiometrics) { Label("Use Face ID / Touch ID", systemImage: "faceid") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(vm.isUnlocking) Text("or enter password") .font(.caption) .foregroundStyle(.secondary) } PasswordField(text: $vm.password, onSubmit: vm.unlockWithPassword) Button("Unlock", action: vm.unlockWithPassword) .buttonStyle(.bordered) .disabled(vm.password.isEmpty || vm.isUnlocking) Button("Choose different file…") { vm.showFilePicker = true } .font(.caption) .foregroundStyle(.secondary) } } @ViewBuilder private var openFileSection: some View { VStack(spacing: 16) { Text("No vault selected. Open a .kdbx file to get started.") .multilineTextAlignment(.center) .foregroundStyle(.secondary) Button(action: { vm.showFilePicker = true }) { Label("Open KDBX File…", systemImage: "doc.badge.plus") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) } } } /// A password field with a trailing eye button to reveal/hide the typed value. private struct PasswordField: View { @Binding var text: String var onSubmit: () -> Void @State private var isRevealed = false var body: some View { HStack { Group { if isRevealed { TextField("Master Password", text: $text) } else { SecureField("Master Password", text: $text) } } .textFieldStyle(.plain) .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) #endif .onSubmit(onSubmit) Button { isRevealed.toggle() } label: { Image(systemName: isRevealed ? "eye.slash" : "eye") .foregroundStyle(.secondary) } .buttonStyle(.plain) } .padding(8) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color.secondary.opacity(0.3), lineWidth: 1) ) } }