VoiceWake: make tab content scrollable
parent
1d807911e4
commit
9d0415f9e9
|
|
@ -268,81 +268,83 @@ struct VoiceWakeSettings: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 14) {
|
ScrollView(.vertical) {
|
||||||
SettingsToggleRow(
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
title: "Enable Voice Wake",
|
SettingsToggleRow(
|
||||||
subtitle: "Listen for a wake phrase (e.g. \"Claude\") before running voice commands. "
|
title: "Enable Voice Wake",
|
||||||
+ "Voice recognition runs fully on-device.",
|
subtitle: "Listen for a wake phrase (e.g. \"Claude\") before running voice commands. "
|
||||||
binding: self.$state.swabbleEnabled)
|
+ "Voice recognition runs fully on-device.",
|
||||||
.disabled(!voiceWakeSupported)
|
binding: self.$state.swabbleEnabled)
|
||||||
|
.disabled(!voiceWakeSupported)
|
||||||
|
|
||||||
if !voiceWakeSupported {
|
if !voiceWakeSupported {
|
||||||
Label("Voice Wake requires macOS 26 or newer.", systemImage: "exclamationmark.triangle.fill")
|
Label("Voice Wake requires macOS 26 or newer.", systemImage: "exclamationmark.triangle.fill")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.yellow)
|
.foregroundStyle(.yellow)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.background(Color.secondary.opacity(0.15))
|
.background(Color.secondary.opacity(0.15))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
}
|
|
||||||
|
|
||||||
self.localePicker
|
|
||||||
self.micPicker
|
|
||||||
self.levelMeter
|
|
||||||
|
|
||||||
self.forwardSection
|
|
||||||
|
|
||||||
self.testCard
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Text("Trigger words")
|
|
||||||
.font(.callout.weight(.semibold))
|
|
||||||
Spacer()
|
|
||||||
Button {
|
|
||||||
self.addWord()
|
|
||||||
} label: {
|
|
||||||
Label("Add word", systemImage: "plus")
|
|
||||||
}
|
|
||||||
.disabled(self.state.swabbleTriggerWords
|
|
||||||
.contains(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
|
|
||||||
|
|
||||||
Button("Reset defaults") { self.state.swabbleTriggerWords = defaultVoiceWakeTriggers }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Table(self.indexedWords) {
|
self.localePicker
|
||||||
TableColumn("Word") { row in
|
self.micPicker
|
||||||
TextField("Wake word", text: self.binding(for: row.id))
|
self.levelMeter
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
}
|
self.forwardSection
|
||||||
TableColumn("") { row in
|
|
||||||
|
self.testCard
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Trigger words")
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
self.removeWord(at: row.id)
|
self.addWord()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "trash")
|
Label("Add word", systemImage: "plus")
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderless)
|
.disabled(self.state.swabbleTriggerWords
|
||||||
.help("Remove trigger word")
|
.contains(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
|
||||||
|
|
||||||
|
Button("Reset defaults") { self.state.swabbleTriggerWords = defaultVoiceWakeTriggers }
|
||||||
}
|
}
|
||||||
.width(36)
|
|
||||||
|
Table(self.indexedWords) {
|
||||||
|
TableColumn("Word") { row in
|
||||||
|
TextField("Wake word", text: self.binding(for: row.id))
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
}
|
||||||
|
TableColumn("") { row in
|
||||||
|
Button {
|
||||||
|
self.removeWord(at: row.id)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
.help("Remove trigger word")
|
||||||
|
}
|
||||||
|
.width(36)
|
||||||
|
}
|
||||||
|
.frame(minHeight: 180)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.stroke(Color.secondary.opacity(0.25), lineWidth: 1))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Clawdis reacts when any trigger appears in a transcription. "
|
||||||
|
+ "Keep them short to avoid false positives.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
.frame(minHeight: 180)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.stroke(Color.secondary.opacity(0.25), lineWidth: 1))
|
|
||||||
|
|
||||||
Text(
|
Spacer(minLength: 8)
|
||||||
"Clawdis reacts when any trigger appears in a transcription. "
|
|
||||||
+ "Keep them short to avoid false positives.")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
Spacer()
|
.padding(.horizontal, 12)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.task { await self.loadMicsIfNeeded() }
|
.task { await self.loadMicsIfNeeded() }
|
||||||
.task { await self.loadLocalesIfNeeded() }
|
.task { await self.loadLocalesIfNeeded() }
|
||||||
.task { await self.restartMeter() }
|
.task { await self.restartMeter() }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue