fix: make TTS button pause when it's stopped by the other TTS button playing (#2831)
This commit is contained in:
parent
c5b7b550f2
commit
3359cfe25d
18 changed files with 174 additions and 261 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(", "),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<ChoicesArray> {
|
|||
? (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!,
|
||||
|
|
|
|||
|
|
@ -60,12 +60,10 @@ class SpanCardState extends State<SpanCard> {
|
|||
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class SettingsLearning extends StatefulWidget {
|
|||
class SettingsLearningController extends State<SettingsLearning> {
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
late Profile _profile;
|
||||
final tts = TtsController();
|
||||
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
String? languageMatchError;
|
||||
|
|
@ -46,12 +45,12 @@ class SettingsLearningController extends State<SettingsLearning> {
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> _availableLangCodes = [];
|
||||
StreamSubscription? _languageSubscription;
|
||||
static List<String> _availableLangCodes = [];
|
||||
|
||||
final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
|
||||
final TextToSpeech _alternativeTTS = TextToSpeech();
|
||||
final StreamController<bool> loadingChoreoStream =
|
||||
static final flutter_tts.FlutterTts _tts = flutter_tts.FlutterTts();
|
||||
static final TextToSpeech _alternativeTTS = TextToSpeech();
|
||||
static final StreamController<bool> loadingChoreoStream =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
bool get _useAlternativeTTS {
|
||||
static bool get _useAlternativeTTS {
|
||||
return PlatformInfos.isWindows;
|
||||
}
|
||||
|
||||
Future<void> 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<void> _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<void> setAvailableLanguages() async {
|
||||
static Future<void> setAvailableLanguages() async {
|
||||
try {
|
||||
if (_useAlternativeTTS) {
|
||||
await _setAvailableAltLanguages();
|
||||
} else {
|
||||
_tts.setErrorHandler(_onError);
|
||||
|
||||
await _tts.awaitSpeakCompletion(true);
|
||||
await _setAvailableBaseLanguages();
|
||||
}
|
||||
|
|
@ -86,7 +68,7 @@ class TtsController {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _setAvailableBaseLanguages() async {
|
||||
static Future<void> _setAvailableBaseLanguages() async {
|
||||
final voices = (await _tts.getVoices) as List?;
|
||||
_availableLangCodes = (voices ?? [])
|
||||
.map((v) {
|
||||
|
|
@ -100,12 +82,12 @@ class TtsController {
|
|||
.toList();
|
||||
}
|
||||
|
||||
Future<void> _setAvailableAltLanguages() async {
|
||||
static Future<void> _setAvailableAltLanguages() async {
|
||||
final languages = await _alternativeTTS.getLanguages();
|
||||
_availableLangCodes = languages.toSet().toList();
|
||||
}
|
||||
|
||||
Future<void> _setSpeakingLanguage(String langCode) async {
|
||||
static Future<void> _setSpeakingLanguage(String langCode) async {
|
||||
String? selectedLangCode;
|
||||
final langCodeShort = langCode.split("-").first;
|
||||
if (_availableLangCodes.contains(langCode)) {
|
||||
|
|
@ -132,7 +114,7 @@ class TtsController {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
static Future<void> 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<void> tryToSpeak(
|
||||
static VoidCallback? _onStop;
|
||||
|
||||
static Future<void> 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<void> _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<void> _speak(
|
||||
static Future<void> _speak(
|
||||
String text,
|
||||
String langCode,
|
||||
List<PangeaTokenText> 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<void> _speakFromChoreo(
|
||||
static Future<void> _speakFromChoreo(
|
||||
String text,
|
||||
String langCode,
|
||||
List<PangeaTokenText> 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<void> _showTTSDisabledPopup(
|
||||
static Future<void> _showTTSDisabledPopup(
|
||||
BuildContext context,
|
||||
String targetID,
|
||||
) async =>
|
||||
|
|
|
|||
|
|
@ -39,9 +39,6 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
|
|||
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<PracticeMatchItem> {
|
|||
}
|
||||
|
||||
if (_isPlaying) {
|
||||
await tts.stop();
|
||||
await TtsController.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
|
|
@ -64,7 +61,7 @@ class PracticeMatchItemState extends State<PracticeMatchItem> {
|
|||
final l2 =
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
if (l2 != null) {
|
||||
await tts.tryToSpeak(
|
||||
await TtsController.tryToSpeak(
|
||||
widget.audioContent!,
|
||||
context: context,
|
||||
targetID: 'word-audio-button',
|
||||
|
|
|
|||
|
|
@ -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<MessageSelectionOverlay>
|
|||
) ==
|
||||
false ||
|
||||
!hideWordCardContent) {
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
TtsController.tryToSpeak(
|
||||
token.text.content,
|
||||
targetID: null,
|
||||
langCode: pangeaMessageEvent!.messageDisplayLangCode,
|
||||
|
|
|
|||
|
|
@ -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<MultipleChoiceActivity> {
|
|||
}
|
||||
}
|
||||
|
||||
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<MultipleChoiceActivity> {
|
|||
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<MultipleChoiceActivity> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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<PracticeActivityCard> {
|
|||
widget.overlayController
|
||||
.onActivityFinish(currentActivity!.activityType, null);
|
||||
|
||||
widget.overlayController.widget.chatController.choreographer.tts.stop();
|
||||
TtsController.stop();
|
||||
} catch (e, s) {
|
||||
_onError();
|
||||
debugger(when: kDebugMode);
|
||||
|
|
|
|||
|
|
@ -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<WordAudioButton> {
|
||||
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<WordAudioButton> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
tts.dispose();
|
||||
TtsController.stop();
|
||||
_loadingChoreoSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -71,45 +84,34 @@ class WordAudioButtonState extends State<WordAudioButton> {
|
|||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<WordTextWithAudioButton> {
|
||||
// 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReadingAssistanceContent> {
|
||||
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<ReadingAssistanceContent> {
|
|||
return WordZoomWidget(
|
||||
token: widget.overlayController.selectedToken!,
|
||||
messageEvent: widget.overlayController.pangeaMessageEvent!,
|
||||
tts: ttsController,
|
||||
overlayController: widget.overlayController,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Matrix> with WidgetsBindingObserver {
|
|||
),
|
||||
);
|
||||
pangeaController = PangeaController(matrix: widget, matrixState: this);
|
||||
TtsController.initialize();
|
||||
// Pangea#
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue