Skip to content

Commit 7a64252

Browse files
committed
feat(ios): add iPad two-column layout with table sidebar and data detail
1 parent 741bcff commit 7a64252

4 files changed

Lines changed: 276 additions & 125 deletions

File tree

TableProMobile/TableProMobile/Views/ConnectedView.swift

Lines changed: 157 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import TableProModels
1111
struct ConnectedView: View {
1212
@Environment(AppState.self) private var appState
1313
@Environment(\.scenePhase) private var scenePhase
14+
@Environment(\.horizontalSizeClass) private var sizeClass
1415
let connection: DatabaseConnection
1516

1617
private static let logger = Logger(subsystem: "com.TablePro", category: "ConnectedView")
@@ -29,6 +30,7 @@ struct ConnectedView: View {
2930
@State private var activeDatabase: String = ""
3031
@State private var schemas: [String] = []
3132
@State private var activeSchema: String = "public"
33+
@State private var selectedTable: TableInfo?
3234
@State private var isSwitching = false
3335
@State private var isReconnecting = false
3436
@State private var hapticSuccess = false
@@ -129,16 +131,12 @@ struct ConnectedView: View {
129131
} message: {
130132
Text(failureAlertMessage ?? "")
131133
}
132-
.navigationTitle(supportsDatabaseSwitching && databases.count > 1 ? "" : displayName)
134+
.navigationTitle(sizeClass != .regular ? (supportsDatabaseSwitching && databases.count > 1 ? "" : displayName) : "")
133135
.navigationBarTitleDisplayMode(.inline)
134136
.safeAreaInset(edge: .top) {
135-
Picker("Tab", selection: selectedTabBinding) {
136-
Text("Tables").tag(ConnectedTab.tables)
137-
Text("Query").tag(ConnectedTab.query)
137+
if sizeClass != .regular {
138+
tabPicker
138139
}
139-
.pickerStyle(.segmented)
140-
.padding(.horizontal)
141-
.padding(.vertical, 8)
142140
}
143141
.background {
144142
Button("") { selectedTabRaw = ConnectedTab.tables.rawValue }
@@ -149,63 +147,23 @@ struct ConnectedView: View {
149147
.hidden()
150148
}
151149
.toolbar {
152-
if connection.safeModeLevel != .off {
153-
ToolbarItem(placement: .topBarTrailing) {
154-
Image(systemName: connection.safeModeLevel == .readOnly ? "lock.fill" : "shield.fill")
155-
.foregroundStyle(connection.safeModeLevel == .readOnly ? .red : .orange)
156-
.font(.caption)
150+
if sizeClass != .regular {
151+
if connection.safeModeLevel != .off {
152+
ToolbarItem(placement: .topBarTrailing) {
153+
Image(systemName: connection.safeModeLevel == .readOnly ? "lock.fill" : "shield.fill")
154+
.foregroundStyle(connection.safeModeLevel == .readOnly ? .red : .orange)
155+
.font(.caption)
156+
}
157157
}
158-
}
159-
if supportsDatabaseSwitching && databases.count > 1 {
160-
ToolbarItem(placement: .topBarLeading) {
161-
Menu {
162-
ForEach(databases, id: \.self) { db in
163-
Button {
164-
Task { await switchDatabase(to: db) }
165-
} label: {
166-
if db == activeDatabase {
167-
Label(db, systemImage: "checkmark")
168-
} else {
169-
Text(db)
170-
}
171-
}
172-
}
173-
} label: {
174-
HStack(spacing: 4) {
175-
Text(activeDatabase)
176-
.font(.subheadline)
177-
if isSwitching {
178-
ProgressView()
179-
.controlSize(.mini)
180-
} else {
181-
Image(systemName: "chevron.down")
182-
.font(.caption2)
183-
.foregroundStyle(.secondary)
184-
}
185-
}
158+
if supportsDatabaseSwitching && databases.count > 1 {
159+
ToolbarItem(placement: .topBarLeading) {
160+
databaseSwitcherMenu
186161
}
187-
.disabled(isSwitching)
188162
}
189-
}
190-
if supportsSchemas && schemas.count > 1 && selectedTab == .tables {
191-
ToolbarItem(placement: .topBarTrailing) {
192-
Menu {
193-
ForEach(schemas, id: \.self) { schema in
194-
Button {
195-
Task { await switchSchema(to: schema) }
196-
} label: {
197-
if schema == activeSchema {
198-
Label(schema, systemImage: "checkmark")
199-
} else {
200-
Text(schema)
201-
}
202-
}
203-
}
204-
} label: {
205-
Label(activeSchema, systemImage: "square.3.layers.3d")
206-
.font(.subheadline)
163+
if supportsSchemas && schemas.count > 1 && selectedTab == .tables {
164+
ToolbarItem(placement: .topBarTrailing) {
165+
schemaSwitcherMenu
207166
}
208-
.disabled(isSwitching)
209167
}
210168
}
211169
}
@@ -219,6 +177,9 @@ struct ConnectedView: View {
219177
let key = connection.id.uuidString
220178
activeDatabase = UserDefaults.standard.string(forKey: "lastDB.\(key)") ?? ""
221179
activeSchema = UserDefaults.standard.string(forKey: "lastSchema.\(key)") ?? "public"
180+
if let savedTable = UserDefaults.standard.string(forKey: "lastTable.\(key)") {
181+
selectedTable = tables.first { $0.name == savedTable }
182+
}
222183

223184
let hasDriver = appState.connectionManager.session(for: connection.id)?.driver != nil
224185
if !hasDriver, !isConnecting, appError == nil {
@@ -232,14 +193,90 @@ struct ConnectedView: View {
232193
.onChange(of: activeSchema) { _, newValue in
233194
UserDefaults.standard.set(newValue, forKey: "lastSchema.\(connection.id.uuidString)")
234195
}
196+
.onChange(of: selectedTable) { _, newValue in
197+
UserDefaults.standard.set(newValue?.name, forKey: "lastTable.\(connection.id.uuidString)")
198+
}
235199
.onChange(of: scenePhase) { _, phase in
236200
if phase == .active, session != nil {
237201
Task { await reconnectIfNeeded() }
238202
}
239203
}
240204
}
241205

206+
@ViewBuilder
242207
private var connectedContent: some View {
208+
if sizeClass == .regular {
209+
iPadContent
210+
} else {
211+
iPhoneContent
212+
}
213+
}
214+
215+
private var iPadContent: some View {
216+
NavigationSplitView {
217+
TableListView(
218+
connection: connection,
219+
tables: tables,
220+
session: session,
221+
selectedTable: $selectedTable,
222+
onRefresh: { await refreshTables() }
223+
)
224+
.navigationTitle(displayName)
225+
.navigationBarTitleDisplayMode(.inline)
226+
.toolbar {
227+
if connection.safeModeLevel != .off {
228+
ToolbarItem(placement: .topBarTrailing) {
229+
Image(systemName: connection.safeModeLevel == .readOnly ? "lock.fill" : "shield.fill")
230+
.foregroundStyle(connection.safeModeLevel == .readOnly ? .red : .orange)
231+
.font(.caption)
232+
}
233+
}
234+
if supportsDatabaseSwitching && databases.count > 1 {
235+
ToolbarItem(placement: .topBarLeading) {
236+
databaseSwitcherMenu
237+
}
238+
}
239+
if supportsSchemas && schemas.count > 1 && selectedTab == .tables {
240+
ToolbarItem(placement: .topBarTrailing) {
241+
schemaSwitcherMenu
242+
}
243+
}
244+
}
245+
} detail: {
246+
NavigationStack {
247+
switch selectedTab {
248+
case .tables:
249+
if let table = selectedTable {
250+
DataBrowserView(connection: connection, table: table, session: session)
251+
.id(table.id)
252+
} else {
253+
ContentUnavailableView(
254+
"Select a Table",
255+
systemImage: "tablecells",
256+
description: Text("Choose a table from the sidebar.")
257+
)
258+
}
259+
case .query:
260+
QueryEditorView(
261+
session: session,
262+
tables: tables,
263+
databaseType: connection.type,
264+
safeModeLevel: connection.safeModeLevel,
265+
queryHistory: $queryHistory,
266+
connectionId: connection.id,
267+
historyStorage: historyStorage
268+
)
269+
}
270+
}
271+
}
272+
.navigationSplitViewStyle(.balanced)
273+
.safeAreaInset(edge: .bottom) {
274+
tabPicker
275+
.background(.bar)
276+
}
277+
}
278+
279+
private var iPhoneContent: some View {
243280
VStack(spacing: 0) {
244281
switch selectedTab {
245282
case .tables:
@@ -263,6 +300,66 @@ struct ConnectedView: View {
263300
}
264301
}
265302

303+
private var tabPicker: some View {
304+
Picker("Tab", selection: selectedTabBinding) {
305+
Text("Tables").tag(ConnectedTab.tables)
306+
Text("Query").tag(ConnectedTab.query)
307+
}
308+
.pickerStyle(.segmented)
309+
.padding(.horizontal)
310+
.padding(.vertical, 8)
311+
}
312+
313+
private var databaseSwitcherMenu: some View {
314+
Menu {
315+
ForEach(databases, id: \.self) { db in
316+
Button {
317+
Task { await switchDatabase(to: db) }
318+
} label: {
319+
if db == activeDatabase {
320+
Label(db, systemImage: "checkmark")
321+
} else {
322+
Text(db)
323+
}
324+
}
325+
}
326+
} label: {
327+
HStack(spacing: 4) {
328+
Text(activeDatabase)
329+
.font(.subheadline)
330+
if isSwitching {
331+
ProgressView()
332+
.controlSize(.mini)
333+
} else {
334+
Image(systemName: "chevron.down")
335+
.font(.caption2)
336+
.foregroundStyle(.secondary)
337+
}
338+
}
339+
}
340+
.disabled(isSwitching)
341+
}
342+
343+
private var schemaSwitcherMenu: some View {
344+
Menu {
345+
ForEach(schemas, id: \.self) { schema in
346+
Button {
347+
Task { await switchSchema(to: schema) }
348+
} label: {
349+
if schema == activeSchema {
350+
Label(schema, systemImage: "checkmark")
351+
} else {
352+
Text(schema)
353+
}
354+
}
355+
}
356+
} label: {
357+
Label(activeSchema, systemImage: "square.3.layers.3d")
358+
.font(.subheadline)
359+
}
360+
.disabled(isSwitching)
361+
}
362+
266363
private func connect() async {
267364
guard !isConnectInProgress else { return }
268365
guard session == nil else {

TableProMobile/TableProMobile/Views/ConnectionListView.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ struct ConnectionListView: View {
115115
navigateToPendingConnection(appState.pendingConnectionId)
116116
}
117117
} detail: {
118-
NavigationStack {
118+
if sizeClass == .regular {
119119
if let connection = selectedConnection {
120120
ConnectedView(connection: connection)
121121
.id(connection.id)
@@ -126,6 +126,19 @@ struct ConnectionListView: View {
126126
description: Text("Choose a connection from the sidebar.")
127127
)
128128
}
129+
} else {
130+
NavigationStack {
131+
if let connection = selectedConnection {
132+
ConnectedView(connection: connection)
133+
.id(connection.id)
134+
} else {
135+
ContentUnavailableView(
136+
"Select a Connection",
137+
systemImage: "server.rack",
138+
description: Text("Choose a connection from the sidebar.")
139+
)
140+
}
141+
}
129142
}
130143
}
131144
.sheet(isPresented: $showingAddConnection) {

TableProMobile/TableProMobile/Views/DataBrowserView.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ struct DataBrowserView: View {
105105

106106
var body: some View {
107107
searchableContent
108+
.navigationDestination(for: Int.self) { index in
109+
RowDetailView(
110+
columns: columns,
111+
rows: rows,
112+
initialIndex: index,
113+
table: table,
114+
session: session,
115+
columnDetails: columnDetails,
116+
databaseType: connection.type,
117+
safeModeLevel: connection.safeModeLevel,
118+
foreignKeys: foreignKeys,
119+
onSaved: { Task { await loadData() } }
120+
)
121+
}
108122
.userActivity("com.TablePro.viewTable") { activity in
109123
activity.title = table.name
110124
activity.isEligibleForHandoff = true
@@ -248,20 +262,7 @@ struct DataBrowserView: View {
248262
private var rowList: some View {
249263
List {
250264
ForEach(Array(rows.enumerated()), id: \.offset) { index, row in
251-
NavigationLink {
252-
RowDetailView(
253-
columns: columns,
254-
rows: rows,
255-
initialIndex: index,
256-
table: table,
257-
session: session,
258-
columnDetails: columnDetails,
259-
databaseType: connection.type,
260-
safeModeLevel: connection.safeModeLevel,
261-
foreignKeys: foreignKeys,
262-
onSaved: { Task { await loadData() } }
263-
)
264-
} label: {
265+
NavigationLink(value: index) {
265266
RowCard(
266267
columns: columns,
267268
columnDetails: columnDetails,

0 commit comments

Comments
 (0)