Files
rzen 798abf3b90 Watch: show version/build on screen, surface recording errors in UI
A tester on real hardware has no console, so a failed start looked like
a dead button and there was no way to tell which build the watch had
actually installed. Show the stamped version/build under the record
button, display the last start error in place of the hint text, and
fall back to the default audio session mode if .spokenAudio is rejected
with the .record category.

Claude-Session: https://claude.ai/code/session_01GKfAiKiQKLDTmbiGva4FGE
2026-07-15 16:07:32 -04:00

102 lines
3.7 KiB
Swift

import SwiftUI
/// The watch app's single screen: a big record/stop button, an elapsed timer
/// while recording, and a footer counting recordings still waiting to reach the
/// iPhone. Shows a permission-denied state when the mic is refused.
struct RecordView: View {
@Environment(WatchAppServices.self) private var services
var body: some View {
let recorder = services.recorder
VStack(spacing: 10) {
if recorder.permissionDenied {
permissionDenied
} else {
recordButton(isRecording: recorder.isRecording)
if recorder.isRecording {
Text(Self.elapsedString(recorder.elapsed))
.font(.system(.title3, design: .rounded).monospacedDigit())
.foregroundStyle(.secondary)
} else if let error = services.lastError {
Text(error)
.font(.caption2)
.foregroundStyle(.red)
.multilineTextAlignment(.center)
.lineLimit(3)
} else {
Text("Tap to record")
.font(.footnote)
.foregroundStyle(.secondary)
}
pendingFooter
}
buildFooter
}
.padding()
.containerBackground(.red.gradient, for: .navigation)
.navigationTitle("Contextful")
}
private func recordButton(isRecording: Bool) -> some View {
Button {
services.toggleRecording()
} label: {
ZStack {
Circle()
.fill(.red)
.frame(width: 96, height: 96)
Image(systemName: isRecording ? "stop.fill" : "mic.fill")
.font(.system(size: 40))
.foregroundStyle(.white)
.contentTransition(.symbolEffect(.replace))
}
}
.buttonStyle(.plain)
.accessibilityLabel(isRecording ? "Stop recording" : "Start recording")
}
@ViewBuilder
private var pendingFooter: some View {
let count = services.transfer.pendingCount
if count > 0 {
Label(
count == 1 ? "1 waiting for iPhone" : "\(count) waiting for iPhone",
systemImage: "arrow.up.circle"
)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
private var permissionDenied: some View {
VStack(spacing: 8) {
Image(systemName: "mic.slash.fill")
.font(.title)
.foregroundStyle(.secondary)
Text("Microphone access is off")
.font(.headline)
.multilineTextAlignment(.center)
Text("Enable it in the Watch app on iPhone under Contextful.")
.font(.caption2)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}
/// Version + build stamped by update_build_number.sh, so a tester can tell
/// at a glance which build the watch actually installed.
private var buildFooter: some View {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "?"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "?"
return Text("v\(version) (\(build))")
.font(.system(size: 10))
.foregroundStyle(.tertiary)
}
/// mm:ss for the elapsed readout.
static func elapsedString(_ interval: TimeInterval) -> String {
let total = Int(interval)
return String(format: "%02d:%02d", total / 60, total % 60)
}
}