diff --git a/.vscode/settings.json b/.vscode/settings.json index dd15e28ee..59cdec5c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { "dart.previewLsp": true, "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true, - "source.sortMembers": false + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.sortMembers": "never" }, "editor.formatOnSave": true } \ No newline at end of file diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index a859c6229..1330fdb16 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3956,5 +3956,6 @@ "inNoSpaces": "You are not a member of any classes or exchanges", "successfullySubscribed": "You have successfully subscribed!", "clickToManageSubscription": "Click here to manage your subscription.", - "emptyInviteWarning": "Add this chat to a class or exchange to invite other users." -} \ No newline at end of file + "emptyInviteWarning": "Add this chat to a class or exchange to invite other users.", + "showDefinition": "Show Definition" +} diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index 8fc28bd42..2ff8a6605 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -1,9 +1,3 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; -import 'package:swipe_to_action/swipe_to_action.dart'; - import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; @@ -12,6 +6,11 @@ import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; +import 'package:swipe_to_action/swipe_to_action.dart'; + import '../../../config/app_config.dart'; import 'message_content.dart'; import 'message_reactions.dart'; diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index a498d22e2..69cc94860 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -1,9 +1,3 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pages/chat/events/video_player.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; @@ -12,6 +6,11 @@ import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; + import '../../../config/app_config.dart'; import '../../../utils/platform_infos.dart'; import '../../../utils/url_launcher.dart'; diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 04be9b485..549b8afa5 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -1,20 +1,18 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/models/span_card_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/overlay.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; - import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/models/span_card_model.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; import '../constants/model_keys.dart'; -import '../utils/overlay.dart'; import '../widgets/igc/span_card.dart'; -import '../widgets/igc/word_data_card.dart'; import 'language_detection_model.dart'; // import 'package:language_tool/language_tool.dart'; @@ -150,35 +148,30 @@ class IGCTextData { } } - int tokenIndexByOffset( - cursorOffset, - ) => - tokens.indexWhere( + int tokenIndexByOffset(cursorOffset) => tokens.indexWhere( (token) => - token.text.offset <= cursorOffset && - cursorOffset <= token.text.offset + token.text.length, + token.text.offset <= cursorOffset && cursorOffset <= token.end, ); - List getMatchIndicesForToken(PangeaToken token) => - matchIndicesByOffset(token.text.offset); + List matchIndicesByOffset(int offset) { + final List matchesForOffset = []; + for (final (index, match) in matches.indexed) { + if (match.isOffsetInMatchSpan(offset)) { + matchesForOffset.add(index); + } + } + return matchesForOffset; + } int getTopMatchIndexForOffset(int offset) { final List matchesForToken = matchIndicesByOffset(offset); - if (matchesForToken.isEmpty) return -1; - for (final matchIndex in matchesForToken) { + final int matchIndex = matchesForToken.indexWhere((matchIndex) { final match = matches[matchIndex]; - if (enableIT) { - if (match.isITStart || match.isl1SpanMatch) { - return matchIndex; - } - } - if (enableIGC) { - if (match.isGrammarMatch) { - return matchIndex; - } - } - } - return -1; + return (enableIT && (match.isITStart || match.isl1SpanMatch)) || + (enableIGC && match.isGrammarMatch); + }); + if (matchIndex == -1) return -1; + return matchesForToken[matchIndex]; } PangeaMatch? getTopMatchForToken(PangeaToken token) { @@ -187,23 +180,8 @@ class IGCTextData { return matches[topMatchIndex]; } - List matchIndicesByOffset(int offset) { - final List matchesForOffset = []; - - for (final (index, match) in matches.indexed) { - if (match.isOffsetInMatchSpan(offset)) { - matchesForOffset.add(index); - } - } - - return matchesForOffset; - } - - int getAfterTokenSpacingByIndex( - int tokenIndex, - ) { - final int endOfToken = - tokens[tokenIndex].text.offset + tokens[tokenIndex].text.length; + int getAfterTokenSpacingByIndex(int tokenIndex) { + final int endOfToken = tokens[tokenIndex].end; if (tokenIndex + 1 < tokens.length) { final spaceBetween = tokens[tokenIndex + 1].text.offset - endOfToken; @@ -218,7 +196,7 @@ class IGCTextData { ), ); ErrorHandler.logError( - m: "wierd token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}", + m: "weird token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}", ); return 0; } @@ -234,20 +212,42 @@ class IGCTextData { decorationThickness: 5, ); - static const _hasDefinitionStyle = TextStyle( - decoration: TextDecoration.underline, - decorationColor: Color.fromARGB(148, 83, 97, 255), - decorationThickness: 4, - ); - static TextStyle hasDefinitionStyle(TextStyle? existingStyle) => - existingStyle?.merge(_hasDefinitionStyle) ?? _hasDefinitionStyle; + List getMatchTokens() { + final List matchTokens = []; + int? endTokenIndex; + PangeaMatch? topMatch; + for (final (i, token) in tokens.indexed) { + if (endTokenIndex != null) { + if (i <= endTokenIndex) { + matchTokens.add( + MatchToken( + token: token, + match: topMatch, + ), + ); + continue; + } + endTokenIndex = null; + } + topMatch = getTopMatchForToken(token); + if (topMatch != null) { + endTokenIndex = tokens.indexWhere((e) => e.end >= topMatch!.end, i); + } + matchTokens.add( + MatchToken( + token: token, + match: topMatch, + ), + ); + } + return matchTokens; + } //PTODO - handle multitoken spans List constructTokenSpan({ required BuildContext context, TextStyle? defaultStyle, required SpanCardModel? spanCardModel, - required bool showTokens, required bool handleClick, required String transformTargetId, required Room room, @@ -263,73 +263,77 @@ class IGCTextData { ]; } - // or could make big strings for non-match text and therefore make less textspans. - // would that be more performant? - tokens.asMap().forEach( - (index, token) { - final PangeaMatch? topTokenMatch = getTopMatchForToken( - tokens[index], - ); - // if (index == 3) { - // debugPrint( - // "constructing span with topTokenMatch: ${topTokenMatch?.match.rule.id}"); - // } - - final Widget cardToShow = spanCardModel != null && topTokenMatch != null - ? SpanCard( - scm: spanCardModel, - ) - : WordDataCard( - fullText: originalInput, - fullTextLang: detections.first.langCode, - word: token.text.content, - wordLang: detections.first.langCode, - hasInfo: token.hasInfo, - room: room, - ); - - final TextStyle tokenStyle = topTokenMatch != null - ? topTokenMatch.textStyle(defaultStyle) - : hasDefinitionStyle(defaultStyle); + final List matchTokens = getMatchTokens(); + + for (int tokenIndex = 0; tokenIndex < matchTokens.length; tokenIndex++) { + final MatchToken matchToken = matchTokens[tokenIndex]; + final Widget? cardToShow = + matchToken.match != null && spanCardModel != null + ? SpanCard(scm: spanCardModel) + : null; + + int nextTokenIndex = matchTokens.indexWhere( + (e) => matchToken.match != null + ? e.match != matchToken.match + : e.match != null, + tokenIndex, + ); + + if (nextTokenIndex < 0) { + nextTokenIndex = matchTokens.length; + } + + final String matchText = originalInput.substring( + matchTokens[tokenIndex].token.text.offset, + matchTokens[nextTokenIndex - 1].token.end, + ); + items.add( + TextSpan( + text: matchText, + style: matchTokens[tokenIndex].match?.textStyle(defaultStyle) ?? + defaultStyle, + recognizer: handleClick && cardToShow != null + ? (TapGestureRecognizer() + ..onTapDown = (details) => OverlayUtil.showPositionedCard( + context: context, + cardToShow: cardToShow, + cardSize: + matchTokens[tokenIndex].match?.isITStart ?? false + ? const Size(350, 220) + : const Size(350, 400), + transformTargetId: transformTargetId, + )) + : null, + ), + ); + + final String beforeNextToken = originalInput.substring( + matchTokens[nextTokenIndex - 1].token.end, + nextTokenIndex < matchTokens.length + ? matchTokens[nextTokenIndex].token.text.offset + : originalInput.length, + ); + + if (beforeNextToken.isNotEmpty) { items.add( TextSpan( - text: token.text.content, - style: tokenStyle, - recognizer: handleClick - ? (TapGestureRecognizer() - ..onTapDown = (details) => OverlayUtil.showPositionedCard( - context: context, - cardToShow: cardToShow, - cardSize: topTokenMatch?.isITStart ?? false - ? const Size(350, 220) - : const Size(350, 400), - transformTargetId: transformTargetId, - )) - : null, + text: beforeNextToken, + style: defaultStyle, ), ); + } - final int charBetween = getAfterTokenSpacingByIndex( - index, - ); - - if (charBetween > 0) { - items.add( - TextSpan( - text: " " * charBetween, - style: topTokenMatch != null && - token.text.offset + token.text.length + charBetween <= - topTokenMatch.match.offset + - topTokenMatch.match.length - ? tokenStyle - : defaultStyle, - ), - ); - } - }, - ); + tokenIndex = nextTokenIndex - 1; + } return items; } } + +class MatchToken { + final PangeaToken token; + final PangeaMatch? match; + + MatchToken({required this.token, this.match}); +} diff --git a/lib/pangea/models/pangea_match_model.dart b/lib/pangea/models/pangea_match_model.dart index c7e97f4c0..827816890 100644 --- a/lib/pangea/models/pangea_match_model.dart +++ b/lib/pangea/models/pangea_match_model.dart @@ -1,10 +1,10 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/span_data_type.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/enum/span_data_type.dart'; -import 'package:fluffychat/pangea/utils/error_handler.dart'; import '../constants/match_rule_ids.dart'; import 'igc_text_data_model.dart'; import 'span_data.dart'; @@ -127,4 +127,9 @@ class PangeaMatch { IGCTextData.underlineStyle(underlineColor); PangeaMatch get copyWith => PangeaMatch.fromJson(toJson()); + + int get beginning => match.offset < 0 ? 0 : match.offset; + int get end => match.offset + match.length > match.fullText.length + ? match.fullText.length + : match.offset + match.length; } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 217cdc55b..19eaba750 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; - import 'package:sentry_flutter/sentry_flutter.dart'; import '../constants/model_keys.dart'; @@ -65,6 +64,8 @@ class PangeaToken { _hasInfoKey: hasInfo, _lemmaKey: lemmas.map((e) => e.toJson()).toList(), }; + + int get end => text.offset + text.length; } class PangeaTokenText { diff --git a/lib/pangea/utils/any_state_holder.dart b/lib/pangea/utils/any_state_holder.dart index 46efff974..e7ee11451 100644 --- a/lib/pangea/utils/any_state_holder.dart +++ b/lib/pangea/utils/any_state_holder.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; - import 'package:sentry_flutter/sentry_flutter.dart'; import '../models/widget_measurement.dart'; @@ -38,6 +37,12 @@ class PangeaAnyState { _layerLinkAndKeys.remove(transformTargetId); } + void openOverlay(OverlayEntry entry, BuildContext context) { + closeOverlay(); + overlay = entry; + Overlay.of(context).insert(overlay!); + } + void closeOverlay() { if (overlay != null) { overlay!.remove(); diff --git a/lib/pangea/utils/overlay.dart b/lib/pangea/utils/overlay.dart index 33dea2018..2d8c465eb 100644 --- a/lib/pangea/utils/overlay.dart +++ b/lib/pangea/utils/overlay.dart @@ -1,64 +1,101 @@ import 'dart:developer'; import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/utils/any_state_holder.dart'; import 'package:fluffychat/pangea/widgets/common_widgets/overlay_container.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import '../../config/themes.dart'; import '../../widgets/matrix.dart'; import 'error_handler.dart'; class OverlayUtil { - static showPositionedCard({ + static showOverlay({ required BuildContext context, - required Widget cardToShow, - required Size cardSize, + required Widget child, + required Size size, required String transformTargetId, + Offset? offset, backDropToDismiss = true, Color? borderColor, }) { try { MatrixState.pAnyState.closeOverlay(); - final LayerLinkAndKey layerLinkAndKey = MatrixState.pAnyState.layerLinkAndKey(transformTargetId); - final Offset cardOffset = _calculateCardOffset( - cardSize: cardSize, - transformTargetKey: layerLinkAndKey.key, - ); - - MatrixState.pAnyState.overlay = OverlayEntry( + final OverlayEntry entry = OverlayEntry( builder: (context) => Stack( children: [ + // GestureDetector to detect when dismissed by clicking outside + Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + MatrixState.pAnyState.closeOverlay(); + }, + ), + ), if (backDropToDismiss) const TransparentBackdrop(), Positioned( - width: cardSize.width, - height: cardSize.height, + width: size.width, + height: size.height, child: CompositedTransformFollower( link: layerLinkAndKey.link, showWhenUnlinked: false, - offset: cardOffset, - child: Material( - borderOnForeground: false, - color: Colors.transparent, - clipBehavior: Clip.antiAlias, - child: OverlayContainer( - cardToShow: cardToShow, - borderColor: borderColor, - ), - ), + offset: offset ?? Offset.zero, + child: child, ), ), ], ), ); - Overlay.of(layerLinkAndKey.key.currentContext!) - .insert(MatrixState.pAnyState.overlay!); + MatrixState.pAnyState.openOverlay(entry, context); + } catch (err, stack) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: stack); + } + } + + static showPositionedCard({ + required BuildContext context, + required Widget cardToShow, + required Size cardSize, + required String transformTargetId, + backDropToDismiss = true, + Color? borderColor, + }) { + try { + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(transformTargetId); + + final Offset cardOffset = _calculateCardOffset( + cardSize: cardSize, + transformTargetKey: layerLinkAndKey.key, + ); + + final Widget child = Material( + borderOnForeground: false, + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: OverlayContainer( + cardToShow: cardToShow, + borderColor: borderColor, + ), + ); + + showOverlay( + context: context, + child: child, + size: cardSize, + transformTargetId: transformTargetId, + offset: cardOffset, + backDropToDismiss: backDropToDismiss, + borderColor: borderColor, + ); } catch (err, stack) { debugger(when: kDebugMode); ErrorHandler.logError(e: err, s: stack); @@ -132,6 +169,8 @@ class OverlayUtil { return Offset(dx, dy); } + + static bool get isOverlayOpen => MatrixState.pAnyState.overlay != null; } class TransparentBackdrop extends StatelessWidget { diff --git a/lib/pangea/utils/show_defintion_util.dart b/lib/pangea/utils/show_defintion_util.dart new file mode 100644 index 000000000..000251102 --- /dev/null +++ b/lib/pangea/utils/show_defintion_util.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:fluffychat/pangea/utils/any_state_holder.dart'; +import 'package:fluffychat/pangea/utils/overlay.dart'; +import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class ShowDefintionUtil { + final String messageText; + final String langCode; + final String targetId; + final FocusNode focusNode = FocusNode(); + final Room room; + TextSelection? textSelection; + bool inCooldown = false; + + ShowDefintionUtil({ + required this.targetId, + required this.room, + required this.langCode, + required this.messageText, + }); + + void onTextSelection( + TextSelection selection, + SelectionChangedCause? cause, + BuildContext context, + ) { + selection.isCollapsed + ? clearTextSelection() + : setTextSelection( + selection, + cause, + context, + ); + } + + void setTextSelection( + TextSelection selection, + SelectionChangedCause? cause, + BuildContext context, + ) { + textSelection = selection; + if (BrowserContextMenu.enabled && kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + + if (kIsWeb && cause != SelectionChangedCause.tap) { + handleToolbar(context); + } + } + + void clearTextSelection() { + textSelection = null; + if (kIsWeb && !BrowserContextMenu.enabled) { + BrowserContextMenu.enableContextMenu(); + } + } + + void handleToolbar(BuildContext context) async { + if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return; + inCooldown = true; + Timer(const Duration(milliseconds: 750), () => inCooldown = false); + await Future.delayed(const Duration(milliseconds: 750)); + showToolbar(context); + } + + void showDefinition(BuildContext context) { + final String? fullText = textSelection?.textInside(messageText); + if (fullText == null) return; + OverlayUtil.showPositionedCard( + context: context, + cardToShow: WordDataCard( + word: fullText, + wordLang: langCode, + fullText: messageText, + fullTextLang: langCode, + hasInfo: false, + room: room, + ), + cardSize: const Size(300, 300), + transformTargetId: targetId, + backDropToDismiss: false, + ); + } + + // web toolbar + Future showToolbar(BuildContext context) async { + final LayerLinkAndKey layerLinkAndKey = + MatrixState.pAnyState.layerLinkAndKey(targetId); + final RenderBox? targetRenderBox = + (layerLinkAndKey.key.currentContext!.findRenderObject() as RenderBox?); + final Size? transformTargetSize = targetRenderBox?.size; + + Offset? transformTargetOffset; + if (transformTargetSize != null) { + transformTargetOffset = Offset( + (transformTargetSize.width / 2) - 65, + transformTargetSize.height * -1, + ); + } + + OverlayUtil.showOverlay( + context: context, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: Size.zero, + padding: EdgeInsets.zero, + ), + onPressed: () { + showDefinition(context); + }, + child: Text( + L10n.of(context)!.showDefinition, + style: const TextStyle( + fontSize: 14, + ), + ), + ), + size: const Size(130, 45), + transformTargetId: targetId, + offset: transformTargetOffset, + ); + } +} diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 746c65cc2..781f476bd 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -1,11 +1,7 @@ +import 'dart:async'; import 'dart:developer'; import 'dart:ui'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/constants/language_keys.dart'; @@ -13,12 +9,17 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/models/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:fluffychat/pangea/utils/show_defintion_util.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../../models/igc_text_data_model.dart'; import '../../models/language_detection_model.dart'; import '../../models/pangea_match_model.dart'; import '../../models/pangea_representation_event.dart'; -import '../../utils/bot_style.dart'; import '../../utils/instructions.dart'; class PangeaRichText extends StatefulWidget { @@ -51,6 +52,7 @@ class PangeaRichTextState extends State { bool _fetchingTokens = false; double get blur => _fetchingRepresentation && widget.immersionMode ? 5 : 0; List textSpan = []; + ShowDefintionUtil? messageToolbar; @override void initState() { @@ -68,6 +70,14 @@ class PangeaRichTextState extends State { Widget build(BuildContext context) { //TODO - take out of build function of every message // if (areLanguagesSet) { + messageToolbar = ShowDefintionUtil( + targetId: widget.pangeaMessageEvent.eventId, + room: widget.pangeaMessageEvent.room, + langCode: widget.selectedDisplayLang?.langCode ?? + userL2LangCode ?? + LanguageKeys.unknownLanguage, + messageText: textSpan.map((x) => x.text).join(), + ); if (!widget.selected && widget.selectedDisplayLang != null && @@ -85,29 +95,51 @@ class PangeaRichTextState extends State { ); } - final Widget richText = RichText( - text: TextSpan( - children: [ - ...textSpan, - if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) - // if (widget.selected) - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(left: 5.0), - child: SizedBox( - height: 14, - width: 14, - child: CircularProgressIndicator( - strokeWidth: 2.0, - color: AppConfig.secondaryColor, - ), + final TextSpan richTextSpan = TextSpan( + children: [ + ...textSpan, + if (widget.selected && (_fetchingRepresentation || _fetchingTokens)) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: 5.0), + child: SizedBox( + height: 14, + width: 14, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: AppConfig.secondaryColor, ), ), ), - ], - ), + ), + ], ); + final Widget richText = widget.selected + ? SelectableText.rich( + richTextSpan, + onSelectionChanged: (selection, cause) => kIsWeb + ? messageToolbar?.onTextSelection(selection, cause, context) + : null, + focusNode: messageToolbar?.focusNode, + contextMenuBuilder: (context, selection) { + return AdaptiveTextSelectionToolbar.buttonItems( + anchors: selection.contextMenuAnchors, + buttonItems: [ + ...selection.contextMenuButtonItems, + ContextMenuButtonItem( + label: L10n.of(context)!.showDefinition, + onPressed: () { + messageToolbar?.showDefinition(context); + messageToolbar?.focusNode.unfocus(); + }, + ), + ], + ); + }, + ) + : RichText(text: richTextSpan); + return blur > 0 ? ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), @@ -190,10 +222,9 @@ class PangeaRichTextState extends State { userL1: userL1LangCode ?? LanguageKeys.unknownLanguage, ).constructTokenSpan( context: context, - defaultStyle: textStyle(repEvent, context), + defaultStyle: widget.existingStyle, handleClick: true, spanCardModel: null, - showTokens: widget.definitions, transformTargetId: widget.pangeaMessageEvent.eventId, room: widget.pangeaMessageEvent.room, ); @@ -213,20 +244,10 @@ class PangeaRichTextState extends State { [ TextSpan( text: repEvent.text, - style: textStyle(repEvent, context), + style: widget.existingStyle, ), ]; - TextStyle? textStyle(RepresentationEvent repEvent, BuildContext context) => - // !repEvent.botAuthored - true - ? widget.existingStyle - : BotStyle.text( - context, - existingStyle: widget.existingStyle, - setColor: false, - ); - bool get areLanguagesSet => userL2LangCode != null && userL2LangCode != LanguageKeys.unknownLanguage; @@ -258,4 +279,75 @@ class PangeaRichTextState extends State { Future onSentenceRewrite(String sentenceRewrite) async { debugPrint("PTODO implement onSentenceRewrite"); } + + // void onTextSelection( + // TextSelection selection, + // SelectionChangedCause? _, + // ) => + // selection.isCollapsed + // ? clearTextSelection() + // : setTextSelection(selection); + + // void setTextSelection(TextSelection selection) { + // textSelection = selection; + // if (BrowserContextMenu.enabled && kIsWeb) { + // BrowserContextMenu.disableContextMenu(); + // } + // kIsWeb ? showToolbar() : showDefinition(); + // } + + // void clearTextSelection() { + // textSelection = null; + // if (kIsWeb && !BrowserContextMenu.enabled) { + // BrowserContextMenu.enableContextMenu(); + // } + // } + + // void showToolbar() async { + // if (toolbarShowing || !kIsWeb) return; + // toolbarShowing = true; + // await Future.delayed(const Duration(seconds: 2)); + + // final toolbarFuture = MessageToolbar.showToolbar( + // context, + // widget.pangeaMessageEvent.eventId, + // _focusNode.offset, + // ); + + // final resp = await toolbarFuture; + // toolbarShowing = false; + + // switch (resp) { + // case null: + // break; + // case 1: + // showDefinition(); + // break; + // default: + // break; + // } + // } + + // void showDefinition() { + // final String messageText = textSpan.map((x) => x.text).join(); + // final String fullText = textSelection!.textInside(messageText); + // final String langCode = widget.selectedDisplayLang?.langCode ?? + // userL2LangCode ?? + // LanguageKeys.unknownLanguage; + + // OverlayUtil.showPositionedCard( + // context: context, + // cardToShow: WordDataCard( + // word: fullText, + // wordLang: langCode, + // fullText: messageText, + // fullTextLang: langCode, + // hasInfo: false, + // room: widget.pangeaMessageEvent.room, + // ), + // cardSize: const Size(300, 300), + // transformTargetId: widget.pangeaMessageEvent.eventId, + // backDropToDismiss: false, + // ); + // } } diff --git a/lib/pangea/widgets/igc/pangea_text_controller.dart b/lib/pangea/widgets/igc/pangea_text_controller.dart index 5e64b4bef..886274316 100644 --- a/lib/pangea/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/widgets/igc/pangea_text_controller.dart @@ -1,13 +1,11 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; -import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import '../../choreographer/controllers/choreographer.dart'; import '../../enum/edit_type.dart'; -import '../../models/pangea_token_model.dart'; import '../../models/span_card_model.dart'; import '../../models/widget_measurement.dart'; import '../../utils/overlay.dart'; @@ -53,12 +51,11 @@ class PangeaTextController extends TextEditingController { if (tokenIndex == -1) return; - final PangeaToken token = choreographer.igc.igcTextData!.tokens[tokenIndex]; final int matchIndex = choreographer.igc.igcTextData!.getTopMatchIndexForOffset( selection.baseOffset, ); - final Widget cardToShow = matchIndex != -1 + final Widget? cardToShow = matchIndex != -1 ? SpanCard( scm: SpanCardModel( // igcTextData: choreographer.igc.igcTextData!, @@ -80,27 +77,19 @@ class PangeaTextController extends TextEditingController { ), roomId: choreographer.roomId, ) - : WordDataCard( - fullText: text, - fullTextLang: - choreographer.igc.igcTextData!.detections.first.langCode, - word: token.text.content, - //Note: this assumes that the token must be in the target language - //since it didn't have a match - wordLang: choreographer.itController.targetLangCode, - hasInfo: token.hasInfo, - room: choreographer.chatController.room, - ); - - OverlayUtil.showPositionedCard( - context: context, - cardSize: matchIndex != -1 && - choreographer.igc.igcTextData!.matches[matchIndex].isITStart - ? const Size(350, 220) - : const Size(350, 400), - cardToShow: cardToShow, - transformTargetId: choreographer.inputTransformTargetKey, - ); + : null; + + if (cardToShow != null) { + OverlayUtil.showPositionedCard( + context: context, + cardSize: matchIndex != -1 && + choreographer.igc.igcTextData!.matches[matchIndex].isITStart + ? const Size(350, 220) + : const Size(350, 400), + cardToShow: cardToShow, + transformTargetId: choreographer.inputTransformTargetKey, + ); + } } @override @@ -139,7 +128,6 @@ class PangeaTextController extends TextEditingController { ...choreographer.igc.igcTextData!.constructTokenSpan( context: context, defaultStyle: style, - showTokens: choreographer.definitionsEnabled, spanCardModel: null, handleClick: false, transformTargetId: choreographer.inputTransformTargetKey, diff --git a/needed-translations.txt b/needed-translations.txt index cdb97658e..becd1bd8f 100644 --- a/needed-translations.txt +++ b/needed-translations.txt @@ -765,7 +765,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "bn": [ @@ -1539,7 +1540,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "bo": [ @@ -2313,7 +2315,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ca": [ @@ -3082,7 +3085,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "cs": [ @@ -3851,7 +3855,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "de": [ @@ -4620,7 +4625,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "el": [ @@ -5394,7 +5400,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "eo": [ @@ -6163,7 +6170,12 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" + ], + + "es": [ + "showDefinition" ], "et": [ @@ -6932,7 +6944,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "eu": [ @@ -7701,7 +7714,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "fa": [ @@ -8470,7 +8484,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "fi": [ @@ -9239,7 +9254,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "fr": [ @@ -10008,7 +10024,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ga": [ @@ -10777,7 +10794,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "gl": [ @@ -11546,7 +11564,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "he": [ @@ -12315,7 +12334,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "hi": [ @@ -13089,7 +13109,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "hr": [ @@ -13858,7 +13879,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "hu": [ @@ -14627,7 +14649,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "id": [ @@ -15396,7 +15419,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ie": [ @@ -16167,7 +16191,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "it": [ @@ -16936,7 +16961,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ja": [ @@ -17705,7 +17731,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ko": [ @@ -18474,7 +18501,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "lt": [ @@ -19243,7 +19271,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "lv": [ @@ -20017,7 +20046,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "nb": [ @@ -20786,7 +20816,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "nl": [ @@ -21555,7 +21586,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pl": [ @@ -22324,7 +22356,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pt": [ @@ -23098,7 +23131,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pt_BR": [ @@ -23867,7 +23901,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "pt_PT": [ @@ -24636,7 +24671,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ro": [ @@ -25405,7 +25441,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ru": [ @@ -26174,7 +26211,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sk": [ @@ -26944,7 +26982,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sl": [ @@ -27716,7 +27755,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sr": [ @@ -28485,7 +28525,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "sv": [ @@ -29254,7 +29295,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "ta": [ @@ -30028,7 +30070,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "th": [ @@ -30802,7 +30845,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "tr": [ @@ -31571,7 +31615,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "uk": [ @@ -32340,7 +32385,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "vi": [ @@ -33112,7 +33158,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "zh": [ @@ -33881,7 +33928,8 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ], "zh_Hant": [ @@ -34650,6 +34698,7 @@ "activateTrial", "successfullySubscribed", "clickToManageSubscription", - "emptyInviteWarning" + "emptyInviteWarning", + "showDefinition" ] } diff --git a/pubspec.yaml b/pubspec.yaml index 926c61523..0b4b59916 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -138,6 +138,7 @@ flutter: generate: true uses-material-design: true assets: + - .env - assets/ # #Pangea - assets/pangea/