new release script
This commit is contained in:
+21
-6
@@ -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 {
|
||||
|
||||
+109
-24
@@ -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<Int> = []
|
||||
@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 {
|
||||
|
||||
@@ -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