feat: copper accent theme, always-expanded group filter, app icon; rename MyPass to KeeVault
- Custom copper AccentColor and warm VaultBackground color assets (light/dark), applied to both the main app and the AutoFill extension (which needed its own copy of the asset catalog plus ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME and an explicit .tint() modifier, since extensions don't pick up the app-wide accent color the same way a full SwiftUI App/Scene does). - GroupFilterView no longer uses DisclosureGroup for subgroups -- the whole tree renders always-expanded (indented per depth) so active filters are visible at a glance instead of requiring tap-to-expand. - Added a placeholder app icon (padlock glyph, light/dark/tinted variants plus macOS sizes) -- previously the icon slot existed but had no actual images, which blocks App Store/TestFlight archive validation. - Added ITSAppUsesNonExemptEncryption = NO (standard encryption only, via KeePassKit) to skip the export-compliance prompt on each upload. - Renamed the app from MyPass to KeeVault throughout: Xcode project/targets/ schemes, the MyPassCore package (now KeeVaultCore) and every import site, folder and file names, bundle identifiers (org.antiloop222.keevault) and their App Group/Keychain-group entitlements, and remaining UI/string references -- MyPass was already taken as an App Store app name.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// 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(systemName: "lock.shield.fill")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(.tint)
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user