Files
reyna-cli/backup/macmini-automation-baseline-2026-08-03/macmini-mcp/WatchHermes/ContentView.swift
T
2026-08-03 11:47:09 -04:00

136 lines
4.0 KiB
Swift

import SwiftUI
struct ContentView: View {
@StateObject private var streamManager: AudioStreamManager
@StateObject private var captureManager = AudioCaptureManager()
@State private var isPressing = false
init() {
let manager = AudioStreamManager(endpoint: URL(string: "ws://192.168.68.126:8642/stream")!)
_streamManager = StateObject(wrappedValue: manager)
}
var body: some View {
VStack(spacing: 14) {
Text("Hermes")
.font(.headline)
Text(statusText)
.font(.caption2)
.foregroundStyle(statusColor)
.lineLimit(2)
.multilineTextAlignment(.center)
.frame(minHeight: 28)
Circle()
.fill(buttonFill)
.overlay {
Image(systemName: isPressing ? "waveform" : "mic.fill")
.font(.system(size: 36, weight: .semibold))
.foregroundStyle(.white)
}
.overlay {
Circle()
.stroke(.white.opacity(isPressing ? 0.9 : 0.25), lineWidth: 3)
}
.frame(width: 112, height: 112)
.scaleEffect(isPressing ? 0.94 : 1.0)
.animation(.spring(response: 0.2, dampingFraction: 0.75), value: isPressing)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
guard !isPressing else { return }
isPressing = true
startPTT()
}
.onEnded { _ in
isPressing = false
stopPTT()
}
)
Text(isPressing ? "Release to send" : "Hold to talk")
.font(.caption2)
.foregroundStyle(.secondary)
}
.padding()
.onAppear {
streamManager.connect()
captureManager.onPCMChunk = { [streamManager] chunk in
streamManager.sendPCMChunk(chunk)
}
}
.onDisappear {
captureManager.stopRecording()
streamManager.disconnect()
}
}
private var statusText: String {
switch captureManager.state {
case .recording:
return "Listening"
case .starting:
return "Starting mic"
case .failed(let message):
return message
case .idle:
break
}
switch streamManager.playbackState {
case .playing:
return "Playing response"
case .failed(let message):
return message
case .idle:
break
}
switch streamManager.connectionState {
case .connected:
return "Ready"
case .connecting:
return "Connecting"
case .disconnected:
return "Disconnected"
case .failed(let message):
return message
}
}
private var statusColor: Color {
if case .failed = captureManager.state { return .red }
if case .failed = streamManager.connectionState { return .red }
if case .failed = streamManager.playbackState { return .red }
if captureManager.state == .recording { return .green }
if streamManager.playbackState == .playing { return .blue }
return .secondary
}
private var buttonFill: Color {
if captureManager.state == .recording { return .red }
if streamManager.playbackState == .playing { return .blue }
return .accentColor
}
private func startPTT() {
streamManager.connect()
streamManager.sendControlEvent("start")
Task {
await captureManager.startRecording()
}
}
private func stopPTT() {
captureManager.stopRecording()
streamManager.sendControlEvent("stop")
}
}
#Preview {
ContentView()
}