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
102 lines
3.7 KiB
Swift
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)
|
|
}
|
|
}
|