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 d90acf55f..7cd9f89fd 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -12,7 +12,6 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/lemmas/lemma.dart'; import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart'; import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -98,7 +97,6 @@ class VocabDetailsView extends StatelessWidget { const SizedBox(width: 10.0), WordAudioButton( text: _construct.lemma, - ttsController: TtsController(), size: 24, ), ], diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index db533c3ce..03f3367ab 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -77,9 +77,6 @@ class Choreographer { trialStream = pangeaController .subscriptionController.trialActivationStream.stream .listen((_) => _onChangeListener); - - tts.setupTTS(); - clear(); } diff --git a/lib/pangea/toolbar/controllers/tts_controller.dart b/lib/pangea/toolbar/controllers/tts_controller.dart index aaadb5900..45c405689 100644 --- a/lib/pangea/toolbar/controllers/tts_controller.dart +++ b/lib/pangea/toolbar/controllers/tts_controller.dart @@ -22,7 +22,7 @@ import 'package:fluffychat/pangea/user/controllers/user_controller.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class TtsController extends ChangeNotifier { +class TtsController { final ChatController? chatController; String? get l2LangCode => @@ -35,6 +35,8 @@ class TtsController extends ChangeNotifier { final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts(); final TextToSpeech _alternativeTTS = TextToSpeech(); StreamSubscription? _languageSubscription; + final StreamController loadingChoreoStream = + StreamController.broadcast(); UserController get userController => MatrixState.pangeaController.userController; @@ -49,20 +51,10 @@ class TtsController extends ChangeNotifier { return PlatformInfos.isWindows; } - bool? _hasLoadedTextToSpeech; - bool? get hasLoadedTextToSpeech => _hasLoadedTextToSpeech; - set hasLoadedTextToSpeech(bool? value) { - if (_hasLoadedTextToSpeech != value) { - _hasLoadedTextToSpeech = value; - notifyListeners(); - } - } - - @override Future dispose() async { await _tts.stop(); await _languageSubscription?.cancel(); - super.dispose(); + await loadingChoreoStream.close(); } void _onError(dynamic message) { @@ -269,9 +261,10 @@ class TtsController extends ChangeNotifier { } Future _speakFromChoreo(String text) async { + TextToSpeechResponse? ttsRes; try { - hasLoadedTextToSpeech = false; - final ttsRes = await chatController?.pangeaController.textToSpeech.get( + loadingChoreoStream.add(true); + ttsRes = await chatController?.pangeaController.textToSpeech.get( TextToSpeechRequest( text: text, langCode: l2LangCode ?? LanguageKeys.unknownLanguage, @@ -280,11 +273,23 @@ class TtsController extends ChangeNotifier { userL2: LanguageKeys.unknownLanguage, ), ); - hasLoadedTextToSpeech = true; - if (ttsRes != null) { - final audioContent = base64Decode(ttsRes.audioContent); - await playAudio(audioContent, ttsRes.mimeType); - } + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + 'text': text, + }, + ); + } finally { + loadingChoreoStream.add(false); + } + + if (ttsRes == null) return; + + try { + final audioContent = base64Decode(ttsRes.audioContent); + await playAudio(audioContent, ttsRes.mimeType); } catch (e, s) { ErrorHandler.logError( e: e, 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 1fef5908e..144d3ed29 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart @@ -212,7 +212,6 @@ class MultipleChoiceActivityState extends State { ActivityTypeEnum.wordFocusListening) WordAudioButton( text: practiceActivity.content.answers.first, - ttsController: tts, ), if (practiceActivity.activityType == ActivityTypeEnum.hiddenWordListening) @@ -242,25 +241,18 @@ class MultipleChoiceActivityState extends State { ], ); - return practiceActivity.activityType == - ActivityTypeEnum.hiddenWordListening || - practiceActivity.activityType == ActivityTypeEnum.messageMeaning - ? ConstrainedBox( - constraints: const BoxConstraints( - // see https://github.com/pangeachat/client/issues/1422 - maxWidth: AppConfig.toolbarMinWidth, - maxHeight: AppConfig.toolbarMaxHeight, - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: content, - ), - ), - ) - : Padding( - padding: const EdgeInsets.all(8), - child: content, - ); + return ConstrainedBox( + constraints: const BoxConstraints( + // see https://github.com/pangeachat/client/issues/1422 + maxWidth: AppConfig.toolbarMinWidth, + maxHeight: AppConfig.toolbarMaxHeight, + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: content, + ), + ), + ); } } 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 cd0ad9881..26aaa7718 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/word_audio_button.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/word_audio_button.dart @@ -8,13 +8,11 @@ import 'package:fluffychat/widgets/matrix.dart'; class WordAudioButton extends StatefulWidget { final String text; - final TtsController ttsController; final double size; const WordAudioButton({ super.key, required this.text, - required this.ttsController, this.size = 24, }); @@ -23,8 +21,15 @@ class WordAudioButton extends StatefulWidget { } class WordAudioButtonState extends State { + final TtsController tts = TtsController(); bool _isPlaying = false; + @override + void dispose() { + tts.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return CompositedTransformTarget( @@ -40,7 +45,7 @@ class WordAudioButtonState extends State { iconSize: widget.size, onPressed: () async { if (_isPlaying) { - await widget.ttsController.stop(); + await tts.stop(); if (mounted) { setState(() => _isPlaying = false); } @@ -49,7 +54,7 @@ class WordAudioButtonState extends State { setState(() => _isPlaying = true); } try { - await widget.ttsController.tryToSpeak( + await tts.tryToSpeak( widget.text, context, targetID: 'word-audio-button', 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 a20eb87c1..6a87fdc3e 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,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -6,13 +8,11 @@ import 'package:fluffychat/widgets/matrix.dart'; class WordTextWithAudioButton extends StatefulWidget { final String text; - final TtsController ttsController; final double? textSize; const WordTextWithAudioButton({ super.key, required this.text, - required this.ttsController, this.textSize, }); @@ -21,28 +21,31 @@ class WordTextWithAudioButton extends StatefulWidget { } class WordAudioButtonState extends State { - bool _isPlaying = false; // 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(); - widget.ttsController.addListener(_onTtsControllerChange); + _loadingChoreoSubscription = tts.loadingChoreoStream.stream.listen((val) { + if (mounted) setState(() => _isLoading = val); + }); } @override void dispose() { - widget.ttsController.removeListener(_onTtsControllerChange); + _loadingChoreoSubscription?.cancel(); + tts.dispose(); super.dispose(); } - void _onTtsControllerChange() { - setState(() {}); - } - double get textSize => widget.textSize ?? Theme.of(context).textTheme.titleLarge?.fontSize ?? 16; @@ -65,7 +68,7 @@ class WordAudioButtonState extends State { return; } if (_isPlaying) { - await widget.ttsController.stop(); + await tts.stop(); if (mounted) { setState(() => _isPlaying = false); } @@ -74,7 +77,7 @@ class WordAudioButtonState extends State { setState(() => _isPlaying = true); } try { - await widget.ttsController.tryToSpeak( + await tts.tryToSpeak( widget.text, context, targetID: 'text-audio-button', @@ -118,7 +121,7 @@ class WordAudioButtonState extends State { ), ), const SizedBox(width: 4), - if (widget.ttsController.hasLoadedTextToSpeech == false) + if (_isLoading) const Padding( padding: EdgeInsets.only( left: 4, diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart index dc06f3ac0..fb2df9d50 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_widget.dart @@ -162,17 +162,14 @@ class LemmaWidgetState extends State { ); } - return Flexible( - child: Tooltip( - triggerMode: TooltipTriggerMode.tap, - message: L10n.of(context).doubleClickToEdit, - child: GestureDetector( - onLongPress: () => _toggleEditMode(true), - onDoubleTap: () => _toggleEditMode(true), - child: WordTextWithAudioButton( - text: widget.token.lemma.text, - ttsController: widget.tts, - ), + return Tooltip( + triggerMode: TooltipTriggerMode.tap, + message: L10n.of(context).doubleClickToEdit, + child: GestureDetector( + onLongPress: () => _toggleEditMode(true), + onDoubleTap: () => _toggleEditMode(true), + child: WordTextWithAudioButton( + text: widget.token.lemma.text, ), ), ); diff --git a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart index 3005938b7..79b1d4377 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/morphs/morphological_center_widget.dart @@ -211,7 +211,6 @@ class MorphFocusWidgetState extends State { child: Padding( padding: const EdgeInsets.all(4.0), child: Column( - spacing: 4.0, children: [ Text( "${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).chooseCorrectLabel}", @@ -219,13 +218,13 @@ class MorphFocusWidgetState extends State { style: const TextStyle(fontStyle: FontStyle.italic), ), Container( - constraints: const BoxConstraints(maxHeight: 70), + constraints: const BoxConstraints(maxHeight: 50), child: Scrollbar( controller: _scrollController, thumbVisibility: true, child: SingleChildScrollView( controller: _scrollController, - scrollDirection: Axis.vertical, + scrollDirection: Axis.horizontal, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: FutureBuilder( @@ -237,8 +236,7 @@ class MorphFocusWidgetState extends State { .getDisplayTags(widget.morphFeature); return snapshot.connectionState == ConnectionState.done - ? Wrap( - alignment: WrapAlignment.center, + ? Row( children: allMorphTagsForEdit.map((tag) { return Container( margin: const EdgeInsets.all(2), 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 34e245038..0cdbf2553 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -78,18 +78,20 @@ class WordZoomWidget extends StatelessWidget { ), isSelected: _mode == MessageMode.wordEmoji, ), - LemmaWidget( - token: _selectedToken, - pangeaMessageEvent: messageEvent, - // onEdit: () => _setHideCenterContent(true), - onEdit: () { - debugPrint("what are we doing edits with?"); - }, - onEditDone: () { - debugPrint("what are we doing edits with?"); - onEditDone(); - }, - tts: tts, + Expanded( + child: LemmaWidget( + token: _selectedToken, + pangeaMessageEvent: messageEvent, + // onEdit: () => _setHideCenterContent(true), + onEdit: () { + debugPrint("what are we doing edits with?"); + }, + onEditDone: () { + debugPrint("what are we doing edits with?"); + onEditDone(); + }, + tts: tts, + ), ), ConstructXpWidget( id: token.vocabConstructID, @@ -129,7 +131,6 @@ class WordZoomWidget extends StatelessWidget { if (!_selectedToken.doesLemmaTextMatchTokenText) WordTextWithAudioButton( text: _selectedToken.text.content, - ttsController: tts, textSize: Theme.of(context).textTheme.titleMedium?.fontSize, ),