new release script
This commit is contained in:
+21
-6
@@ -8,10 +8,12 @@ extension Notification.Name {
|
|||||||
@main
|
@main
|
||||||
struct DBSynApp: App {
|
struct DBSynApp: App {
|
||||||
@StateObject private var model = DatabaseModel()
|
@StateObject private var model = DatabaseModel()
|
||||||
|
@AppStorage("preferences.appearance") private var appearance = "system"
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup("DBSyn") {
|
WindowGroup("DBSyn") {
|
||||||
WorkbenchLaunchView()
|
WorkbenchLaunchView()
|
||||||
|
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if let iconURL = Bundle.main.url(forResource: "DBSyn", withExtension: "icns"),
|
if let iconURL = Bundle.main.url(forResource: "DBSyn", withExtension: "icns"),
|
||||||
let icon = NSImage(contentsOf: iconURL) {
|
let icon = NSImage(contentsOf: iconURL) {
|
||||||
@@ -30,7 +32,7 @@ struct DBSynApp: App {
|
|||||||
NSApplication.shared.orderFrontStandardAboutPanel(options: [
|
NSApplication.shared.orderFrontStandardAboutPanel(options: [
|
||||||
.applicationName: "DBSyn",
|
.applicationName: "DBSyn",
|
||||||
.applicationIcon: icon ?? NSApplication.shared.applicationIconImage,
|
.applicationIcon: icon ?? NSApplication.shared.applicationIconImage,
|
||||||
.version: "Phase 59.2",
|
.version: "Phase 61",
|
||||||
.credits: NSAttributedString(string: "Native MySQL client for macOS")
|
.credits: NSAttributedString(string: "Native MySQL client for macOS")
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -74,7 +76,7 @@ final class WorkbenchPreferences: ObservableObject {
|
|||||||
var alternatingRows = true
|
var alternatingRows = true
|
||||||
|
|
||||||
@AppStorage("preferences.appearance")
|
@AppStorage("preferences.appearance")
|
||||||
var appearance = "light"
|
var appearance = "system"
|
||||||
|
|
||||||
@AppStorage("preferences.autoExpandTables")
|
@AppStorage("preferences.autoExpandTables")
|
||||||
var autoExpandTables = true
|
var autoExpandTables = true
|
||||||
@@ -112,6 +114,9 @@ struct WorkbenchSettingsView: View {
|
|||||||
@AppStorage("preferences.autoExpandTables")
|
@AppStorage("preferences.autoExpandTables")
|
||||||
private var autoExpandTables = true
|
private var autoExpandTables = true
|
||||||
|
|
||||||
|
@AppStorage("preferences.appearance")
|
||||||
|
private var appearance = "system"
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView {
|
||||||
|
|
||||||
@@ -148,7 +153,7 @@ struct WorkbenchSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: 620, height: 390)
|
.frame(width: 620, height: 390)
|
||||||
.preferredColorScheme(.light)
|
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var editorSettings: some View {
|
private var editorSettings: some View {
|
||||||
@@ -273,6 +278,16 @@ struct WorkbenchSettingsView: View {
|
|||||||
private var generalSettings: some View {
|
private var generalSettings: some View {
|
||||||
settingsPage("General") {
|
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") {
|
settingsRow("Navigator") {
|
||||||
Toggle(
|
Toggle(
|
||||||
"Automatically expand table folders",
|
"Automatically expand table folders",
|
||||||
@@ -392,14 +407,14 @@ struct WorkbenchSplashView: View {
|
|||||||
.background(
|
.background(
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
Color(red: 0.96, green: 0.98, blue: 1),
|
Color(nsColor: .windowBackgroundColor),
|
||||||
Color(red: 0.88, green: 0.93, blue: 1)
|
Color(nsColor: .controlBackgroundColor)
|
||||||
],
|
],
|
||||||
startPoint: .topLeading,
|
startPoint: .topLeading,
|
||||||
endPoint: .bottomTrailing
|
endPoint: .bottomTrailing
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.preferredColorScheme(.light)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func feature(_ symbol: String, _ title: String) -> some View {
|
private func feature(_ symbol: String, _ title: String) -> some View {
|
||||||
|
|||||||
+109
-24
@@ -8,18 +8,10 @@ import UniformTypeIdentifiers
|
|||||||
// MARK: - Classic Workbench Colours
|
// MARK: - Classic Workbench Colours
|
||||||
|
|
||||||
private enum WB {
|
private enum WB {
|
||||||
static let toolbar = Color(nsColor: NSColor(
|
static let toolbar = Color(nsColor: .controlBackgroundColor)
|
||||||
calibratedWhite: 0.88, alpha: 1
|
static let sidebar = Color(nsColor: .underPageBackgroundColor)
|
||||||
))
|
static let border = Color(nsColor: .separatorColor)
|
||||||
static let sidebar = Color(nsColor: NSColor(
|
static let header = Color(nsColor: .controlBackgroundColor)
|
||||||
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
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Main Window
|
// MARK: - Main Window
|
||||||
@@ -85,11 +77,6 @@ struct MainView: View {
|
|||||||
statusBar
|
statusBar
|
||||||
}
|
}
|
||||||
.frame(minWidth: 800, minHeight: 520)
|
.frame(minWidth: 800, minHeight: 520)
|
||||||
.preferredColorScheme(
|
|
||||||
UserDefaults.standard.string(
|
|
||||||
forKey: "preferences.appearance"
|
|
||||||
) == "dark" ? .dark : .light
|
|
||||||
)
|
|
||||||
.background(Color(nsColor: .windowBackgroundColor))
|
.background(Color(nsColor: .windowBackgroundColor))
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .workbenchRevealSQLEditor)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .workbenchRevealSQLEditor)) { _ in
|
||||||
administrationPage = nil
|
administrationPage = nil
|
||||||
@@ -1376,6 +1363,7 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
@Binding var selectedSQL: String
|
@Binding var selectedSQL: String
|
||||||
var focusRequest: Int = 0
|
var focusRequest: Int = 0
|
||||||
var completions: [String] = []
|
var completions: [String] = []
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(self)
|
Coordinator(self)
|
||||||
@@ -1385,11 +1373,12 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
let scroll = NSTextView.scrollableTextView()
|
let scroll = NSTextView.scrollableTextView()
|
||||||
let editor = scroll.documentView as! NSTextView
|
let editor = scroll.documentView as! NSTextView
|
||||||
|
|
||||||
scroll.appearance = NSAppearance(named: .aqua)
|
let nativeAppearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua)
|
||||||
editor.appearance = NSAppearance(named: .aqua)
|
scroll.appearance = nativeAppearance
|
||||||
editor.backgroundColor = .white
|
editor.appearance = nativeAppearance
|
||||||
editor.textColor = .black
|
editor.backgroundColor = .textBackgroundColor
|
||||||
scroll.backgroundColor = .white
|
editor.textColor = .textColor
|
||||||
|
scroll.backgroundColor = .textBackgroundColor
|
||||||
|
|
||||||
editor.isEditable = true
|
editor.isEditable = true
|
||||||
editor.isRichText = false
|
editor.isRichText = false
|
||||||
@@ -1429,6 +1418,13 @@ struct SQLTextView: NSViewRepresentable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
context.coordinator.parent = self
|
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 {
|
if context.coordinator.lastFocusRequest != focusRequest {
|
||||||
context.coordinator.lastFocusRequest = focusRequest
|
context.coordinator.lastFocusRequest = focusRequest
|
||||||
DispatchQueue.main.async { [weak editor] in
|
DispatchQueue.main.async { [weak editor] in
|
||||||
@@ -2073,6 +2069,7 @@ struct NativeResultGrid: NSViewRepresentable {
|
|||||||
let columns: [String]
|
let columns: [String]
|
||||||
let rows: [[String]]
|
let rows: [[String]]
|
||||||
var hiddenColumnIndices: Set<Int> = []
|
var hiddenColumnIndices: Set<Int> = []
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator()
|
Coordinator()
|
||||||
@@ -2084,10 +2081,13 @@ struct NativeResultGrid: NSViewRepresentable {
|
|||||||
scroll.hasHorizontalScroller = true
|
scroll.hasHorizontalScroller = true
|
||||||
scroll.autohidesScrollers = true
|
scroll.autohidesScrollers = true
|
||||||
scroll.drawsBackground = true
|
scroll.drawsBackground = true
|
||||||
scroll.backgroundColor = .white
|
scroll.backgroundColor = .textBackgroundColor
|
||||||
|
|
||||||
let table = NSTableView()
|
let table = NSTableView()
|
||||||
table.headerView = NSTableHeaderView()
|
table.headerView = NSTableHeaderView()
|
||||||
|
let nativeAppearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua)
|
||||||
|
scroll.appearance = nativeAppearance
|
||||||
|
table.appearance = nativeAppearance
|
||||||
table.usesAlternatingRowBackgroundColors =
|
table.usesAlternatingRowBackgroundColors =
|
||||||
UserDefaults.standard.object(
|
UserDefaults.standard.object(
|
||||||
forKey: "preferences.alternatingRows"
|
forKey: "preferences.alternatingRows"
|
||||||
@@ -2102,7 +2102,7 @@ struct NativeResultGrid: NSViewRepresentable {
|
|||||||
table.allowsColumnResizing = true
|
table.allowsColumnResizing = true
|
||||||
table.allowsColumnReordering = true
|
table.allowsColumnReordering = true
|
||||||
table.columnAutoresizingStyle = .noColumnAutoresizing
|
table.columnAutoresizingStyle = .noColumnAutoresizing
|
||||||
table.backgroundColor = .white
|
table.backgroundColor = .textBackgroundColor
|
||||||
|
|
||||||
context.coordinator.columns = columns
|
context.coordinator.columns = columns
|
||||||
context.coordinator.rows = rows
|
context.coordinator.rows = rows
|
||||||
@@ -2126,6 +2126,12 @@ struct NativeResultGrid: NSViewRepresentable {
|
|||||||
return
|
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
|
let oldColumns = context.coordinator.columns
|
||||||
|
|
||||||
context.coordinator.columns = columns
|
context.coordinator.columns = columns
|
||||||
@@ -2963,6 +2969,42 @@ struct WorkbenchDatabaseTransferView: View {
|
|||||||
@State private var loadingDatabases = false
|
@State private var loadingDatabases = false
|
||||||
@State private var lastBackupURL: URL?
|
@State private var lastBackupURL: URL?
|
||||||
@State private var lastVerificationReport = ""
|
@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? {
|
private var selectedProfile: ConnectionProfile? {
|
||||||
model.selected
|
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") {
|
GroupBox("Progress") {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
ProgressView(value: progress)
|
ProgressView(value: progress)
|
||||||
@@ -3721,6 +3805,7 @@ struct WorkbenchDatabaseTransferView: View {
|
|||||||
)
|
)
|
||||||
try manifest.write(for: url)
|
try manifest.write(for: url)
|
||||||
lastBackupURL = url
|
lastBackupURL = url
|
||||||
|
rememberBackup(url)
|
||||||
progress = 1
|
progress = 1
|
||||||
status = "Export complete and SHA-256 verified: \(tables.count) tables, \(views.count) views, \(transferredRows.formatted()) rows"
|
status = "Export complete and SHA-256 verified: \(tables.count) tables, \(views.count) views, \(transferredRows.formatted()) rows"
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user