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 @EnvironmentObject private var session: SessionController var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 12) { header Divider() recordingSection Divider() permissionsSection Divider() backendSection Divider() footer } .padding(14) .frame(width: 320) } .onAppear { permissions.refresh() } .task { await refreshHealth() } } // MARK: Recording private var canRecord: Bool { permissions.microphone == .granted && permissions.screenRecording == .granted } private var recordingSection: some View { VStack(alignment: .leading, spacing: 8) { HStack { Text("Recording").font(.subheadline).bold() Spacer() if session.state == .recording { Text(timeString(session.elapsed)) .font(.system(.caption, design: .monospaced)) .foregroundStyle(.secondary) } } Text(detectionText) .font(.caption) .foregroundStyle(.secondary) Button { session.toggle() } label: { Label(recordButtonTitle, systemImage: recordButtonIcon) .frame(maxWidth: .infinity) } .controlSize(.large) .tint(session.state == .recording ? .red : .accentColor) .disabled(recordButtonDisabled) if session.state == .recording { LevelBar(label: "Mic", level: session.micLevel) LevelBar(label: "System", level: session.systemLevel) } if !canRecord && !session.isBusy { Text("Grant Microphone + Screen Recording above to record.") .font(.caption) .foregroundStyle(.secondary) } if case .error(let message) = session.state { Text(message).font(.caption).foregroundStyle(.red) } if let warning = session.warning { Text(warning).font(.caption).foregroundStyle(.orange) } if let last = session.lastSession { Button { NSWorkspace.shared.activateFileViewerSelecting([last.mixedURL]) } label: { Text("Last: \(Int(last.duration.rounded()))s · \(last.selfSpanCount) self-spans · \(last.visualSegmentCount.map { "\($0) visual segments" } ?? "audio-only") — reveal in Finder") .font(.caption) } .buttonStyle(.link) HStack { Button("Send to backend") { session.processLastSession() } .disabled(transcriptProcessing) Spacer() } if !transcriptText.isEmpty { Text(transcriptText).font(.caption).foregroundStyle(transcriptColor) } } } } private var recordButtonTitle: String { switch session.state { case .starting: return "Starting…" case .recording: return "Stop Recording" case .finishing: return "Finishing…" case .idle, .error: return "Start Recording" } } private var recordButtonIcon: String { session.state == .recording ? "stop.circle.fill" : "record.circle" } private var recordButtonDisabled: Bool { switch session.state { case .starting, .finishing: return true case .recording: return false case .idle, .error: return !canRecord } } private func timeString(_ t: TimeInterval) -> String { let total = Int(t) return String(format: "%02d:%02d", total / 60, total % 60) } private var detectionText: String { switch session.detectionStatus { case .disabled: return "Auto-detect off" case .listening: return "Listening for calls…" case .inCall(let app): return "In call: \(app.display)" } } private var transcriptProcessing: Bool { if case .processing = session.transcriptStatus { return true } return false } private var transcriptText: String { switch session.transcriptStatus { case .idle: return "" case .processing(let d, let t): return "Transcribing… chunk \(d)/\(t)" case .done(let s, let seg): return "Transcript ready · \(s) speakers · \(seg) segments" case .failed(let m): return "Transcript failed: \(m)" } } private var transcriptColor: Color { switch session.transcriptStatus { case .failed: return .red case .done: return .green default: return .secondary } } 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)" } } }