- Schema v2: Note.AudioInfo, .m4a sidecar next to the note JSON in iCloud - AudioRecorderService (iOS/macOS) + SpeechAnalyzer transcription pipeline with enabled-language set and confidence-based auto-pick, re-transcribe menu - Settings > Transcription > Languages with asset download/reserve handling - watchOS app + accessory complication: one-button recorder, WCSession transferFile to phone, WatchInbox staging, direct-upsert ingestion - Player UI, transcription status chips, quick-capture mic in notes list - Version 0.2, changelog + README updated
85 lines
2.9 KiB
Swift
85 lines
2.9 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 {
|
|
Text("Tap to record")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
pendingFooter
|
|
}
|
|
}
|
|
.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)
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|