From d768294458670e109637c608118106c64be6f6b4 Mon Sep 17 00:00:00 2001 From: JHubi1 Date: Tue, 25 Jun 2024 21:08:59 +0200 Subject: [PATCH] Voice Mode improvements --- lib/l10n/app_en.arb | 11 +++ lib/screen_voice.dart | 166 ++++++++++++++++++++-------------------- lib/settings/voice.dart | 131 +++++++++++++++++++++---------- 3 files changed, 185 insertions(+), 123 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6050513..2112a67 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -433,11 +433,22 @@ "description": "Text displayed while loading voice permissions", "context": "Visible in the settings view" }, + "settingsVoiceTtsNotSupported": "Text-to-speech not supported", + "@settingsVoiceTtsNotSupported": { + "description": "Text displayed when text-to-speech is not supported", + "context": "Visible in the settings view" + }, + "settingsVoiceTtsNotSupportedDescription": "Text-to-speech services are not supported for the selected language. Select a different language in the language drawer to reenable them.\nOther services like voice recognition and AI thinking will still work as usual, but interaction might not be as fluent.", "settingsVoicePermissionNot": "Permissions not granted", "@settingsVoicePermissionNot": { "description": "Text displayed when voice permissions are not granted", "context": "Visible in the settings view" }, + "settingsVoiceNotEnabled": "Voice mode not enabled", + "@settingsVoiceNotEnabled": { + "description": "Text displayed when voice mode is not enabled", + "context": "Visible in the settings view" + }, "settingsVoiceNotSupported": "Voice mode not supported", "@settingsVoiceNotSupported": { "description": "Text displayed when voice mode is not supported", diff --git a/lib/screen_voice.dart b/lib/screen_voice.dart index 5e4b6cf..850dab2 100644 --- a/lib/screen_voice.dart +++ b/lib/screen_voice.dart @@ -85,7 +85,7 @@ class _ScreenVoiceState extends State { text = ""; speech.listen( - localeId: (prefs!.getString("voiceLanguage") ?? ""), + localeId: (prefs!.getString("voiceLanguage") ?? "en-US"), listenOptions: stt.SpeechListenOptions(listenMode: stt.ListenMode.dictation), onResult: (result) { @@ -172,91 +172,93 @@ class _ScreenVoiceState extends State { lightHaptic(); }); - if (done && - (await voice.getLanguages as List).contains( - (prefs!.getString("voiceLanguage") ?? "en_US") - .replaceAll("_", "-"))) { + // var volume = await VolumeController().getVolume(); + // var voicesTmp1 = await voice.getLanguages; + // var voices = jsonEncode(voicesTmp1); + // var isVoiceAvailable = (await voice.isLanguageAvailable( + // (prefs!.getString("voiceLanguage") ?? "en_US") + // .replaceAll("_", "-"))) + // .toString(); + // var voices2Tmp1 = await speech.locales(); + // var voices2Tmp2 = []; + // for (var voice in voices2Tmp1) { + // voices2Tmp2.add(voice.localeId.replaceAll("_", "-")); + // } + // var voices2 = jsonEncode(voices2Tmp2); + // await showDialog( + // // ignore: use_build_context_synchronously + // context: context, + // builder: (context) { + // return Dialog.fullscreen( + // child: ListView(children: [ + // const Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisSize: MainAxisSize.max, + // children: [ + // Expanded(child: Divider(color: Colors.red)), + // SizedBox(width: 8), + // Text("START", style: TextStyle(color: Colors.red)), + // SizedBox(width: 8), + // Expanded(child: Divider(color: Colors.red)) + // ]), + // Text((prefs!.getString("voiceLanguage") ?? "en_US") + // .replaceAll("_", "-")), + // const Divider(), + // Text(volume.toString()), + // const Divider(), + // Text(voices), + // const Divider(), + // Text(voicesTmp1 + // .contains((prefs!.getString("voiceLanguage") ?? "en_US") + // .replaceAll("_", "-")) + // .toString()), + // const Divider(), + // Text(isVoiceAvailable), + // const Divider(), + // Text(voices2), + // const Divider(), + // Text(voices2Tmp2 + // .contains((prefs!.getString("voiceLanguage") ?? "en_US") + // .replaceAll("_", "-")) + // .toString()), + // const Divider(), + // Text(speech.isAvailable.toString()), + // const Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisSize: MainAxisSize.max, + // children: [ + // Expanded(child: Divider(color: Colors.red)), + // SizedBox(width: 8), + // Text("END", style: TextStyle(color: Colors.red)), + // SizedBox(width: 8), + // Expanded(child: Divider(color: Colors.red)) + // ]) + // ])); + // }); + + if (done) { aiThinking = false; heavyHaptic(); - voice.setLanguage((prefs!.getString("voiceLanguage") ?? "en_US") - .replaceAll("_", "-")); - voice.setSpeechRate(0.6); - voice.setCompletionHandler(() async { - speaking = false; - try { - setState(() {}); - } catch (_) {} - process(); - }); - var tmp = aiText; - tmp.replaceAll("-", "."); - tmp.replaceAll("*", "."); - // var volume = await VolumeController().getVolume(); - // var voicesTmp1 = await voice.getLanguages; - // var voices = jsonEncode(voicesTmp1); - // var isVoiceAvailable = (await voice.isLanguageAvailable( - // (prefs!.getString("voiceLanguage") ?? "en_US") - // .replaceAll("_", "-"))) - // .toString(); - // var voices2Tmp1 = await speech.locales(); - // var voices2Tmp2 = []; - // for (var voice in voices2Tmp1) { - // voices2Tmp2.add(voice.localeId.replaceAll("_", "-")); - // } - // var voices2 = jsonEncode(voices2Tmp2); - // await showDialog( - // // ignore: use_build_context_synchronously - // context: context, - // builder: (context) { - // return Dialog.fullscreen( - // child: ListView(children: [ - // const Row( - // crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisSize: MainAxisSize.max, - // children: [ - // Expanded(child: Divider(color: Colors.red)), - // SizedBox(width: 8), - // Text("START", style: TextStyle(color: Colors.red)), - // SizedBox(width: 8), - // Expanded(child: Divider(color: Colors.red)) - // ]), - // Text((prefs!.getString("voiceLanguage") ?? "en_US") - // .replaceAll("_", "-")), - // const Divider(), - // Text(volume.toString()), - // const Divider(), - // Text(voices), - // const Divider(), - // Text(voicesTmp1 - // .contains((prefs!.getString("voiceLanguage") ?? "en_US") - // .replaceAll("_", "-")) - // .toString()), - // const Divider(), - // Text(isVoiceAvailable), - // const Divider(), - // Text(voices2), - // const Divider(), - // Text(voices2Tmp2 - // .contains((prefs!.getString("voiceLanguage") ?? "en_US") - // .replaceAll("_", "-")) - // .toString()), - // const Divider(), - // Text(speech.isAvailable.toString()), - // const Row( - // crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisSize: MainAxisSize.max, - // children: [ - // Expanded(child: Divider(color: Colors.red)), - // SizedBox(width: 8), - // Text("END", style: TextStyle(color: Colors.red)), - // SizedBox(width: 8), - // Expanded(child: Divider(color: Colors.red)) - // ]) - // ])); - // }); + if ((await voice.getLanguages as List).contains( + (prefs!.getString("voiceLanguage") ?? "en_US") + .replaceAll("_", "-"))) { + voice.setLanguage((prefs!.getString("voiceLanguage") ?? "en_US") + .replaceAll("_", "-")); + voice.setSpeechRate(0.6); + voice.setCompletionHandler(() async { + speaking = false; + try { + setState(() {}); + } catch (_) {} + process(); + }); + var tmp = aiText; + tmp.replaceAll("-", "."); + tmp.replaceAll("*", "."); - voice.speak(tmp); + voice.speak(tmp); + } } }, addToSystem: (prefs!.getBool("voiceLimitLanguage") ?? true) diff --git a/lib/settings/voice.dart b/lib/settings/voice.dart index ddb6cf7..9e870a4 100644 --- a/lib/settings/voice.dart +++ b/lib/settings/voice.dart @@ -27,20 +27,35 @@ class _ScreenSettingsVoiceState extends State { Iterable languageOptionIds = []; Iterable languageOptions = []; + List voiceLanguageOptionsAvailable = []; + List voiceLanguageOptions = []; + + bool dialogMustLoad = true; + + void load() async { + var tmp = await speech.locales(); + languageOptionIds = tmp.map((e) => e.localeId); + languageOptions = tmp.map((e) => e.name); + + permissionRecord = await Permission.microphone.isGranted; + permissionBluetooth = await Permission.bluetoothConnect.isGranted; + permissionLoading = false; + + voiceLanguageOptions = await voice.getLanguages as List; + + for (int i = 0; i < languageOptionIds.length; i++) { + if (voiceLanguageOptions + .contains(languageOptionIds.elementAt(i).replaceAll("_", "-"))) { + voiceLanguageOptionsAvailable.add(languageOptionIds.elementAt(i)); + } + } + + setState(() {}); + } + @override void initState() { super.initState(); - void load() async { - var tmp = await speech.locales(); - languageOptionIds = tmp.map((e) => e.localeId); - languageOptions = tmp.map((e) => e.name); - - permissionRecord = await Permission.microphone.isGranted; - permissionBluetooth = await Permission.bluetoothConnect.isGranted; - permissionLoading = false; - setState(() {}); - } - load(); } @@ -56,25 +71,48 @@ class _ScreenSettingsVoiceState extends State { child: Column(children: [ Expanded( child: ListView(children: [ - // const SizedBox(height: 8), - ((prefs!.getBool("voiceModeEnabled") ?? false) || - permissionLoading || + (permissionLoading || (permissionBluetooth && permissionRecord && - voiceSupported)) + voiceSupported && + voiceLanguageOptionsAvailable.contains( + (prefs!.getString("voiceLanguage") ?? + "en_US")))) ? const SizedBox.shrink() : button( permissionLoading ? AppLocalizations.of(context)! .settingsVoicePermissionLoading - : (permissionBluetooth && permissionRecord) + : (!voiceLanguageOptionsAvailable.contains( + (prefs!.getString( + "voiceLanguage") ?? + "en_US")) && + (prefs!.getBool("voiceModeEnabled") ?? + false)) ? AppLocalizations.of(context)! - .settingsVoiceNotSupported - : AppLocalizations.of(context)! - .settingsVoicePermissionNot, + .settingsVoiceTtsNotSupported + : !(permissionBluetooth && permissionRecord) + ? AppLocalizations.of(context)! + .settingsVoicePermissionNot + : !(prefs!.getBool( + "voiceModeEnabled") ?? + false) + ? AppLocalizations.of(context)! + .settingsVoiceNotEnabled + : AppLocalizations.of(context)! + .settingsVoiceNotSupported, Icons.info_rounded, () { if (permissionLoading) return; - if (!(permissionBluetooth && permissionRecord)) { + if (!voiceLanguageOptions.contains( + (prefs!.getString("voiceLanguage") ?? + "en_US"))) { + selectionHaptic(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(AppLocalizations.of(context)! + .settingsVoiceTtsNotSupportedDescription), + showCloseIcon: true)); + } else if (!(permissionBluetooth && + permissionRecord)) { void load() async { try { if (await Permission @@ -135,33 +173,42 @@ class _ScreenSettingsVoiceState extends State { Icons.language_rounded, () { int usedIndex = -1; Function? setModalState; - void load() async { - var tmp = await speech.locales(); - languageOptionIds = tmp.map((e) => e.localeId); - languageOptions = tmp.map((e) => e.name); - - if ((prefs!.getString("voiceLanguage") ?? "") != "") { - for (int i = 0; i < languageOptionIds.length; i++) { - if (languageOptionIds.elementAt(i) == - (prefs!.getString("voiceLanguage") ?? "")) { - usedIndex = i; - setModalState!(() {}); - break; - } - } - } - } selectionHaptic(); - load(); - showModalBottomSheet( context: context, builder: (context) => StatefulBuilder( builder: (context, setLocalState) { setModalState = setLocalState; + + void loadSelected() async { + if ((prefs!.getString("voiceLanguage") ?? + "") != + "") { + for (int i = 0; + i < languageOptionIds.length; + i++) { + if (languageOptionIds.elementAt(i) == + (prefs!.getString( + "voiceLanguage") ?? + "")) { + setModalState!(() { + usedIndex = i; + }); + break; + } + } + } + } + + if (dialogMustLoad) { + load(); + loadSelected(); + dialogMustLoad = false; + } + return PopScope( onPopInvoked: (didPop) { if (usedIndex == -1) return; @@ -169,7 +216,9 @@ class _ScreenSettingsVoiceState extends State { "voiceLanguage", languageOptionIds .elementAt(usedIndex)); - setState(() {}); + setState(() { + dialogMustLoad = true; + }); }, child: Container( width: double.infinity, @@ -226,8 +275,8 @@ class _ScreenSettingsVoiceState extends State { avatar: (usedIndex == index) ? null - : (languageOptionIds.elementAt(index).startsWith(AppLocalizations.of(context)!.localeName)) - ? const Icon(Icons.star_rounded) + : (voiceLanguageOptionsAvailable.contains(languageOptionIds.elementAt(index))) + ? const Icon(Icons.spatial_tracking_rounded) : null, checkmarkColor: (usedIndex == index)