b2ae3a62b9
Native SwiftUI menu-bar app (LSUIElement, macOS 13+), generated from project.yml via XcodeGen. Includes: - PermissionsManager (Microphone / Screen Recording / Accessibility) + UI - SparkControlHealth: GET /api/status over self-signed TLS (InsecureTrustDelegate) - AppSettings persistence (host, TLS-skip, output folder, adapter toggles) - Menu-bar panel + Settings, app sandbox & hardened runtime off (LAN tool)
117 lines
3.7 KiB
Swift
117 lines
3.7 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
/// The menu-bar panel: permission statuses, backend health, and a link to
|
|
/// Settings. Shown when the user clicks the status-bar item.
|
|
struct MenuBarView: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
@EnvironmentObject private var permissions: PermissionsManager
|
|
@EnvironmentObject private var health: SparkControlHealth
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
header
|
|
Divider()
|
|
permissionsSection
|
|
Divider()
|
|
backendSection
|
|
Divider()
|
|
footer
|
|
}
|
|
.padding(14)
|
|
.frame(width: 320)
|
|
}
|
|
.onAppear { permissions.refresh() }
|
|
.task { await refreshHealth() }
|
|
}
|
|
|
|
private var header: some View {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("Ten31 Transcripts").font(.headline)
|
|
Text("Phase 0 · setup & status")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
private var permissionsSection: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Permissions").font(.subheadline).bold()
|
|
PermissionRow(
|
|
title: "Microphone",
|
|
state: permissions.microphone,
|
|
onGrant: permissions.requestMicrophone,
|
|
onOpenSettings: { permissions.openSettings(.microphone) }
|
|
)
|
|
PermissionRow(
|
|
title: "Screen Recording",
|
|
state: permissions.screenRecording,
|
|
onGrant: permissions.requestScreenRecording,
|
|
onOpenSettings: { permissions.openSettings(.screenRecording) }
|
|
)
|
|
PermissionRow(
|
|
title: "Accessibility",
|
|
state: permissions.accessibility,
|
|
onGrant: permissions.requestAccessibility,
|
|
onOpenSettings: { permissions.openSettings(.accessibility) }
|
|
)
|
|
}
|
|
}
|
|
|
|
private var backendSection: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text("Backend").font(.subheadline).bold()
|
|
Spacer()
|
|
Button("Check") { Task { await refreshHealth() } }
|
|
.disabled(health.status == .checking)
|
|
}
|
|
Text(settings.backendBaseURL)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
HStack(spacing: 8) {
|
|
StatusDot(color: healthColor)
|
|
Text(healthText).font(.caption)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var footer: some View {
|
|
HStack {
|
|
NavigationLink("Settings…") {
|
|
SettingsView()
|
|
}
|
|
Spacer()
|
|
Button("Quit") { NSApplication.shared.terminate(nil) }
|
|
}
|
|
}
|
|
|
|
private func refreshHealth() async {
|
|
await health.check(
|
|
baseURL: settings.backendBaseURL,
|
|
skipTLS: settings.skipTLSVerification
|
|
)
|
|
}
|
|
|
|
private var healthColor: Color {
|
|
switch health.status {
|
|
case .online: return .green
|
|
case .offline: return .red
|
|
case .checking: return .orange
|
|
case .unknown: return .gray
|
|
}
|
|
}
|
|
|
|
private var healthText: String {
|
|
switch health.status {
|
|
case .unknown: return "Not checked yet"
|
|
case .checking: return "Checking…"
|
|
case .online(let detail): return "Online · \(detail)"
|
|
case .offline(let error): return "Offline · \(error)"
|
|
}
|
|
}
|
|
}
|