From fb4dec383be903e95eaf5729e38a6b13bd544dc4 Mon Sep 17 00:00:00 2001 From: Max Goedjen Date: Wed, 27 Aug 2025 23:45:56 -0700 Subject: [PATCH] Move delete to use a confirmation dialog + various other fixes. (#645) --- .../Sources/SecretKit/Erasers/AnySecret.swift | 10 +- .../SecureEnclaveSecret.swift | 4 + Sources/Secretive.xcodeproj/project.pbxproj | 4 + .../Secretive/Views/ActionButtonStyle.swift | 24 +++++ .../Secretive/Views/CreateSecretView.swift | 1 + .../Secretive/Views/DeleteSecretView.swift | 97 +++++++++---------- .../Secretive/Views/SecretListItemView.swift | 36 ++----- Sources/Secretive/Views/StoreListView.swift | 4 +- 8 files changed, 96 insertions(+), 84 deletions(-) create mode 100644 Sources/Secretive/Views/ActionButtonStyle.swift diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift index 3d3bc73..17ba732 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift @@ -4,27 +4,27 @@ import Foundation public struct AnySecret: Secret, @unchecked Sendable { public let base: any Secret - private let hashable: AnyHashable private let _id: () -> AnyHashable private let _name: () -> String private let _publicKey: () -> Data private let _attributes: () -> Attributes + private let _eq: (AnySecret) -> Bool public init(_ secret: T) where T: Secret { if let secret = secret as? AnySecret { base = secret.base - hashable = secret.hashable _id = secret._id _name = secret._name _publicKey = secret._publicKey _attributes = secret._attributes + _eq = secret._eq } else { base = secret - self.hashable = secret _id = { secret.id as AnyHashable } _name = { secret.name } _publicKey = { secret.publicKey } _attributes = { secret.attributes } + _eq = { secret == $0.base as? T } } } @@ -45,11 +45,11 @@ public struct AnySecret: Secret, @unchecked Sendable { } public static func == (lhs: AnySecret, rhs: AnySecret) -> Bool { - lhs.hashable == rhs.hashable + lhs._eq(rhs) } public func hash(into hasher: inout Hasher) { - hashable.hash(into: &hasher) + id.hash(into: &hasher) } } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift index f193478..7a53d5a 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveSecret.swift @@ -23,6 +23,10 @@ extension SecureEnclave { self.attributes = attributes } + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + } } diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index 8f60638..459af4a 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B8550C24138C4F009958AC /* DeleteSecretView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; + 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -140,6 +141,7 @@ 50B8550C24138C4F009958AC /* DeleteSecretView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSecretView.swift; sourceTree = ""; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = ""; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = ""; }; + 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -241,6 +243,7 @@ children = ( 50617D8423FCE48E0099B055 /* ContentView.swift */, 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */, + 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */, 50153E21250DECA300525160 /* SecretListItemView.swift */, 50C385A42407A76D00AF2719 /* SecretDetailView.swift */, @@ -435,6 +438,7 @@ 5066A6C22516F303004B5A36 /* SetupView.swift in Sources */, 5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */, 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */, + 50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */, 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */, 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, diff --git a/Sources/Secretive/Views/ActionButtonStyle.swift b/Sources/Secretive/Views/ActionButtonStyle.swift new file mode 100644 index 0000000..4d7455f --- /dev/null +++ b/Sources/Secretive/Views/ActionButtonStyle.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct PrimaryButtonModifier: ViewModifier { + + @Environment(\.colorScheme) var colorScheme + + func body(content: Content) -> some View { + // Tinted glass prominent is really hard to read on 26.0. + if #available(macOS 26.0, *), colorScheme == .dark { + content.buttonStyle(.glassProminent) + } else { + content.buttonStyle(.borderedProminent) + } + } + +} + +extension View { + + func primary() -> some View { + modifier(PrimaryButtonModifier()) + } + +} diff --git a/Sources/Secretive/Views/CreateSecretView.swift b/Sources/Secretive/Views/CreateSecretView.swift index 7185bf2..b5f17b5 100644 --- a/Sources/Secretive/Views/CreateSecretView.swift +++ b/Sources/Secretive/Views/CreateSecretView.swift @@ -103,6 +103,7 @@ struct CreateSecretView: View { showing = false } Button(.createSecretCreateButton, action: save) + .primary() .disabled(name.isEmpty) } .padding() diff --git a/Sources/Secretive/Views/DeleteSecretView.swift b/Sources/Secretive/Views/DeleteSecretView.swift index e53fda9..2deee63 100644 --- a/Sources/Secretive/Views/DeleteSecretView.swift +++ b/Sources/Secretive/Views/DeleteSecretView.swift @@ -1,63 +1,56 @@ import SwiftUI import SecretKit -struct DeleteSecretView: View { +extension View { - @State var store: StoreType - let secret: StoreType.SecretType - var dismissalBlock: (Bool) -> () - - @State private var confirm = "" - @State var errorText: String? - - var body: some View { - VStack { - HStack { - Image(nsImage: NSApplication.shared.applicationIconImage) - .resizable() - .frame(width: 64, height: 64) - .padding() - VStack { - HStack { - Text(.deleteConfirmationTitle(secretName: secret.name)).bold() - Spacer() - } - HStack { - Text(.deleteConfirmationDescription(secretName: secret.name, confirmSecretName: secret.name)) - Spacer() - } - HStack { - Text(.deleteConfirmationConfirmNameLabel) - TextField(secret.name, text: $confirm) - } - } - } - if let errorText { - Text(verbatim: errorText) - .foregroundStyle(.red) - .font(.callout) - } - HStack { - Spacer() - Button(.deleteConfirmationDeleteButton, action: delete) - .disabled(confirm != secret.name) - Button(.deleteConfirmationCancelButton) { - dismissalBlock(false) - } - .keyboardShortcut(.cancelAction) - } - } - .padding() - .frame(minWidth: 400) - .onExitCommand { - dismissalBlock(false) - } + func showingDeleteConfirmation(isPresented: Binding, _ secret: AnySecret, _ store: AnySecretStoreModifiable?, dismissalBlock: @escaping (Bool) -> ()) -> some View { + modifier(DeleteSecretConfirmationModifier(isPresented: isPresented, secret: secret, store: store, dismissalBlock: dismissalBlock)) } - + +} + +struct DeleteSecretConfirmationModifier: ViewModifier { + + var isPresented: Binding + var secret: AnySecret + var store: AnySecretStoreModifiable? + var dismissalBlock: (Bool) -> () + @State var confirmedSecretName = "" + @State private var errorText: String? + + func body(content: Content) -> some View { + content + .confirmationDialog( + .deleteConfirmationTitle(secretName: secret.name), + isPresented: isPresented, + titleVisibility: .visible, + actions: { + TextField(secret.name, text: $confirmedSecretName) + if let errorText { + Text(verbatim: errorText) + .foregroundStyle(.red) + .font(.callout) + } + Button(.deleteConfirmationDeleteButton, action: delete) + .disabled(confirmedSecretName != secret.name) + Button(.deleteConfirmationCancelButton, role: .cancel) { + dismissalBlock(false) + } + }, + message: { + Text(.deleteConfirmationDescription(secretName: secret.name, confirmSecretName: secret.name)) + } + ) + .dialogIcon(Image(systemName: "lock.trianglebadge.exclamationmark.fill")) + .onExitCommand { + dismissalBlock(false) + } + } + func delete() { Task { do { - try await store.delete(secret: secret) + try await store!.delete(secret: secret) dismissalBlock(true) } catch { errorText = error.localizedDescription diff --git a/Sources/Secretive/Views/SecretListItemView.swift b/Sources/Secretive/Views/SecretListItemView.swift index 357dc25..41e742b 100644 --- a/Sources/Secretive/Views/SecretListItemView.swift +++ b/Sources/Secretive/Views/SecretListItemView.swift @@ -12,18 +12,6 @@ struct SecretListItemView: View { var deletedSecret: (AnySecret) -> Void var renamedSecret: (AnySecret) -> Void - private var showingPopup: Binding { - Binding( - get: { isDeleting || isRenaming }, - set: { - if $0 == false { - isDeleting = false - isRenaming = false - } - } - ) - } - var body: some View { NavigationLink(value: secret) { if secret.authenticationRequirement.required { @@ -48,21 +36,17 @@ struct SecretListItemView: View { } } } - .sheet(isPresented: showingPopup) { + .showingDeleteConfirmation(isPresented: $isDeleting, secret, store as? AnySecretStoreModifiable) { deleted in + if deleted { + deletedSecret(secret) + } + } + .sheet(isPresented: $isRenaming) { if let modifiable = store as? AnySecretStoreModifiable { - if isDeleting { - DeleteSecretView(store: modifiable, secret: secret) { deleted in - isDeleting = false - if deleted { - deletedSecret(secret) - } - } - } else if isRenaming { - EditSecretView(store: modifiable, secret: secret) { renamed in - isRenaming = false - if renamed { - renamedSecret(secret) - } + EditSecretView(store: modifiable, secret: secret) { renamed in + isRenaming = false + if renamed { + renamedSecret(secret) } } } diff --git a/Sources/Secretive/Views/StoreListView.swift b/Sources/Secretive/Views/StoreListView.swift index 2c0dc2c..2c8d439 100644 --- a/Sources/Secretive/Views/StoreListView.swift +++ b/Sources/Secretive/Views/StoreListView.swift @@ -12,6 +12,8 @@ struct StoreListView: View { } private func secretRenamed(secret: AnySecret) { + // Toggle so name updates in list. + activeSecret = nil activeSecret = secret } @@ -56,7 +58,7 @@ struct StoreListView: View { extension StoreListView { private var nextDefaultSecret: AnySecret? { - return storeList.stores.first(where: { !$0.secrets.isEmpty })?.secrets.first + return storeList.allSecrets.first } }