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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- iOS Settings: iCloud Sync toggle (off keeps connections, groups, and tags on this device only and disables the sync toolbar button), Rows per Page picker (50/100/200/500, applied to new data browser sessions), Default Safe Mode picker (applied when adding a new connection)
- iOS: alert when the active connection is deleted mid-session (for example via iCloud sync from another device), so a stale screen no longer fails silently on the next action
- iOS: Face ID, Touch ID, or Optic ID lock with cold-launch protection and idle timeout (1, 5, 15, or 60 minutes), opt-in from Settings
- iOS: Connection Info tab replaces the per-connection Settings tab, showing host, SSL, SSH tunnel, active database, and live connection status
Expand Down
73 changes: 73 additions & 0 deletions TableProMobile/TableProMobile/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,46 @@
}
}
},
"%@, %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@, %2$@"
}
}
}
},
"%@, %@, %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@, %2$@, %3$@"
}
}
}
},
"%@, %@, %@, tag %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@, %2$@, %3$@, tag %4$@"
}
}
}
},
"%@, %@, %lld rows" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@, %2$@, %3$lld rows"
}
}
}
},
"%@.%@" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -1218,6 +1258,9 @@
},
"Default DB" : {

},
"Default Safe Mode" : {

},
"Default: %@" : {
"localizations" : {
Expand All @@ -1234,6 +1277,9 @@
}
}
}
},
"Defaults applied when adding a new connection and when opening a table for the first time." : {

},
"Delete" : {
"localizations" : {
Expand Down Expand Up @@ -1922,6 +1968,9 @@
}
}
}
},
"iCloud Sync" : {

},
"Immediately" : {

Expand Down Expand Up @@ -2174,6 +2223,9 @@
}
}
}
},
"New Connections" : {

},
"New Database" : {
"localizations" : {
Expand Down Expand Up @@ -2628,6 +2680,12 @@
}
}
}
},
"Opens table data" : {

},
"Opens this connection" : {

},
"Operator" : {
"localizations" : {
Expand Down Expand Up @@ -3581,6 +3639,9 @@
}
}
}
},
"Sync" : {

},
"Sync from iCloud" : {
"localizations" : {
Expand All @@ -3597,6 +3658,9 @@
}
}
}
},
"Sync with iCloud" : {

},
"Syncing from iCloud..." : {
"localizations" : {
Expand Down Expand Up @@ -3630,6 +3694,9 @@
}
}
}
},
"Table" : {

},
"Table Structure" : {
"localizations" : {
Expand Down Expand Up @@ -4071,6 +4138,9 @@
}
}
}
},
"View" : {

},
"Views" : {
"extractionState" : "stale",
Expand Down Expand Up @@ -4104,6 +4174,9 @@
}
}
}
},
"When off, connections, groups, and tags stay on this device only. Existing iCloud data is not deleted." : {

},
"Write Query Blocked" : {
"localizations" : {
Expand Down
26 changes: 26 additions & 0 deletions TableProMobile/TableProMobile/Platform/AppPreferences.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation
import TableProModels

enum AppPreferences {
static let cloudSyncEnabledKey = "com.TablePro.settings.cloudSyncEnabled"
static let defaultPageSizeKey = "com.TablePro.settings.defaultPageSize"
static let defaultSafeModeKey = "com.TablePro.settings.defaultSafeMode"

static let pageSizeOptions: [Int] = [50, 100, 200, 500]

static var isCloudSyncEnabled: Bool {
UserDefaults.standard.object(forKey: cloudSyncEnabledKey) as? Bool ?? true
}

static var defaultPageSize: Int {
guard let stored = UserDefaults.standard.object(forKey: defaultPageSizeKey) as? Int,
pageSizeOptions.contains(stored) else { return 100 }
return stored
}

static var defaultSafeMode: SafeModeLevel {
guard let raw = UserDefaults.standard.string(forKey: defaultSafeModeKey),
let level = SafeModeLevel(rawValue: raw) else { return .off }
return level
}
}
16 changes: 9 additions & 7 deletions TableProMobile/TableProMobile/TableProMobileApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,15 @@ struct TableProMobileApp: App {
switch phase {
case .active:
MemoryPressureMonitor.shared.start()
syncTask?.cancel()
syncTask = Task {
await appState.syncCoordinator.sync(
localConnections: appState.connections,
localGroups: appState.groups,
localTags: appState.tags
)
if AppPreferences.isCloudSyncEnabled {
syncTask?.cancel()
syncTask = Task {
await appState.syncCoordinator.sync(
localConnections: appState.connections,
localGroups: appState.groups,
localTags: appState.tags
)
}
}
if heartbeatTask == nil {
let provider = IOSAnalyticsProvider.shared
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ final class ConnectionFormViewModel {

init(editing: DatabaseConnection? = nil) {
self.existingConnection = editing
guard let conn = editing else { return }
guard let conn = editing else {
safeModeLevel = AppPreferences.defaultSafeMode
return
}
name = conn.name
type = conn.type
host = conn.host
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ final class DataBrowserViewModel {

private(set) var columnDetails: [ColumnInfo] = []
private(set) var foreignKeys: [ForeignKeyInfo] = []
private(set) var pagination = PaginationState(pageSize: 100, currentPage: 0)
private(set) var pagination: PaginationState
var sortState = SortState()
var filters: [TableFilter] = []
var filterLogicMode: FilterLogicMode = .and
Expand All @@ -53,6 +53,7 @@ final class DataBrowserViewModel {

init(windowCapacity: Int = 1_000) {
self.window = RowWindow(capacity: windowCapacity)
self.pagination = PaginationState(pageSize: AppPreferences.defaultPageSize, currentPage: 0)
}

// MARK: - Computed
Expand Down
8 changes: 6 additions & 2 deletions TableProMobile/TableProMobile/Views/ConnectionListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct ConnectionListView: View {
@State private var showingTagManagement = false
@AppStorage("lastFilterTagId") private var filterTagIdString: String?
@AppStorage("groupByGroup") private var groupByGroup = false
@AppStorage(AppPreferences.cloudSyncEnabledKey) private var cloudSyncEnabled = true
@State private var editMode: EditMode = .inactive
@State private var connectionToDelete: DatabaseConnection?
@State private var showingSettings = false
Expand Down Expand Up @@ -94,10 +95,12 @@ struct ConnectionListView: View {
ProgressView()
.controlSize(.small)
} else {
Image(systemName: "arrow.triangle.2.circlepath.icloud")
Image(systemName: cloudSyncEnabled
? "arrow.triangle.2.circlepath.icloud"
: "icloud.slash")
}
}
.disabled(isSyncing)
.disabled(isSyncing || !cloudSyncEnabled)
.accessibilityLabel(Text("Sync with iCloud"))

Button {
Expand Down Expand Up @@ -221,6 +224,7 @@ struct ConnectionListView: View {
}
.environment(\.editMode, $editMode)
.refreshable {
guard cloudSyncEnabled else { return }
await appState.syncCoordinator.sync(
localConnections: appState.connections,
localGroups: appState.groups,
Expand Down
36 changes: 36 additions & 0 deletions TableProMobile/TableProMobile/Views/SettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import SwiftUI
import TableProModels

struct SettingsView: View {
@AppStorage("com.TablePro.settings.shareAnalytics") private var shareAnalytics = true
@AppStorage(AppLockState.lockEnabledKey) private var lockEnabled = false
@AppStorage(AppLockState.lockTimeoutKey) private var lockTimeoutSeconds = AppLockState.AutoLockTimeout.fiveMinutes.rawValue
@AppStorage(AppPreferences.cloudSyncEnabledKey) private var cloudSyncEnabled = true
@AppStorage(AppPreferences.defaultPageSizeKey) private var defaultPageSize = 100
@AppStorage(AppPreferences.defaultSafeModeKey) private var defaultSafeModeRaw = SafeModeLevel.off.rawValue

private let auth = BiometricAuthService()

var body: some View {
Form {
biometricSection
syncSection
defaultsSection

Section("Privacy") {
Toggle(String(localized: "Share anonymous usage data"), isOn: $shareAnalytics)
Expand Down Expand Up @@ -53,6 +59,36 @@ struct SettingsView: View {
}
}

private var syncSection: some View {
Section {
Toggle(String(localized: "iCloud Sync"), isOn: $cloudSyncEnabled)
} header: {
Text("Sync")
} footer: {
Text("When off, connections, groups, and tags stay on this device only. Existing iCloud data is not deleted.")
}
}

private var defaultsSection: some View {
Section {
Picker(String(localized: "Rows per Page"), selection: $defaultPageSize) {
ForEach(AppPreferences.pageSizeOptions, id: \.self) { size in
Text("\(size) rows").tag(size)
}
}

Picker(String(localized: "Default Safe Mode"), selection: $defaultSafeModeRaw) {
ForEach(SafeModeLevel.allCases) { level in
Text(level.displayName).tag(level.rawValue)
}
}
} header: {
Text("New Connections")
} footer: {
Text("Defaults applied when adding a new connection and when opening a table for the first time.")
}
}

private func toggleLabel(for availability: BiometricAuthService.Availability) -> String {
switch availability {
case .faceID: String(localized: "Require Face ID")
Expand Down
Loading