From 8e01068ad320f12e2d1c5efac0fe9c1f10dd80df Mon Sep 17 00:00:00 2001 From: Trey T Date: Thu, 16 Apr 2026 20:21:14 -0500 Subject: [PATCH] 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) --- .../VNCUI/Edit/AddConnectionView.swift | 84 +++++++++++++------ .../VNCUI/List/ConnectionListView.swift | 19 ++++- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/Packages/VNCUI/Sources/VNCUI/Edit/AddConnectionView.swift b/Packages/VNCUI/Sources/VNCUI/Edit/AddConnectionView.swift index 1a6909a..68084b1 100644 --- a/Packages/VNCUI/Sources/VNCUI/Edit/AddConnectionView.swift +++ b/Packages/VNCUI/Sources/VNCUI/Edit/AddConnectionView.swift @@ -19,6 +19,7 @@ public struct AddConnectionView: View { @Environment(\.dismiss) private var dismiss let prefill: AddConnectionPrefill? + let editing: SavedConnection? @State private var displayName = "" @State private var host = "" @@ -30,11 +31,15 @@ public struct AddConnectionView: View { @State private var clipboardSync = true @State private var viewOnly = false @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.editing = editing } + private var isEditing: Bool { editing != nil } + public var body: some View { NavigationStack { Form { @@ -51,7 +56,8 @@ public struct AddConnectionView: View { #endif } 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).") .font(.caption) .foregroundStyle(.secondary) @@ -86,7 +92,7 @@ public struct AddConnectionView: View { .lineLimit(2...6) } } - .navigationTitle("New Connection") + .navigationTitle(isEditing ? "Edit Connection" : "New Connection") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif @@ -95,38 +101,66 @@ public struct AddConnectionView: View { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { - Button("Save") { save() } + Button(isEditing ? "Save" : "Add") { save() } .disabled(displayName.isEmpty || host.isEmpty) } } - .onAppear { applyPrefillIfNeeded() } + .onAppear { loadExistingIfNeeded() } } } - private func applyPrefillIfNeeded() { - guard let prefill, displayName.isEmpty, host.isEmpty else { return } - displayName = prefill.displayName - host = prefill.host - port = String(prefill.port) + private func loadExistingIfNeeded() { + guard !hasLoadedExisting else { return } + hasLoadedExisting = true + if let existing = editing { + 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() { let portInt = Int(port) ?? 5900 - 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) + if let existing = editing { + existing.displayName = displayName + existing.host = host + existing.port = portInt + existing.colorTag = colorTag + existing.quality = quality + existing.inputMode = inputMode + existing.clipboardSyncEnabled = clipboardSync + existing.viewOnly = viewOnly + existing.notes = notes + if !password.isEmpty { + try? KeychainService().storePassword(password, account: existing.keychainTag) + } + } else { + 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() dismiss() diff --git a/Packages/VNCUI/Sources/VNCUI/List/ConnectionListView.swift b/Packages/VNCUI/Sources/VNCUI/List/ConnectionListView.swift index d485f23..f3cf346 100644 --- a/Packages/VNCUI/Sources/VNCUI/List/ConnectionListView.swift +++ b/Packages/VNCUI/Sources/VNCUI/List/ConnectionListView.swift @@ -10,6 +10,7 @@ public struct ConnectionListView: View { @State private var showingAdd = false @State private var showingSettings = false @State private var addPrefill: AddConnectionPrefill? + @State private var editingConnection: SavedConnection? @State private var path: [SessionRoute] = [] @State private var resolvingHostID: String? @@ -59,6 +60,9 @@ public struct ConnectionListView: View { .sheet(isPresented: $showingAdd) { AddConnectionView(prefill: addPrefill) } + .sheet(item: $editingConnection) { connection in + AddConnectionView(editing: connection) + } .sheet(isPresented: $showingSettings) { SettingsView() } @@ -121,6 +125,11 @@ public struct ConnectionListView: View { ConnectionCard(connection: connection) } .contextMenu { + Button { + editingConnection = connection + } label: { + Label("Edit", systemImage: "pencil") + } #if os(iOS) Button { openWindow(value: connection.id) @@ -134,7 +143,15 @@ public struct ConnectionListView: View { Label("Delete", systemImage: "trash") } } - .swipeActions { + .swipeActions(edge: .leading) { + Button { + editingConnection = connection + } label: { + Label("Edit", systemImage: "pencil") + } + .tint(.blue) + } + .swipeActions(edge: .trailing) { Button(role: .destructive) { delete(connection) } label: {