Files
notes/Notes/Views/Settings/TranscriptionLanguagesView.swift
T
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

120 lines
4.1 KiB
Swift

import SwiftUI
/// Settings screen for choosing which languages voice notes can be transcribed
/// in. Enabling a language reserves it and downloads its on-device model; the
/// system caps how many can be reserved at once, so the list disables further
/// toggles at capacity and explains the limit in the footer.
struct TranscriptionLanguagesView: View {
@Environment(TranscriptionSettings.self) private var settings
@State private var errorMessage: String?
var body: some View {
List {
if let errorMessage {
Section {
Label(errorMessage, systemImage: "exclamationmark.triangle")
.font(.footnote)
.foregroundStyle(.red)
}
}
if !enabledLocales.isEmpty {
Section("Enabled") {
ForEach(enabledLocales, id: \.identifier) { row($0) }
}
}
Section {
ForEach(otherLocales, id: \.identifier) { row($0) }
} header: {
Text("All Languages")
} footer: {
Text(footerText)
}
}
.navigationTitle("Languages")
.overlay {
if settings.supportedLocales.isEmpty {
ProgressView()
}
}
.task { await settings.prepare() }
}
private var enabledLocales: [Locale] {
settings.supportedLocales.filter { settings.isEnabled($0) }
}
private var otherLocales: [Locale] {
settings.supportedLocales.filter { !settings.isEnabled($0) }
}
private var footerText: String {
guard settings.maximumReserved > 0 else {
return "Each new note is transcribed on device in your enabled languages."
}
return "Enable up to \(settings.maximumReserved) languages. Each new voice note is sampled in your enabled languages and transcribed in whichever fits best."
}
private func row(_ locale: Locale) -> some View {
let enabled = settings.isEnabled(locale)
return HStack {
VStack(alignment: .leading, spacing: 2) {
Text(TranscriptionSettings.displayName(for: locale))
stateSubtitle(for: locale)
}
Spacer()
Toggle("", isOn: Binding(
get: { settings.isEnabled(locale) },
set: { toggle(locale, on: $0) }
))
.labelsHidden()
.disabled(!enabled && settings.isAtCapacity)
}
}
@ViewBuilder
private func stateSubtitle(for locale: Locale) -> some View {
switch settings.assetStates[TranscriptionSettings.canonicalID(locale)] {
case .installed:
Label("Installed", systemImage: "checkmark.circle.fill")
.font(.caption)
.foregroundStyle(.green)
case .downloading(let fraction):
HStack(spacing: 6) {
ProgressView().controlSize(.small)
if let fraction {
Text("Downloading \(Int(fraction * 100))%")
} else {
Text("Downloading…")
}
}
.font(.caption)
.foregroundStyle(.secondary)
case .notInstalled:
Label("Available to download", systemImage: "icloud.and.arrow.down")
.font(.caption)
.foregroundStyle(.secondary)
case .unsupported, .none:
EmptyView()
}
}
private func toggle(_ locale: Locale, on: Bool) {
errorMessage = nil
Task {
if on {
do {
try await settings.enable(locale)
} catch TranscriptionSettings.SettingsError.atCapacity {
errorMessage = "You can enable up to \(settings.maximumReserved) languages. Turn one off first."
} catch {
errorMessage = "Couldn't enable \(TranscriptionSettings.displayName(for: locale)): \(error.localizedDescription)"
}
} else {
await settings.disable(locale)
}
}
}
}