Files
OrionToolbar/OrionToolbar/OrionMenuView.swift
T
2026-10-23 07:52:57 +01:00

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)
}
}