new ui
This commit is contained in:
@@ -8,10 +8,12 @@ struct WorkbenchReplacementApp: App {
|
||||
WindowGroup("Workbench Replacement") {
|
||||
WorkbenchLaunchView()
|
||||
.environmentObject(model)
|
||||
.frame(minWidth: 1000, minHeight: 650)
|
||||
.frame(minWidth: 800, minHeight: 520)
|
||||
}
|
||||
.defaultSize(width: 1200, height: 800)
|
||||
.windowResizability(.contentSize)
|
||||
.commands {
|
||||
CommandGroup(replacing: .newItem) {}
|
||||
WorkbenchFileCommands(model: model)
|
||||
}
|
||||
|
||||
|
||||
@@ -390,3 +392,38 @@ struct WorkbenchSplashView: View {
|
||||
.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()
|
||||
}
|
||||
.keyboardShortcut("t", modifiers: .command)
|
||||
}
|
||||
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("Open SQL Script…") {
|
||||
model.openSQLScript()
|
||||
}
|
||||
.keyboardShortcut("o", modifiers: .command)
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Security
|
||||
|
||||
struct ConnectionProfile: Identifiable, Codable, Hashable {
|
||||
@@ -39,10 +40,40 @@ enum PasswordStore {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct WorkbenchQueryTab: Identifiable {
|
||||
let id: UUID
|
||||
var title: String
|
||||
var sql: String
|
||||
var result: QueryResult
|
||||
var fileURL: URL?
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
title: String,
|
||||
sql: String = "",
|
||||
result: QueryResult = QueryResult(),
|
||||
fileURL: URL? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.sql = sql
|
||||
self.result = result
|
||||
self.fileURL = fileURL
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class DatabaseModel: ObservableObject {
|
||||
@Published var profiles: [ConnectionProfile] = []
|
||||
@Published var selectedID: UUID?
|
||||
@Published var queryTabs: [WorkbenchQueryTab] = [
|
||||
WorkbenchQueryTab(
|
||||
title: "Query 1",
|
||||
sql: "SELECT VERSION() AS server_version;"
|
||||
)
|
||||
]
|
||||
@Published var activeQueryID: UUID?
|
||||
@Published var sql = "SELECT VERSION() AS server_version;"
|
||||
@Published var result = QueryResult()
|
||||
@Published var databases: [String] = []
|
||||
@@ -58,7 +89,109 @@ final class DatabaseModel: ObservableObject {
|
||||
let decoded = try? JSONDecoder().decode([ConnectionProfile].self, from: data) { profiles = decoded }
|
||||
if profiles.isEmpty { profiles = [ConnectionProfile()] }
|
||||
selectedID = profiles.first?.id
|
||||
activeQueryID = queryTabs.first?.id
|
||||
}
|
||||
|
||||
func newQueryTab() {
|
||||
saveActiveQuery()
|
||||
let tab = WorkbenchQueryTab(
|
||||
title: "Query \\(queryTabs.count + 1)"
|
||||
)
|
||||
queryTabs.append(tab)
|
||||
activeQueryID = tab.id
|
||||
sql = tab.sql
|
||||
result = tab.result
|
||||
}
|
||||
|
||||
func saveActiveQuery() {
|
||||
guard let id = activeQueryID,
|
||||
let index = queryTabs.firstIndex(where: { $0.id == id })
|
||||
else { return }
|
||||
|
||||
queryTabs[index].sql = sql
|
||||
queryTabs[index].result = result
|
||||
}
|
||||
|
||||
func activateQuery(_ id: UUID) {
|
||||
saveActiveQuery()
|
||||
|
||||
guard let tab = queryTabs.first(where: { $0.id == id })
|
||||
else { return }
|
||||
|
||||
activeQueryID = tab.id
|
||||
sql = tab.sql
|
||||
result = tab.result
|
||||
}
|
||||
|
||||
func closeQuery(_ id: UUID) {
|
||||
saveActiveQuery()
|
||||
queryTabs.removeAll { $0.id == id }
|
||||
|
||||
if queryTabs.isEmpty {
|
||||
let tab = WorkbenchQueryTab(title: "Query 1")
|
||||
queryTabs.append(tab)
|
||||
}
|
||||
|
||||
if activeQueryID == id {
|
||||
activeQueryID = nil
|
||||
activateQuery(queryTabs[0].id)
|
||||
}
|
||||
}
|
||||
|
||||
func openSQLScript() {
|
||||
let panel = NSOpenPanel()
|
||||
panel.allowedContentTypes = [.plainText]
|
||||
panel.allowsOtherFileTypes = true
|
||||
panel.canChooseDirectories = false
|
||||
|
||||
guard panel.runModal() == .OK,
|
||||
let url = panel.url,
|
||||
let contents = try? String(contentsOf: url, encoding: .utf8)
|
||||
else { return }
|
||||
|
||||
saveActiveQuery()
|
||||
|
||||
let tab = WorkbenchQueryTab(
|
||||
title: url.lastPathComponent,
|
||||
sql: contents,
|
||||
fileURL: url
|
||||
)
|
||||
|
||||
queryTabs.append(tab)
|
||||
activateQuery(tab.id)
|
||||
status = "Opened \\(url.lastPathComponent)"
|
||||
}
|
||||
|
||||
func saveSQLScript(asNewFile: Bool = false) {
|
||||
saveActiveQuery()
|
||||
|
||||
guard let id = activeQueryID,
|
||||
let index = queryTabs.firstIndex(where: { $0.id == id })
|
||||
else { return }
|
||||
|
||||
var destination = asNewFile ? nil : queryTabs[index].fileURL
|
||||
|
||||
if destination == nil {
|
||||
let panel = NSSavePanel()
|
||||
panel.nameFieldStringValue = "query.sql"
|
||||
panel.canCreateDirectories = true
|
||||
|
||||
guard panel.runModal() == .OK else { return }
|
||||
destination = panel.url
|
||||
}
|
||||
|
||||
guard let url = destination else { return }
|
||||
|
||||
do {
|
||||
try sql.write(to: url, atomically: true, encoding: .utf8)
|
||||
queryTabs[index].fileURL = url
|
||||
queryTabs[index].title = url.lastPathComponent
|
||||
status = "Saved \\(url.lastPathComponent)"
|
||||
} catch {
|
||||
status = "Save failed: \\(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
var selected: ConnectionProfile? { profiles.first { $0.id == selectedID } }
|
||||
func save(_ profile: ConnectionProfile, password: String) {
|
||||
if let i = profiles.firstIndex(where: { $0.id == profile.id }) { profiles[i] = profile }
|
||||
@@ -178,6 +311,11 @@ final class DatabaseModel: ObservableObject {
|
||||
status = "Connected to \(profile.name)"
|
||||
} else {
|
||||
result = parsed
|
||||
if let id = activeQueryID,
|
||||
let index = queryTabs.firstIndex(where: { $0.id == id }) {
|
||||
queryTabs[index].sql = sql
|
||||
queryTabs[index].result = parsed
|
||||
}
|
||||
status = "Query completed (\(parsed.rows.count) rows)"
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -44,18 +44,18 @@ struct MainView: View {
|
||||
HSplitView {
|
||||
navigator
|
||||
.frame(
|
||||
minWidth: 210,
|
||||
idealWidth: 265,
|
||||
minWidth: 180,
|
||||
idealWidth: 250,
|
||||
maxWidth: 420
|
||||
)
|
||||
|
||||
workspace
|
||||
.frame(minWidth: 600)
|
||||
.frame(minWidth: 480)
|
||||
}
|
||||
|
||||
statusBar
|
||||
}
|
||||
.frame(minWidth: 1100, minHeight: 720)
|
||||
.frame(minWidth: 800, minHeight: 520)
|
||||
.preferredColorScheme(
|
||||
UserDefaults.standard.string(
|
||||
forKey: "preferences.appearance"
|
||||
@@ -83,7 +83,7 @@ struct MainView: View {
|
||||
|
||||
Text("MySQL Workbench")
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.foregroundStyle(.black)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Rectangle()
|
||||
.fill(WB.border)
|
||||
@@ -152,7 +152,7 @@ struct MainView: View {
|
||||
Text(title)
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
.foregroundStyle(.black)
|
||||
.foregroundStyle(.primary)
|
||||
.frame(minWidth: 65)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -455,32 +455,64 @@ struct MainView: View {
|
||||
|
||||
VSplitView {
|
||||
sqlEditor
|
||||
.frame(minHeight: 230)
|
||||
.frame(minHeight: 150)
|
||||
|
||||
bottomWorkspace
|
||||
.frame(minHeight: 230)
|
||||
.frame(minHeight: 150)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var queryTabBar: some View {
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "doc.text")
|
||||
.foregroundStyle(.blue)
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 2) {
|
||||
ForEach(model.queryTabs) { tab in
|
||||
HStack(spacing: 8) {
|
||||
Button {
|
||||
model.activateQuery(tab.id)
|
||||
} label: {
|
||||
Label(tab.title, systemImage: "doc.text")
|
||||
.lineLimit(1)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text("Query 1")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 9))
|
||||
.foregroundStyle(.secondary)
|
||||
Button {
|
||||
model.closeQuery(tab.id)
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 9))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.font(.system(size: 12))
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 30)
|
||||
.background(
|
||||
model.activeQueryID == tab.id
|
||||
? Color(nsColor: .textBackgroundColor)
|
||||
: Color.clear
|
||||
)
|
||||
.clipShape(
|
||||
UnevenRoundedRectangle(
|
||||
topLeadingRadius: 5,
|
||||
topTrailingRadius: 5
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 29)
|
||||
.background(Color(nsColor: .textBackgroundColor))
|
||||
.scrollIndicators(.hidden)
|
||||
|
||||
Spacer()
|
||||
Button {
|
||||
model.newQueryTab()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.padding(.horizontal, 10)
|
||||
.help("New Query Tab")
|
||||
}
|
||||
.background(WB.header)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user