fix: prevent overflows in token info feedback dialog on mobile, add more specific unsubscribed error in future loading dialog (#4333)

This commit is contained in:
ggurdin 2025-10-10 14:14:33 -04:00 committed by GitHub
parent 5e1f4f3123
commit c4ff6b0ac4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 422 additions and 549 deletions

View file

@ -5310,5 +5310,6 @@
"tokenInfoFeedbackDialogDesc": "AI makes mistakes. Please describe any issues you found with the information above.",
"noPublicCoursesFound": "No public courses found. Would you like to create one?",
"noCourseTemplatesFound": "We couldn't find any courses for your target language. You can chat with Pangea Bot in the meantime, and check back later for more courses.",
"botActivityJoinFailMessage": "Pangea Bot is taking a while to respond. Please try again later, or invite a friend."
"botActivityJoinFailMessage": "Pangea Bot is taking a while to respond. Please try again later, or invite a friend.",
"unsubscribedResponseError": "This feature requires a subscription"
}

View file

@ -308,38 +308,23 @@ class VocabTile extends StatelessWidget {
OverlayUtil.showPositionedCard(
overlayKey: "activity-vocab-${vocab.lemma}",
context: context,
cardToShow: Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4.0,
),
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
),
),
child: WordZoomWidget(
token: PangeaTokenText(
content: vocab.lemma,
length: vocab.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: vocab.lemma,
type: ConstructTypeEnum.vocab,
category: vocab.pos,
),
langCode: langCode,
onClose: () {
MatrixState.pAnyState.closeOverlay(
"activity-vocab-${vocab.lemma}",
);
},
),
cardToShow: WordZoomWidget(
token: PangeaTokenText(
content: vocab.lemma,
length: vocab.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: vocab.lemma,
type: ConstructTypeEnum.vocab,
category: vocab.pos,
),
langCode: langCode,
onClose: () {
MatrixState.pAnyState.closeOverlay(
"activity-vocab-${vocab.lemma}",
);
},
),
transformTargetId: "activity-vocab-${vocab.lemma}",
closePrevOverlay: false,

View file

@ -202,42 +202,23 @@ class ActivitySummary extends StatelessWidget {
overlayKey:
"activity-summary-vocab-${vocab.lemma}",
context: context,
cardToShow: Material(
type: MaterialType.transparency,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
color: Theme.of(context)
.colorScheme
.primary,
width: 4.0,
),
borderRadius: const BorderRadius.all(
Radius.circular(
AppConfig.borderRadius,
),
),
),
child: WordZoomWidget(
token: PangeaTokenText(
content: vocab.lemma,
length: vocab.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: vocab.lemma,
type: ConstructTypeEnum.vocab,
category: vocab.pos,
),
langCode: activity.req.targetLanguage,
onClose: () {
MatrixState.pAnyState.closeOverlay(
"activity-summary-vocab-${vocab.lemma}",
);
},
),
cardToShow: WordZoomWidget(
token: PangeaTokenText(
content: vocab.lemma,
length: vocab.lemma.characters.length,
offset: 0,
),
construct: ConstructIdentifier(
lemma: vocab.lemma,
type: ConstructTypeEnum.vocab,
category: vocab.pos,
),
langCode: activity.req.targetLanguage,
onClose: () {
MatrixState.pAnyState.closeOverlay(
"activity-summary-vocab-${vocab.lemma}",
);
},
),
transformTargetId:
"activity-summary-vocab-${vocab.lemma}",

View file

@ -121,6 +121,17 @@ class _TokenInfoFeedbackDialogState extends State<TokenInfoFeedbackDialog> {
return response.userFriendlyMessage;
}
Future<void> _submit() async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _submitFeedback(),
);
if (!resp.isError) {
Navigator.of(context).pop(resp.result!);
}
}
Future<void> _updateLemmaInfo(
PangeaToken token,
LemmaInfoResponse response,
@ -164,112 +175,97 @@ class _TokenInfoFeedbackDialogState extends State<TokenInfoFeedbackDialog> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: SizedBox(
child: Container(
width: 325.0,
constraints: const BoxConstraints(
maxHeight: 600.0,
),
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 20.0,
mainAxisSize: MainAxisSize.min,
children: [
// Header with title and close button
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Text(
L10n.of(context).tokenInfoFeedbackDialogTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Text(
L10n.of(context).tokenInfoFeedbackDialogTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(
width: 40.0,
height: 40.0,
child: Center(
child: Icon(Icons.flag_outlined),
),
),
const SizedBox(
width: 40.0,
height: 40.0,
child: Center(
child: Icon(Icons.flag_outlined),
),
],
),
),
],
),
// Content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20.0,
),
child: Column(
spacing: 20.0,
mainAxisSize: MainAxisSize.min,
children: [
// Placeholder for word card
Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(8.0),
),
child: Center(
Expanded(
child: SingleChildScrollView(
child: Column(
spacing: 20.0,
mainAxisSize: MainAxisSize.min,
children: [
// Placeholder for word card
Center(
child: WordZoomWidget(
token: selectedToken.text,
construct: selectedToken.vocabConstructID,
langCode: widget.langCode,
),
),
),
// Description text
Text(
L10n.of(context).tokenInfoFeedbackDialogDesc,
textAlign: TextAlign.center,
),
// Feedback text field
TextField(
controller: _feedbackController,
decoration: InputDecoration(
hintText: L10n.of(context).feedbackHint,
// Description text
Text(
L10n.of(context).tokenInfoFeedbackDialogDesc,
textAlign: TextAlign.center,
),
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: 5,
),
// Submit button
ValueListenableBuilder<TextEditingValue>(
valueListenable: _feedbackController,
builder: (context, value, _) {
final isNotEmpty = value.text.isNotEmpty;
return ElevatedButton(
onPressed: isNotEmpty
? () async {
final resp = await showFutureLoadingDialog(
context: context,
future: () => _submitFeedback(),
);
if (!resp.isError) {
Navigator.of(context).pop(resp.result!);
}
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).feedbackButton),
],
),
);
},
),
const SizedBox.shrink(),
],
// Feedback text field
TextFormField(
controller: _feedbackController,
decoration: InputDecoration(
hintText: L10n.of(context).feedbackHint,
),
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: 5,
onFieldSubmitted: _feedbackController.text.isNotEmpty
? (_) => _submit()
: null,
),
],
),
),
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _feedbackController,
builder: (context, value, _) {
final isNotEmpty = value.text.isNotEmpty;
return ElevatedButton(
onPressed: isNotEmpty ? _submit : null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(L10n.of(context).feedbackButton),
],
),
);
},
),
],
),
),

View file

@ -35,7 +35,6 @@ import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dar
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
@ -91,8 +90,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
List<PangeaTokenText>? _highlightedTokens;
bool initialized = false;
final GlobalKey<ReadingAssistanceContentState> wordZoomKey = GlobalKey();
ReadingAssistanceMode? readingAssistanceMode; // default mode
SpeechToTextModel? transcription;
@ -234,8 +231,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
phase == SchedulerPhase.postFrameCallbacks)) {
// It's safe to call setState immediately
try {
wordZoomKey.currentState?.setState(() {});
super.setState(fn);
} catch (e, s) {
ErrorHandler.logError(

View file

@ -1,25 +1,16 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:matrix/matrix_api_lite/model/message_types.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.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';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart';
import 'package:fluffychat/widgets/matrix.dart';
const double minCardHeight = 70;
class ReadingAssistanceContent extends StatefulWidget {
class ReadingAssistanceContent extends StatelessWidget {
final MessageOverlayController overlayController;
final Duration animationDuration;
@ -30,51 +21,15 @@ class ReadingAssistanceContent extends StatefulWidget {
});
@override
ReadingAssistanceContentState createState() =>
ReadingAssistanceContentState();
}
class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
Widget? toolbarContent(BuildContext context) {
final bool? subscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
if (subscribed != null && !subscribed) {
return const MessageUnsubscribedCard();
Widget build(BuildContext context) {
if (![MessageTypes.Text, MessageTypes.Audio].contains(
overlayController.pangeaMessageEvent.event.messageType,
)) {
return const SizedBox();
}
if (widget.overlayController.practiceSelection?.hasHiddenWordActivity ??
false) {
return PracticeActivityCard(
overlayController: widget.overlayController,
targetTokensAndActivityType: widget.overlayController.practiceSelection!
.nextActivity(ActivityTypeEnum.hiddenWordListening)!,
);
}
if (widget.overlayController.practiceSelection?.hasMessageMeaningActivity ??
false) {
return PracticeActivityCard(
overlayController: widget.overlayController,
targetTokensAndActivityType: widget.overlayController.practiceSelection!
.nextActivity(ActivityTypeEnum.messageMeaning)!,
);
}
if (!widget.overlayController.initialized) {
return const ToolbarContentLoadingIndicator();
}
// final unlocked = widget.overlayController.toolbarMode
// .isUnlocked(widget.overlayController);
// if (!unlocked) {
// return MessageModeLockedCard(controller: widget.overlayController);
// }
final tokens =
widget.overlayController.pangeaMessageEvent.originalSent?.tokens;
final selectedToken = widget.overlayController.selectedToken;
final tokens = overlayController.pangeaMessageEvent.originalSent?.tokens;
final selectedToken = overlayController.selectedToken;
final selectedTokenIndex = selectedToken != null
? tokens?.indexWhere(
(t) => t.text == selectedToken.text,
@ -82,121 +37,33 @@ class ReadingAssistanceContentState extends State<ReadingAssistanceContent> {
-1
: -1;
switch (widget.overlayController.toolbarMode) {
case MessageMode.messageTranslation:
// return MessageTranslationCard(
// messageEvent: widget.pangeaMessageEvent,
// );
case MessageMode.messageSpeechToText:
// return MessageSpeechToTextCard(
// messageEvent: widget.pangeaMessageEvent,
// );
case MessageMode.noneSelected:
// return Padding(
// padding: const EdgeInsets.all(8),
// child: Text(
// L10n.of(context).clickWordsInstructions,
// textAlign: TextAlign.center,
// ),
// );
case MessageMode.messageMeaning:
// return MessageMeaningCard(controller: widget.overlayController);
case MessageMode.listening:
// return MessageAudioCard(
// messageEvent: widget.overlayController.pangeaMessageEvent!,
// overlayController: widget.overlayController,
// setIsPlayingAudio: widget.overlayController.setIsPlayingAudio);
case MessageMode.practiceActivity:
case MessageMode.wordZoom:
case MessageMode.wordEmoji:
case MessageMode.wordMorph:
case MessageMode.wordMeaning:
if (widget.overlayController.selectedToken == null) {
return Padding(
padding: const EdgeInsets.all(16),
child: Text(
L10n.of(context).clickWordsInstructions,
textAlign: TextAlign.center,
),
);
}
return WordZoomWidget(
key: MatrixState.pAnyState
.layerLinkAndKey(
"word-zoom-card-${widget.overlayController.selectedToken!.text.uniqueKey}",
)
.key,
token: widget.overlayController.selectedToken!.text,
construct: widget.overlayController.selectedToken!.vocabConstructID,
event: widget.overlayController.event,
wordIsNew: widget.overlayController
.isNewToken(widget.overlayController.selectedToken!),
onClose: () => widget.overlayController.updateSelectedSpan(null),
langCode: widget
.overlayController.pangeaMessageEvent.messageDisplayLangCode,
onDismissNewWordOverlay: () =>
widget.overlayController.setState(() {}),
requestData: selectedTokenIndex >= 0
? TokenInfoFeedbackRequestData(
userId: Matrix.of(context).client.userID!,
roomId: widget.overlayController.event.room.id,
fullText: widget
.overlayController.pangeaMessageEvent.messageDisplayText,
detectedLanguage: widget.overlayController.pangeaMessageEvent
.messageDisplayLangCode,
tokens: tokens ?? [],
selectedToken: selectedTokenIndex,
wordCardL1: MatrixState.pangeaController.languageController
.activeL1Code()!,
)
: null,
pangeaMessageEvent: widget.overlayController.pangeaMessageEvent,
);
}
}
@override
Widget build(BuildContext context) {
if (![MessageTypes.Text, MessageTypes.Audio].contains(
widget.overlayController.pangeaMessageEvent.event.messageType,
)) {
return const SizedBox();
}
return Material(
type: MaterialType.transparency,
child: SelectionArea(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4.0,
),
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
),
),
constraints: BoxConstraints(
minWidth: min(
AppConfig.toolbarMinWidth,
widget.overlayController.maxWidth,
),
maxWidth: widget.overlayController.maxWidth,
),
height: AppConfig.toolbarMaxHeight,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedSize(
duration: widget.animationDuration,
child: toolbarContent(context),
),
],
),
),
),
return WordZoomWidget(
key: MatrixState.pAnyState
.layerLinkAndKey(
"word-zoom-card-${overlayController.selectedToken!.text.uniqueKey}",
)
.key,
token: overlayController.selectedToken!.text,
construct: overlayController.selectedToken!.vocabConstructID,
event: overlayController.event,
wordIsNew: overlayController.isNewToken(overlayController.selectedToken!),
onClose: () => overlayController.updateSelectedSpan(null),
langCode: overlayController.pangeaMessageEvent.messageDisplayLangCode,
onDismissNewWordOverlay: () => overlayController.setState(() {}),
requestData: selectedTokenIndex >= 0
? TokenInfoFeedbackRequestData(
userId: Matrix.of(context).client.userID!,
roomId: overlayController.event.room.id,
fullText: overlayController.pangeaMessageEvent.messageDisplayText,
detectedLanguage:
overlayController.pangeaMessageEvent.messageDisplayLangCode,
tokens: tokens ?? [],
selectedToken: selectedTokenIndex,
wordCardL1: MatrixState.pangeaController.languageController
.activeL1Code()!,
)
: null,
pangeaMessageEvent: overlayController.pangeaMessageEvent,
);
}
}

View file

@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/lemmas/lemma_reaction_picker.dart';
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart';
import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_button.dart';
import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_unsubscribed_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_builder.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart';
@ -53,239 +54,280 @@ class WordZoomWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool? subscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
final overlayColor = Theme.of(context).scaffoldBackgroundColor;
return Stack(
children: [
Container(
height: AppConfig.toolbarMaxHeight - 8,
padding: const EdgeInsets.all(12.0),
constraints: const BoxConstraints(
maxWidth: AppConfig.toolbarMinWidth,
),
child: CompositedTransformTarget(
link: layerLink,
child: SingleChildScrollView(
child: Column(
spacing: 12.0,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
onClose != null
? SizedBox(
width: 24.0,
height: 24.0,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onClose,
child: const Icon(
Icons.close,
size: 16.0,
),
),
),
)
: const SizedBox(
width: 24.0,
height: 24.0,
),
Flexible(
child: Text(
token.content,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.w600,
height: 1.2,
color:
Theme.of(context).brightness == Brightness.light
? AppConfig.yellowDark
: AppConfig.yellowLight,
),
),
),
requestData != null && pangeaMessageEvent != null
? TokenInfoFeedbackButton(
requestData: requestData!,
langCode: langCode,
event: pangeaMessageEvent!,
onUpdate: () {
// close the zoom when updating
if (onClose != null) {
onClose!();
}
},
)
: const SizedBox(
width: 24.0,
height: 24.0,
),
],
),
LemmaMeaningBuilder(
langCode: langCode,
constructId: construct,
builder: (context, controller) {
if (controller.editMode) {
return Column(
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
construct.lemma,
construct.category,
)}",
textAlign: TextAlign.center,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: TextField(
minLines: 1,
maxLines: 3,
controller: controller.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () =>
controller.toggleEditMode(false),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
),
child: Text(L10n.of(context).cancel),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () => controller.controller.text !=
controller.lemmaInfo?.meaning &&
controller.controller.text.isNotEmpty
? controller.editLemmaMeaning(
controller.controller.text,
)
: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
),
child: Text(L10n.of(context).saveChanges),
),
],
),
],
);
}
return Column(
spacing: 12.0,
mainAxisSize: MainAxisSize.min,
children: [
if (MatrixState.pangeaController.languageController
.showTranscription)
PhoneticTranscriptionWidget(
text: token.content,
textLanguage: PLanguageStore.byLangCode(
langCode,
) ??
LanguageModel.unknown,
style: const TextStyle(fontSize: 14.0),
iconSize: 24.0,
)
else
WordAudioButton(
text: token.content,
uniqueID: "lemma-content-${token.content}",
langCode: langCode,
iconSize: 24.0,
),
LemmaReactionPicker(
emojis: controller.lemmaInfo?.emoji ?? [],
loading: controller.isLoading,
event: event,
),
if (controller.error != null)
ErrorIndicator(
message: L10n.of(context).errorFetchingDefinition,
style: const TextStyle(fontSize: 14.0),
)
else if (controller.isLoading ||
controller.lemmaInfo == null)
const CircularProgressIndicator.adaptive()
else
GestureDetector(
onLongPress: () =>
controller.toggleEditMode(true),
onDoubleTap: () =>
controller.toggleEditMode(true),
child: construct.lemma.toLowerCase() ==
token.content.toLowerCase()
? Text(
controller.lemmaInfo!.meaning,
style: const TextStyle(fontSize: 14.0),
textAlign: TextAlign.center,
)
: RichText(
text: TextSpan(
style: DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 14.0,
),
children: [
TextSpan(text: construct.lemma),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
const TextSpan(text: ":"),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
TextSpan(
text: controller.lemmaInfo!.meaning,
),
],
final Widget content = subscribed != null && !subscribed
? const MessageUnsubscribedCard()
: Stack(
children: [
Container(
height: AppConfig.toolbarMaxHeight - 8,
padding: const EdgeInsets.all(12.0),
constraints: const BoxConstraints(
maxWidth: AppConfig.toolbarMinWidth,
),
child: CompositedTransformTarget(
link: layerLink,
child: SingleChildScrollView(
child: Column(
spacing: 12.0,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
onClose != null
? SizedBox(
width: 24.0,
height: 24.0,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onClose,
child: const Icon(
Icons.close,
size: 16.0,
),
),
),
)
: const SizedBox(
width: 24.0,
height: 24.0,
),
Flexible(
child: Text(
token.content,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.w600,
height: 1.2,
color: Theme.of(context).brightness ==
Brightness.light
? AppConfig.yellowDark
: AppConfig.yellowLight,
),
),
),
],
);
},
requestData != null && pangeaMessageEvent != null
? TokenInfoFeedbackButton(
requestData: requestData!,
langCode: langCode,
event: pangeaMessageEvent!,
onUpdate: () {
// close the zoom when updating
if (onClose != null) {
onClose!();
}
},
)
: const SizedBox(
width: 24.0,
height: 24.0,
),
],
),
LemmaMeaningBuilder(
langCode: langCode,
constructId: construct,
builder: (context, controller) {
if (controller.editMode) {
return Column(
children: [
Text(
"${L10n.of(context).pangeaBotIsFallible} ${L10n.of(context).whatIsMeaning(
construct.lemma,
construct.category,
)}",
textAlign: TextAlign.center,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: TextField(
minLines: 1,
maxLines: 3,
controller: controller.controller,
decoration: InputDecoration(
hintText: controller.lemmaInfo?.meaning,
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () =>
controller.toggleEditMode(false),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
),
child: Text(L10n.of(context).cancel),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () =>
controller.controller.text !=
controller.lemmaInfo
?.meaning &&
controller.controller.text
.isNotEmpty
? controller.editLemmaMeaning(
controller.controller.text,
)
: null,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
),
child:
Text(L10n.of(context).saveChanges),
),
],
),
],
);
}
return Column(
spacing: 12.0,
mainAxisSize: MainAxisSize.min,
children: [
if (MatrixState.pangeaController
.languageController.showTranscription)
PhoneticTranscriptionWidget(
text: token.content,
textLanguage: PLanguageStore.byLangCode(
langCode,
) ??
LanguageModel.unknown,
style: const TextStyle(fontSize: 14.0),
iconSize: 24.0,
)
else
WordAudioButton(
text: token.content,
uniqueID: "lemma-content-${token.content}",
langCode: langCode,
iconSize: 24.0,
),
LemmaReactionPicker(
emojis: controller.lemmaInfo?.emoji ?? [],
loading: controller.isLoading,
event: event,
),
if (controller.error != null)
ErrorIndicator(
message: L10n.of(context)
.errorFetchingDefinition,
style: const TextStyle(fontSize: 14.0),
)
else if (controller.isLoading ||
controller.lemmaInfo == null)
const CircularProgressIndicator.adaptive()
else
GestureDetector(
onLongPress: () =>
controller.toggleEditMode(true),
onDoubleTap: () =>
controller.toggleEditMode(true),
child: construct.lemma.toLowerCase() ==
token.content.toLowerCase()
? Text(
controller.lemmaInfo!.meaning,
style:
const TextStyle(fontSize: 14.0),
textAlign: TextAlign.center,
)
: RichText(
text: TextSpan(
style:
DefaultTextStyle.of(context)
.style
.copyWith(
fontSize: 14.0,
),
children: [
TextSpan(text: construct.lemma),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
const TextSpan(text: ":"),
const WidgetSpan(
child: SizedBox(width: 8.0),
),
TextSpan(
text: controller
.lemmaInfo!.meaning,
),
],
),
),
),
],
);
},
),
],
),
),
],
),
),
wordIsNew
? NewWordOverlay(
key: ValueKey(transformTargetId),
overlayColor: overlayColor,
transformTargetId: transformTargetId,
onDismiss: onDismissNewWordOverlay,
)
: const SizedBox.shrink(),
],
);
return Material(
type: MaterialType.transparency,
child: SelectionArea(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4.0,
),
borderRadius: const BorderRadius.all(
Radius.circular(AppConfig.borderRadius),
),
),
height: AppConfig.toolbarMaxHeight,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
content,
],
),
),
wordIsNew
? NewWordOverlay(
key: ValueKey(transformTargetId),
overlayColor: overlayColor,
transformTargetId: transformTargetId,
onDismiss: onDismissNewWordOverlay,
)
: const SizedBox.shrink(),
],
),
);
}
}

View file

@ -8,6 +8,7 @@ import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
import 'package:fluffychat/utils/other_party_can_receive.dart';
import 'uia_request_manager.dart';
@ -29,6 +30,11 @@ extension LocalizedExceptionExtension on Object {
BuildContext context, [
ExceptionContext? exceptionContext,
]) {
// #Pangea
if (this is UnsubscribedException) {
return L10n.of(context).unsubscribedResponseError;
}
// Pangea#
if (this is FileTooBigMatrixException) {
final exception = this as FileTooBigMatrixException;
return L10n.of(context).fileIsTooBigForServer(