import SwiftUI import AppKit extension Notification.Name { static let workbenchRevealSQLEditor = Notification.Name("DBSyn.RevealSQLEditor") } @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) { 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 61", .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 = "system" @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 @AppStorage("preferences.appearance") private var appearance = "system" 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(appearance == "dark" ? .dark : appearance == "light" ? .light : nil) } 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, 186) Divider() .padding(.vertical, 12) Text("Examples") .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) 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("Appearance") { Picker("Appearance", selection: $appearance) { Text("System").tag("system") Text("Light").tag("light") Text("Dark").tag("dark") } .pickerStyle(.segmented) .labelsHidden() .accessibilityLabel("Application appearance") .frame(width: 260, alignment: .leading) } Text(appearance == "system" ? "DBSyn follows your Mac’s appearance setting." : appearance == "light" ? "DBSyn always uses Light appearance." : "DBSyn always uses Dark appearance.") .font(.caption) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 186) settingsRow("Navigator") { Toggle( "Automatically expand table folders", isOn: $autoExpandTables ) } } } private func settingsPage( _ 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( _ title: String, @ViewBuilder content: () -> Content ) -> some View { HStack(alignment: .center, spacing: 16) { Text(title) .font(.system(size: 13)) .frame(width: 170, alignment: .leading) 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(nsColor: .windowBackgroundColor), Color(nsColor: .controlBackgroundColor) ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } 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]) } } }