PadXcode-Daemon/UI/MenuBarView.swift

171 lines
7.3 KiB
Swift
Raw Normal View History

2026-04-12 00:42:51 -07:00
import SwiftUI
struct MenuBarView: View {
2026-04-12 22:14:13 -07:00
@ObservedObject var controller: DaemonController
2026-04-12 00:42:51 -07:00
@ObservedObject var preferences: AppPreferences
@Environment(\.openWindow) private var openWindow
var body: some View {
VStack(spacing: 0) {
header.padding(.horizontal,14).padding(.top,14).padding(.bottom,10)
Divider()
statusGrid.padding(.horizontal,14).padding(.vertical,10)
Divider()
if !controller.connectedClients.isEmpty {
connectionsSection.padding(.horizontal,14).padding(.vertical,8)
Divider()
}
actionsSection.padding(.horizontal,14).padding(.vertical,8)
Divider()
footer.padding(.horizontal,14).padding(.vertical,8)
}
.frame(width: 300)
}
private var header: some View {
HStack(spacing:10) {
Circle().fill(stateColor).frame(width:10,height:10).shadow(color:stateColor.opacity(0.6),radius:4)
VStack(alignment:.leading,spacing:2) {
Text("PadXcode Daemon").font(.headline)
Text(controller.serverState.statusLabel).font(.subheadline).foregroundStyle(.secondary)
}
Spacer()
2026-04-12 22:14:13 -07:00
Button {
Task {
if controller.serverState.isRunning { await controller.stop() }
else if case .starting = controller.serverState { } // ignore during startup
else { await controller.start() }
}
}
label: {
if case .starting = controller.serverState {
ProgressView().scaleEffect(0.6).frame(width:44)
} else {
Text(controller.serverState.isRunning ? "Stop" : "Start").frame(width:44)
}
}
.buttonStyle(.bordered).controlSize(.small)
.tint(controller.serverState.isRunning ? .red : .green)
.disabled({ if case .starting = controller.serverState { return true }; return false }())
2026-04-12 00:42:51 -07:00
}
}
private var statusGrid: some View {
Grid(alignment:.leading,horizontalSpacing:12,verticalSpacing:6) {
GridRow {
StatusCell(label:"Port", value:"\(preferences.port)", icon:"antenna.radiowaves.left.and.right")
StatusCell(label:"Uptime", value:controller.serverState.uptime ?? "", icon:"clock")
}
GridRow {
StatusCell(label:"Tailscale", value:preferences.tailscaleIP, icon:"network")
StatusCell(label:"Builds", value:"\(controller.buildCount)", icon:"hammer")
}
GridRow {
StatusCell(label:"Clients", value:"\(controller.connectedClients.count)", icon:"ipad.and.iphone")
StatusCell(label:"Config", value:preferences.developmentTeam.isEmpty ? "⚠️ No Team ID" : "✓ Signed", icon:"checkmark.seal")
}
}
}
private var connectionsSection: some View {
VStack(alignment:.leading,spacing:4) {
Text("Active Connections").font(.caption.bold()).foregroundStyle(.secondary)
ForEach(controller.connectedClients) { c in
HStack(spacing:6) {
Circle().fill(.green).frame(width:6,height:6)
Text(c.type.rawValue).font(.caption)
Spacer()
Text(c.sessionId.prefix(8)).font(.system(.caption2,design:.monospaced)).foregroundStyle(.secondary)
}
}
}
}
private var actionsSection: some View {
VStack(spacing:2) {
MenuAction(label:"Restart Server", icon:"arrow.clockwise") { Task { await controller.restart() } }
MenuAction(label:"View Log", icon:"doc.text.magnifyingglass") { openWindow(id:"log"); NSApp.activate(ignoringOtherApps:true) }
MenuAction(label:"Copy Server URL", icon:"doc.on.clipboard") {
let url = "http://\(preferences.tailscaleIP):\(preferences.port)"
NSPasteboard.general.clearContents(); NSPasteboard.general.setString(url, forType:.string)
}
MenuAction(label:"Open Config Folder", icon:"folder") {
NSWorkspace.shared.open(URL(fileURLWithPath:NSHomeDirectory()).appendingPathComponent(".padxcode"))
}
MenuAction(label:"Settings…", icon:"gearshape", shortcut:",") { openSettingsWindow() }
}
}
private var footer: some View {
HStack {
Text("v1.0.0").font(.caption2).foregroundStyle(.tertiary)
Spacer()
Button("Quit PadXcode Daemon") { Task { await controller.stop(); NSApp.terminate(nil) } }
.buttonStyle(.plain).font(.caption).foregroundStyle(.secondary)
}
}
private var stateColor: Color {
switch controller.serverState {
case .running: return .green; case .starting: return .yellow
case .error: return .red; case .stopped: return .secondary
}
}
private func openSettingsWindow() {
openWindow(id:"settings")
DispatchQueue.main.asyncAfter(deadline:.now()+0.05) {
NSApp.setActivationPolicy(.regular); NSApp.activate(ignoringOtherApps:true)
NSApp.windows.first(where:{ $0.title == "PadXcode Daemon Settings" })?.makeKeyAndOrderFront(nil)
}
}
}
struct StatusCell: View {
let label: String; let value: String; let icon: String
var body: some View {
HStack(spacing:5) {
Image(systemName:icon).font(.caption).foregroundStyle(.secondary).frame(width:14)
VStack(alignment:.leading,spacing:0) {
Text(label).font(.caption2).foregroundStyle(.tertiary)
Text(value).font(.system(.caption,design:.monospaced)).lineLimit(1)
}
}.frame(maxWidth:.infinity,alignment:.leading)
}
}
struct MenuAction: View {
let label: String; let icon: String; var shortcut: String? = nil; let action: () -> Void
var body: some View {
Button(action:action) {
HStack {
Image(systemName:icon).frame(width:16).foregroundStyle(.secondary)
Text(label)
Spacer()
if let sc = shortcut { Text("\(sc)").font(.caption).foregroundStyle(.tertiary) }
}.contentShape(Rectangle()).padding(.horizontal,4).padding(.vertical,5)
}.buttonStyle(.plain)
}
}
struct MenuBarIcon: View {
let controller: DaemonController?
var body: some View {
HStack(spacing:3) {
Image(systemName: iconName).symbolRenderingMode(.palette).foregroundStyle(iconColor, .primary)
if let u = controller?.serverState.uptime { Text(u).font(.system(size:10,design:.monospaced)) }
}
}
private var iconName: String {
switch controller?.serverState {
case .running: return "dot.radiowaves.left.and.right"; case .starting: return "arrow.clockwise.circle"
case .error: return "exclamationmark.circle"; default: return "antenna.radiowaves.left.and.right.slash"
}
}
private var iconColor: Color {
switch controller?.serverState {
case .running: return .green; case .starting: return .yellow; case .error: return .red; default: return .secondary
}
}
}