Files
OrionToolbar/backups/vm-fix-20261023-074349/OrionMenuView.swift
T
2026-10-23 07:52:57 +01:00

416 lines
12 KiB
Swift

import AppKit
import SwiftUI
struct OrionMenuView: View {
@EnvironmentObject var vm: OrionVMController
@Environment(\.openSettings) private var openSettings
@State private var expandedGroups: Set<String> = []
@State private var expandedServers: Set<UUID> = []
private var groups: [String] {
let preferred = [
"Windows", "Ubuntu", "Debian", "Fedora",
"CentOS", "Alpine", "Oracle Linux",
"Rocky Linux", "AlmaLinux", "Arch Linux",
"Gentoo", "FreeBSD", "Other"
]
return preferred.filter { os in
vm.discoveredVMs.contains {
$0.operatingSystem == os
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
header
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 4) {
discoverySection
Divider()
.padding(.vertical, 10)
integratedSection
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
}
.frame(maxHeight: 560)
Divider()
footer
}
.frame(width: 410)
.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 var header: some View {
HStack {
Image(systemName: "server.rack")
.foregroundStyle(.blue)
Text("OrionToolbar")
.font(.headline)
Spacer()
if vm.discovering {
ProgressView()
.controlSize(.small)
}
Button {
Task { await vm.discoverVMs() }
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.borderless)
.disabled(vm.discovering)
.help("Refresh Parallels virtual machines")
}
.padding(16)
}
private var discoverySection: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("PARALLELS VIRTUAL MACHINES")
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
Spacer()
Text("\(vm.discoveredVMs.count)")
.font(.caption)
.foregroundStyle(.secondary)
}
if vm.discoveredVMs.isEmpty {
ContentUnavailableView(
"No Virtual Machines",
systemImage: "desktopcomputer",
description: Text(
"Refresh to discover Parallels virtual machines."
)
)
.frame(height: 120)
}
ForEach(groups, id: \.self) { group in
operatingSystemGroup(group)
}
if !vm.discoveredVMs.isEmpty {
Divider()
.padding(.vertical, 6)
HStack {
Text("\(vm.selectedVMIDs.count) selected")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Button("Select All") {
vm.selectAllVMs()
}
Button("Clear") {
vm.clearVMSelection()
}
.disabled(vm.selectedVMIDs.isEmpty)
}
Button {
vm.integrateSelectedVMs()
} label: {
Label(
"Integrate Selected",
systemImage: "plus.circle.fill"
)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.disabled(vm.selectedVMIDs.isEmpty)
}
}
}
private func operatingSystemGroup(
_ group: String
) -> some View {
let machines = vm.discoveredVMs.filter {
$0.operatingSystem == group
}
return VStack(alignment: .leading, spacing: 3) {
Button {
if expandedGroups.contains(group) {
expandedGroups.remove(group)
} else {
expandedGroups.insert(group)
}
} label: {
HStack(spacing: 10) {
Image(systemName:
expandedGroups.contains(group)
? "chevron.down"
: "chevron.right"
)
.font(.caption)
.frame(width: 12)
Image(systemName: machines.first?.symbol
?? "desktopcomputer"
)
.frame(width: 22)
Text(group)
.fontWeight(.medium)
Spacer()
Text("\(machines.count)")
.foregroundStyle(.secondary)
}
.contentShape(Rectangle())
.padding(.vertical, 9)
}
.buttonStyle(.plain)
if expandedGroups.contains(group) {
ForEach(machines) { machine in
machineRow(machine)
}
}
}
}
private func machineRow(
_ machine: ParallelsVM
) -> some View {
HStack(alignment: .center, spacing: 10) {
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)
if let icon = vm.icon(for: machine) {
Image(nsImage: icon)
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
} else {
Image(systemName: machine.symbol)
.font(.title3)
.frame(width: 30, height: 30)
.foregroundStyle(.secondary)
}
VStack(alignment: .leading, spacing: 3) {
Text(machine.name)
.font(.system(size: 13, weight: .medium))
HStack(spacing: 5) {
Circle()
.fill(
machine.status.lowercased() == "running"
? Color.green : Color.gray
)
.frame(width: 7, height: 7)
Text(machine.status.capitalized)
if vm.isIntegrated(machine) {
Text("· Integrated")
.foregroundStyle(.green)
}
}
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
.padding(.leading, 22)
.padding(.vertical, 6)
}
private var integratedSection: some View {
VStack(alignment: .leading, spacing: 8) {
Text("INTEGRATED SERVERS")
.font(.caption)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
ForEach(vm.servers) { server in
integratedServerRow(server)
}
}
}
private func integratedServerRow(
_ server: ServerDefinition
) -> some View {
let status = vm.status(for: server)
let expanded = expandedServers.contains(server.id)
return VStack(alignment: .leading, spacing: 5) {
Button {
if expanded {
expandedServers.remove(server.id)
} else {
expandedServers.insert(server.id)
}
} label: {
HStack(spacing: 10) {
Image(systemName:
expanded ? "chevron.down" : "chevron.right"
)
.font(.caption)
Circle()
.fill(status == .running
? Color.green : Color.gray)
.frame(width: 9, height: 9)
VStack(alignment: .leading, spacing: 2) {
Text(server.name)
.fontWeight(.medium)
Text(status.title)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.contentShape(Rectangle())
.padding(.vertical, 8)
}
.buttonStyle(.plain)
if expanded {
VStack(alignment: .leading, spacing: 5) {
if status == .stopped {
action("Start", "play.fill") {
vm.start(server)
}
} else {
action("Stop", "stop.fill") {
vm.stop(server)
}
.disabled(vm.isBusy(server))
action("Restart", "arrow.clockwise") {
vm.restart(server)
}
.disabled(
vm.isBusy(server) ||
status != .running
)
action(
"Force Stop",
"exclamationmark.triangle"
) {
vm.forceStop(server)
}
.disabled(vm.isBusy(server))
}
action("Open SSH in Terminal", "terminal") {
vm.openSSH(server)
}
.disabled(
status != .running ||
server.host.isEmpty
)
action("Open Mailpit", "envelope") {
vm.openMailpit(server)
}
.disabled(
status != .running ||
server.mailpitPort == nil ||
server.host.isEmpty
)
}
.padding(.leading, 26)
.padding(.bottom, 8)
}
}
}
private func action(
_ title: String,
_ symbol: String,
perform: @escaping () -> Void
) -> some View {
Button(action: perform) {
Label(title, systemImage: symbol)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 5)
}
.buttonStyle(.plain)
}
private var footer: some View {
VStack(spacing: 2) {
action("Open Parallels Desktop", "macwindow") {
vm.openParallels()
}
action("Settings…", "gearshape") {
openSettings()
NSApplication.shared.activate(
ignoringOtherApps: true
)
}
Divider()
action("Quit OrionToolbar", "xmark") {
NSApplication.shared.terminate(nil)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
}