Files
keevault/KeeVault/Views/UnlockView.swift
T
kriss b6d113edcd feat: switch accent to Plum, finalize vault-door app icon, group filter Clear button
- Replace Copper accent with Plum (light #6B4577 / dark #A67FB0) across the main
  app and AutoFill extension.
- Replace the padlock app icon with a vault-door design (ring, hub, locking
  handle) in Plum, plus all macOS sizes.
- Add an AppIconImage asset (separate from the AppIcon icon-set, which SwiftUI
  can't reference directly at runtime) and show it on the unlock screen instead
  of a generic SF Symbol.
- GroupFilterView: move "Clear Filter" out of the list and into a leading
  toolbar button ("Clear", disabled when no filter is active), mirroring the
  trailing "Done" button.
2026-09-20 18:25:41 +02:00

128 lines
3.7 KiB
Swift

//
// 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)
)
}
}