Initial commit
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainView: View {
|
||||
@EnvironmentObject private var model: DatabaseModel
|
||||
@State private var showingEditor = false
|
||||
@State private var editing = ConnectionProfile()
|
||||
@State private var editingPassword = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
navigator
|
||||
} detail: {
|
||||
editorPanel
|
||||
}
|
||||
.sheet(isPresented: $showingEditor) {
|
||||
ConnectionEditor(profile: $editing, password: $editingPassword) {
|
||||
model.save(editing, password: editingPassword)
|
||||
showingEditor = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var navigator: some View {
|
||||
List(selection: $model.selectedID) {
|
||||
Section("MYSQL CONNECTIONS") {
|
||||
ForEach(model.profiles) { profile in
|
||||
Label(profile.name, systemImage: "server.rack")
|
||||
.tag(profile.id)
|
||||
.contextMenu {
|
||||
Button("Edit Connection") { edit(profile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("SCHEMAS") {
|
||||
ForEach(model.databases, id: \.self) { db in
|
||||
Button {
|
||||
model.selectedDatabase = db
|
||||
} label: {
|
||||
Label(db, systemImage: "cylinder.split.1x2")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.navigationTitle("Navigator")
|
||||
.toolbar {
|
||||
Button {
|
||||
editing = ConnectionProfile(name: "New Connection")
|
||||
editingPassword = ""
|
||||
showingEditor = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.help("New connection")
|
||||
Button {
|
||||
model.loadDatabases()
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
.help("Connect / refresh schemas")
|
||||
}
|
||||
}
|
||||
|
||||
private var editorPanel: some View {
|
||||
VStack(spacing: 0) {
|
||||
editorToolbar
|
||||
Divider()
|
||||
TextEditor(text: $model.sql)
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
.frame(minHeight: 190)
|
||||
.padding(8)
|
||||
Divider()
|
||||
resultsPanel
|
||||
Divider()
|
||||
statusBar
|
||||
}
|
||||
.navigationTitle("Workbench Replacement")
|
||||
}
|
||||
|
||||
private var editorToolbar: some View {
|
||||
HStack {
|
||||
Label("SQL Editor", systemImage: "chevron.left.forwardslash.chevron.right")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button("Edit Connection") {
|
||||
if let profile = model.selected { edit(profile) }
|
||||
}
|
||||
Button("Execute") { model.runSQL() }
|
||||
.keyboardShortcut(.return, modifiers: [.command])
|
||||
.disabled(model.isRunning)
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
|
||||
private var resultsPanel: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Result Grid").font(.headline)
|
||||
Spacer()
|
||||
Text(model.result.message).foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(12)
|
||||
if model.result.columns.isEmpty {
|
||||
ContentUnavailableView("No Results", systemImage: "tablecells",
|
||||
description: Text("Run a query to display results."))
|
||||
} else {
|
||||
resultsGrid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var resultsGrid: some View {
|
||||
ScrollView([.horizontal, .vertical]) {
|
||||
Grid(alignment: .leading, horizontalSpacing: 14, verticalSpacing: 8) {
|
||||
GridRow {
|
||||
ForEach(model.result.columns, id: \.self) { name in
|
||||
Text(name)
|
||||
.fontWeight(.semibold)
|
||||
.frame(minWidth: 130, alignment: .leading)
|
||||
}
|
||||
}
|
||||
ForEach(model.result.rows.indices, id: \.self) { rowIndex in
|
||||
resultRow(model.result.rows[rowIndex])
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
}
|
||||
}
|
||||
|
||||
private func resultRow(_ row: [String]) -> some View {
|
||||
GridRow {
|
||||
ForEach(model.result.columns.indices, id: \.self) { index in
|
||||
Text(index < row.count ? row[index] : "")
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.frame(minWidth: 130, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var statusBar: some View {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(model.status == "Error" ? Color.red : Color.green)
|
||||
.frame(width: 8, height: 8)
|
||||
Text(model.status).lineLimit(1)
|
||||
Spacer()
|
||||
Text(model.selected?.name ?? "No connection")
|
||||
}
|
||||
.font(.caption)
|
||||
.padding(9)
|
||||
}
|
||||
|
||||
private func edit(_ profile: ConnectionProfile) {
|
||||
editing = profile
|
||||
editingPassword = PasswordStore.read(profile.id)
|
||||
showingEditor = true
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectionEditor: View {
|
||||
@Binding var profile: ConnectionProfile
|
||||
@Binding var password: String
|
||||
let save: () -> Void
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Connection Settings").font(.title2.bold())
|
||||
Form {
|
||||
TextField("Connection Name", text: $profile.name)
|
||||
TextField("Hostname", text: $profile.host)
|
||||
TextField("Port", value: $profile.port, format: .number)
|
||||
TextField("Username", text: $profile.username)
|
||||
SecureField("Password (stored in macOS Keychain)", text: $password)
|
||||
TextField("Default Schema", text: $profile.database)
|
||||
}
|
||||
Text("Built-in MySQL protocol client. This first version supports loopback TCP connections and local SSH tunnels. Remote TLS and full caching_sha2 authentication are not implemented yet.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Cancel") { dismiss() }
|
||||
Button("Save") { save() }.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
.frame(width: 520)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user