Add edit-connection flow

Edit reuses AddConnectionView with an `editing:` parameter that prefills the
form and updates in-place; password field becomes optional ("leave blank to
keep current"). Surfaced via context menu and a leading-edge swipe action on
each saved row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-16 20:21:14 -05:00
parent fcad267493
commit 8e01068ad3
2 changed files with 77 additions and 26 deletions

View File

@@ -19,6 +19,7 @@ public struct AddConnectionView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
let prefill: AddConnectionPrefill? let prefill: AddConnectionPrefill?
let editing: SavedConnection?
@State private var displayName = "" @State private var displayName = ""
@State private var host = "" @State private var host = ""
@@ -30,11 +31,15 @@ public struct AddConnectionView: View {
@State private var clipboardSync = true @State private var clipboardSync = true
@State private var viewOnly = false @State private var viewOnly = false
@State private var notes = "" @State private var notes = ""
@State private var hasLoadedExisting = false
public init(prefill: AddConnectionPrefill? = nil) { public init(prefill: AddConnectionPrefill? = nil, editing: SavedConnection? = nil) {
self.prefill = prefill self.prefill = prefill
self.editing = editing
} }
private var isEditing: Bool { editing != nil }
public var body: some View { public var body: some View {
NavigationStack { NavigationStack {
Form { Form {
@@ -51,7 +56,8 @@ public struct AddConnectionView: View {
#endif #endif
} }
Section("Authentication") { Section("Authentication") {
SecureField("VNC password", text: $password) SecureField(isEditing ? "Replace password (leave blank to keep current)" : "VNC password",
text: $password)
Text("Stored in iOS Keychain (this device only).") Text("Stored in iOS Keychain (this device only).")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -86,7 +92,7 @@ public struct AddConnectionView: View {
.lineLimit(2...6) .lineLimit(2...6)
} }
} }
.navigationTitle("New Connection") .navigationTitle(isEditing ? "Edit Connection" : "New Connection")
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#endif #endif
@@ -95,38 +101,66 @@ public struct AddConnectionView: View {
Button("Cancel") { dismiss() } Button("Cancel") { dismiss() }
} }
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
Button("Save") { save() } Button(isEditing ? "Save" : "Add") { save() }
.disabled(displayName.isEmpty || host.isEmpty) .disabled(displayName.isEmpty || host.isEmpty)
} }
} }
.onAppear { applyPrefillIfNeeded() } .onAppear { loadExistingIfNeeded() }
} }
} }
private func applyPrefillIfNeeded() { private func loadExistingIfNeeded() {
guard let prefill, displayName.isEmpty, host.isEmpty else { return } guard !hasLoadedExisting else { return }
displayName = prefill.displayName hasLoadedExisting = true
host = prefill.host if let existing = editing {
port = String(prefill.port) displayName = existing.displayName
host = existing.host
port = String(existing.port)
colorTag = existing.colorTag
quality = existing.quality
inputMode = existing.inputMode
clipboardSync = existing.clipboardSyncEnabled
viewOnly = existing.viewOnly
notes = existing.notes
} else if let prefill, displayName.isEmpty, host.isEmpty {
displayName = prefill.displayName
host = prefill.host
port = String(prefill.port)
}
} }
private func save() { private func save() {
let portInt = Int(port) ?? 5900 let portInt = Int(port) ?? 5900
let connection = SavedConnection( if let existing = editing {
displayName: displayName, existing.displayName = displayName
host: host, existing.host = host
port: portInt, existing.port = portInt
colorTag: colorTag, existing.colorTag = colorTag
quality: quality, existing.quality = quality
inputMode: inputMode, existing.inputMode = inputMode
viewOnly: viewOnly, existing.clipboardSyncEnabled = clipboardSync
curtainMode: false, existing.viewOnly = viewOnly
clipboardSyncEnabled: clipboardSync, existing.notes = notes
notes: notes if !password.isEmpty {
) try? KeychainService().storePassword(password, account: existing.keychainTag)
context.insert(connection) }
if !password.isEmpty { } else {
try? KeychainService().storePassword(password, account: connection.keychainTag) let connection = SavedConnection(
displayName: displayName,
host: host,
port: portInt,
colorTag: colorTag,
quality: quality,
inputMode: inputMode,
viewOnly: viewOnly,
curtainMode: false,
clipboardSyncEnabled: clipboardSync,
notes: notes
)
context.insert(connection)
if !password.isEmpty {
try? KeychainService().storePassword(password, account: connection.keychainTag)
}
} }
try? context.save() try? context.save()
dismiss() dismiss()

View File

@@ -10,6 +10,7 @@ public struct ConnectionListView: View {
@State private var showingAdd = false @State private var showingAdd = false
@State private var showingSettings = false @State private var showingSettings = false
@State private var addPrefill: AddConnectionPrefill? @State private var addPrefill: AddConnectionPrefill?
@State private var editingConnection: SavedConnection?
@State private var path: [SessionRoute] = [] @State private var path: [SessionRoute] = []
@State private var resolvingHostID: String? @State private var resolvingHostID: String?
@@ -59,6 +60,9 @@ public struct ConnectionListView: View {
.sheet(isPresented: $showingAdd) { .sheet(isPresented: $showingAdd) {
AddConnectionView(prefill: addPrefill) AddConnectionView(prefill: addPrefill)
} }
.sheet(item: $editingConnection) { connection in
AddConnectionView(editing: connection)
}
.sheet(isPresented: $showingSettings) { .sheet(isPresented: $showingSettings) {
SettingsView() SettingsView()
} }
@@ -121,6 +125,11 @@ public struct ConnectionListView: View {
ConnectionCard(connection: connection) ConnectionCard(connection: connection)
} }
.contextMenu { .contextMenu {
Button {
editingConnection = connection
} label: {
Label("Edit", systemImage: "pencil")
}
#if os(iOS) #if os(iOS)
Button { Button {
openWindow(value: connection.id) openWindow(value: connection.id)
@@ -134,7 +143,15 @@ public struct ConnectionListView: View {
Label("Delete", systemImage: "trash") Label("Delete", systemImage: "trash")
} }
} }
.swipeActions { .swipeActions(edge: .leading) {
Button {
editingConnection = connection
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
delete(connection) delete(connection)
} label: { } label: {