Skip to content

Commit aaa4d10

Browse files
committed
fix: 6 review issues — force unwrap, reorder batch, SSH creds, color picker scroll, toolbar, widget data
1 parent f76c670 commit aaa4d10

6 files changed

Lines changed: 100 additions & 79 deletions

File tree

TableProMobile/TableProMobile/AppState.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ final class AppState {
120120
}
121121
}
122122

123+
func reorderGroups(_ reordered: [ConnectionGroup]) {
124+
groups = reordered
125+
groupStorage.save(groups)
126+
for group in reordered {
127+
syncCoordinator.markDirtyGroup(group.id)
128+
}
129+
syncCoordinator.scheduleSyncAfterChange()
130+
}
131+
123132
func deleteGroup(_ groupId: UUID) {
124133
groups.removeAll { $0.id == groupId }
125134
groupStorage.save(groups)
@@ -129,6 +138,7 @@ final class AppState {
129138
syncCoordinator.markDirty(connections[index].id)
130139
}
131140
storage.save(connections)
141+
updateWidgetData()
132142

133143
syncCoordinator.markDeletedGroup(groupId)
134144
syncCoordinator.scheduleSyncAfterChange()
@@ -163,6 +173,7 @@ final class AppState {
163173
syncCoordinator.markDirty(connections[index].id)
164174
}
165175
storage.save(connections)
176+
updateWidgetData()
166177

167178
syncCoordinator.markDeletedTag(tagId)
168179
syncCoordinator.scheduleSyncAfterChange()

TableProMobile/TableProMobile/Views/Components/ConnectionColorPicker.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,31 @@ struct ConnectionColorPicker: View {
1010
@Binding var selection: ConnectionColor
1111

1212
var body: some View {
13-
HStack(spacing: 12) {
14-
ForEach(ConnectionColor.allCases) { color in
15-
Button {
16-
selection = color
17-
} label: {
18-
ZStack {
19-
Circle()
20-
.fill(Self.swiftUIColor(for: color))
21-
.frame(width: 28, height: 28)
13+
ScrollView(.horizontal, showsIndicators: false) {
14+
HStack(spacing: 12) {
15+
ForEach(ConnectionColor.allCases) { color in
16+
Button {
17+
selection = color
18+
} label: {
19+
ZStack {
20+
Circle()
21+
.fill(Self.swiftUIColor(for: color))
22+
.frame(width: 28, height: 28)
2223

23-
if selection == color {
24-
Image(systemName: "checkmark")
25-
.font(.caption.bold())
26-
.foregroundStyle(.white)
24+
if selection == color {
25+
Image(systemName: "checkmark")
26+
.font(.caption.bold())
27+
.foregroundStyle(.white)
28+
}
2729
}
30+
.frame(minWidth: 44, minHeight: 44)
31+
.contentShape(Circle())
2832
}
29-
.frame(minWidth: 44, minHeight: 44)
30-
.contentShape(Circle())
33+
.buttonStyle(.plain)
3134
}
32-
.buttonStyle(.plain)
3335
}
36+
.padding(.vertical, 4)
3437
}
35-
.padding(.vertical, 4)
3638
}
3739

3840
static func swiftUIColor(for color: ConnectionColor) -> Color {

TableProMobile/TableProMobile/Views/ConnectionFormView.swift

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ struct ConnectionFormView: View {
2222
@State private var database = ""
2323
@State private var sslEnabled = false
2424

25-
// SQLite file picker
26-
@State private var showFilePicker = false
25+
// File pickers
26+
enum ActiveFilePicker: Identifiable {
27+
case sqliteDatabase
28+
case sshKey
29+
var id: Int { hashValue }
30+
}
31+
@State private var activeFilePicker: ActiveFilePicker?
2732
@State private var selectedFileURL: URL?
2833
@State private var showNewDatabaseAlert = false
2934
@State private var newDatabaseName = ""
@@ -43,7 +48,12 @@ struct ConnectionFormView: View {
4348
@State private var sshKeyContent = ""
4449
@State private var sshKeyPassphrase = ""
4550
@State private var sshKeyInputMode = KeyInputMode.file
46-
@State private var showSSHKeyPicker = false
51+
private var showFilePicker: Binding<Bool> {
52+
Binding(
53+
get: { activeFilePicker != nil },
54+
set: { if !$0 { activeFilePicker = nil } }
55+
)
56+
}
4757

4858
enum KeyInputMode: String, CaseIterable {
4959
case file = "Import File"
@@ -207,13 +217,11 @@ struct ConnectionFormView: View {
207217
if let stored = try? appState.secureStore.retrieve(forKey: connKey), !stored.isEmpty {
208218
password = stored
209219
}
210-
if sshEnabled {
211-
if let sshPwd = try? appState.secureStore.retrieve(forKey: "com.TablePro.sshpassword.\(conn.id.uuidString)"), !sshPwd.isEmpty {
212-
sshPassword = sshPwd
213-
}
214-
if let passphrase = try? appState.secureStore.retrieve(forKey: "com.TablePro.keypassphrase.\(conn.id.uuidString)"), !passphrase.isEmpty {
215-
sshKeyPassphrase = passphrase
216-
}
220+
if let sshPwd = try? appState.secureStore.retrieve(forKey: "com.TablePro.sshpassword.\(conn.id.uuidString)"), !sshPwd.isEmpty {
221+
sshPassword = sshPwd
222+
}
223+
if let passphrase = try? appState.secureStore.retrieve(forKey: "com.TablePro.keypassphrase.\(conn.id.uuidString)"), !passphrase.isEmpty {
224+
sshKeyPassphrase = passphrase
217225
}
218226
}
219227
}
@@ -229,26 +237,32 @@ struct ConnectionFormView: View {
229237
}
230238
}
231239
.fileImporter(
232-
isPresented: $showSSHKeyPicker,
233-
allowedContentTypes: [.data],
240+
isPresented: showFilePicker,
241+
allowedContentTypes: activeFilePicker == .sqliteDatabase ? sqliteContentTypes : [.data],
234242
allowsMultipleSelection: false
235243
) { result in
236-
if case .success(let urls) = result, let url = urls.first {
237-
guard url.startAccessingSecurityScopedResource() else { return }
238-
defer { url.stopAccessingSecurityScopedResource() }
239-
240-
// Read key content directly — more reliable than copying file
241-
if let content = try? String(contentsOf: url, encoding: .utf8) {
242-
sshKeyContent = content
243-
sshKeyInputMode = .paste
244-
} else {
245-
// Fallback: copy to app Documents
246-
guard let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
247-
let dest = docsDir.appendingPathComponent("ssh_" + url.lastPathComponent)
248-
try? FileManager.default.removeItem(at: dest)
249-
try? FileManager.default.copyItem(at: url, to: dest)
250-
sshKeyPath = dest.path
244+
let picker = activeFilePicker
245+
activeFilePicker = nil
246+
switch picker {
247+
case .sqliteDatabase:
248+
handleFilePickerResult(result)
249+
case .sshKey:
250+
if case .success(let urls) = result, let url = urls.first {
251+
guard url.startAccessingSecurityScopedResource() else { return }
252+
defer { url.stopAccessingSecurityScopedResource() }
253+
if let content = try? String(contentsOf: url, encoding: .utf8) {
254+
sshKeyContent = content
255+
sshKeyInputMode = .paste
256+
} else {
257+
guard let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
258+
let dest = docsDir.appendingPathComponent("ssh_" + url.lastPathComponent)
259+
try? FileManager.default.removeItem(at: dest)
260+
try? FileManager.default.copyItem(at: url, to: dest)
261+
sshKeyPath = dest.path
262+
}
251263
}
264+
case nil:
265+
break
252266
}
253267
}
254268
.alert("New Database", isPresented: $showNewDatabaseAlert) {
@@ -293,7 +307,7 @@ struct ConnectionFormView: View {
293307
}
294308

295309
Button {
296-
showFilePicker = true
310+
activeFilePicker = .sqliteDatabase
297311
} label: {
298312
Label("Open Database File", systemImage: "folder")
299313
}
@@ -304,13 +318,6 @@ struct ConnectionFormView: View {
304318
Label("Create New Database", systemImage: "plus.circle")
305319
}
306320
}
307-
.fileImporter(
308-
isPresented: $showFilePicker,
309-
allowedContentTypes: sqliteContentTypes,
310-
allowsMultipleSelection: false
311-
) { result in
312-
handleFilePickerResult(result)
313-
}
314321
}
315322

316323
// MARK: - Server Section (MySQL, PostgreSQL, Redis)
@@ -377,7 +384,7 @@ struct ConnectionFormView: View {
377384

378385
if sshKeyInputMode == .file {
379386
Button {
380-
showSSHKeyPicker = true
387+
activeFilePicker = .sshKey
381388
} label: {
382389
HStack {
383390
Text(sshKeyPath.isEmpty

TableProMobile/TableProMobile/Views/GroupManagementView.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,10 @@ struct GroupManagementView: View {
4646
.onMove { source, destination in
4747
var sorted = appState.groups.sorted(by: { $0.sortOrder < $1.sortOrder })
4848
sorted.move(fromOffsets: source, toOffset: destination)
49-
for (index, group) in sorted.enumerated() {
50-
var updated = group
51-
updated.sortOrder = index
52-
appState.updateGroup(updated)
49+
for index in sorted.indices {
50+
sorted[index].sortOrder = index
5351
}
52+
appState.reorderGroups(sorted)
5453
}
5554
}
5655
.overlay {
@@ -65,7 +64,7 @@ struct GroupManagementView: View {
6564
.navigationTitle("Groups")
6665
.navigationBarTitleDisplayMode(.inline)
6766
.toolbar {
68-
ToolbarItem(placement: .cancellationAction) {
67+
ToolbarItem(placement: .topBarLeading) {
6968
Button("Done") { dismiss() }
7069
}
7170
ToolbarItemGroup(placement: .topBarTrailing) {

TableProMobile/TableProMobile/Views/TagManagementView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,16 @@ struct TagManagementView: View {
6666
.navigationTitle("Tags")
6767
.navigationBarTitleDisplayMode(.inline)
6868
.toolbar {
69-
ToolbarItem(placement: .confirmationAction) {
69+
ToolbarItem(placement: .topBarLeading) {
70+
Button("Done") { dismiss() }
71+
}
72+
ToolbarItemGroup(placement: .topBarTrailing) {
7073
Button {
7174
showingAddTag = true
7275
} label: {
7376
Image(systemName: "plus")
7477
}
7578
}
76-
ToolbarItem(placement: .cancellationAction) {
77-
Button("Done") { dismiss() }
78-
}
7979
}
8080
.sheet(isPresented: $showingAddTag) {
8181
TagFormSheet { tag in

TableProMobile/TableProWidget/Views/MediumWidgetView.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,28 @@ struct MediumWidgetView: View {
3131
} else {
3232
LazyVGrid(columns: columns, spacing: 8) {
3333
ForEach(displayedConnections) { connection in
34-
Link(destination: URL(string: "tablepro://connect/\(connection.id.uuidString)")!) {
35-
HStack(spacing: 8) {
36-
Image(systemName: DatabaseTypeStyle.iconName(for: connection.type))
37-
.font(.callout)
38-
.foregroundStyle(DatabaseTypeStyle.iconColor(for: connection.type))
39-
.frame(width: 28, height: 28)
40-
.background(DatabaseTypeStyle.iconColor(for: connection.type).opacity(0.15))
41-
.clipShape(RoundedRectangle(cornerRadius: 6))
34+
if let url = URL(string: "tablepro://connect/\(connection.id.uuidString)") {
35+
Link(destination: url) {
36+
HStack(spacing: 8) {
37+
Image(systemName: DatabaseTypeStyle.iconName(for: connection.type))
38+
.font(.callout)
39+
.foregroundStyle(DatabaseTypeStyle.iconColor(for: connection.type))
40+
.frame(width: 28, height: 28)
41+
.background(DatabaseTypeStyle.iconColor(for: connection.type).opacity(0.15))
42+
.clipShape(RoundedRectangle(cornerRadius: 6))
4243

43-
Text(connection.name)
44-
.font(.caption)
45-
.foregroundStyle(.primary)
46-
.lineLimit(1)
44+
Text(connection.name)
45+
.font(.caption)
46+
.foregroundStyle(.primary)
47+
.lineLimit(1)
4748

48-
Spacer(minLength: 0)
49+
Spacer(minLength: 0)
50+
}
51+
.padding(.horizontal, 8)
52+
.padding(.vertical, 6)
53+
.background(.fill.quaternary)
54+
.clipShape(RoundedRectangle(cornerRadius: 8))
4955
}
50-
.padding(.horizontal, 8)
51-
.padding(.vertical, 6)
52-
.background(.fill.quaternary)
53-
.clipShape(RoundedRectangle(cornerRadius: 8))
5456
}
5557
}
5658
}

0 commit comments

Comments
 (0)