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
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Contextful</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user