From 3359cfe25d4661e853bb9f067b5a4670e3c9c152 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Mon, 19 May 2025 12:19:17 -0400 Subject: [PATCH] fix: make TTS button pause when it's stopped by the other TTS button playing (#2831) --- lib/pages/chat/events/message_content.dart | 3 +- .../vocab_analytics_details_view.dart | 7 +- .../controllers/choreographer.dart | 4 +- .../choreographer/widgets/choice_array.dart | 11 +- .../choreographer/widgets/igc/span_card.dart | 5 +- lib/pangea/choreographer/widgets/it_bar.dart | 1 - .../pages/settings_learning.dart | 5 +- .../toolbar/controllers/tts_controller.dart | 141 +++++++++++------- .../practice_match_item.dart | 7 +- .../widgets/message_selection_overlay.dart | 3 +- .../multiple_choice_activity.dart | 10 +- .../practice_activity_card.dart | 3 +- .../practice_activity/word_audio_button.dart | 80 +++++----- .../word_text_with_audio_button.dart | 139 +++-------------- .../widgets/reading_assistance_content.dart | 5 - .../widgets/word_zoom/lemma_widget.dart | 3 - .../widgets/word_zoom/word_zoom_widget.dart | 6 +- lib/widgets/matrix.dart | 2 + 18 files changed, 174 insertions(+), 261 deletions(-) diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index 789255010..1d2bb18dc 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; @@ -142,7 +143,7 @@ class MessageContent extends StatelessWidget { const Duration( milliseconds: AppConfig.overlayAnimationDuration, ), () { - controller.choreographer.tts.tryToSpeak( + TtsController.tryToSpeak( token.text.content, langCode: pangeaMessageEvent!.messageDisplayLangCode, ); diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index 183ef1ae4..18a4d1d77 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -56,6 +56,7 @@ class VocabDetailsView extends StatelessWidget { ), iconSize: _iconSize, uniqueID: "${_construct.lemma}-${_construct.category}", + langCode: _userL2!, ), subtitle: Column( children: [ @@ -140,8 +141,12 @@ class VocabDetailsView extends StatelessWidget { children: [ WordTextWithAudioButton( text: form, - style: Theme.of(context).textTheme.bodyLarge, + style: + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: textColor, + ), uniqueID: "$form-${_construct.lemma}-$i", + langCode: _userL2!, ), if (i != forms.length - 1) const Text(", "), ], diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index da59e978d..301fda833 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -43,7 +43,6 @@ class Choreographer { late ITController itController; late IgcController igc; late ErrorService errorService; - late TtsController tts; bool isFetching = false; int _timesClicked = 0; @@ -64,7 +63,6 @@ class Choreographer { _initialize(); } _initialize() { - tts = TtsController(chatController: chatController); _textController = PangeaTextController(choreographer: this); InputPasteListener(_textController, onPaste); itController = ITController(this); @@ -566,7 +564,7 @@ class Choreographer { _textController.dispose(); _languageStream?.cancel(); stateStream.close(); - tts.dispose(); + TtsController.stop(); } LanguageModel? get l2Lang { diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index 36be9c2b1..8b26e0e5a 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -29,10 +29,6 @@ class ChoicesArray extends StatefulWidget { final int? selectedChoiceIndex; final String originalSpan; - /// If null then should not be used - /// We don't want tts in the case of L1 options - final TtsController? tts; - final bool enableAudio; /// language code for the TTS @@ -62,7 +58,6 @@ class ChoicesArray extends StatefulWidget { required this.onPressed, required this.originalSpan, required this.selectedChoiceIndex, - required this.tts, this.enableAudio = true, this.langCode, this.isActive = true, @@ -111,10 +106,8 @@ class ChoicesArrayState extends State { ? (String value, int index) { widget.onPressed(value, index); // TODO - what to pass here as eventID? - if (widget.enableAudio && - widget.tts != null && - widget.langCode != null) { - widget.tts?.tryToSpeak( + if (widget.enableAudio && widget.langCode != null) { + TtsController.tryToSpeak( value, targetID: null, langCode: widget.langCode!, diff --git a/lib/pangea/choreographer/widgets/igc/span_card.dart b/lib/pangea/choreographer/widgets/igc/span_card.dart index 0fd6b5bb1..f5662f3ba 100644 --- a/lib/pangea/choreographer/widgets/igc/span_card.dart +++ b/lib/pangea/choreographer/widgets/igc/span_card.dart @@ -60,12 +60,10 @@ class SpanCardState extends State { @override void dispose() { - tts.stop(); + TtsController.stop(); super.dispose(); } - TtsController get tts => widget.scm.choreographer.tts; - //get selected choice SpanChoice? get selectedChoice { if (selectedChoiceIndex == null) return null; @@ -263,7 +261,6 @@ class WordMatchContent extends StatelessWidget { onPressed: (value, index) => controller.onChoiceSelect(index), selectedChoiceIndex: controller.selectedChoiceIndex, - tts: controller.tts, id: controller.widget.scm.pangeaMatch!.hashCode .toString(), langCode: MatrixState.pangeaController.languageController diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 57ec5be8c..085156c54 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -418,7 +418,6 @@ class ITChoices extends StatelessWidget { onPressed: (value, index) => selectContinuance(index, context), onLongPress: (value, index) => showCard(context, index), selectedChoiceIndex: null, - tts: controller.choreographer.tts, langCode: controller.choreographer.pangeaController.languageController .activeL2Code(), ); diff --git a/lib/pangea/learning_settings/pages/settings_learning.dart b/lib/pangea/learning_settings/pages/settings_learning.dart index 92845f6d4..e099b1f10 100644 --- a/lib/pangea/learning_settings/pages/settings_learning.dart +++ b/lib/pangea/learning_settings/pages/settings_learning.dart @@ -35,7 +35,6 @@ class SettingsLearning extends StatefulWidget { class SettingsLearningController extends State { PangeaController pangeaController = MatrixState.pangeaController; late Profile _profile; - final tts = TtsController(); final GlobalKey formKey = GlobalKey(); String? languageMatchError; @@ -46,12 +45,12 @@ class SettingsLearningController extends State { void initState() { super.initState(); _profile = pangeaController.userController.profile.copy(); - tts.setAvailableLanguages().then((_) => setState(() {})); + TtsController.setAvailableLanguages().then((_) => setState(() {})); } @override void dispose() { - tts.dispose(); + TtsController.stop(); scrollController.dispose(); super.dispose(); } diff --git a/lib/pangea/toolbar/controllers/tts_controller.dart b/lib/pangea/toolbar/controllers/tts_controller.dart index f40c83481..57c42ace4 100644 --- a/lib/pangea/toolbar/controllers/tts_controller.dart +++ b/lib/pangea/toolbar/controllers/tts_controller.dart @@ -24,55 +24,37 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; class TtsController { - final ChatController? chatController; - TtsController({this.chatController}) { + static void initialize() { setAvailableLanguages(); - _languageSubscription = - MatrixState.pangeaController.userController.stateStream.listen( - (_) => setAvailableLanguages(), - ); } - List _availableLangCodes = []; - StreamSubscription? _languageSubscription; + static List _availableLangCodes = []; - final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts(); - final TextToSpeech _alternativeTTS = TextToSpeech(); - final StreamController loadingChoreoStream = + static final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts(); + static final TextToSpeech _alternativeTTS = TextToSpeech(); + static final StreamController loadingChoreoStream = StreamController.broadcast(); - bool get _useAlternativeTTS { + static bool get _useAlternativeTTS { return PlatformInfos.isWindows; } - Future dispose() async { - await _tts.stop(); - await _languageSubscription?.cancel(); - await loadingChoreoStream.close(); - } - - void _onError(dynamic message) { - // the package treats this as an error, but it's not - // don't send to sentry - if (message == 'canceled' || message == 'interrupted') { - return; + static Future _onError(dynamic message) async { + if (message != 'canceled' && message != 'interrupted') { + ErrorHandler.logError( + e: 'TTS error', + data: { + 'message': message, + }, + ); } - - ErrorHandler.logError( - e: 'TTS error', - data: { - 'message': message, - }, - ); } - Future setAvailableLanguages() async { + static Future setAvailableLanguages() async { try { if (_useAlternativeTTS) { await _setAvailableAltLanguages(); } else { - _tts.setErrorHandler(_onError); - await _tts.awaitSpeakCompletion(true); await _setAvailableBaseLanguages(); } @@ -86,7 +68,7 @@ class TtsController { } } - Future _setAvailableBaseLanguages() async { + static Future _setAvailableBaseLanguages() async { final voices = (await _tts.getVoices) as List?; _availableLangCodes = (voices ?? []) .map((v) { @@ -100,12 +82,12 @@ class TtsController { .toList(); } - Future _setAvailableAltLanguages() async { + static Future _setAvailableAltLanguages() async { final languages = await _alternativeTTS.getLanguages(); _availableLangCodes = languages.toSet().toList(); } - Future _setSpeakingLanguage(String langCode) async { + static Future _setSpeakingLanguage(String langCode) async { String? selectedLangCode; final langCodeShort = langCode.split("-").first; if (_availableLangCodes.contains(langCode)) { @@ -132,7 +114,7 @@ class TtsController { } } - Future stop() async { + static Future stop() async { try { // return type is dynamic but apparent its supposed to be 1 // https://pub.dev/packages/flutter_tts @@ -157,26 +139,67 @@ class TtsController { } } - /// A safer version of speak, that handles the case of - /// the language not being supported by the TTS engine - Future tryToSpeak( + static VoidCallback? _onStop; + + static Future tryToSpeak( String text, { required String langCode, // Target ID for where to show warning popup String? targetID, BuildContext? context, + ChatController? chatController, + VoidCallback? onStart, + VoidCallback? onStop, + }) async { + final prevOnStop = _onStop; + _onStop = onStop; + + _tts.setErrorHandler((message) { + _onError(message); + prevOnStop?.call(); + }); + + onStart?.call(); + + await _tryToSpeak( + text, + langCode: langCode, + targetID: targetID, + context: context, + chatController: chatController, + onStart: onStart, + onStop: onStop, + ); + + onStop?.call(); + } + + /// A safer version of speak, that handles the case of + /// the language not being supported by the TTS engine + static Future _tryToSpeak( + String text, { + required String langCode, + // Target ID for where to show warning popup + String? targetID, + BuildContext? context, + ChatController? chatController, + VoidCallback? onStart, + VoidCallback? onStop, }) async { chatController?.stopMediaStream.add(null); await _setSpeakingLanguage(langCode); final enableTTS = MatrixState .pangeaController.userController.profile.toolSettings.enableTTS; + if (enableTTS) { final token = PangeaTokenText( offset: 0, content: text, length: text.length, ); + + onStart?.call(); await (_isLangFullySupported(langCode) ? _speak( text, @@ -191,31 +214,33 @@ class TtsController { } else if (targetID != null && context != null) { await _showTTSDisabledPopup(context, targetID); } + + onStop?.call(); } - Future _speak( + static Future _speak( String text, String langCode, List tokens, ) async { try { - stop(); + await stop(); text = text.toLowerCase(); Logs().i('Speaking: $text, langCode: $langCode'); final result = await Future( () => (_useAlternativeTTS - ? _alternativeTTS.speak(text) - : _tts.speak(text)) - .timeout( - const Duration(seconds: 5), - onTimeout: () { - ErrorHandler.logError( - e: "Timeout on tts.speak", - data: {"text": text}, - ); - }, - ), + ? _alternativeTTS.speak(text) + : _tts.speak(text)), + // .timeout( + // const Duration(seconds: 5), + // // onTimeout: () { + // // ErrorHandler.logError( + // // e: "Timeout on tts.speak", + // // data: {"text": text}, + // // ); + // // }, + // ), ); Logs().i('Finished speaking: $text, result: $result'); @@ -241,10 +266,12 @@ class TtsController { }, ); await _speakFromChoreo(text, langCode, tokens); + } finally { + stop(); } } - Future _speakFromChoreo( + static Future _speakFromChoreo( String text, String langCode, List tokens, @@ -252,7 +279,7 @@ class TtsController { TextToSpeechResponse? ttsRes; try { loadingChoreoStream.add(true); - ttsRes = await chatController?.pangeaController.textToSpeech.get( + ttsRes = await MatrixState.pangeaController.textToSpeech.get( TextToSpeechRequest( text: text, langCode: langCode, @@ -304,7 +331,7 @@ class TtsController { } } - bool _isLangFullySupported(String langCode) { + static bool _isLangFullySupported(String langCode) { if (_availableLangCodes.contains(langCode)) { return true; } @@ -317,7 +344,7 @@ class TtsController { return _availableLangCodes.any((lang) => lang.startsWith(langCodeShort)); } - Future _showTTSDisabledPopup( + static Future _showTTSDisabledPopup( BuildContext context, String targetID, ) async => diff --git a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart index 795e25857..dd06e779a 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart @@ -39,9 +39,6 @@ class PracticeMatchItemState extends State { bool _isHovered = false; bool _isPlaying = false; - TtsController get tts => - widget.overlayController.widget.chatController.choreographer.tts; - bool get isSelected => widget.isSelected; bool? get isCorrect => widget.isCorrect; @@ -52,7 +49,7 @@ class PracticeMatchItemState extends State { } if (_isPlaying) { - await tts.stop(); + await TtsController.stop(); if (mounted) { setState(() => _isPlaying = false); } @@ -64,7 +61,7 @@ class PracticeMatchItemState extends State { final l2 = MatrixState.pangeaController.languageController.activeL2Code(); if (l2 != null) { - await tts.tryToSpeak( + await TtsController.tryToSpeak( widget.audioContent!, context: context, targetID: 'word-audio-button', diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index c5327dac3..844eafc26 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -27,6 +27,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection.dart'; import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; +import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart'; @@ -546,7 +547,7 @@ class MessageOverlayController extends State ) == false || !hideWordCardContent) { - widget.chatController.choreographer.tts.tryToSpeak( + TtsController.tryToSpeak( token.text.content, targetID: null, langCode: pangeaMessageEvent!.messageDisplayLangCode, diff --git a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart index 47a36c938..3bf05cebf 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart @@ -13,7 +13,6 @@ import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_record.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart'; @@ -80,9 +79,6 @@ class MultipleChoiceActivityState extends State { } } - TtsController get tts => - widget.overlayController.widget.chatController.choreographer.tts; - void updateChoice(String value, int index) { final bool isCorrect = widget.currentActivity.multipleChoiceContent!.isCorrect(value, index); @@ -232,7 +228,7 @@ class MultipleChoiceActivityState extends State { text: practiceActivity.multipleChoiceContent!.answers.first, uniqueID: "audio-activity-${widget.event.eventId}", langCode: widget - .overlayController.pangeaMessageEvent?.messageDisplayLangCode, + .overlayController.pangeaMessageEvent!.messageDisplayLangCode, ), if (practiceActivity.activityType == ActivityTypeEnum.hiddenWordListening) @@ -251,8 +247,8 @@ class MultipleChoiceActivityState extends State { choices: choices(context), isActive: true, id: currentRecordModel?.hashCode.toString(), - tts: practiceActivity.activityType.includeTTSOnClick ? tts : null, - enableAudio: !widget.overlayController.isPlayingAudio, + enableAudio: !widget.overlayController.isPlayingAudio && + practiceActivity.activityType.includeTTSOnClick, langCode: MatrixState.pangeaController.languageController.activeL2Code(), getDisplayCopy: _getDisplayCopy, diff --git a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart index cc7bf514b..fbc456635 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart @@ -15,6 +15,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_activity_model.da import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart'; import 'package:fluffychat/pangea/practice_activities/practice_record.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart'; import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/practice_match_card.dart'; @@ -231,7 +232,7 @@ class PracticeActivityCardState extends State { widget.overlayController .onActivityFinish(currentActivity!.activityType, null); - widget.overlayController.widget.chatController.choreographer.tts.stop(); + TtsController.stop(); } catch (e, s) { _onError(); debugger(when: kDebugMode); diff --git a/lib/pangea/toolbar/widgets/practice_activity/word_audio_button.dart b/lib/pangea/toolbar/widgets/practice_activity/word_audio_button.dart index 583a1b3d0..37ce838d8 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/word_audio_button.dart @@ -1,8 +1,9 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -11,7 +12,7 @@ class WordAudioButton extends StatefulWidget { final bool isSelected; final double baseOpacity; final String uniqueID; - final String? langCode; + final String langCode; final EdgeInsets? padding; /// If defined, this callback will be called instead of the default one @@ -21,10 +22,10 @@ class WordAudioButton extends StatefulWidget { super.key, required this.text, required this.uniqueID, + required this.langCode, this.isSelected = false, this.baseOpacity = 1, this.callbackOverride, - this.langCode, this.padding, }); @@ -33,8 +34,19 @@ class WordAudioButton extends StatefulWidget { } class WordAudioButtonState extends State { - final TtsController tts = TtsController(); + late TtsController tts; bool _isPlaying = false; + bool _isLoading = false; + StreamSubscription? _loadingChoreoSubscription; + + @override + void initState() { + super.initState(); + _loadingChoreoSubscription = + TtsController.loadingChoreoStream.stream.listen((val) { + if (mounted) setState(() => _isLoading = val); + }); + } @override void didUpdateWidget(covariant WordAudioButton oldWidget) { @@ -47,7 +59,8 @@ class WordAudioButtonState extends State { @override void dispose() { - tts.dispose(); + TtsController.stop(); + _loadingChoreoSubscription?.cancel(); super.dispose(); } @@ -71,45 +84,34 @@ class WordAudioButtonState extends State { onTap: widget.callbackOverride ?? () async { if (_isPlaying) { - await tts.stop(); - if (mounted) { - setState(() => _isPlaying = false); - } + await TtsController.stop(); } else { - if (mounted) { - setState(() => _isPlaying = true); - } - try { - if (widget.langCode != null) { - await tts.tryToSpeak( - widget.text, - context: context, - targetID: 'word-audio-button-${widget.uniqueID}', - langCode: widget.langCode!, - ); - } - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "text": widget.text, - }, - ); - } finally { - if (mounted) { - setState(() => _isPlaying = false); - } - } + await TtsController.tryToSpeak( + widget.text, + context: context, + targetID: 'word-audio-button-${widget.uniqueID}', + langCode: widget.langCode, + onStart: () => setState(() => _isPlaying = true), + onStop: () => setState(() => _isPlaying = false), + ); } }, child: Padding( padding: widget.padding ?? const EdgeInsets.all(0.0), - child: Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - color: - _isPlaying ? Theme.of(context).colorScheme.primary : null, - ), + child: _isLoading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 3, + ), + ) + : Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, + color: _isPlaying + ? Theme.of(context).colorScheme.primary + : null, + ), ), ), ), diff --git a/lib/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart b/lib/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart index 6a4f586ad..9297d3b3a 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/word_text_with_audio_button.dart @@ -1,139 +1,46 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; -import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; -class WordTextWithAudioButton extends StatefulWidget { +class WordTextWithAudioButton extends StatelessWidget { final String text; final String uniqueID; final TextStyle? style; final double? iconSize; + final String langCode; const WordTextWithAudioButton({ super.key, required this.text, required this.uniqueID, + required this.langCode, this.style, this.iconSize, }); - @override - WordAudioButtonState createState() => WordAudioButtonState(); -} - -class WordAudioButtonState extends State { - // initialize as null because we don't know if we need to load - // audio from choreo yet. This shall remain null if user device support - // text to speech - final bool? _isLoadingAudio = null; - final TtsController tts = TtsController(); - - bool _isPlaying = false; - bool _isLoading = false; - StreamSubscription? _loadingChoreoSubscription; - - @override - void initState() { - super.initState(); - _loadingChoreoSubscription = tts.loadingChoreoStream.stream.listen((val) { - if (mounted) setState(() => _isLoading = val); - }); - } - - @override - void dispose() { - _loadingChoreoSubscription?.cancel(); - tts.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return CompositedTransformTarget( - link: MatrixState.pAnyState - .layerLinkAndKey('text-audio-button-${widget.uniqueID}') - .link, - child: MouseRegion( - key: MatrixState.pAnyState - .layerLinkAndKey('text-audio-button-${widget.uniqueID}') - .key, - cursor: SystemMouseCursors.click, - onEnter: (event) => setState(() {}), - onExit: (event) => setState(() {}), - child: GestureDetector( - onTap: () async { - if (_isLoadingAudio == true) { - return; - } - if (_isPlaying) { - await tts.stop(); - if (mounted) { - setState(() => _isPlaying = false); - } - } else { - if (mounted) { - setState(() => _isPlaying = true); - } - try { - final l2 = MatrixState.pangeaController.languageController - .activeL2Code(); - if (l2 != null) { - await tts.tryToSpeak( - widget.text, - context: context, - targetID: 'text-audio-button-${widget.uniqueID}', - langCode: l2, - ); - } - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "text": widget.text, - }, - ); - } finally { - if (mounted) { - setState(() => _isPlaying = false); - } - } - } - }, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 180), - child: Text( - widget.text, - style: widget.style ?? Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, - ), - ), - if (_isLoading) - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 3, - ), - ) - else - Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - color: - _isPlaying ? Theme.of(context).colorScheme.primary : null, - size: widget.iconSize, - ), - ], + return Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 180), + child: Text( + text, + style: style ?? Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, ), ), - ), + WordAudioButton( + text: text, + uniqueID: uniqueID, + isSelected: false, + baseOpacity: 1, + langCode: langCode, + padding: const EdgeInsets.only(left: 8.0), + ), + ], ); } } diff --git a/lib/pangea/toolbar/widgets/reading_assistance_content.dart b/lib/pangea/toolbar/widgets/reading_assistance_content.dart index facdb86e5..7886915fb 100644 --- a/lib/pangea/toolbar/widgets/reading_assistance_content.dart +++ b/lib/pangea/toolbar/widgets/reading_assistance_content.dart @@ -9,7 +9,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_unsubscribed_card.dart'; @@ -38,9 +37,6 @@ class ReadingAssistanceContent extends StatefulWidget { } class ReadingAssistanceContentState extends State { - TtsController get ttsController => - widget.overlayController.widget.chatController.choreographer.tts; - Widget? toolbarContent(BuildContext context) { final bool? subscribed = MatrixState.pangeaController.subscriptionController.isSubscribed; @@ -123,7 +119,6 @@ class ReadingAssistanceContentState extends State { return WordZoomWidget( token: widget.overlayController.selectedToken!, messageEvent: widget.overlayController.pangeaMessageEvent!, - tts: ttsController, overlayController: widget.overlayController, ); } diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart index 7a0fbe1d5..d8786f438 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart @@ -8,7 +8,6 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -18,7 +17,6 @@ class LemmaWidget extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final VoidCallback onEdit; final VoidCallback onEditDone; - final TtsController tts; final MessageOverlayController? overlayController; const LemmaWidget({ @@ -27,7 +25,6 @@ class LemmaWidget extends StatefulWidget { required this.pangeaMessageEvent, required this.onEdit, required this.onEditDone, - required this.tts, required this.overlayController, }); diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index ae0d41789..37f1dd6cc 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart'; import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; @@ -22,14 +21,12 @@ import 'package:fluffychat/widgets/matrix.dart'; class WordZoomWidget extends StatelessWidget { final PangeaToken token; final PangeaMessageEvent messageEvent; - final TtsController tts; final MessageOverlayController overlayController; const WordZoomWidget({ super.key, required this.token, required this.messageEvent, - required this.tts, required this.overlayController, }); @@ -93,7 +90,6 @@ class WordZoomWidget extends StatelessWidget { debugPrint("what are we doing edits with?"); _onEditDone(); }, - tts: tts, overlayController: overlayController, ), ConstructXpWidget( @@ -181,7 +177,7 @@ class WordZoomWidget extends StatelessWidget { baseOpacity: 0.4, uniqueID: "word-zoom-audio-${_selectedToken.text.content}", langCode: overlayController - .pangeaMessageEvent?.messageDisplayLangCode, + .pangeaMessageEvent!.messageDisplayLangCode, ), ], ..._selectedToken.morphsBasicallyEligibleForPracticeByPriority diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 3f1673033..7c071fbc6 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -20,6 +20,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -249,6 +250,7 @@ class MatrixState extends State with WidgetsBindingObserver { ), ); pangeaController = PangeaController(matrix: widget, matrixState: this); + TtsController.initialize(); // Pangea# }