Files
notes/Notes Watch Widget/ContextfulWatchWidget.swift
rzen 86d74806c9 Add voice notes: on-device transcription, multi-language, watch capture
- 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
2026-07-15 13:30:43 -04:00

92 lines
3.0 KiB
Swift

import SwiftUI
import WidgetKit
// A launcher complication: a static button on the watch face that opens the
// Contextful watch app straight into recording. It carries no data, so the
// timeline is a single entry that never refreshes. The `widgetURL` deep link
// (`contextful://record?autostart=1`) is what tells the app to auto-start —
// only a complication tap carries it, so a plain launch never auto-records.
private struct LauncherEntry: TimelineEntry {
let date: Date
}
private struct LauncherProvider: TimelineProvider {
func placeholder(in context: Context) -> LauncherEntry {
LauncherEntry(date: .now)
}
func getSnapshot(in context: Context, completion: @escaping (LauncherEntry) -> Void) {
completion(LauncherEntry(date: .now))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<LauncherEntry>) -> Void) {
// Nothing ever changes — one entry, never reload.
completion(Timeline(entries: [LauncherEntry(date: .now)], policy: .never))
}
}
private struct LauncherView: View {
@Environment(\.widgetFamily) private var family
private let glyph = "mic.fill"
// `.widgetAccentable()` puts the glyph in the accent group so the watch face
// tints it with its main color instead of leaving it plain white.
private var mic: some View {
Image(systemName: glyph).widgetAccentable()
}
var body: some View {
Group {
switch family {
case .accessoryInline:
// Inline templates render the symbol upright next to text.
Label("Record", systemImage: glyph)
case .accessoryRectangular:
HStack(spacing: 6) {
mic.font(.title3)
Text("Record a Note").font(.headline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
case .accessoryCorner:
mic
.font(.title2)
.widgetLabel("Record")
default: // .accessoryCircular and any future families
ZStack {
AccessoryWidgetBackground()
mic.font(.title3)
}
}
}
.widgetURL(URL(string: "contextful://record?autostart=1"))
}
}
struct ContextfulRecorderLauncher: Widget {
private let kind = "ContextfulRecorderLauncher"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: LauncherProvider()) { _ in
LauncherView()
.containerBackground(.clear, for: .widget)
}
.configurationDisplayName("Record a Note")
.description("Tap to start recording a voice note.")
.supportedFamilies([
.accessoryCircular,
.accessoryCorner,
.accessoryInline,
.accessoryRectangular,
])
}
}
@main
struct ContextfulWatchWidgetBundle: WidgetBundle {
var body: some Widget {
ContextfulRecorderLauncher()
}
}