Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- **Agent** mode, giving one session the whole connection window: its sessions, its conversation, and what it ran.
- Row previews and the query editor sized to the display on iPad and on iPhone Duo's inner display.
- Table list and table browser side by side on iPad and on iPhone Duo's inner display.
- **View > Mode**, with **Toggle Agent Mode** on ⌥⇧⌘A.
- Agent mode holds its connection at Safe Mode **Alert** while it is on, and hands back the level you set on the way out.
- **Open in Agent Mode** on a connection in the welcome window.
Expand Down Expand Up @@ -48,6 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- A column resized moments before its table was dropped saving the layout back over the clear.
- A connection left pointing at a database that was dropped.
- A dropped table's saved filters, column layout, highlight rules, value formats and label columns coming back on a table recreated with its name.
- Database switcher, Safe Mode badge and multi-select actions missing from the side bar on iPhone Duo.
- Sort, Filter, More, Insert Row, Share, Edit and Add Tag missing from the side bar on iPhone Duo.
- Safe Mode badge with no accessibility label.
- Keyboard bar separator measured against the main screen instead of the display the editor is on.
- A dropped database or schema leaving every one of its tables' saved settings behind.
- Favorites rows left behind by a dropped table, schema or database.
- Column and operator pull-downs in **Highlight Rules** snapping back to their previous value, leaving every rule on **equals** and on the column it was created with. (#3015)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ final class ConnectionCoordinator {

private(set) var session: ConnectionSession?
private(set) var phase: ConnectionPhase = .connecting
private(set) var tables: [TableInfo] = []
private(set) var tables: [TableInfo] = [] {
didSet { selectedTable = TableSelectionResolver.keeping(selectedTable, in: tables) }
}
private(set) var databases: [String] = []
private(set) var schemas: [String] = []
private(set) var activeDatabase: String = ""
Expand All @@ -29,7 +31,7 @@ final class ConnectionCoordinator {
}
var pendingQuery: String?
var pendingTableName: String?
var tablesPath = NavigationPath()
var selectedTable: TableInfo?

private(set) var queryHistory: [QueryHistoryItem] = []
private let historyStorage = QueryHistoryStorage()
Expand Down Expand Up @@ -371,13 +373,10 @@ final class ConnectionCoordinator {
}

func navigateToPendingTable() {
guard let tableName = pendingTableName,
let table = tables.first(where: { $0.name == tableName }) else { return }
guard let table = TableSelectionResolver.resolve(pendingName: pendingTableName, in: tables) else { return }
pendingTableName = nil
selectedTab = .tables
Task { @MainActor in
tablesPath.append(table)
}
selectedTable = table
}

// MARK: - Private Helpers
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import CoreGraphics

/// A detached view reports a display scale of zero until it joins a window, and `1 / 0` is a
/// constant `NSLayoutConstraint` rejects, so the scale is clamped before it becomes a thickness.
nonisolated enum HairlineThickness {
static func points(forDisplayScale scale: CGFloat) -> CGFloat {
guard scale.isFinite, scale >= 1 else { return 1 }
return 1 / scale
}
}
9 changes: 4 additions & 5 deletions TableProMobile/TableProMobile/Views/Components/RowCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ struct RowCard: View {
let columns: [ColumnInfo]
let columnDetails: [ColumnInfo]
let row: [String?]

private static let maxPreview = 4
var previewFieldCount: Int = DuoLayoutResolver.compactPreviewFieldCount

private var pkNames: Set<String> {
Set(columnDetails.filter(\.isPrimaryKey).map(\.name))
Expand All @@ -26,7 +25,7 @@ struct RowCard: View {
let title = titlePair?.name
return zip(columns, row)
.filter { !pks.contains($0.0.name) && $0.0.name != title }
.prefix(Self.maxPreview - 1)
.prefix(max(previewFieldCount - 1, 0))
.map { ($0.0.name, $0.1 ?? "NULL") }
}

Expand Down Expand Up @@ -56,8 +55,8 @@ struct RowCard: View {
}
}

if columns.count > Self.maxPreview {
Text("+\(columns.count - Self.maxPreview) more columns")
if columns.count > previewFieldCount {
Text("+\(columns.count - previewFieldCount) more columns")
.font(.caption2)
.foregroundStyle(.quaternary)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ struct SQLHighlightTextView: UIViewRepresentable {
separator.translatesAutoresizingMaskIntoConstraints = false
toolbar.addSubview(separator)

let separatorHeight = separator.heightAnchor.constraint(
equalToConstant: HairlineThickness.points(forDisplayScale: toolbar.traitCollection.displayScale)
)
toolbar.registerForTraitChanges([UITraitDisplayScale.self]) { (view: UIView, _) in
separatorHeight.constant = HairlineThickness.points(forDisplayScale: view.traitCollection.displayScale)
}

let scrollView = UIScrollView()
scrollView.showsHorizontalScrollIndicator = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
Expand Down Expand Up @@ -127,7 +134,7 @@ struct SQLHighlightTextView: UIViewRepresentable {
separator.topAnchor.constraint(equalTo: toolbar.topAnchor),
separator.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor),
separator.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor),
separator.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
separatorHeight,

scrollView.topAnchor.constraint(equalTo: toolbar.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor, constant: 8),
Expand Down
43 changes: 21 additions & 22 deletions TableProMobile/TableProMobile/Views/ConnectedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,26 @@ struct ConnectedView: View {
@Bindable var coordinator = coordinator
return TabView(selection: $coordinator.selectedTab) {
Tab("Tables", systemImage: "tablecells", value: .tables) {
NavigationStack(path: $coordinator.tablesPath) {
NavigationSplitView {
tabChrome(coordinator) {
TableListView(connectionId: connection.id)
}
.navigationDestination(for: TableInfo.self) { table in
DataBrowserView(table: table)
.environment(coordinator)
} detail: {
NavigationStack {
if let table = coordinator.selectedTable {
DataBrowserView(table: table)
.environment(coordinator)
.id(table)
} else {
ContentUnavailableView(
"No Table Selected",
systemImage: "tablecells",
description: Text("Pick a table to browse its rows.")
)
}
}
}
.navigationSplitViewStyle(.balanced)
}
Tab("Query", systemImage: "terminal", value: .query) {
NavigationStack {
Expand Down Expand Up @@ -265,16 +276,14 @@ struct ConnectedView: View {
Button {
presenter.presentConnectionEditor(for: connection.id)
} label: {
Image(systemName: "pencil")
.accessibilityLabel(Text("Edit Connection"))
Label("Edit Connection", systemImage: "pencil")
}
}
}
if connection.safeModeLevel != .off {
if let badge = SafeModeBadge(level: connection.safeModeLevel) {
ToolbarItem(placement: .topBarTrailing) {
Image(systemName: connection.safeModeLevel == .readOnly ? "lock.fill" : "shield.fill")
.foregroundStyle(connection.safeModeLevel == .readOnly ? .red : .orange)
.font(.caption)
Label(badge.title, systemImage: badge.symbolName)
.foregroundStyle(badge.tint == .blocked ? Color.red : Color.orange)
}
}
if coordinator.supportsDatabaseSwitching && coordinator.databases.count > 1 {
Expand All @@ -292,18 +301,8 @@ struct ConnectedView: View {
}
}
} label: {
HStack(spacing: 4) {
Text(coordinator.activeDatabase)
.font(.subheadline)
if coordinator.isSwitching {
ProgressView()
.controlSize(.mini)
} else {
Image(systemName: "chevron.down")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
Label(coordinator.activeDatabase, systemImage: "cylinder.split.1x2")
.font(.subheadline)
}
.disabled(coordinator.isSwitching)
}
Expand Down
22 changes: 15 additions & 7 deletions TableProMobile/TableProMobile/Views/ConnectionListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,16 @@ struct ConnectionListView: View {
ToolbarItemGroup(placement: .topBarTrailing) {
moreMenu
if hasLibraryItems {
Button(isEditing ? String(localized: "Done") : String(localized: "Edit")) {
Button {
withAnimation {
editMode = isEditing ? .inactive : .active
}
} label: {
if isEditing {
Label("Done", systemImage: "checkmark")
} else {
Label("Edit", systemImage: "checklist")
}
}
}
Button {
Expand All @@ -583,15 +589,17 @@ struct ConnectionListView: View {
if isEditing {
ToolbarItemGroup(placement: .bottomBar) {
let ids = selectedConnectionIds
Button("Move") {
Button {
presenter.present(.moveConnections(ids))
} label: {
Label("Move", systemImage: "folder")
}
.disabled(ids.isEmpty)
Spacer()
selectionFavoriteButton(ids)
Spacer()
Button(String(localized: "Delete"), role: .destructive) {
Button(role: .destructive) {
connectionsPendingDeletion = Set(ids)
} label: {
Label("Delete", systemImage: "trash")
}
.disabled(ids.isEmpty)
}
Expand All @@ -606,9 +614,9 @@ struct ConnectionListView: View {
appState.setFavorite(selected, isFavorite: !allFavorites)
} label: {
if allFavorites {
Text("Unfavorite")
Label("Unfavorite", systemImage: "star.slash")
} else {
Text("Favorite")
Label("Favorite", systemImage: "star")
}
}
.disabled(ids.isEmpty)
Expand Down
23 changes: 15 additions & 8 deletions TableProMobile/TableProMobile/Views/DataBrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import TableProQuery
struct DataBrowserView: View {
@Environment(AppState.self) private var appState
@Environment(ConnectionCoordinator.self) private var coordinator
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
let table: TableInfo

private var connection: DatabaseConnection { coordinator.connection }
Expand Down Expand Up @@ -287,7 +288,12 @@ struct DataBrowserView: View {
}
)
} label: {
RowCard(columns: columns, columnDetails: viewModel.columnDetails, row: row)
RowCard(
columns: columns,
columnDetails: viewModel.columnDetails,
row: row,
previewFieldCount: DuoLayoutResolver.previewFieldCount(for: duoWidthClass)
)
}
.hoverEffect()
.contextMenu { rowContextMenu(row: row) }
Expand Down Expand Up @@ -377,19 +383,17 @@ struct DataBrowserView: View {
.pickerStyle(.inline)
}
} label: {
Image(systemName: viewModel.sortState.isSorting
Label("Sort", systemImage: viewModel.sortState.isSorting
? "arrow.up.arrow.down.circle.fill"
: "arrow.up.arrow.down.circle")
.accessibilityLabel(Text("Sort"))
}
.disabled(columns.isEmpty)
}
ToolbarItem(placement: .topBarTrailing) {
Button { showFilterSheet = true } label: {
Image(systemName: viewModel.hasActiveFilters
Label("Filter", systemImage: viewModel.hasActiveFilters
? "line.3.horizontal.decrease.circle.fill"
: "line.3.horizontal.decrease.circle")
.accessibilityLabel(Text("Filter"))
}
.badge(viewModel.activeFilterCount)
}
Expand All @@ -414,19 +418,22 @@ struct DataBrowserView: View {
}
}
} label: {
Image(systemName: "ellipsis.circle")
Label("More", systemImage: "ellipsis.circle")
}
}
if canInsertRow {
ToolbarItem(placement: .primaryAction) {
Button { showInsertSheet = true } label: {
Image(systemName: "plus")
.accessibilityLabel(Text("Insert Row"))
Label("Insert Row", systemImage: "plus")
}
}
}
}

private var duoWidthClass: DuoWidthClass {
horizontalSizeClass == .regular ? .regular : .compact
}

private var showsPaginationBar: Bool {
viewModel.showsPaginationBar && !searchFocused
}
Expand Down
67 changes: 67 additions & 0 deletions TableProMobile/TableProMobile/Views/DuoLayoutResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import CoreGraphics
import Foundation

nonisolated enum DuoWidthClass: Equatable, Sendable {
case compact
case regular
}

nonisolated struct DuoLayoutContext: Equatable, Sendable {
let size: CGSize
let widthClass: DuoWidthClass
let showsResult: Bool

init(size: CGSize, widthClass: DuoWidthClass, showsResult: Bool = false) {
self.size = size
self.widthClass = widthClass
self.showsResult = showsResult
}
}

nonisolated struct DuoLayoutPlan: Equatable, Sendable {
let previewFieldCount: Int
let editorMaxHeight: CGFloat
let revealsDetailAlongsideList: Bool

static let compactDefault = DuoLayoutPlan(
previewFieldCount: DuoLayoutResolver.compactPreviewFieldCount,
editorMaxHeight: DuoLayoutResolver.compactEditorHeightWithoutResult,
revealsDetailAlongsideList: false
)
}

nonisolated enum DuoLayoutResolver {
static let compactPreviewFieldCount = 4
static let regularPreviewFieldCount = 8
static let compactEditorHeightWithResult: CGFloat = 120
static let compactEditorHeightWithoutResult: CGFloat = 250
static let minimumEditorHeight: CGFloat = 120
static let maximumEditorHeight: CGFloat = 640

static func plan(for context: DuoLayoutContext) -> DuoLayoutPlan {
DuoLayoutPlan(
previewFieldCount: previewFieldCount(for: context.widthClass),
editorMaxHeight: editorMaxHeight(for: context),
revealsDetailAlongsideList: context.widthClass == .regular
)
}

static func previewFieldCount(for widthClass: DuoWidthClass) -> Int {
switch widthClass {
case .compact: return compactPreviewFieldCount
case .regular: return regularPreviewFieldCount
}
}

static func editorMaxHeight(for context: DuoLayoutContext) -> CGFloat {
guard context.widthClass == .regular else {
return context.showsResult ? compactEditorHeightWithResult : compactEditorHeightWithoutResult
}
let height = context.size.height
guard height.isFinite, height > 0 else {
return context.showsResult ? compactEditorHeightWithResult : compactEditorHeightWithoutResult
}
let fraction: CGFloat = context.showsResult ? 0.38 : 0.6
return min(max(height * fraction, minimumEditorHeight), maximumEditorHeight)
}
}
Loading
Loading