feat(profile): Add Android assistant trigger customization option
This commit is contained in:
@@ -29,6 +29,7 @@ final class PreferenceKeys {
|
||||
static const String ttsServerVoiceId = 'tts_server_voice_id';
|
||||
static const String ttsServerVoiceName = 'tts_server_voice_name';
|
||||
static const String voiceSilenceDuration = 'voice_silence_duration';
|
||||
static const String androidAssistantTrigger = 'android_assistant_trigger';
|
||||
}
|
||||
|
||||
final class LegacyPreferenceKeys {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer' as developer;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../persistence/hive_bootstrap.dart';
|
||||
import '../persistence/hive_boxes.dart';
|
||||
@@ -17,6 +18,22 @@ enum SttPreference { deviceOnly, serverOnly }
|
||||
/// TTS engine selection
|
||||
enum TtsEngine { device, server }
|
||||
|
||||
/// Action to take when the Android digital assistant is triggered.
|
||||
enum AndroidAssistantTrigger { overlay, newChat, voiceCall }
|
||||
|
||||
extension AndroidAssistantTriggerStorage on AndroidAssistantTrigger {
|
||||
String get storageValue {
|
||||
switch (this) {
|
||||
case AndroidAssistantTrigger.overlay:
|
||||
return 'overlay';
|
||||
case AndroidAssistantTrigger.newChat:
|
||||
return 'new_chat';
|
||||
case AndroidAssistantTrigger.voiceCall:
|
||||
return 'voice_call';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service for managing app-wide settings including accessibility preferences
|
||||
class SettingsService {
|
||||
static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
|
||||
@@ -41,6 +58,8 @@ class SettingsService {
|
||||
// Voice silence duration for auto-stop (milliseconds)
|
||||
static const String _voiceSilenceDurationKey =
|
||||
PreferenceKeys.voiceSilenceDuration;
|
||||
static const String _androidAssistantTriggerKey =
|
||||
PreferenceKeys.androidAssistantTrigger;
|
||||
static Box<dynamic> _preferencesBox() =>
|
||||
Hive.box<dynamic>(HiveBoxNames.preferences);
|
||||
|
||||
@@ -153,6 +172,8 @@ class SettingsService {
|
||||
PreferenceKeys.ttsEngine: settings.ttsEngine.name,
|
||||
PreferenceKeys.voiceSttPreference: settings.sttPreference.name,
|
||||
_voiceSilenceDurationKey: settings.voiceSilenceDuration,
|
||||
_androidAssistantTriggerKey:
|
||||
settings.androidAssistantTrigger.storageValue,
|
||||
};
|
||||
|
||||
await box.putAll(updates);
|
||||
@@ -191,6 +212,8 @@ class SettingsService {
|
||||
} else {
|
||||
await box.delete(PreferenceKeys.ttsServerVoiceName);
|
||||
}
|
||||
|
||||
await _writeAssistantTriggerToSharedPrefs(settings.androidAssistantTrigger);
|
||||
}
|
||||
|
||||
static TtsEngine _parseTtsEngine(String? raw) {
|
||||
@@ -219,6 +242,20 @@ class SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
static AndroidAssistantTrigger _parseAndroidAssistantTrigger(String? raw) {
|
||||
switch ((raw ?? '').toLowerCase()) {
|
||||
case 'new_chat':
|
||||
case 'newchat':
|
||||
return AndroidAssistantTrigger.newChat;
|
||||
case 'voice_call':
|
||||
case 'voicecall':
|
||||
return AndroidAssistantTrigger.voiceCall;
|
||||
case 'overlay':
|
||||
default:
|
||||
return AndroidAssistantTrigger.overlay;
|
||||
}
|
||||
}
|
||||
|
||||
// Voice input specific settings
|
||||
static Future<String?> getVoiceLocaleId() {
|
||||
final value = _preferencesBox().get(_voiceLocaleKey) as String?;
|
||||
@@ -309,6 +346,30 @@ class SettingsService {
|
||||
return _preferencesBox().put(_voiceSilenceDurationKey, sanitized);
|
||||
}
|
||||
|
||||
static Future<void> setAndroidAssistantTrigger(
|
||||
AndroidAssistantTrigger trigger,
|
||||
) async {
|
||||
await _preferencesBox().put(
|
||||
_androidAssistantTriggerKey,
|
||||
trigger.storageValue,
|
||||
);
|
||||
await _writeAssistantTriggerToSharedPrefs(trigger);
|
||||
}
|
||||
|
||||
static Future<void> _writeAssistantTriggerToSharedPrefs(
|
||||
AndroidAssistantTrigger trigger,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
PreferenceKeys.androidAssistantTrigger,
|
||||
trigger.storageValue,
|
||||
);
|
||||
} catch (_) {
|
||||
// SharedPreferences writes are best-effort for Android assistant access
|
||||
}
|
||||
}
|
||||
|
||||
/// Get effective animation duration considering all settings
|
||||
static Duration getEffectiveAnimationDuration(
|
||||
BuildContext context,
|
||||
@@ -372,6 +433,9 @@ class SettingsService {
|
||||
sttPreference: _parseSttPreference(
|
||||
box.get(PreferenceKeys.voiceSttPreference) as String?,
|
||||
),
|
||||
androidAssistantTrigger: _parseAndroidAssistantTrigger(
|
||||
box.get(_androidAssistantTriggerKey) as String?,
|
||||
),
|
||||
voiceSilenceDuration: (box.get(_voiceSilenceDurationKey) as int? ?? 2000)
|
||||
.clamp(300, 3000),
|
||||
);
|
||||
@@ -406,6 +470,7 @@ class AppSettings {
|
||||
final TtsEngine ttsEngine;
|
||||
final String? ttsServerVoiceId;
|
||||
final String? ttsServerVoiceName;
|
||||
final AndroidAssistantTrigger androidAssistantTrigger;
|
||||
final int voiceSilenceDuration;
|
||||
const AppSettings({
|
||||
this.reduceMotion = false,
|
||||
@@ -429,6 +494,7 @@ class AppSettings {
|
||||
this.ttsEngine = TtsEngine.device,
|
||||
this.ttsServerVoiceId,
|
||||
this.ttsServerVoiceName,
|
||||
this.androidAssistantTrigger = AndroidAssistantTrigger.overlay,
|
||||
this.voiceSilenceDuration = 2000,
|
||||
});
|
||||
|
||||
@@ -455,6 +521,7 @@ class AppSettings {
|
||||
Object? ttsServerVoiceId = const _DefaultValue(),
|
||||
Object? ttsServerVoiceName = const _DefaultValue(),
|
||||
int? voiceSilenceDuration,
|
||||
AndroidAssistantTrigger? androidAssistantTrigger,
|
||||
}) {
|
||||
return AppSettings(
|
||||
reduceMotion: reduceMotion ?? this.reduceMotion,
|
||||
@@ -486,6 +553,8 @@ class AppSettings {
|
||||
ttsServerVoiceName: ttsServerVoiceName is _DefaultValue
|
||||
? this.ttsServerVoiceName
|
||||
: ttsServerVoiceName as String?,
|
||||
androidAssistantTrigger:
|
||||
androidAssistantTrigger ?? this.androidAssistantTrigger,
|
||||
voiceSilenceDuration: voiceSilenceDuration ?? this.voiceSilenceDuration,
|
||||
);
|
||||
}
|
||||
@@ -513,6 +582,7 @@ class AppSettings {
|
||||
other.ttsEngine == ttsEngine &&
|
||||
other.ttsServerVoiceId == ttsServerVoiceId &&
|
||||
other.ttsServerVoiceName == ttsServerVoiceName &&
|
||||
other.androidAssistantTrigger == androidAssistantTrigger &&
|
||||
other.voiceSilenceDuration == voiceSilenceDuration &&
|
||||
_listEquals(other.quickPills, quickPills);
|
||||
// socketTransportMode intentionally not included in == to avoid frequent rebuilds
|
||||
@@ -541,6 +611,7 @@ class AppSettings {
|
||||
ttsEngine,
|
||||
ttsServerVoiceId,
|
||||
ttsServerVoiceName,
|
||||
androidAssistantTrigger,
|
||||
voiceSilenceDuration,
|
||||
Object.hashAllUnordered(quickPills),
|
||||
]);
|
||||
@@ -715,6 +786,16 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
await SettingsService.setVoiceSilenceDuration(milliseconds);
|
||||
}
|
||||
|
||||
Future<void> setAndroidAssistantTrigger(
|
||||
AndroidAssistantTrigger trigger,
|
||||
) async {
|
||||
if (state.androidAssistantTrigger == trigger) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(androidAssistantTrigger: trigger);
|
||||
await SettingsService.setAndroidAssistantTrigger(trigger);
|
||||
}
|
||||
|
||||
Future<void> resetToDefaults() async {
|
||||
const defaultSettings = AppSettings();
|
||||
await SettingsService.saveSettings(defaultSettings);
|
||||
|
||||
@@ -46,19 +46,27 @@ class AndroidAssistantHandler {
|
||||
await _processScreenshot(screenshotPath);
|
||||
} else if (call.method == 'startVoiceCall') {
|
||||
await _startVoiceCall();
|
||||
} else if (call.method == 'startNewChat') {
|
||||
await _startNewChat();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processScreenshot(String screenshotPath) async {
|
||||
try {
|
||||
DebugLogger.log('Processing screenshot: $screenshotPath', scope: 'assistant');
|
||||
DebugLogger.log(
|
||||
'Processing screenshot: $screenshotPath',
|
||||
scope: 'assistant',
|
||||
);
|
||||
|
||||
// Wait for app to be ready (authenticated and model available)
|
||||
final navState = _ref.read(authNavigationStateProvider);
|
||||
final model = _ref.read(selectedModelProvider);
|
||||
|
||||
if (navState != AuthNavigationState.authenticated || model == null) {
|
||||
DebugLogger.log('App not ready for screenshot processing', scope: 'assistant');
|
||||
DebugLogger.log(
|
||||
'App not ready for screenshot processing',
|
||||
scope: 'assistant',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +83,10 @@ class AndroidAssistantHandler {
|
||||
// Add screenshot as attachment
|
||||
final file = File(screenshotPath);
|
||||
if (!await file.exists()) {
|
||||
DebugLogger.log('Screenshot file not found: $screenshotPath', scope: 'assistant');
|
||||
DebugLogger.log(
|
||||
'Screenshot file not found: $screenshotPath',
|
||||
scope: 'assistant',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,15 +102,23 @@ class AndroidAssistantHandler {
|
||||
// Enqueue upload via task queue
|
||||
final activeConv = _ref.read(activeConversationProvider);
|
||||
try {
|
||||
await _ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
|
||||
await _ref
|
||||
.read(taskQueueProvider.notifier)
|
||||
.enqueueUploadMedia(
|
||||
conversationId: activeConv?.id,
|
||||
filePath: attachment.file.path,
|
||||
fileName: attachment.displayName,
|
||||
fileSize: await attachment.file.length(),
|
||||
);
|
||||
DebugLogger.log('Screenshot uploaded successfully', scope: 'assistant');
|
||||
DebugLogger.log(
|
||||
'Screenshot uploaded successfully',
|
||||
scope: 'assistant',
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLogger.log('Failed to upload screenshot: $e', scope: 'assistant');
|
||||
DebugLogger.log(
|
||||
'Failed to upload screenshot: $e',
|
||||
scope: 'assistant',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -130,7 +149,10 @@ class AndroidAssistantHandler {
|
||||
// Get the current BuildContext from the navigation service
|
||||
final context = NavigationService.navigatorKey.currentContext;
|
||||
if (context == null) {
|
||||
DebugLogger.log('No context available for voice call navigation', scope: 'assistant');
|
||||
DebugLogger.log(
|
||||
'No context available for voice call navigation',
|
||||
scope: 'assistant',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,4 +169,28 @@ class AndroidAssistantHandler {
|
||||
DebugLogger.log('Failed to start voice call: $e', scope: 'assistant');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startNewChat() async {
|
||||
try {
|
||||
DebugLogger.log('Starting new chat from assistant', scope: 'assistant');
|
||||
|
||||
final navState = _ref.read(authNavigationStateProvider);
|
||||
final model = _ref.read(selectedModelProvider);
|
||||
|
||||
if (navState != AuthNavigationState.authenticated || model == null) {
|
||||
DebugLogger.log('App not ready for new chat', scope: 'assistant');
|
||||
return;
|
||||
}
|
||||
|
||||
final isOnChatRoute = NavigationService.currentRoute == Routes.chat;
|
||||
if (!isOnChatRoute) {
|
||||
await NavigationService.navigateToChat();
|
||||
}
|
||||
|
||||
startNewChat(_ref);
|
||||
DebugLogger.log('New chat started from assistant', scope: 'assistant');
|
||||
} catch (e) {
|
||||
DebugLogger.log('Failed to start new chat: $e', scope: 'assistant');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +407,10 @@ class AppCustomizationPage extends ConsumerWidget {
|
||||
final transportLabel = activeTransportMode == 'polling'
|
||||
? l10n.transportModePolling
|
||||
: l10n.transportModeWs;
|
||||
final assistantTriggerLabel = _androidAssistantTriggerLabel(
|
||||
l10n,
|
||||
settings.androidAssistantTrigger,
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -467,10 +471,164 @@ class AppCustomizationPage extends ConsumerWidget {
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setSendOnEnter(!settings.sendOnEnter),
|
||||
),
|
||||
if (Platform.isAndroid) ...[
|
||||
const SizedBox(height: Spacing.sm),
|
||||
_CustomizationTile(
|
||||
leading: _buildIconBadge(
|
||||
context,
|
||||
Icons.assistant,
|
||||
color: theme.buttonPrimary,
|
||||
),
|
||||
title: l10n.androidAssistantTitle,
|
||||
subtitle: assistantTriggerLabel,
|
||||
onTap: () =>
|
||||
_showAndroidAssistantTriggerSheet(context, ref, settings),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _androidAssistantTriggerLabel(
|
||||
AppLocalizations l10n,
|
||||
AndroidAssistantTrigger trigger,
|
||||
) {
|
||||
switch (trigger) {
|
||||
case AndroidAssistantTrigger.overlay:
|
||||
return l10n.androidAssistantOverlayOption;
|
||||
case AndroidAssistantTrigger.newChat:
|
||||
return l10n.androidAssistantNewChatOption;
|
||||
case AndroidAssistantTrigger.voiceCall:
|
||||
return l10n.androidAssistantVoiceCallOption;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showAndroidAssistantTriggerSheet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppSettings settings,
|
||||
) async {
|
||||
final theme = context.conduitTheme;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final options = <({AndroidAssistantTrigger value, String label})>[
|
||||
(
|
||||
value: AndroidAssistantTrigger.overlay,
|
||||
label: l10n.androidAssistantOverlayOption,
|
||||
),
|
||||
(
|
||||
value: AndroidAssistantTrigger.newChat,
|
||||
label: l10n.androidAssistantNewChatOption,
|
||||
),
|
||||
(
|
||||
value: AndroidAssistantTrigger.voiceCall,
|
||||
label: l10n.androidAssistantVoiceCallOption,
|
||||
),
|
||||
];
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: theme.sidebarBackground,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(AppBorderRadius.modal),
|
||||
),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Spacing.lg,
|
||||
vertical: Spacing.md,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.androidAssistantTitle,
|
||||
style:
|
||||
theme.headingSmall?.copyWith(
|
||||
color: theme.sidebarForeground,
|
||||
) ??
|
||||
TextStyle(
|
||||
color: theme.sidebarForeground,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Spacing.xs),
|
||||
Text(
|
||||
l10n.androidAssistantDescription,
|
||||
style:
|
||||
theme.bodySmall?.copyWith(
|
||||
color: theme.sidebarForeground.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
) ??
|
||||
TextStyle(
|
||||
color: theme.sidebarForeground.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: theme.iconPrimary),
|
||||
onPressed: () => Navigator.of(sheetContext).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
for (var i = 0; i < options.length; i++) ...[
|
||||
() {
|
||||
final option = options[i];
|
||||
final selected =
|
||||
settings.androidAssistantTrigger == option.value;
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
selected ? Icons.check_circle : Icons.circle_outlined,
|
||||
color: selected
|
||||
? theme.buttonPrimary
|
||||
: theme.iconSecondary,
|
||||
),
|
||||
title: Text(
|
||||
option.label,
|
||||
style: theme.bodyMedium?.copyWith(
|
||||
color: theme.sidebarForeground,
|
||||
fontWeight: selected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (!selected) {
|
||||
ref
|
||||
.read(appSettingsProvider.notifier)
|
||||
.setAndroidAssistantTrigger(option.value);
|
||||
}
|
||||
Navigator.of(sheetContext).pop();
|
||||
},
|
||||
);
|
||||
}(),
|
||||
if (i != options.length - 1) const Divider(height: 1),
|
||||
],
|
||||
const SizedBox(height: Spacing.lg),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSttSection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "Chat",
|
||||
"sendOnEnter": "Mit Enter senden",
|
||||
"sendOnEnterDescription": "Enter sendet (Soft-Tastatur). Cmd/Ctrl+Enter ebenfalls verfügbar",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "Sprache zu Text",
|
||||
"sttEngineLabel": "Erkennungs-Engine",
|
||||
"sttEngineDevice": "Auf dem Gerät",
|
||||
|
||||
@@ -1223,6 +1223,26 @@
|
||||
"@sendOnEnterDescription": {
|
||||
"description": "Explanation of how the Send on Enter toggle behaves."
|
||||
},
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"@androidAssistantTitle": {
|
||||
"description": "Tile title for configuring the Android digital assistant trigger."
|
||||
},
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"@androidAssistantDescription": {
|
||||
"description": "Helper text describing the Android digital assistant trigger setting."
|
||||
},
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"@androidAssistantOverlayOption": {
|
||||
"description": "Option label for keeping the current assistant overlay."
|
||||
},
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"@androidAssistantNewChatOption": {
|
||||
"description": "Option label for opening the app to a fresh chat from the assistant trigger."
|
||||
},
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"@androidAssistantVoiceCallOption": {
|
||||
"description": "Option label for jumping straight into voice call from assistant trigger."
|
||||
},
|
||||
"sttSettings": "Speech to Text",
|
||||
"@sttSettings": {
|
||||
"description": "Section header for speech-to-text settings."
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "Conversación",
|
||||
"sendOnEnter": "Enviar con Enter",
|
||||
"sendOnEnterDescription": "Enter envía (teclado virtual). Cmd/Ctrl+Enter también disponible",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "Voz a texto",
|
||||
"sttEngineLabel": "Motor de reconocimiento",
|
||||
"sttEngineDevice": "En el dispositivo",
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "Discussion",
|
||||
"sendOnEnter": "Envoyer avec Entrée",
|
||||
"sendOnEnterDescription": "Entrée envoie (clavier logiciel). Cmd/Ctrl+Entrée aussi disponible",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "Voix vers texte",
|
||||
"sttEngineLabel": "Moteur de reconnaissance",
|
||||
"sttEngineDevice": "Sur l’appareil",
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "Chat",
|
||||
"sendOnEnter": "Invia con Invio",
|
||||
"sendOnEnterDescription": "Invio invia (tastiera software). Cmd/Ctrl+Invio disponibile",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "Voce in testo",
|
||||
"sttEngineLabel": "Motore di riconoscimento",
|
||||
"sttEngineDevice": "Sul dispositivo",
|
||||
|
||||
@@ -434,6 +434,11 @@
|
||||
"chatSettings": "채팅",
|
||||
"sendOnEnter": "Enter로 전송",
|
||||
"sendOnEnterDescription": "Enter로 전송 (소프트 키보드). Cmd/Ctrl+Enter도 사용 가능",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "음성 텍스트 변환",
|
||||
"sttEngineLabel": "인식 엔진",
|
||||
"sttEngineDevice": "기기에서",
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "Chat",
|
||||
"sendOnEnter": "Verzenden met Enter",
|
||||
"sendOnEnterDescription": "Enter verzendt (softtoetsenbord). Cmd/Ctrl+Enter ook beschikbaar",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "Spraak naar tekst",
|
||||
"sttEngineLabel": "Herkenningsengine",
|
||||
"sttEngineDevice": "Op het apparaat",
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "Чат",
|
||||
"sendOnEnter": "Отправка по Enter",
|
||||
"sendOnEnterDescription": "Enter отправляет (программная клавиатура). Также доступно Cmd/Ctrl+Enter",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "Речь в текст",
|
||||
"sttEngineLabel": "Движок распознавания",
|
||||
"sttEngineDevice": "На устройстве",
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "对话",
|
||||
"sendOnEnter": "回车发送",
|
||||
"sendOnEnterDescription": "回车发送(软键盘)。Cmd/Ctrl+Enter 也可用",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "语音转文字",
|
||||
"sttEngineLabel": "识别引擎",
|
||||
"sttEngineDevice": "本机",
|
||||
|
||||
@@ -308,6 +308,11 @@
|
||||
"chatSettings": "對話",
|
||||
"sendOnEnter": "回車發送",
|
||||
"sendOnEnterDescription": "回車發送(軟鍵盤)。Cmd/Ctrl+Enter 也可用",
|
||||
"androidAssistantTitle": "Android digital assistant",
|
||||
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||
"sttSettings": "語音轉文字",
|
||||
"sttEngineLabel": "識別引擎",
|
||||
"sttEngineDevice": "本機",
|
||||
|
||||
Reference in New Issue
Block a user