From 73e9a3efe2e495f521b0813fbaaed930157c5c9f Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 18 Oct 2026 15:41:16 +0100 Subject: [PATCH] new release script --- DBSyn/App.swift | 27 ++++-- DBSyn/MainView.swift | 133 ++++++++++++++++++++++++------ docs/PHASE61-1-LIGHT-DARK-MODE.md | 10 +++ docs/PHASE61-BACKUP-CATALOG.md | 17 ++++ 4 files changed, 157 insertions(+), 30 deletions(-) create mode 100644 docs/PHASE61-1-LIGHT-DARK-MODE.md create mode 100644 docs/PHASE61-BACKUP-CATALOG.md diff --git a/DBSyn/App.swift b/DBSyn/App.swift index 1c7edef..dfe2489 100644 --- a/DBSyn/App.swift +++ b/DBSyn/App.swift @@ -8,10 +8,12 @@ extension Notification.Name { @main struct DBSynApp: App { @StateObject private var model = DatabaseModel() + @AppStorage("preferences.appearance") private var appearance = "system" var body: some Scene { WindowGroup("DBSyn") { WorkbenchLaunchView() + .preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil) .onAppear { if let iconURL = Bundle.main.url(forResource: "DBSyn", withExtension: "icns"), let icon = NSImage(contentsOf: iconURL) { @@ -30,7 +32,7 @@ struct DBSynApp: App { NSApplication.shared.orderFrontStandardAboutPanel(options: [ .applicationName: "DBSyn", .applicationIcon: icon ?? NSApplication.shared.applicationIconImage, - .version: "Phase 59.2", + .version: "Phase 61", .credits: NSAttributedString(string: "Native MySQL client for macOS") ]) } @@ -74,7 +76,7 @@ final class WorkbenchPreferences: ObservableObject { var alternatingRows = true @AppStorage("preferences.appearance") - var appearance = "light" + var appearance = "system" @AppStorage("preferences.autoExpandTables") var autoExpandTables = true @@ -112,6 +114,9 @@ struct WorkbenchSettingsView: View { @AppStorage("preferences.autoExpandTables") private var autoExpandTables = true + @AppStorage("preferences.appearance") + private var appearance = "system" + var body: some View { TabView { @@ -148,7 +153,7 @@ struct WorkbenchSettingsView: View { } } .frame(width: 620, height: 390) - .preferredColorScheme(.light) + .preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil) } private var editorSettings: some View { @@ -273,6 +278,16 @@ struct WorkbenchSettingsView: View { private var generalSettings: some View { settingsPage("General") { + settingsRow("Appearance") { + Picker("Appearance", selection: $appearance) { + Text("System").tag("system") + Text("Light").tag("light") + Text("Dark").tag("dark") + } + .labelsHidden() + .frame(width: 170) + } + settingsRow("Navigator") { Toggle( "Automatically expand table folders", @@ -392,14 +407,14 @@ struct WorkbenchSplashView: View { .background( LinearGradient( colors: [ - Color(red: 0.96, green: 0.98, blue: 1), - Color(red: 0.88, green: 0.93, blue: 1) + Color(nsColor: .windowBackgroundColor), + Color(nsColor: .controlBackgroundColor) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) - .preferredColorScheme(.light) + } private func feature(_ symbol: String, _ title: String) -> some View { diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index 2c1c5b7..cd3f067 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -8,18 +8,10 @@ import UniformTypeIdentifiers // MARK: - Classic Workbench Colours private enum WB { - static let toolbar = Color(nsColor: NSColor( - calibratedWhite: 0.88, alpha: 1 - )) - static let sidebar = Color(nsColor: NSColor( - calibratedWhite: 0.94, alpha: 1 - )) - static let border = Color(nsColor: NSColor( - calibratedWhite: 0.76, alpha: 1 - )) - static let header = Color(nsColor: NSColor( - calibratedWhite: 0.91, alpha: 1 - )) + static let toolbar = Color(nsColor: .controlBackgroundColor) + static let sidebar = Color(nsColor: .underPageBackgroundColor) + static let border = Color(nsColor: .separatorColor) + static let header = Color(nsColor: .controlBackgroundColor) } // MARK: - Main Window @@ -85,11 +77,6 @@ struct MainView: View { statusBar } .frame(minWidth: 800, minHeight: 520) - .preferredColorScheme( - UserDefaults.standard.string( - forKey: "preferences.appearance" - ) == "dark" ? .dark : .light - ) .background(Color(nsColor: .windowBackgroundColor)) .onReceive(NotificationCenter.default.publisher(for: .workbenchRevealSQLEditor)) { _ in administrationPage = nil @@ -1376,6 +1363,7 @@ struct SQLTextView: NSViewRepresentable { @Binding var selectedSQL: String var focusRequest: Int = 0 var completions: [String] = [] + @Environment(\.colorScheme) private var colorScheme func makeCoordinator() -> Coordinator { Coordinator(self) @@ -1385,11 +1373,12 @@ struct SQLTextView: NSViewRepresentable { let scroll = NSTextView.scrollableTextView() let editor = scroll.documentView as! NSTextView - scroll.appearance = NSAppearance(named: .aqua) - editor.appearance = NSAppearance(named: .aqua) - editor.backgroundColor = .white - editor.textColor = .black - scroll.backgroundColor = .white + let nativeAppearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua) + scroll.appearance = nativeAppearance + editor.appearance = nativeAppearance + editor.backgroundColor = .textBackgroundColor + editor.textColor = .textColor + scroll.backgroundColor = .textBackgroundColor editor.isEditable = true editor.isRichText = false @@ -1429,6 +1418,13 @@ struct SQLTextView: NSViewRepresentable { return } context.coordinator.parent = self + let nativeAppearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua) + scroll.appearance = nativeAppearance + editor.appearance = nativeAppearance + editor.backgroundColor = .textBackgroundColor + editor.textColor = .textColor + scroll.backgroundColor = .textBackgroundColor + context.coordinator.highlight(editor) if context.coordinator.lastFocusRequest != focusRequest { context.coordinator.lastFocusRequest = focusRequest DispatchQueue.main.async { [weak editor] in @@ -2073,6 +2069,7 @@ struct NativeResultGrid: NSViewRepresentable { let columns: [String] let rows: [[String]] var hiddenColumnIndices: Set = [] + @Environment(\.colorScheme) private var colorScheme func makeCoordinator() -> Coordinator { Coordinator() @@ -2084,10 +2081,13 @@ struct NativeResultGrid: NSViewRepresentable { scroll.hasHorizontalScroller = true scroll.autohidesScrollers = true scroll.drawsBackground = true - scroll.backgroundColor = .white + scroll.backgroundColor = .textBackgroundColor let table = NSTableView() table.headerView = NSTableHeaderView() + let nativeAppearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua) + scroll.appearance = nativeAppearance + table.appearance = nativeAppearance table.usesAlternatingRowBackgroundColors = UserDefaults.standard.object( forKey: "preferences.alternatingRows" @@ -2102,7 +2102,7 @@ struct NativeResultGrid: NSViewRepresentable { table.allowsColumnResizing = true table.allowsColumnReordering = true table.columnAutoresizingStyle = .noColumnAutoresizing - table.backgroundColor = .white + table.backgroundColor = .textBackgroundColor context.coordinator.columns = columns context.coordinator.rows = rows @@ -2126,6 +2126,12 @@ struct NativeResultGrid: NSViewRepresentable { return } + let nativeAppearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua) + scroll.appearance = nativeAppearance + table.appearance = nativeAppearance + scroll.backgroundColor = .textBackgroundColor + table.backgroundColor = .textBackgroundColor + let oldColumns = context.coordinator.columns context.coordinator.columns = columns @@ -2963,6 +2969,42 @@ struct WorkbenchDatabaseTransferView: View { @State private var loadingDatabases = false @State private var lastBackupURL: URL? @State private var lastVerificationReport = "" + // Phase 61: Recent backups are kept locally, never uploaded or copied. + @State private var recentBackups: [String] = UserDefaults.standard.stringArray(forKey: "DBSyn.recentBackups.v1") ?? [] + @State private var selectedBackupPath = "" + + private func rememberBackup(_ url: URL) { + let path = url.standardizedFileURL.path + recentBackups.removeAll { $0 == path } + recentBackups.insert(path, at: 0) + recentBackups = Array(recentBackups.prefix(20)) + selectedBackupPath = path + UserDefaults.standard.set(recentBackups, forKey: "DBSyn.recentBackups.v1") + } + + private var selectedBackupURL: URL? { + guard !selectedBackupPath.isEmpty, + FileManager.default.fileExists(atPath: selectedBackupPath) else { return nil } + return URL(fileURLWithPath: selectedBackupPath) + } + + private func verifyRecentBackup() { + guard let url = selectedBackupURL else { return } + operationTask = Task { + isWorking = true + defer { isWorking = false; operationTask = nil } + do { + try await Task.detached(priority: .utility) { + try WorkbenchBackupIntegrity.verify(url) + }.value + status = "Verified SHA-256: \(url.lastPathComponent)" + progress = 1 + } catch { + status = "Verification failed: \(error.localizedDescription)" + } + } + } + private var selectedProfile: ConnectionProfile? { model.selected @@ -3075,6 +3117,48 @@ struct WorkbenchDatabaseTransferView: View { } } + GroupBox("Recent Backups") { + VStack(alignment: .leading, spacing: 10) { + Text("Previously exported SQL backups. Files stay in their original locations; missing files cannot be verified or restored.") + .font(.caption).foregroundStyle(.secondary) + if recentBackups.isEmpty { + Text("No DBSyn backups recorded yet.").foregroundStyle(.secondary) + } else { + Picker("Backup", selection: $selectedBackupPath) { + Text("Select a backup").tag("") + ForEach(recentBackups, id: \.self) { path in + Text(URL(fileURLWithPath: path).lastPathComponent + + (FileManager.default.fileExists(atPath: path) ? "" : " (missing)")) + .tag(path) + } + } + .labelsHidden() + HStack { + Button("Reveal in Finder") { + if let url = selectedBackupURL { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + }.disabled(selectedBackupURL == nil) + Button("Verify SHA-256") { verifyRecentBackup() } + .disabled(selectedBackupURL == nil || isWorking) + Button("Restore Selected…") { + if let url = selectedBackupURL { + selectedMode = 1 + pendingImportURL = url + showingImportConfirmation = true + } + }.disabled(selectedBackupURL == nil || isWorking || database.isEmpty || selectedProfile == nil) + Spacer() + Button("Clear History") { + recentBackups = [] + selectedBackupPath = "" + UserDefaults.standard.removeObject(forKey: "DBSyn.recentBackups.v1") + } + } + } + } + } + GroupBox("Progress") { VStack(alignment: .leading, spacing: 10) { ProgressView(value: progress) @@ -3721,6 +3805,7 @@ struct WorkbenchDatabaseTransferView: View { ) try manifest.write(for: url) lastBackupURL = url + rememberBackup(url) progress = 1 status = "Export complete and SHA-256 verified: \(tables.count) tables, \(views.count) views, \(transferredRows.formatted()) rows" } catch { diff --git a/docs/PHASE61-1-LIGHT-DARK-MODE.md b/docs/PHASE61-1-LIGHT-DARK-MODE.md new file mode 100644 index 0000000..72a9b9d --- /dev/null +++ b/docs/PHASE61-1-LIGHT-DARK-MODE.md @@ -0,0 +1,10 @@ +# DBSyn Phase 61.1 — Light and Dark Appearance + +- Appearance setting under Settings → General: System, Light, Dark; stored in `preferences.appearance`. +- Appearance selection applies to the application window and preferences; System follows macOS automatically. +- Replaced fixed light-mode toolbar, navigator, header, and border colors with semantic macOS colors. +- Removed light-only overrides from settings and splash screen; splash gradient uses adaptive system colors. +- SQL NSTextView and native NSTableView results use light/dark AppKit appearances and semantic text/background colors, refreshed when appearance changes. +- Existing database, charts, SQL editor, backup catalog and administration features retained. + +Validation: Swift syntax parsed using `swiftc -frontend -parse`; full macOS Xcode build and visual testing are not available in this environment. diff --git a/docs/PHASE61-BACKUP-CATALOG.md b/docs/PHASE61-BACKUP-CATALOG.md new file mode 100644 index 0000000..fc6c748 --- /dev/null +++ b/docs/PHASE61-BACKUP-CATALOG.md @@ -0,0 +1,17 @@ +# DBSyn Phase 61 — Backup Catalog and Recovery + +- Recent SQL backups are recorded automatically after successful export, including SHA-256 checksum and manifest generation. +- Backup catalog remembers up to 20 local file paths across app launches; no backup contents are copied or uploaded. +- Select a backup to reveal it in Finder, verify its SHA-256 sidecar, or initiate restore through the existing confirmation dialog. +- Missing backup files are visibly identified and cannot be selected for verification or restore. +- Clearing the history does not delete backup files. +- Existing export snapshot consistency, backup verification, manifest validation, and import safeguards are retained. +- Includes `scripts/build-run-dbsyn.sh` for the established Debug build-and-launch workflow. + +## Caveats + +Restores are not transactional. Back up the destination database and verify the chosen backup before restoring. The catalog stores file paths rather than security-scoped bookmarks; files outside permitted locations may require re-selection through the standard file picker. + +## Validation + +Swift syntax parsed with `swiftc -frontend -parse` where available. Full Xcode build and MySQL integration tests must be run on macOS.