chore: fix scrolling bug and issues with lemma edit widget (#2133)

This commit is contained in:
ggurdin 2025-03-12 13:17:25 -04:00 committed by GitHub
parent 6df46c73a1
commit c6e5c2ad29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 86 additions and 90 deletions

View file

@ -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,
),
],

View file

@ -77,9 +77,6 @@ class Choreographer {
trialStream = pangeaController
.subscriptionController.trialActivationStream.stream
.listen((_) => _onChangeListener);
tts.setupTTS();
clear();
}

View file

@ -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<bool> loadingChoreoStream =
StreamController<bool>.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<void> 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<void> _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,

View file

@ -212,7 +212,6 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
ActivityTypeEnum.wordFocusListening)
WordAudioButton(
text: practiceActivity.content.answers.first,
ttsController: tts,
),
if (practiceActivity.activityType ==
ActivityTypeEnum.hiddenWordListening)
@ -242,25 +241,18 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
],
);
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,
),
),
);
}
}

View file

@ -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<WordAudioButton> {
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<WordAudioButton> {
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<WordAudioButton> {
setState(() => _isPlaying = true);
}
try {
await widget.ttsController.tryToSpeak(
await tts.tryToSpeak(
widget.text,
context,
targetID: 'word-audio-button',

View file

@ -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<WordTextWithAudioButton> {
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<WordTextWithAudioButton> {
return;
}
if (_isPlaying) {
await widget.ttsController.stop();
await tts.stop();
if (mounted) {
setState(() => _isPlaying = false);
}
@ -74,7 +77,7 @@ class WordAudioButtonState extends State<WordTextWithAudioButton> {
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<WordTextWithAudioButton> {
),
),
const SizedBox(width: 4),
if (widget.ttsController.hasLoadedTextToSpeech == false)
if (_isLoading)
const Padding(
padding: EdgeInsets.only(
left: 4,

View file

@ -162,17 +162,14 @@ class LemmaWidgetState extends State<LemmaWidget> {
);
}
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,
),
),
);

View file

@ -211,7 +211,6 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
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<MorphFocusWidget> {
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<MorphFocusWidget> {
.getDisplayTags(widget.morphFeature);
return snapshot.connectionState == ConnectionState.done
? Wrap(
alignment: WrapAlignment.center,
? Row(
children: allMorphTagsForEdit.map((tag) {
return Container(
margin: const EdgeInsets.all(2),

View file

@ -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,
),