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
2 changes: 1 addition & 1 deletion TablePro/AppDelegate+FileOpen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Core/Services/Infrastructure/AppNotifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
9 changes: 9 additions & 0 deletions TablePro/Core/Storage/AppSettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -217,6 +225,7 @@ final class AppSettingsManager {
tabs = .default
keyboard = .default
ai = .default
sync = .default
storage.resetToDefaults()
}
}
13 changes: 13 additions & 0 deletions TablePro/Models/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -608,5 +610,8 @@ private struct OpenWindowHandler: View {
openWindow(id: "main", value: EditorTabPayload(connectionId: connectionId))
}
}
.onReceive(NotificationCenter.default.publisher(for: .openSettingsWindow)) { _ in
openSettings()
}
}
}
6 changes: 2 additions & 4 deletions TablePro/Views/Components/SyncStatusIndicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI

struct SyncStatusIndicator: View {
@Environment(\.openSettings) private var openSettings
private let syncCoordinator = SyncCoordinator.shared
@State private var showActivationSheet = false

Expand Down Expand Up @@ -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()
}
}
}
Expand Down
3 changes: 0 additions & 3 deletions TablePro/Views/Settings/Appearance/ThemeListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
13 changes: 8 additions & 5 deletions TablePro/Views/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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: {
Expand All @@ -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)
}
16 changes: 10 additions & 6 deletions TablePro/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 18 additions & 31 deletions TablePro/Views/Settings/SyncSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -32,7 +31,7 @@ struct SyncSettingsView: View {
.foregroundStyle(.secondary)
}

if syncSettings.enabled {
if settingsManager.sync.enabled {
statusSection

syncCategoriesSection
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -167,20 +160,17 @@ 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)
}
}

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 {
Expand All @@ -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")
}
}

Expand Down
Loading