Skip to content

Commit 5091ef6

Browse files
authored
refactor: restructure settings system for correct data flow and native macOS patterns (#549)
1 parent 732a250 commit 5091ef6

File tree

10 files changed

+70
-50
lines changed

10 files changed

+70
-50
lines changed

TablePro/AppDelegate+FileOpen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ extension AppDelegate {
258258
fileOpenLogger.info("Installed plugin '\(entry.name)' from Finder")
259259

260260
UserDefaults.standard.set(SettingsTab.plugins.rawValue, forKey: "selectedSettingsTab")
261-
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
261+
NotificationCenter.default.post(name: .openSettingsWindow, object: nil)
262262
} catch {
263263
fileOpenLogger.error("Plugin install failed: \(error.localizedDescription)")
264264
AlertHelper.showErrorSheet(

TablePro/Core/Services/Infrastructure/AppNotifications.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ extension Notification.Name {
3535
// MARK: - SQL Favorites
3636

3737
static let sqlFavoritesDidUpdate = Notification.Name("sqlFavoritesDidUpdate")
38+
39+
// MARK: - Settings Window
40+
41+
static let openSettingsWindow = Notification.Name("com.TablePro.openSettingsWindow")
3842
}

TablePro/Core/Storage/AppSettingsManager.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ final class AppSettingsManager {
124124
}
125125
}
126126

127+
var sync: SyncSettings {
128+
didSet {
129+
storage.saveSync(sync)
130+
SyncChangeTracker.shared.markDirty(.settings, id: "sync")
131+
}
132+
}
133+
127134
@ObservationIgnored private let storage = AppSettingsStorage.shared
128135
/// Reentrancy guard for didSet validation that re-assigns the property.
129136
@ObservationIgnored private var isValidating = false
@@ -145,6 +152,7 @@ final class AppSettingsManager {
145152
self.tabs = storage.loadTabs()
146153
self.keyboard = storage.loadKeyboard()
147154
self.ai = storage.loadAI()
155+
self.sync = storage.loadSync()
148156

149157
// Apply language immediately
150158
general.language.apply()
@@ -217,6 +225,7 @@ final class AppSettingsManager {
217225
tabs = .default
218226
keyboard = .default
219227
ai = .default
228+
sync = .default
220229
storage.resetToDefaults()
221230
}
222231
}

TablePro/Models/Settings/AppSettings.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,19 @@ struct HistorySettings: Codable, Equatable {
417417
autoCleanup: true
418418
)
419419

420+
init(maxEntries: Int = 10_000, maxDays: Int = 90, autoCleanup: Bool = true) {
421+
self.maxEntries = maxEntries
422+
self.maxDays = maxDays
423+
self.autoCleanup = autoCleanup
424+
}
425+
426+
init(from decoder: Decoder) throws {
427+
let container = try decoder.container(keyedBy: CodingKeys.self)
428+
maxEntries = try container.decodeIfPresent(Int.self, forKey: .maxEntries) ?? 10_000
429+
maxDays = try container.decodeIfPresent(Int.self, forKey: .maxDays) ?? 90
430+
autoCleanup = try container.decodeIfPresent(Bool.self, forKey: .autoCleanup) ?? true
431+
}
432+
420433
// MARK: - Validated Properties
421434

422435
/// Validated maxEntries (>= 0)

TablePro/TableProApp.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,8 @@ struct CheckForUpdatesView: View {
590590
private struct OpenWindowHandler: View {
591591
@Environment(\.openWindow)
592592
private var openWindow
593+
@Environment(\.openSettings)
594+
private var openSettings
593595

594596
var body: some View {
595597
Color.clear
@@ -608,5 +610,8 @@ private struct OpenWindowHandler: View {
608610
openWindow(id: "main", value: EditorTabPayload(connectionId: connectionId))
609611
}
610612
}
613+
.onReceive(NotificationCenter.default.publisher(for: .openSettingsWindow)) { _ in
614+
openSettings()
615+
}
611616
}
612617
}

TablePro/Views/Components/SyncStatusIndicator.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import SwiftUI
99

1010
struct SyncStatusIndicator: View {
11+
@Environment(\.openSettings) private var openSettings
1112
private let syncCoordinator = SyncCoordinator.shared
1213
@State private var showActivationSheet = false
1314

@@ -120,10 +121,7 @@ struct SyncStatusIndicator: View {
120121
showActivationSheet = true
121122
default:
122123
UserDefaults.standard.set(SettingsTab.sync.rawValue, forKey: "selectedSettingsTab")
123-
Task { @MainActor in
124-
try? await Task.sleep(for: .milliseconds(100))
125-
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
126-
}
124+
openSettings()
127125
}
128126
}
129127
}

TablePro/Views/Settings/Appearance/ThemeListView.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,6 @@ internal struct ThemeListView: View {
117117
.padding(.horizontal, 8)
118118
.padding(.vertical, 4)
119119
}
120-
.onChange(of: selectedThemeId) {
121-
engine.activateTheme(id: selectedThemeId)
122-
}
123120
.alert(String(localized: "Delete Theme"), isPresented: $showDeleteConfirmation) {
124121
Button(String(localized: "Delete"), role: .destructive) {
125122
deleteSelectedTheme()

TablePro/Views/Settings/GeneralSettingsView.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import SwiftUI
1010

1111
struct GeneralSettingsView: View {
1212
@Binding var settings: GeneralSettings
13+
@Binding var tabSettings: TabSettings
1314
var updaterBridge: UpdaterBridge
14-
@Bindable private var settingsManager = AppSettingsManager.shared
15+
var onResetAll: () -> Void
1516
@State private var initialLanguage: AppLanguage?
1617
@State private var showResetConfirmation = false
1718

@@ -77,13 +78,13 @@ struct GeneralSettingsView: View {
7778
}
7879

7980
Section("Tabs") {
80-
Toggle("Enable preview tabs", isOn: $settingsManager.tabs.enablePreviewTabs)
81+
Toggle("Enable preview tabs", isOn: $tabSettings.enablePreviewTabs)
8182

8283
Text("Single-clicking a table opens a temporary tab that gets replaced on next click.")
8384
.font(.caption)
8485
.foregroundStyle(.secondary)
8586

86-
Toggle("Group all connections in one window", isOn: $settingsManager.tabs.groupAllConnectionTabs)
87+
Toggle("Group all connections in one window", isOn: $tabSettings.groupAllConnectionTabs)
8788

8889
Text("When enabled, tabs from different connections share the same window instead of opening separate windows.")
8990
.font(.caption)
@@ -100,7 +101,7 @@ struct GeneralSettingsView: View {
100101
.scrollContentBackground(.hidden)
101102
.alert(String(localized: "Reset All Settings"), isPresented: $showResetConfirmation) {
102103
Button(String(localized: "Reset"), role: .destructive) {
103-
settingsManager.resetToDefaults()
104+
onResetAll()
104105
}
105106
Button(String(localized: "Cancel"), role: .cancel) {}
106107
} message: {
@@ -118,7 +119,9 @@ struct GeneralSettingsView: View {
118119
#Preview {
119120
GeneralSettingsView(
120121
settings: .constant(.default),
121-
updaterBridge: UpdaterBridge()
122+
tabSettings: .constant(.default),
123+
updaterBridge: UpdaterBridge(),
124+
onResetAll: {}
122125
)
123126
.frame(width: 450, height: 300)
124127
}

TablePro/Views/Settings/SettingsView.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ struct SettingsView: View {
1717
@Bindable private var settingsManager = AppSettingsManager.shared
1818
@Environment(UpdaterBridge.self) var updaterBridge
1919
@AppStorage("selectedSettingsTab") private var selectedTab: String = SettingsTab.general.rawValue
20-
2120
var body: some View {
2221
TabView(selection: $selectedTab) {
23-
GeneralSettingsView(settings: $settingsManager.general, updaterBridge: updaterBridge)
24-
.tabItem {
25-
Label("General", systemImage: "gearshape")
26-
}
27-
.tag(SettingsTab.general.rawValue)
22+
GeneralSettingsView(
23+
settings: $settingsManager.general,
24+
tabSettings: $settingsManager.tabs,
25+
updaterBridge: updaterBridge,
26+
onResetAll: { settingsManager.resetToDefaults() }
27+
)
28+
.tabItem {
29+
Label("General", systemImage: "gearshape")
30+
}
31+
.tag(SettingsTab.general.rawValue)
2832

2933
AppearanceSettingsView(settings: $settingsManager.appearance)
3034
.tabItem {

TablePro/Views/Settings/SyncSettingsView.swift

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88
import SwiftUI
99

1010
struct SyncSettingsView: View {
11+
@Bindable private var settingsManager = AppSettingsManager.shared
1112
@Bindable private var syncCoordinator = SyncCoordinator.shared
12-
@State private var syncSettings: SyncSettings = AppSettingsStorage.shared.loadSync()
1313

1414
private let licenseManager = LicenseManager.shared
1515

1616
var body: some View {
1717
Form {
1818
Section("iCloud Sync") {
19-
Toggle("iCloud Sync:", isOn: $syncSettings.enabled)
20-
.onChange(of: syncSettings.enabled) { _, newValue in
21-
persistSettings()
19+
Toggle("iCloud Sync:", isOn: $settingsManager.sync.enabled)
20+
.onChange(of: settingsManager.sync.enabled) { _, newValue in
2221
updatePasswordSyncFlag()
2322
if newValue {
2423
syncCoordinator.enableSync()
@@ -32,7 +31,7 @@ struct SyncSettingsView: View {
3231
.foregroundStyle(.secondary)
3332
}
3433

35-
if syncSettings.enabled {
34+
if settingsManager.sync.enabled {
3635
statusSection
3736

3837
syncCategoriesSection
@@ -106,20 +105,17 @@ struct SyncSettingsView: View {
106105

107106
private var syncCategoriesSection: some View {
108107
Section("Sync Categories") {
109-
Toggle("Connections:", isOn: $syncSettings.syncConnections)
110-
.onChange(of: syncSettings.syncConnections) { _, newValue in
111-
persistSettings()
112-
if !newValue, syncSettings.syncPasswords {
113-
syncSettings.syncPasswords = false
114-
persistSettings()
108+
Toggle("Connections:", isOn: $settingsManager.sync.syncConnections)
109+
.onChange(of: settingsManager.sync.syncConnections) { _, newValue in
110+
if !newValue, settingsManager.sync.syncPasswords {
111+
settingsManager.sync.syncPasswords = false
115112
onPasswordSyncChanged(false)
116113
}
117114
}
118115

119-
if syncSettings.syncConnections {
120-
Toggle("Passwords:", isOn: $syncSettings.syncPasswords)
121-
.onChange(of: syncSettings.syncPasswords) { _, newValue in
122-
persistSettings()
116+
if settingsManager.sync.syncConnections {
117+
Toggle("Passwords:", isOn: $settingsManager.sync.syncPasswords)
118+
.onChange(of: settingsManager.sync.syncPasswords) { _, newValue in
123119
onPasswordSyncChanged(newValue)
124120
}
125121
.padding(.leading, 20)
@@ -130,14 +126,11 @@ struct SyncSettingsView: View {
130126
.padding(.leading, 20)
131127
}
132128

133-
Toggle("Groups & Tags:", isOn: $syncSettings.syncGroupsAndTags)
134-
.onChange(of: syncSettings.syncGroupsAndTags) { _, _ in persistSettings() }
129+
Toggle("Groups & Tags:", isOn: $settingsManager.sync.syncGroupsAndTags)
135130

136-
Toggle("SSH Profiles:", isOn: $syncSettings.syncSSHProfiles)
137-
.onChange(of: syncSettings.syncSSHProfiles) { _, _ in persistSettings() }
131+
Toggle("SSH Profiles:", isOn: $settingsManager.sync.syncSSHProfiles)
138132

139-
Toggle("Settings:", isOn: $syncSettings.syncSettings)
140-
.onChange(of: syncSettings.syncSettings) { _, _ in persistSettings() }
133+
Toggle("Settings:", isOn: $settingsManager.sync.syncSettings)
141134
}
142135
}
143136

@@ -167,20 +160,17 @@ struct SyncSettingsView: View {
167160

168161
// MARK: - Helpers
169162

170-
private func persistSettings() {
171-
AppSettingsStorage.shared.saveSync(syncSettings)
172-
}
173-
174163
private func onPasswordSyncChanged(_ enabled: Bool) {
175-
let effective = syncSettings.enabled && syncSettings.syncConnections && enabled
164+
let effective = settingsManager.sync.enabled && settingsManager.sync.syncConnections && enabled
176165
Task.detached {
177166
KeychainHelper.shared.migratePasswordSyncState(synchronizable: effective)
178167
UserDefaults.standard.set(effective, forKey: KeychainHelper.passwordSyncEnabledKey)
179168
}
180169
}
181170

182171
private func updatePasswordSyncFlag() {
183-
let effective = syncSettings.enabled && syncSettings.syncConnections && syncSettings.syncPasswords
172+
let sync = settingsManager.sync
173+
let effective = sync.enabled && sync.syncConnections && sync.syncPasswords
184174
let current = UserDefaults.standard.bool(forKey: KeychainHelper.passwordSyncEnabledKey)
185175
guard effective != current else { return }
186176
Task.detached {
@@ -190,10 +180,7 @@ struct SyncSettingsView: View {
190180
}
191181

192182
private func openLicenseSettings() {
193-
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
194-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
195-
UserDefaults.standard.set(SettingsTab.license.rawValue, forKey: "selectedSettingsTab")
196-
}
183+
UserDefaults.standard.set(SettingsTab.license.rawValue, forKey: "selectedSettingsTab")
197184
}
198185
}
199186

0 commit comments

Comments
 (0)