diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 1731cadf2..fdc574009 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4215,8 +4215,9 @@ "l2SupportAlpha": "Alpha", "l2SupportBeta": "Beta", "l2SupportFull": "Full", - "voiceNotAvailable": "It looks like you don't have a voice installed for this language.", - "openVoiceSettings": "Click here to open voice settings", + "missingVoiceTitle": "Missing voice", + "voiceNotAvailable": "You don't have a voice installed for this language.", + "openVoiceSettings": "Open voice settings", "playAudio": "Play", "stop": "Stop", "grammarCopySCONJ": "Subordinating Conjunction", diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 5e82651ce..b091be925 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -56,7 +56,6 @@ class ChatEventList extends StatelessWidget { context, InstructionsEnum.clickMessage, msgEvents[0].eventId, - true, ); }); // Pangea# diff --git a/lib/pangea/choreographer/widgets/it_bar_buttons.dart b/lib/pangea/choreographer/widgets/it_bar_buttons.dart index 9fcaa927e..d6d7caa3c 100644 --- a/lib/pangea/choreographer/widgets/it_bar_buttons.dart +++ b/lib/pangea/choreographer/widgets/it_bar_buttons.dart @@ -41,7 +41,6 @@ class ITBotButton extends StatelessWidget { context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, - true, ); return IconButton( @@ -51,7 +50,7 @@ class ITBotButton extends StatelessWidget { context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, - false, + showToggle: false, ), ); } diff --git a/lib/pangea/enum/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart index a42a01643..64bad0fb3 100644 --- a/lib/pangea/enum/instructions_enum.dart +++ b/lib/pangea/enum/instructions_enum.dart @@ -15,6 +15,7 @@ enum InstructionsEnum { l1Translation, translationChoices, clickAgainToDeselect, + missingVoice, } extension InstructionsEnumExtension on InstructionsEnum { @@ -28,6 +29,8 @@ extension InstructionsEnumExtension on InstructionsEnum { return l10n.blurMeansTranslateTitle; case InstructionsEnum.tooltipInstructions: return l10n.tooltipInstructionsTitle; + case InstructionsEnum.missingVoice: + return l10n.missingVoiceTitle; case InstructionsEnum.clickAgainToDeselect: case InstructionsEnum.speechToText: case InstructionsEnum.l1Translation: @@ -64,6 +67,8 @@ extension InstructionsEnumExtension on InstructionsEnum { return PlatformInfos.isMobile ? l10n.tooltipInstructionsMobileBody : l10n.tooltipInstructionsBrowserBody; + case InstructionsEnum.missingVoice: + return l10n.voiceNotAvailable; } } @@ -87,6 +92,8 @@ extension InstructionsEnumExtension on InstructionsEnum { return instructionSettings.showedTranslationChoicesTooltip; case InstructionsEnum.clickAgainToDeselect: return instructionSettings.showedClickAgainToDeselect; + case InstructionsEnum.missingVoice: + return instructionSettings.showedMissingVoice; } } } diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index 1fdebef3a..ba55330f3 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -185,6 +185,7 @@ class UserInstructions { bool showedClickMessage; bool showedBlurMeansTranslate; bool showedTooltipInstructions; + bool showedMissingVoice; bool showedSpeechToTextTooltip; bool showedL1TranslationTooltip; @@ -200,6 +201,7 @@ class UserInstructions { this.showedL1TranslationTooltip = false, this.showedTranslationChoicesTooltip = false, this.showedClickAgainToDeselect = false, + this.showedMissingVoice = false, }); factory UserInstructions.fromJson(Map json) => @@ -219,6 +221,8 @@ class UserInstructions { json[InstructionsEnum.speechToText.toString()] ?? false, showedClickAgainToDeselect: json[InstructionsEnum.clickAgainToDeselect.toString()] ?? false, + showedMissingVoice: + json[InstructionsEnum.missingVoice.toString()] ?? false, ); Map toJson() { @@ -236,6 +240,7 @@ class UserInstructions { data[InstructionsEnum.speechToText.toString()] = showedSpeechToTextTooltip; data[InstructionsEnum.clickAgainToDeselect.toString()] = showedClickAgainToDeselect; + data[InstructionsEnum.missingVoice.toString()] = showedMissingVoice; return data; } diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 681c0de08..a4e8ef151 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -56,6 +56,9 @@ class InstructionsController { case InstructionsEnum.clickAgainToDeselect: profile.instructionSettings.showedClickAgainToDeselect = value; break; + case InstructionsEnum.missingVoice: + profile.instructionSettings.showedMissingVoice = value; + break; } return profile; }); @@ -66,9 +69,10 @@ class InstructionsController { Future showInstructionsPopup( BuildContext context, InstructionsEnum key, - String transformTargetKey, [ + String transformTargetKey, { bool showToggle = true, - ]) async { + Widget? customContent, + }) async { final bool userLangsSet = await _pangeaController.userController.areUserLanguagesSet; if (!userLangsSet) { @@ -115,6 +119,7 @@ class InstructionsController { style: botStyle, ), ), + if (customContent != null) customContent, if (showToggle) InstructionsToggle(instructionsKey: key), ], ), diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index 302dcdef9..cc41605c9 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -71,7 +71,11 @@ class MessageAudioCardState extends State { final PangeaTokenText selection = widget.selection!; final tokenText = selection.content; - await widget.tts.speak(tokenText); + await widget.tts.tryToSpeak( + tokenText, + context, + widget.messageEvent.eventId, + ); } void setSectionStartAndEnd(int? start, int? end) => mounted @@ -196,19 +200,13 @@ class MessageAudioCardState extends State { child: _isLoading ? const ToolbarContentLoadingIndicator() : audioFile != null - ? Column( - children: [ - AudioPlayerWidget( - null, - matrixFile: audioFile, - sectionStartMS: sectionStartMS, - sectionEndMS: sectionEndMS, - color: - Theme.of(context).colorScheme.onPrimaryContainer, - setIsPlayingAudio: widget.setIsPlayingAudio, - ), - widget.tts.missingVoiceButton, - ], + ? AudioPlayerWidget( + null, + matrixFile: audioFile, + sectionStartMS: sectionStartMS, + sectionEndMS: sectionEndMS, + color: Theme.of(context).colorScheme.onPrimaryContainer, + setIsPlayingAudio: widget.setIsPlayingAudio, ) : const CardErrorWidget( error: "Null audio file in message_audio_card", diff --git a/lib/pangea/widgets/chat/missing_voice_button.dart b/lib/pangea/widgets/chat/missing_voice_button.dart index 5ea13164d..67c94494b 100644 --- a/lib/pangea/widgets/chat/missing_voice_button.dart +++ b/lib/pangea/widgets/chat/missing_voice_button.dart @@ -2,20 +2,17 @@ import 'dart:io'; import 'package:android_intent_plus/android_intent.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; class MissingVoiceButton extends StatelessWidget { - final String targetLangCode; - - const MissingVoiceButton({ - required this.targetLangCode, - super.key, - }); + const MissingVoiceButton({super.key}); Future launchTTSSettings(BuildContext context) async { - if (Platform.isAndroid) { + if (!kIsWeb && Platform.isAndroid) { const intent = AndroidIntent( action: 'com.android.settings.TTS_SETTINGS', package: 'com.talktolearn.chat', @@ -30,36 +27,18 @@ class MissingVoiceButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(maxWidth: AppConfig.toolbarMinWidth), - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.1), - borderRadius: const BorderRadius.all( - Radius.circular(AppConfig.borderRadius), + return TextButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + AppConfig.primaryColor.withOpacity(0.1), ), ), - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.only(top: 8), - child: SizedBox( - width: AppConfig.toolbarMinWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - L10n.of(context)!.voiceNotAvailable, - textAlign: TextAlign.center, - ), - TextButton( - onPressed: () => launchTTSSettings(context), - style: const ButtonStyle( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text(L10n.of(context)!.openVoiceSettings), - ), - ], - ), + onPressed: () async { + MatrixState.pAnyState.closeOverlay(); + await launchTTSSettings(context); + }, + child: Center( + child: Text(L10n.of(context)!.openVoiceSettings), ), ); } diff --git a/lib/pangea/widgets/chat/tts_controller.dart b/lib/pangea/widgets/chat/tts_controller.dart index baffb7b9b..4c178d440 100644 --- a/lib/pangea/widgets/chat/tts_controller.dart +++ b/lib/pangea/widgets/chat/tts_controller.dart @@ -1,8 +1,8 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/missing_voice_button.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -85,6 +85,37 @@ class TtsController { } } + Future showMissingVoicePopup( + BuildContext context, + String eventID, + ) async { + await MatrixState.pangeaController.instructions.showInstructionsPopup( + context, + InstructionsEnum.missingVoice, + eventID, + showToggle: false, + customContent: const Padding( + padding: EdgeInsets.only(top: 12), + child: MissingVoiceButton(), + ), + ); + return; + } + + /// A safer version of speak, that handles the case of + /// the language not being supported by the TTS engine + Future tryToSpeak( + String text, + BuildContext context, + String eventID, + ) async { + if (isLanguageFullySupported) { + await speak(text); + } else { + await showMissingVoicePopup(context, eventID); + } + } + Future speak(String text) async { try { stop(); @@ -112,11 +143,4 @@ class TtsController { bool get isLanguageFullySupported => availableLangCodes.contains(targetLanguage); - - Widget get missingVoiceButton => targetLanguage != null && - (kIsWeb || isLanguageFullySupported || !PlatformInfos.isAndroid) - ? const SizedBox.shrink() - : MissingVoiceButton( - targetLangCode: targetLanguage!, - ); } diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index dda975c33..a477efa9b 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -18,12 +18,14 @@ class MultipleChoiceActivity extends StatefulWidget { final PracticeActivityCardState practiceCardController; final PracticeActivityModel currentActivity; final TtsController tts; + final String eventID; const MultipleChoiceActivity({ super.key, required this.practiceCardController, required this.currentActivity, required this.tts, + required this.eventID, }); @override @@ -117,6 +119,7 @@ class MultipleChoiceActivityState extends State { WordAudioButton( text: practiceActivity.content.answer, ttsController: widget.tts, + eventID: widget.eventID, ), ChoicesArray( isLoading: false, diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 570cb0576..1e97f2fe2 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -302,6 +302,7 @@ class PracticeActivityCardState extends State { practiceCardController: this, currentActivity: currentActivity!, tts: widget.tts, + eventID: widget.pangeaMessageEvent.eventId, ); case ActivityTypeEnum.wordFocusListening: // return WordFocusListeningActivity( @@ -310,6 +311,7 @@ class PracticeActivityCardState extends State { practiceCardController: this, currentActivity: currentActivity!, tts: widget.tts, + eventID: widget.pangeaMessageEvent.eventId, ); // default: // ErrorHandler.logError( diff --git a/lib/pangea/widgets/practice_activity/word_audio_button.dart b/lib/pangea/widgets/practice_activity/word_audio_button.dart index 591228e18..03147ef0d 100644 --- a/lib/pangea/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/widgets/practice_activity/word_audio_button.dart @@ -5,11 +5,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; class WordAudioButton extends StatefulWidget { final String text; final TtsController ttsController; + final String eventID; const WordAudioButton({ super.key, required this.text, required this.ttsController, + required this.eventID, }); @override @@ -22,41 +24,40 @@ class WordAudioButtonState extends State { @override Widget build(BuildContext context) { debugPrint('build WordAudioButton'); - return Column( - children: [ - IconButton( - icon: const Icon(Icons.play_arrow_outlined), - isSelected: _isPlaying, - selectedIcon: const Icon(Icons.pause_outlined), - color: _isPlaying ? Colors.white : null, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - _isPlaying - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primaryContainer, - ), - ), - tooltip: - _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, - onPressed: () async { - if (_isPlaying) { - await widget.ttsController.tts.stop(); - if (mounted) { - setState(() => _isPlaying = false); - } - } else { - if (mounted) { - setState(() => _isPlaying = true); - } - await widget.ttsController.speak(widget.text); - if (mounted) { - setState(() => _isPlaying = false); - } - } - }, // Disable button if language isn't supported + return IconButton( + icon: const Icon(Icons.play_arrow_outlined), + isSelected: _isPlaying, + selectedIcon: const Icon(Icons.pause_outlined), + color: _isPlaying ? Colors.white : null, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + _isPlaying + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primaryContainer, ), - widget.ttsController.missingVoiceButton, - ], + ), + tooltip: + _isPlaying ? L10n.of(context)!.stop : L10n.of(context)!.playAudio, + onPressed: () async { + if (_isPlaying) { + await widget.ttsController.tts.stop(); + if (mounted) { + setState(() => _isPlaying = false); + } + } else { + if (mounted) { + setState(() => _isPlaying = true); + } + await widget.ttsController.tryToSpeak( + widget.text, + context, + widget.eventID, + ); + if (mounted) { + setState(() => _isPlaying = false); + } + } + }, // Disable button if language isn't supported ); } }