Files
SQLWorkbench/DBSyn/App.swift
T
Mark Gregory ebd0bd1e29 Improved progress display
Previously, the progress bar updated only after completing an entire table.

For large tables such as LIBARC, this could make the application appear to have stopped responding.

Phase 10 updates the interface while rows are being exported.
2026-10-18 14:43:13 +01:00

468 lines
13 KiB
Swift

import SwiftUI
import AppKit
extension Notification.Name {
static let workbenchRevealSQLEditor = Notification.Name("DBSyn.RevealSQLEditor")
}
@main
struct DBSynApp: App {
@StateObject private var model = DatabaseModel()
var body: some Scene {
WindowGroup("DBSyn") {
WorkbenchLaunchView()
.onAppear {
if let iconURL = Bundle.main.url(forResource: "DBSyn", withExtension: "icns"),
let icon = NSImage(contentsOf: iconURL) {
NSApplication.shared.applicationIconImage = icon
}
}
.environmentObject(model)
.frame(minWidth: 800, minHeight: 520)
}
.defaultSize(width: 1200, height: 800)
.windowResizability(.contentSize)
.commands {
CommandGroup(replacing: .appInfo) {
Button("About DBSyn") {
let icon = NSImage(contentsOfFile: Bundle.main.path(forResource: "DBSynBrand", ofType: "png") ?? "")
NSApplication.shared.orderFrontStandardAboutPanel(options: [
.applicationName: "DBSyn",
.applicationIcon: icon ?? NSApplication.shared.applicationIconImage,
.version: "Phase 59.2",
.credits: NSAttributedString(string: "Native MySQL client for macOS")
])
}
}
WorkbenchFileCommands(model: model)
}
Settings {
WorkbenchSettingsView()
}
}
}
// MARK: - Application Preferences
@MainActor
final class WorkbenchPreferences: ObservableObject {
static let shared = WorkbenchPreferences()
@AppStorage("preferences.editorFontSize")
var editorFontSize = 13.0
@AppStorage("preferences.uppercaseKeywords")
var uppercaseKeywords = true
@AppStorage("preferences.showLineNumbers")
var showLineNumbers = true
@AppStorage("preferences.tabWidth")
var tabWidth = 4
@AppStorage("preferences.resultRowHeight")
var resultRowHeight = 24.0
@AppStorage("preferences.resultColumnWidth")
var resultColumnWidth = 170.0
@AppStorage("preferences.alternatingRows")
var alternatingRows = true
@AppStorage("preferences.appearance")
var appearance = "light"
@AppStorage("preferences.autoExpandTables")
var autoExpandTables = true
}
// MARK: - Workbench Settings
struct WorkbenchSettingsView: View {
@AppStorage("preferences.editorFontSize")
private var editorFontSize = 13.0
@AppStorage("preferences.uppercaseKeywords")
private var uppercaseKeywords = true
@AppStorage("preferences.showLineNumbers")
private var showLineNumbers = true
@AppStorage("preferences.tabWidth")
private var tabWidth = 4
@AppStorage("preferences.defaultRowLimit")
private var defaultRowLimit = 500
@AppStorage("preferences.resultRowHeight")
private var resultRowHeight = 24.0
@AppStorage("preferences.resultColumnWidth")
private var resultColumnWidth = 170.0
@AppStorage("preferences.alternatingRows")
private var alternatingRows = true
@AppStorage("preferences.autoExpandTables")
private var autoExpandTables = true
var body: some View {
TabView {
editorSettings
.tabItem {
Label(
"SQL Editor",
systemImage: "chevron.left.forwardslash.chevron.right"
)
}
executionSettings
.tabItem {
Label(
"Query Execution",
systemImage: "bolt"
)
}
resultSettings
.tabItem {
Label(
"Result Grid",
systemImage: "tablecells"
)
}
generalSettings
.tabItem {
Label(
"General",
systemImage: "gearshape"
)
}
}
.frame(width: 620, height: 390)
.preferredColorScheme(.light)
}
private var editorSettings: some View {
settingsPage("SQL Editor") {
settingsRow("Font Size") {
Stepper(
"\(Int(editorFontSize)) pt",
value: $editorFontSize,
in: 10...24,
step: 1
)
.frame(width: 130)
}
settingsRow("Tab Width") {
Picker("", selection: $tabWidth) {
Text("2 spaces").tag(2)
Text("4 spaces").tag(4)
Text("8 spaces").tag(8)
}
.labelsHidden()
.frame(width: 150)
}
settingsRow("SQL Formatting") {
Toggle(
"Automatically uppercase SQL keywords",
isOn: $uppercaseKeywords
)
}
settingsRow("Editor Display") {
Toggle(
"Show line numbers",
isOn: $showLineNumbers
)
}
}
}
private var executionSettings: some View {
settingsPage("Query Execution") {
settingsRow("Default Row Limit") {
HStack(spacing: 12) {
TextField(
"",
value: $defaultRowLimit,
format: .number
)
.textFieldStyle(.roundedBorder)
.frame(width: 110)
Stepper(
"",
value: $defaultRowLimit,
in: 50...100000,
step: 50
)
.labelsHidden()
}
}
Text(
"Controls the number of rows retrieved when opening a table from the schema navigator."
)
.font(.system(size: 12))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 190)
Divider()
.padding(.vertical, 12)
Text("Examples")
.font(.headline)
Text(
"500 rows: Faster browsing and lower memory usage.\n" +
"5,000 rows: Useful for larger datasets.\n" +
"50,000 rows: May take longer to retrieve."
)
.font(.system(size: 12))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private var resultSettings: some View {
settingsPage("Result Grid") {
settingsRow("Row Height") {
Stepper(
"\(Int(resultRowHeight)) px",
value: $resultRowHeight,
in: 18...40,
step: 1
)
.frame(width: 130)
}
settingsRow("Column Width") {
Stepper(
"\(Int(resultColumnWidth)) px",
value: $resultColumnWidth,
in: 80...400,
step: 10
)
.frame(width: 140)
}
settingsRow("Appearance") {
Toggle(
"Alternating row colours",
isOn: $alternatingRows
)
}
}
}
private var generalSettings: some View {
settingsPage("General") {
settingsRow("Navigator") {
Toggle(
"Automatically expand table folders",
isOn: $autoExpandTables
)
}
}
}
private func settingsPage<Content: View>(
_ title: String,
@ViewBuilder content: () -> Content
) -> some View {
VStack(alignment: .leading, spacing: 18) {
Text(title)
.font(.system(size: 20, weight: .semibold))
Divider()
content()
Spacer(minLength: 0)
}
.padding(26)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(nsColor: .windowBackgroundColor))
}
private func settingsRow<Content: View>(
_ title: String,
@ViewBuilder content: () -> Content
) -> some View {
HStack(alignment: .center, spacing: 16) {
Text(title)
.font(.system(size: 13))
.frame(width: 170, alignment: .trailing)
content()
Spacer(minLength: 0)
}
.frame(height: 30)
}
}
// MARK: - Workbench Splash Screen
struct WorkbenchLaunchView: View {
@State private var showingSplash = true
var body: some View {
ZStack {
MainView()
if showingSplash {
WorkbenchSplashView()
.transition(.opacity)
.zIndex(10)
}
}
.task {
try? await Task.sleep(for: .seconds(3))
withAnimation(.easeOut(duration: 0.4)) {
showingSplash = false
}
}
}
}
struct WorkbenchSplashView: View {
var body: some View {
VStack(spacing: 20) {
Spacer()
if let path = Bundle.main.path(
forResource: "DBSynBrand",
ofType: "png"
), let image = NSImage(contentsOfFile: path) {
Image(nsImage: image)
.resizable()
.scaledToFit()
.frame(width: 160, height: 160)
}
Text("DBSyn")
.font(.system(size: 30, weight: .bold))
Text("Modern MySQL Client for macOS")
.font(.system(size: 15))
.foregroundStyle(.secondary)
HStack(spacing: 30) {
feature("cylinder.split.1x2", "Schemas")
feature("chevron.left.forwardslash.chevron.right", "SQL Editor")
feature("tablecells", "Result Grid")
feature("server.rack", "Connections")
}
.padding(.top, 20)
Spacer()
ProgressView()
.frame(width: 260)
Text("Starting DBSyn…")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.bottom, 30)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
LinearGradient(
colors: [
Color(red: 0.96, green: 0.98, blue: 1),
Color(red: 0.88, green: 0.93, blue: 1)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.preferredColorScheme(.light)
}
private func feature(_ symbol: String, _ title: String) -> some View {
VStack(spacing: 8) {
Image(systemName: symbol)
.font(.system(size: 24))
.foregroundStyle(.blue)
Text(title)
.font(.system(size: 12))
}
.frame(width: 90)
}
}
// MARK: - Workbench File Menu
struct WorkbenchFileCommands: Commands {
@ObservedObject var model: DatabaseModel
var body: some Commands {
CommandGroup(replacing: .newItem) {
Button("New Query Tab") {
model.newQueryTab()
NotificationCenter.default.post(name: .workbenchRevealSQLEditor, object: nil)
}
.keyboardShortcut("t", modifiers: .command)
}
CommandGroup(after: .newItem) {
Button("Open SQL Script…") {
model.openSQLScript()
}
.keyboardShortcut("o", modifiers: .command)
Menu("Open Recent SQL Script") {
if model.recentSQLScripts.isEmpty {
Text("No Recent Scripts")
} else {
ForEach(model.recentSQLScripts, id: \.self) { path in
Button(URL(fileURLWithPath: path).lastPathComponent) {
model.openRecentSQLScript(at: path)
}
.help(path)
}
Divider()
Button("Clear Recent Scripts") { model.clearRecentSQLScripts() }
}
}
Divider()
Button("Save SQL Script") {
model.saveSQLScript()
}
.keyboardShortcut("s", modifiers: .command)
Button("Save SQL Script As…") {
model.saveSQLScript(asNewFile: true)
}
.keyboardShortcut("s", modifiers: [.command, .shift])
}
}
}