Move delete to use a confirmation dialog + various other fixes. (#645)

This commit is contained in:
Max Goedjen 2025-08-27 23:45:56 -07:00 committed by GitHub
parent c5052dd457
commit fb4dec383b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 96 additions and 84 deletions

View File

@ -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<T>(_ 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)
}
}

View File

@ -23,6 +23,10 @@ extension SecureEnclave {
self.attributes = attributes
}
public static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}
}

View File

@ -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 = "<group>"; };
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
/* 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 */,

View File

@ -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())
}
}

View File

@ -103,6 +103,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
showing = false
}
Button(.createSecretCreateButton, action: save)
.primary()
.disabled(name.isEmpty)
}
.padding()

View File

@ -1,63 +1,56 @@
import SwiftUI
import SecretKit
struct DeleteSecretView<StoreType: SecretStoreModifiable>: 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<Bool>, _ 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<Bool>
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

View File

@ -12,18 +12,6 @@ struct SecretListItemView: View {
var deletedSecret: (AnySecret) -> Void
var renamedSecret: (AnySecret) -> Void
private var showingPopup: Binding<Bool> {
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)
}
}
}

View File

@ -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
}
}