import AppKit import SwiftUI @MainActor final class OverlayPanelController { private let panel: NSPanel init(model: AppModel) { panel = NSPanel( contentRect: NSRect(x: 0, y: 0, width: 304, height: 82), styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false ) panel.level = .floating panel.isOpaque = false panel.backgroundColor = .clear panel.hasShadow = true panel.hidesOnDeactivate = false panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary] panel.contentView = NSHostingView(rootView: RecordingOverlay(model: model)) } func show() { positionPanel() panel.orderFrontRegardless() } func hide() { panel.orderOut(nil) } private func positionPanel() { let mouse = NSEvent.mouseLocation let screen = NSScreen.screens.first { NSMouseInRect(mouse, $0.frame, false) } ?? NSScreen.main guard let frame = screen?.visibleFrame else { return } let x = frame.midX - panel.frame.width / 2 let y = frame.minY + 42 panel.setFrameOrigin(NSPoint(x: x, y: y)) } } private struct RecordingOverlay: View { @ObservedObject var model: AppModel private let teal = Color(red: 0.039, green: 0.624, blue: 0.604) var body: some View { HStack(spacing: 14) { signal .frame(width: 46, height: 34) VStack(alignment: .leading, spacing: 3) { Text(title) .font(.system(size: 13, weight: .semibold)) Text(detail) .font(.system(size: 12, design: .monospaced)) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 6) if model.isRecording { Button(action: model.toggleRecording) { Image(systemName: "stop.fill") .font(.system(size: 11, weight: .bold)) .frame(width: 28, height: 28) } .buttonStyle(.borderedProminent) .tint(teal) .clipShape(Circle()) .help("Stop and transcribe") } } .padding(.horizontal, 17) .frame(width: 304, height: 82) .background(.ultraThickMaterial, in: RoundedRectangle(cornerRadius: 19, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 19, style: .continuous) .strokeBorder(.white.opacity(0.13)) } } @ViewBuilder private var signal: some View { switch model.state { case .recording: HStack(alignment: .center, spacing: 3) { ForEach(0..<5, id: \.self) { index in Capsule() .fill(teal) .frame(width: 4, height: barHeight(index)) } } case .preparing, .transcribing: ProgressView().controlSize(.small).tint(teal) case .success: Image(systemName: "checkmark.circle.fill").font(.title2).foregroundStyle(teal) case .failure: Image(systemName: "exclamationmark.triangle.fill").font(.title3).foregroundStyle(.red) case .idle: Image(systemName: "waveform").foregroundStyle(teal) } } private var title: String { switch model.state { case .preparing: "Starting microphone" case .recording: "Recording" case .transcribing: "Transcribing" case .success(let message), .failure(let message): message case .idle: "Ready" } } private var detail: String { switch model.state { case .recording: Self.duration(model.elapsed) case .preparing: "Waiting for permission" case .transcribing: "Sending audio" case .failure: "Open Transui settings to fix this" case .success: "Done" case .idle: model.settings.shortcut.displayName } } private func barHeight(_ index: Int) -> CGFloat { let shape = [0.55, 0.82, 1.0, 0.72, 0.45][index] return 7 + 24 * max(0.08, model.level * shape) } private static func duration(_ interval: TimeInterval) -> String { let seconds = Int(interval) return String(format: "%02d:%02d", seconds / 60, seconds % 60) } }