163 lines
7.7 KiB
Swift
163 lines
7.7 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
struct OrionMenuView: View {
|
|
@EnvironmentObject private var vm: OrionVMController
|
|
@Environment(\.openSettings) private var openSettings
|
|
@State private var expanded: Set<String> = []
|
|
@State private var expandedServers: Set<UUID> = []
|
|
|
|
private var groups: [String] {
|
|
let order = ["Windows", "Ubuntu", "Debian", "Fedora", "CentOS", "Alpine", "Oracle Linux", "Rocky Linux", "AlmaLinux", "Arch Linux", "Gentoo", "FreeBSD", "Other"]
|
|
return order.filter { os in vm.discoveredVMs.contains { $0.operatingSystem == os } }
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
HStack {
|
|
Label("OrionToolbar", systemImage: "server.rack").font(.headline)
|
|
Spacer()
|
|
if vm.discovering { ProgressView().controlSize(.small) }
|
|
Button { Task { await vm.discoverVMs() } } label: { Image(systemName: "arrow.clockwise") }
|
|
.buttonStyle(.borderless).help("Refresh virtual machines")
|
|
}.padding(14)
|
|
Divider()
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text("PARALLELS VIRTUAL MACHINES").font(.caption.bold()).foregroundStyle(.secondary)
|
|
if vm.discoveredVMs.isEmpty {
|
|
Text(vm.discovering ? "Discovering virtual machines…" : "No VMs found. Use Refresh to try again.")
|
|
.foregroundStyle(.secondary).padding(.vertical, 18)
|
|
}
|
|
ForEach(groups, id: \.self) { os in
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Button {
|
|
if !expanded.insert(os).inserted { expanded.remove(os) }
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: expanded.contains(os) ? "chevron.down" : "chevron.right")
|
|
.frame(width: 14)
|
|
Text(os).fontWeight(.semibold)
|
|
Spacer()
|
|
Text("\(vm.discoveredVMs.filter { $0.operatingSystem == os }.count)")
|
|
.foregroundStyle(.secondary)
|
|
}.contentShape(Rectangle()).padding(.vertical, 5)
|
|
}.buttonStyle(.plain)
|
|
if expanded.contains(os) {
|
|
ForEach(vm.discoveredVMs.filter { $0.operatingSystem == os }) { machine in
|
|
vmRow(machine)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !vm.discoveredVMs.isEmpty {
|
|
HStack {
|
|
Button("Select All") { vm.selectAllVMs() }
|
|
Button("Clear") { vm.clearVMSelection() }
|
|
Spacer()
|
|
Button("Integrate Selected") { vm.integrateSelectedVMs() }
|
|
.disabled(vm.selectedVMIDs.isEmpty)
|
|
}.font(.caption)
|
|
}
|
|
Divider()
|
|
Text("INTEGRATED SERVERS").font(.caption.bold()).foregroundStyle(.secondary)
|
|
ForEach(vm.servers) { server in serverRow(server) }
|
|
}.padding(14)
|
|
}.frame(height: 490)
|
|
Divider()
|
|
HStack {
|
|
Button("Parallels") { vm.openParallels() }
|
|
Spacer()
|
|
Button("Settings…") {
|
|
openSettings()
|
|
NSApplication.shared.activate(ignoringOtherApps: true)
|
|
}
|
|
Button("Quit") { NSApplication.shared.terminate(nil) }
|
|
}.padding(14)
|
|
}
|
|
.frame(width: 460)
|
|
.task { await vm.discoverVMs() }
|
|
.alert("OrionToolbar", isPresented: Binding(
|
|
get: { vm.lastError != nil },
|
|
set: { if !$0 { vm.lastError = nil } }
|
|
)) { Button("OK") { vm.lastError = nil } }
|
|
message: { Text(vm.lastError ?? "") }
|
|
}
|
|
|
|
private func vmRow(_ machine: ParallelsVM) -> some View {
|
|
HStack(spacing: 9) {
|
|
Toggle("", isOn: Binding(
|
|
get: { vm.selectedVMIDs.contains(machine.id) },
|
|
set: { selected in
|
|
if selected { vm.selectedVMIDs.insert(machine.id) }
|
|
else { vm.selectedVMIDs.remove(machine.id) }
|
|
}
|
|
)).labelsHidden().toggleStyle(.checkbox)
|
|
vmIcon(machine)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(machine.name).lineLimit(1)
|
|
Text(machine.status.capitalized).font(.caption)
|
|
.foregroundStyle(machine.status == "running" ? Color.green : Color.secondary)
|
|
}
|
|
Spacer(minLength: 4)
|
|
if vm.busyVMIDs.contains(machine.id) { ProgressView().controlSize(.small) }
|
|
else if machine.status == "stopped" {
|
|
Button("Start") { vm.operate(machine, action: "start") }
|
|
} else if machine.status == "running" {
|
|
Button("Stop") { vm.operate(machine, action: "stop") }
|
|
} else if machine.status == "suspended" {
|
|
Button("Resume") { vm.operate(machine, action: "resume") }
|
|
}
|
|
if vm.isIntegrated(machine) {
|
|
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green).help("Integrated")
|
|
} else {
|
|
Button { vm.integrate(machine) } label: { Image(systemName: "plus.circle") }
|
|
.help("Integrate VM")
|
|
}
|
|
}.padding(.leading, 20).padding(.vertical, 4)
|
|
}
|
|
|
|
private func vmIcon(_ machine: ParallelsVM) -> some View {
|
|
Group {
|
|
if let icon = vm.icon(for: machine) {
|
|
Image(nsImage: icon).resizable().scaledToFit()
|
|
} else {
|
|
Image(systemName: machine.symbol).resizable().scaledToFit().padding(4)
|
|
}
|
|
}.frame(width: 30, height: 30)
|
|
}
|
|
|
|
private func serverRow(_ server: ServerDefinition) -> some View {
|
|
let status = vm.status(for: server)
|
|
return VStack(alignment: .leading, spacing: 6) {
|
|
Button {
|
|
if !expandedServers.insert(server.id).inserted { expandedServers.remove(server.id) }
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: expandedServers.contains(server.id) ? "chevron.down" : "chevron.right")
|
|
if let icon = vm.icon(for: server) {
|
|
Image(nsImage: icon).resizable().scaledToFit().frame(width: 25, height: 25)
|
|
} else { Image(systemName: "server.rack") }
|
|
Text(server.name)
|
|
Spacer()
|
|
Text(status.title).foregroundStyle(.secondary)
|
|
}.contentShape(Rectangle())
|
|
}.buttonStyle(.plain)
|
|
if expandedServers.contains(server.id) {
|
|
HStack {
|
|
if status == .stopped {
|
|
Button("Start") { vm.start(server) }
|
|
} else if status == .running {
|
|
Button("Stop") { vm.stop(server) }
|
|
Button("Restart") { vm.restart(server) }
|
|
}
|
|
Button("SSH") { vm.openSSH(server) }
|
|
.disabled(status != .running || server.host.isEmpty || server.sshUser.isEmpty)
|
|
Button("Mailpit") { vm.openMailpit(server) }
|
|
.disabled(status != .running || server.host.isEmpty || server.mailpitPort == nil)
|
|
}.padding(.leading, 30)
|
|
}
|
|
}.padding(.vertical, 4)
|
|
}
|
|
}
|