diff --git a/TablePro/AppDelegate+FileOpen.swift b/TablePro/AppDelegate+FileOpen.swift index b0fcf3c52..45058fca4 100644 --- a/TablePro/AppDelegate+FileOpen.swift +++ b/TablePro/AppDelegate+FileOpen.swift @@ -258,7 +258,7 @@ extension AppDelegate { fileOpenLogger.info("Installed plugin '\(entry.name)' from Finder") UserDefaults.standard.set(SettingsTab.plugins.rawValue, forKey: "selectedSettingsTab") - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) + NotificationCenter.default.post(name: .openSettingsWindow, object: nil) } catch { fileOpenLogger.error("Plugin install failed: \(error.localizedDescription)") AlertHelper.showErrorSheet( diff --git a/TablePro/Core/Services/Infrastructure/AppNotifications.swift b/TablePro/Core/Services/Infrastructure/AppNotifications.swift index dd6bf273a..d0ebc1529 100644 --- a/TablePro/Core/Services/Infrastructure/AppNotifications.swift +++ b/TablePro/Core/Services/Infrastructure/AppNotifications.swift @@ -35,4 +35,8 @@ extension Notification.Name { // MARK: - SQL Favorites static let sqlFavoritesDidUpdate = Notification.Name("sqlFavoritesDidUpdate") + + // MARK: - Settings Window + + static let openSettingsWindow = Notification.Name("com.TablePro.openSettingsWindow") } diff --git a/TablePro/Core/Storage/AppSettingsManager.swift b/TablePro/Core/Storage/AppSettingsManager.swift index 577e25557..fea26200a 100644 --- a/TablePro/Core/Storage/AppSettingsManager.swift +++ b/TablePro/Core/Storage/AppSettingsManager.swift @@ -124,6 +124,13 @@ final class AppSettingsManager { } } + var sync: SyncSettings { + didSet { + storage.saveSync(sync) + SyncChangeTracker.shared.markDirty(.settings, id: "sync") + } + } + @ObservationIgnored private let storage = AppSettingsStorage.shared /// Reentrancy guard for didSet validation that re-assigns the property. @ObservationIgnored private var isValidating = false @@ -145,6 +152,7 @@ final class AppSettingsManager { self.tabs = storage.loadTabs() self.keyboard = storage.loadKeyboard() self.ai = storage.loadAI() + self.sync = storage.loadSync() // Apply language immediately general.language.apply() @@ -217,6 +225,7 @@ final class AppSettingsManager { tabs = .default keyboard = .default ai = .default + sync = .default storage.resetToDefaults() } } diff --git a/TablePro/Models/Settings/AppSettings.swift b/TablePro/Models/Settings/AppSettings.swift index aa0722871..a2070bdb0 100644 --- a/TablePro/Models/Settings/AppSettings.swift +++ b/TablePro/Models/Settings/AppSettings.swift @@ -417,6 +417,19 @@ struct HistorySettings: Codable, Equatable { autoCleanup: true ) + init(maxEntries: Int = 10_000, maxDays: Int = 90, autoCleanup: Bool = true) { + self.maxEntries = maxEntries + self.maxDays = maxDays + self.autoCleanup = autoCleanup + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + maxEntries = try container.decodeIfPresent(Int.self, forKey: .maxEntries) ?? 10_000 + maxDays = try container.decodeIfPresent(Int.self, forKey: .maxDays) ?? 90 + autoCleanup = try container.decodeIfPresent(Bool.self, forKey: .autoCleanup) ?? true + } + // MARK: - Validated Properties /// Validated maxEntries (>= 0) diff --git a/TablePro/TableProApp.swift b/TablePro/TableProApp.swift index 2519cc345..cb942712e 100644 --- a/TablePro/TableProApp.swift +++ b/TablePro/TableProApp.swift @@ -590,6 +590,8 @@ struct CheckForUpdatesView: View { private struct OpenWindowHandler: View { @Environment(\.openWindow) private var openWindow + @Environment(\.openSettings) + private var openSettings var body: some View { Color.clear @@ -608,5 +610,8 @@ private struct OpenWindowHandler: View { openWindow(id: "main", value: EditorTabPayload(connectionId: connectionId)) } } + .onReceive(NotificationCenter.default.publisher(for: .openSettingsWindow)) { _ in + openSettings() + } } } diff --git a/TablePro/Views/Components/SyncStatusIndicator.swift b/TablePro/Views/Components/SyncStatusIndicator.swift index 0960186c1..a40eb7757 100644 --- a/TablePro/Views/Components/SyncStatusIndicator.swift +++ b/TablePro/Views/Components/SyncStatusIndicator.swift @@ -8,6 +8,7 @@ import SwiftUI struct SyncStatusIndicator: View { + @Environment(\.openSettings) private var openSettings private let syncCoordinator = SyncCoordinator.shared @State private var showActivationSheet = false @@ -120,10 +121,7 @@ struct SyncStatusIndicator: View { showActivationSheet = true default: UserDefaults.standard.set(SettingsTab.sync.rawValue, forKey: "selectedSettingsTab") - Task { @MainActor in - try? await Task.sleep(for: .milliseconds(100)) - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } + openSettings() } } } diff --git a/TablePro/Views/Settings/Appearance/ThemeListView.swift b/TablePro/Views/Settings/Appearance/ThemeListView.swift index a3e868b65..16122f97b 100644 --- a/TablePro/Views/Settings/Appearance/ThemeListView.swift +++ b/TablePro/Views/Settings/Appearance/ThemeListView.swift @@ -117,9 +117,6 @@ internal struct ThemeListView: View { .padding(.horizontal, 8) .padding(.vertical, 4) } - .onChange(of: selectedThemeId) { - engine.activateTheme(id: selectedThemeId) - } .alert(String(localized: "Delete Theme"), isPresented: $showDeleteConfirmation) { Button(String(localized: "Delete"), role: .destructive) { deleteSelectedTheme() diff --git a/TablePro/Views/Settings/GeneralSettingsView.swift b/TablePro/Views/Settings/GeneralSettingsView.swift index 3fa448859..97f05346d 100644 --- a/TablePro/Views/Settings/GeneralSettingsView.swift +++ b/TablePro/Views/Settings/GeneralSettingsView.swift @@ -10,8 +10,9 @@ import SwiftUI struct GeneralSettingsView: View { @Binding var settings: GeneralSettings + @Binding var tabSettings: TabSettings var updaterBridge: UpdaterBridge - @Bindable private var settingsManager = AppSettingsManager.shared + var onResetAll: () -> Void @State private var initialLanguage: AppLanguage? @State private var showResetConfirmation = false @@ -77,13 +78,13 @@ struct GeneralSettingsView: View { } Section("Tabs") { - Toggle("Enable preview tabs", isOn: $settingsManager.tabs.enablePreviewTabs) + Toggle("Enable preview tabs", isOn: $tabSettings.enablePreviewTabs) Text("Single-clicking a table opens a temporary tab that gets replaced on next click.") .font(.caption) .foregroundStyle(.secondary) - Toggle("Group all connections in one window", isOn: $settingsManager.tabs.groupAllConnectionTabs) + Toggle("Group all connections in one window", isOn: $tabSettings.groupAllConnectionTabs) Text("When enabled, tabs from different connections share the same window instead of opening separate windows.") .font(.caption) @@ -100,7 +101,7 @@ struct GeneralSettingsView: View { .scrollContentBackground(.hidden) .alert(String(localized: "Reset All Settings"), isPresented: $showResetConfirmation) { Button(String(localized: "Reset"), role: .destructive) { - settingsManager.resetToDefaults() + onResetAll() } Button(String(localized: "Cancel"), role: .cancel) {} } message: { @@ -118,7 +119,9 @@ struct GeneralSettingsView: View { #Preview { GeneralSettingsView( settings: .constant(.default), - updaterBridge: UpdaterBridge() + tabSettings: .constant(.default), + updaterBridge: UpdaterBridge(), + onResetAll: {} ) .frame(width: 450, height: 300) } diff --git a/TablePro/Views/Settings/SettingsView.swift b/TablePro/Views/Settings/SettingsView.swift index 5eb680e69..6a808e187 100644 --- a/TablePro/Views/Settings/SettingsView.swift +++ b/TablePro/Views/Settings/SettingsView.swift @@ -17,14 +17,18 @@ struct SettingsView: View { @Bindable private var settingsManager = AppSettingsManager.shared @Environment(UpdaterBridge.self) var updaterBridge @AppStorage("selectedSettingsTab") private var selectedTab: String = SettingsTab.general.rawValue - var body: some View { TabView(selection: $selectedTab) { - GeneralSettingsView(settings: $settingsManager.general, updaterBridge: updaterBridge) - .tabItem { - Label("General", systemImage: "gearshape") - } - .tag(SettingsTab.general.rawValue) + GeneralSettingsView( + settings: $settingsManager.general, + tabSettings: $settingsManager.tabs, + updaterBridge: updaterBridge, + onResetAll: { settingsManager.resetToDefaults() } + ) + .tabItem { + Label("General", systemImage: "gearshape") + } + .tag(SettingsTab.general.rawValue) AppearanceSettingsView(settings: $settingsManager.appearance) .tabItem { diff --git a/TablePro/Views/Settings/SyncSettingsView.swift b/TablePro/Views/Settings/SyncSettingsView.swift index fe7af2ff3..c29352b1a 100644 --- a/TablePro/Views/Settings/SyncSettingsView.swift +++ b/TablePro/Views/Settings/SyncSettingsView.swift @@ -8,17 +8,16 @@ import SwiftUI struct SyncSettingsView: View { + @Bindable private var settingsManager = AppSettingsManager.shared @Bindable private var syncCoordinator = SyncCoordinator.shared - @State private var syncSettings: SyncSettings = AppSettingsStorage.shared.loadSync() private let licenseManager = LicenseManager.shared var body: some View { Form { Section("iCloud Sync") { - Toggle("iCloud Sync:", isOn: $syncSettings.enabled) - .onChange(of: syncSettings.enabled) { _, newValue in - persistSettings() + Toggle("iCloud Sync:", isOn: $settingsManager.sync.enabled) + .onChange(of: settingsManager.sync.enabled) { _, newValue in updatePasswordSyncFlag() if newValue { syncCoordinator.enableSync() @@ -32,7 +31,7 @@ struct SyncSettingsView: View { .foregroundStyle(.secondary) } - if syncSettings.enabled { + if settingsManager.sync.enabled { statusSection syncCategoriesSection @@ -106,20 +105,17 @@ struct SyncSettingsView: View { private var syncCategoriesSection: some View { Section("Sync Categories") { - Toggle("Connections:", isOn: $syncSettings.syncConnections) - .onChange(of: syncSettings.syncConnections) { _, newValue in - persistSettings() - if !newValue, syncSettings.syncPasswords { - syncSettings.syncPasswords = false - persistSettings() + Toggle("Connections:", isOn: $settingsManager.sync.syncConnections) + .onChange(of: settingsManager.sync.syncConnections) { _, newValue in + if !newValue, settingsManager.sync.syncPasswords { + settingsManager.sync.syncPasswords = false onPasswordSyncChanged(false) } } - if syncSettings.syncConnections { - Toggle("Passwords:", isOn: $syncSettings.syncPasswords) - .onChange(of: syncSettings.syncPasswords) { _, newValue in - persistSettings() + if settingsManager.sync.syncConnections { + Toggle("Passwords:", isOn: $settingsManager.sync.syncPasswords) + .onChange(of: settingsManager.sync.syncPasswords) { _, newValue in onPasswordSyncChanged(newValue) } .padding(.leading, 20) @@ -130,14 +126,11 @@ struct SyncSettingsView: View { .padding(.leading, 20) } - Toggle("Groups & Tags:", isOn: $syncSettings.syncGroupsAndTags) - .onChange(of: syncSettings.syncGroupsAndTags) { _, _ in persistSettings() } + Toggle("Groups & Tags:", isOn: $settingsManager.sync.syncGroupsAndTags) - Toggle("SSH Profiles:", isOn: $syncSettings.syncSSHProfiles) - .onChange(of: syncSettings.syncSSHProfiles) { _, _ in persistSettings() } + Toggle("SSH Profiles:", isOn: $settingsManager.sync.syncSSHProfiles) - Toggle("Settings:", isOn: $syncSettings.syncSettings) - .onChange(of: syncSettings.syncSettings) { _, _ in persistSettings() } + Toggle("Settings:", isOn: $settingsManager.sync.syncSettings) } } @@ -167,12 +160,8 @@ struct SyncSettingsView: View { // MARK: - Helpers - private func persistSettings() { - AppSettingsStorage.shared.saveSync(syncSettings) - } - private func onPasswordSyncChanged(_ enabled: Bool) { - let effective = syncSettings.enabled && syncSettings.syncConnections && enabled + let effective = settingsManager.sync.enabled && settingsManager.sync.syncConnections && enabled Task.detached { KeychainHelper.shared.migratePasswordSyncState(synchronizable: effective) UserDefaults.standard.set(effective, forKey: KeychainHelper.passwordSyncEnabledKey) @@ -180,7 +169,8 @@ struct SyncSettingsView: View { } private func updatePasswordSyncFlag() { - let effective = syncSettings.enabled && syncSettings.syncConnections && syncSettings.syncPasswords + let sync = settingsManager.sync + let effective = sync.enabled && sync.syncConnections && sync.syncPasswords let current = UserDefaults.standard.bool(forKey: KeychainHelper.passwordSyncEnabledKey) guard effective != current else { return } Task.detached { @@ -190,10 +180,7 @@ struct SyncSettingsView: View { } private func openLicenseSettings() { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - UserDefaults.standard.set(SettingsTab.license.rawValue, forKey: "selectedSettingsTab") - } + UserDefaults.standard.set(SettingsTab.license.rawValue, forKey: "selectedSettingsTab") } }