diff --git a/lib/pangea/chat_settings/pages/pangea_room_details.dart b/lib/pangea/chat_settings/pages/pangea_room_details.dart index 497a8493d..c0573ed5c 100644 --- a/lib/pangea/chat_settings/pages/pangea_room_details.dart +++ b/lib/pangea/chat_settings/pages/pangea_room_details.dart @@ -42,7 +42,10 @@ class PangeaRoomDetailsView extends StatelessWidget { ), body: Padding( padding: const EdgeInsetsGeometry.only( - top: 16.0, left: 16.0, right: 16.0), + top: 16.0, + left: 16.0, + right: 16.0, + ), child: MaxWidthBody( maxWidth: 900, showBorder: false, diff --git a/lib/pangea/choreographer/models/igc_text_data_model.dart b/lib/pangea/choreographer/models/igc_text_data_model.dart index 3991aa4f7..e52bad196 100644 --- a/lib/pangea/choreographer/models/igc_text_data_model.dart +++ b/lib/pangea/choreographer/models/igc_text_data_model.dart @@ -1,24 +1,18 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/choreographer/models/span_data.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/igc/autocorrect_popup.dart'; +import 'package:fluffychat/pangea/choreographer/utils/match_style_util.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/igc/autocorrect_span.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; -import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; -// import 'package:language_tool/language_tool.dart'; - class IGCTextData { String originalInput; String? fullTextCorrection; @@ -27,7 +21,6 @@ class IGCTextData { String userL2; bool enableIT; bool enableIGC; - bool loading = false; IGCTextData({ required this.originalInput, @@ -41,8 +34,8 @@ class IGCTextData { factory IGCTextData.fromJson(Map json) { return IGCTextData( - matches: json[_matchesKey] != null - ? (json[_matchesKey] as Iterable) + matches: json["matches"] != null + ? (json["matches"] as Iterable) .map( (e) { return PangeaMatch.fromJson(e as Map); @@ -60,58 +53,151 @@ class IGCTextData { ); } - factory IGCTextData.fromRepresentationEvent( - RepresentationEvent event, - String userL1, - String userL2, - ) { - final PangeaRepresentation content = event.content; - final List matches = event.choreo?.choreoSteps - .map((step) => step.acceptedOrIgnoredMatch) - .whereType() - .toList() ?? - []; - - String originalInput = content.text; - if (matches.isNotEmpty) { - originalInput = matches.first.match.fullText; - } - - return IGCTextData( - originalInput: originalInput, - fullTextCorrection: content.text, - matches: matches, - userL1: userL1, - userL2: userL2, - enableIT: true, - enableIGC: true, - ); - } - - static const String _matchesKey = "matches"; - Map toJson() => { "original_input": originalInput, "full_text_correction": fullTextCorrection, - _matchesKey: matches.map((e) => e.toJson()).toList(), + "matches": matches.map((e) => e.toJson()).toList(), ModelKey.userL1: userL1, ModelKey.userL2: userL2, "enable_it": enableIT, "enable_igc": enableIGC, }; - // reconstruct fullText based on accepted match - //update offsets in existing matches to reflect the change - //if existing matches overlap with the accepted one, remove them?? + 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) => + _matchIndicesByOffset(offset).firstWhereOrNull((matchIndex) { + final match = matches[matchIndex]; + return (enableIT && (match.isITStart || match.needsTranslation)) || + (enableIGC && match.isGrammarMatch); + }) ?? + -1; + + int? get _openMatchIndex { + final RegExp pattern = RegExp(r'span_card_overlay_\d+'); + final String? matchingKeys = + MatrixState.pAnyState.getMatchingOverlayKeys(pattern).firstOrNull; + if (matchingKeys == null) return null; + + final int? index = int.tryParse(matchingKeys.split("_").last); + if (index == null || + matches.length <= index || + matches[index].status != PangeaMatchStatus.open) { + return null; + } + + return index; + } + + InlineSpan _matchSpan( + PangeaMatch match, + TextStyle style, + void Function(PangeaMatch)? onUndo, + ) { + if (match.status == PangeaMatchStatus.automatic) { + final span = originalInput.characters + .getRange( + match.match.offset, + match.match.offset + match.match.length, + ) + .toString(); + + final originalText = match.match.fullText.characters + .getRange( + match.match.offset, + match.match.offset + match.match.length, + ) + .toString(); + + return AutocorrectSpan( + transformTargetId: + "autocorrection_${match.match.offset}_${match.match.length}", + currentText: span, + originalText: originalText, + onUndo: () => onUndo?.call(match), + style: style, + ); + } else { + return TextSpan( + text: originalInput.characters + .getRange( + match.match.offset, + match.match.offset + match.match.length, + ) + .toString(), + style: style, + ); + } + } + + /// Returns a list of [TextSpan]s used to display the text in the input field + /// with the appropriate styling for each error match. + List constructTokenSpan({ + required List choreoSteps, + void Function(PangeaMatch)? onUndo, + TextStyle? defaultStyle, + }) { + final automaticMatches = choreoSteps + .map((s) => s.acceptedOrIgnoredMatch) + .whereType() + .where((m) => m.status == PangeaMatchStatus.automatic) + .toList(); + + final textSpanMatches = [...matches, ...automaticMatches] + ..sort((a, b) => a.match.offset.compareTo(b.match.offset)); + + final spans = []; + int cursor = 0; + + for (final match in textSpanMatches) { + if (cursor < match.match.offset) { + final text = originalInput.characters + .getRange(cursor, match.match.offset) + .toString(); + spans.add(TextSpan(text: text, style: defaultStyle)); + } + + final matchIndex = matches.indexWhere( + (m) => m.match.offset == match.match.offset, + ); + + final style = MatchStyleUtil.textStyle( + match, + defaultStyle, + _openMatchIndex != null && _openMatchIndex == matchIndex, + ); + + spans.add(_matchSpan(match, style, onUndo)); + cursor = match.match.offset + match.match.length; + } + + if (cursor < originalInput.characters.length) { + spans.add( + TextSpan( + text: originalInput.characters + .getRange(cursor, originalInput.characters.length) + .toString(), + style: defaultStyle, + ), + ); + } + + return spans; + } + void acceptReplacement( int matchIndex, int choiceIndex, - ) async { - //should be already added to choreoRecord - //TODO - that should be done in the same function to avoid error potential - - final PangeaMatch pangeaMatch = matches[matchIndex]; - + ) { + final PangeaMatch pangeaMatch = matches.removeAt(matchIndex); if (pangeaMatch.match.choices == null) { debugger(when: kDebugMode); ErrorHandler.logError( @@ -123,41 +209,21 @@ class IGCTextData { return; } - final SpanChoice replacement = pangeaMatch.match.choices![choiceIndex]; - - final newStart = originalInput.characters.take(pangeaMatch.match.offset); - final newEnd = originalInput.characters - .skip(pangeaMatch.match.offset + pangeaMatch.match.length); - final fullText = newStart + replacement.value.characters + newEnd; - originalInput = fullText.toString(); - - // update offsets in existing matches to reflect the change - // Question - remove matches that overlap with the accepted one? - // see case of "quiero ver un fix" - matches.removeAt(matchIndex); - - for (final match in matches) { - match.match.fullText = originalInput; - if (match.match.offset > pangeaMatch.match.offset) { - match.match.offset += - replacement.value.length - pangeaMatch.match.length; - } - } + _runReplacement( + pangeaMatch.match.offset, + pangeaMatch.match.length, + pangeaMatch.match.choices![choiceIndex].value, + ); } - void undoReplacement(PangeaMatch match) async { - if (match.match.choices == null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "pangeaMatch.match.choices is null in undoReplacement", - data: { - "match": match.match.toJson(), - }, - ); - return; - } + void undoReplacement(PangeaMatch match) { + final choice = match.match.choices + ?.firstWhereOrNull( + (c) => c.isBestCorrection, + ) + ?.value; - if (!match.match.choices!.any((c) => c.isBestCorrection)) { + if (choice == null) { debugger(when: kDebugMode); ErrorHandler.logError( m: "pangeaMatch.match.choices has no best correction in undoReplacement", @@ -168,9 +234,6 @@ class IGCTextData { return; } - final bestCorrection = - match.match.choices!.firstWhere((c) => c.isBestCorrection).value; - final String replacement = match.match.fullText.characters .getRange( match.match.offset, @@ -178,267 +241,29 @@ class IGCTextData { ) .toString(); - final newStart = originalInput.characters.take(match.match.offset); - final newEnd = originalInput.characters.skip( - match.match.offset + bestCorrection.characters.length, + _runReplacement( + match.match.offset, + choice.characters.length, + replacement, ); + } + + /// Internal runner for applying a replacement to the current text. + void _runReplacement( + int offset, + int length, + String replacement, + ) { + final newStart = originalInput.characters.take(offset); + final newEnd = originalInput.characters.skip(offset + length); final fullText = newStart + replacement.characters + newEnd; originalInput = fullText.toString(); - for (final remainingMatch in matches) { - remainingMatch.match.fullText = originalInput; - if (remainingMatch.match.offset > match.match.offset) { - remainingMatch.match.offset += - match.match.length - bestCorrection.characters.length; + for (final match in matches) { + match.match.fullText = originalInput; + if (match.match.offset > offset) { + match.match.offset += replacement.characters.length - length; } } } - - 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); - final int matchIndex = matchesForToken.indexWhere((matchIndex) { - final match = matches[matchIndex]; - return (enableIT && (match.isITStart || match.isl1SpanMatch)) || - (enableIGC && match.isGrammarMatch); - }); - if (matchIndex == -1) return -1; - return matchesForToken[matchIndex]; - } - - static TextStyle underlineStyle(Color color) => TextStyle( - decoration: TextDecoration.underline, - decorationColor: color, - decorationThickness: 5, - ); - - TextSpan getSpanItem({ - required int start, - required int end, - TextStyle? style, - }) { - return TextSpan( - text: originalInput.characters.getRange(start, end).toString(), - style: style, - ); - } - - int? get _openMatchIndex { - final RegExp pattern = RegExp(r'span_card_overlay_\d+'); - final String? matchingKeys = - MatrixState.pAnyState.getMatchingOverlayKeys(pattern).firstOrNull; - if (matchingKeys == null) return null; - final int? index = int.tryParse(matchingKeys.split("_").last); - if (index == null || - matches.length <= index || - matches[index].status != PangeaMatchStatus.open) { - return null; - } - - return index; - } - - //PTODO - handle multitoken spans - /// Returns a list of [TextSpan]s used to display the text in the input field - /// with the appropriate styling for each error match. - List constructTokenSpan({ - required List choreoSteps, - void Function(PangeaMatch)? onUndo, - TextStyle? defaultStyle, - }) { - final automaticMatches = choreoSteps - .where( - (step) => - step.acceptedOrIgnoredMatch?.status == - PangeaMatchStatus.automatic, - ) - .map((step) => step.acceptedOrIgnoredMatch) - .whereType() - .toList(); - - final List textSpanMatches = List.from(matches); - textSpanMatches.addAll(automaticMatches); - - final List items = []; - - if (loading) { - return [ - TextSpan( - text: originalInput, - style: defaultStyle, - ), - ]; - } - - textSpanMatches.sort((a, b) => a.match.offset.compareTo(b.match.offset)); - final List> matchRanges = textSpanMatches - .map( - (match) => [ - match.match.offset, - match.match.length + match.match.offset, - ], - ) - .toList(); - - // create a pointer to the current index in the original input - // and iterate until the pointer has reached the end of the input - int currentIndex = 0; - int loops = 0; - final List addedMatches = []; - while (currentIndex < originalInput.characters.length) { - if (loops > 100) { - ErrorHandler.logError( - e: "In constructTokenSpan, infinite loop detected", - data: { - "currentIndex": currentIndex, - "matches": textSpanMatches.map((m) => m.toJson()).toList(), - }, - ); - throw "In constructTokenSpan, infinite loop detected"; - } - - // check if the pointer is at a match, and if so, get the index of the match - final int matchIndex = matchRanges.indexWhere( - (range) => currentIndex >= range[0] && currentIndex < range[1], - ); - final bool inMatch = matchIndex != -1 && - !addedMatches.contains( - textSpanMatches[matchIndex], - ); - - if (matchIndex != -1 && - addedMatches.contains( - textSpanMatches[matchIndex], - )) { - ErrorHandler.logError( - e: "In constructTokenSpan, currentIndex is in match that has already been added", - data: { - "currentIndex": currentIndex, - "matchIndex": matchIndex, - "matches": textSpanMatches.map((m) => m.toJson()).toList(), - }, - ); - throw "In constructTokenSpan, currentIndex is in match that has already been added"; - } - - final prevIndex = currentIndex; - - if (inMatch) { - // if the pointer is in a match, then add that match to items - // and then move the pointer to the end of the match range - final PangeaMatch match = textSpanMatches[matchIndex]; - final style = match.textStyle( - matchIndex, - _openMatchIndex, - defaultStyle, - ); - if (match.status == PangeaMatchStatus.automatic) { - final span = originalInput.characters - .getRange( - match.match.offset, - match.match.offset + match.match.length, - ) - .toString(); - - final originalText = match.match.fullText.characters - .getRange( - match.match.offset, - match.match.offset + match.match.length, - ) - .toString(); - - items.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: CompositedTransformTarget( - link: MatrixState.pAnyState - .layerLinkAndKey("autocorrection_$matchIndex") - .link, - child: Builder( - builder: (context) { - return RichText( - key: MatrixState.pAnyState - .layerLinkAndKey("autocorrection_$matchIndex") - .key, - text: TextSpan( - text: span, - style: style, - recognizer: TapGestureRecognizer() - ..onTap = () { - OverlayUtil.showOverlay( - context: context, - child: AutocorrectPopup( - originalText: originalText, - onUndo: () => onUndo?.call(match), - ), - transformTargetId: "autocorrection_$matchIndex", - ); - }, - ), - ); - }, - ), - ), - ), - ); - - addedMatches.add(match); - currentIndex = match.match.offset + match.match.length; - } else { - items.add( - getSpanItem( - start: match.match.offset, - end: match.match.offset + match.match.length, - style: style, - ), - ); - currentIndex = match.match.offset + match.match.length; - } - } else { - // otherwise, if the pointer is not at a match, then add all the text - // until the next match (or, if there is not next match, the end of the - // text) to items and move the pointer to the start of the next match - final int nextIndex = matchRanges - .firstWhereOrNull( - (range) => range[0] > currentIndex, - ) - ?.first ?? - originalInput.characters.length; - - items.add( - getSpanItem( - start: currentIndex, - end: nextIndex, - style: defaultStyle, - ), - ); - currentIndex = nextIndex; - } - - if (prevIndex >= currentIndex) { - ErrorHandler.logError( - e: "In constructTokenSpan, currentIndex is less than prevIndex", - data: { - "currentIndex": currentIndex, - "prevIndex": prevIndex, - "matches": textSpanMatches.map((m) => m.toJson()).toList(), - }, - ); - throw "In constructTokenSpan, currentIndex is less than prevIndex"; - } - - loops++; - } - - return items; - } } diff --git a/lib/pangea/choreographer/models/pangea_match_model.dart b/lib/pangea/choreographer/models/pangea_match_model.dart index 749a9b7f5..8702ffda3 100644 --- a/lib/pangea/choreographer/models/pangea_match_model.dart +++ b/lib/pangea/choreographer/models/pangea_match_model.dart @@ -1,50 +1,20 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import '../constants/match_rule_ids.dart'; -import 'igc_text_data_model.dart'; import 'span_data.dart'; -enum PangeaMatchStatus { open, ignored, accepted, automatic, unknown } +enum PangeaMatchStatus { + open, + ignored, + accepted, + automatic, + unknown; -class PangeaMatch { - SpanData match; - - PangeaMatchStatus status; - - // String source; - - PangeaMatch({ - required this.match, - required this.status, - // required this.source, - }); - - factory PangeaMatch.fromJson(Map json) { - // try { - return PangeaMatch( - match: SpanData.fromJson(json[_matchKey] as Map), - status: json[_statusKey] != null - ? _statusStringToEnum(json[_statusKey]) - : PangeaMatchStatus.open, - // source: json[_matchKey]["source"] ?? "unk", - ); - // } catch (err) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // m: "unknown error in PangeaMatch.fromJson", data: json); - // rethrow; - // } - } - - String _statusEnumToString(dynamic status) => - status.toString().split('.').last; - - static PangeaMatchStatus _statusStringToEnum(String status) { + static PangeaMatchStatus fromString(String status) { final String lastPart = status.toString().split('.').last; switch (lastPart) { case 'open': @@ -53,16 +23,40 @@ class PangeaMatch { return PangeaMatchStatus.ignored; case 'accepted': return PangeaMatchStatus.accepted; + case 'automatic': + return PangeaMatchStatus.automatic; default: return PangeaMatchStatus.unknown; } } +} + +class PangeaMatch { + SpanData match; + PangeaMatchStatus status; + + PangeaMatch({ + required this.match, + required this.status, + }); + + factory PangeaMatch.fromJson(Map json) { + return PangeaMatch( + match: SpanData.fromJson(json[_matchKey] as Map), + status: json[_statusKey] != null + ? PangeaMatchStatus.fromString(json[_statusKey] as String) + : PangeaMatchStatus.open, + ); + } + + Map toJson() => { + _matchKey: match.toJson(), + _statusKey: status.name, + }; static const _matchKey = "match"; static const _statusKey = "status"; - bool get isl1SpanMatch => needsTranslation; - bool get isITStart => match.rule?.id == MatchRuleIds.interactiveTranslation || [SpanDataTypeEnum.itStart, SpanDataTypeEnum.itStart.name] @@ -79,12 +73,6 @@ class PangeaMatch { bool get isGrammarMatch => !isOutOfTargetMatch; - Map toJson() => { - _matchKey: match.toJson(), - // _detectionsKey: detections.map((e) => e.toJson()).toList(), - _statusKey: _statusEnumToString(status), - }; - String get matchContent { late int beginning; late int end; @@ -111,47 +99,5 @@ class PangeaMatch { bool isOffsetInMatchSpan(int offset) => offset >= match.offset && offset < match.offset + match.length; - Color get underlineColor { - if (status == PangeaMatchStatus.automatic) { - return const Color.fromARGB(187, 132, 96, 224); - } - - switch (match.rule?.id ?? "unknown") { - case MatchRuleIds.interactiveTranslation: - return const Color.fromARGB(187, 132, 96, 224); - case MatchRuleIds.tokenNeedsTranslation: - case MatchRuleIds.tokenSpanNeedsTranslation: - return const Color.fromARGB(186, 255, 132, 0); - default: - return const Color.fromARGB(149, 255, 17, 0); - } - } - - TextStyle textStyle( - int matchIndex, - int? openMatchIndex, - TextStyle? existingStyle, - ) { - double opacityFactor = 1.0; - if (openMatchIndex != null && openMatchIndex != matchIndex) { - opacityFactor = 0.2; - } - - final int alpha = (255 * opacityFactor).round(); - return existingStyle?.merge( - IGCTextData.underlineStyle( - underlineColor.withAlpha(alpha), - ), - ) ?? - IGCTextData.underlineStyle( - underlineColor.withAlpha(alpha), - ); - } - 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/choreographer/utils/match_copy.dart b/lib/pangea/choreographer/utils/match_copy.dart deleted file mode 100644 index 8f0e3a834..000000000 --- a/lib/pangea/choreographer/utils/match_copy.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import '../constants/match_rule_ids.dart'; -import '../models/pangea_match_model.dart'; - -class MatchCopy { - PangeaMatch match; - late String title; - String? description; - - MatchCopy(BuildContext context, this.match) { - if (match.match.rule?.id != null) { - _byMatchRuleId(context); - return; - } - if (match.match.shortMessage != null) { - title = match.match.shortMessage!; - } - if (match.match.message != null) { - description = match.match.message!; - } - if (match.match.shortMessage == null) { - _bySpanDataType(context); - } - } - - _setDefaults() { - try { - title = match.match.shortMessage ?? "unknown"; - description = match.match.message ?? "unknown"; - } catch (err) { - title = "Error"; - description = "Could not find the check info"; - } - } - - void _bySpanDataType(BuildContext context) { - try { - final L10n l10n = L10n.of(context); - switch (match.match.type.typeName) { - case SpanDataTypeEnum.correction: - title = l10n.someErrorTitle; - description = l10n.someErrorBody; - break; - case SpanDataTypeEnum.definition: - title = match.matchContent; - description = null; - break; - case SpanDataTypeEnum.itStart: - title = l10n.needsItShortMessage; - // description = l10n.needsItMessage; - break; - case SpanDataTypeEnum.practice: - title = match.match.shortMessage ?? "Activity"; - description = match.match.message; - break; - } - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: stack, - data: { - "match": match.toJson(), - }, - ); - _setDefaults(); - } - } - - void _byMatchRuleId(BuildContext context) { - try { - if (match.match.rule?.id == null) { - throw Exception("match.match.rule.id is null"); - } - final L10n l10n = L10n.of(context); - - final List splits = match.match.rule!.id.split(":"); - if (splits.length >= 2) { - splits.removeAt(0); - } - final String afterColon = splits.join(); - - debugPrint("grammar rule ${match.match.rule!.id}"); - - switch (afterColon) { - case MatchRuleIds.interactiveTranslation: - title = l10n.needsItShortMessage; - description = l10n.needsItMessage( - MatrixState - .pangeaController.languageController.userL2?.displayName ?? - "target language", - ); - break; - case MatchRuleIds.tokenNeedsTranslation: - title = l10n.tokenTranslationTitle; - description = l10n.spanTranslationDesc; - break; - case MatchRuleIds.tokenSpanNeedsTranslation: - title = l10n.spanTranslationTitle; - description = l10n.spanTranslationDesc; - break; - case MatchRuleIds.l1SpanAndGrammar: - title = l10n.l1SpanAndGrammarTitle; - description = l10n.l1SpanAndGrammarDesc; - break; - // case "PART": - // title = l10n.partTitle; - // description = l10n.partDesc; - // break; - // case "PUNCT": - // title = l10n.punctTitle; - // description = l10n.punctDesc; - // break; - // case "ORTH": - // title = l10n.orthTitle; - // description = l10n.orthDesc; - // break; - // case "SPELL": - // title = l10n.spellTitle; - // description = l10n.spellDesc; - // break; - // case "WO": - // title = l10n.woTitle; - // description = l10n.woDesc; - // break; - // case "MORPH": - // title = l10n.morphTitle; - // description = l10n.morphDesc; - // break; - // case "ADV": - // title = l10n.advTitle; - // description = l10n.advDesc; - // break; - // case "CONTR": - // title = l10n.contrTitle; - // description = l10n.contrDesc; - // break; - // case "CONJ": - // title = l10n.conjTitle; - // description = l10n.conjDesc; - // break; - // case "DET": - // title = l10n.detTitle; - // description = l10n.detDesc; - // break; - // case "DETART": - // title = l10n.detArtTitle; - // description = l10n.detArtDesc; - // break; - // case "PREP": - // title = l10n.prepTitle; - // description = l10n.prepDesc; - // break; - // case "PRON": - // title = l10n.pronTitle; - // description = l10n.pronDesc; - // break; - // case "VERB": - // title = l10n.verbTitle; - // description = l10n.verbDesc; - // break; - // case "VERBFORM": - // title = l10n.verbFormTitle; - // description = l10n.verbFormDesc; - // break; - // case "VERBTENSE": - // title = l10n.verbTenseTitle; - // description = l10n.verbTenseDesc; - // break; - // case "VERBSVA": - // title = l10n.verbSvaTitle; - // description = l10n.verbSvaDesc; - // break; - // case "VERBINFL": - // title = l10n.verbInflTitle; - // description = l10n.verbInflDesc; - // break; - // case "ADJ": - // title = l10n.adjTitle; - // description = l10n.adjDesc; - // break; - // case "ADJFORM": - // title = l10n.adjFormTitle; - // description = l10n.adjFormDesc; - // break; - // case "NOUN": - // title = l10n.nounTitle; - // description = l10n.nounDesc; - // break; - // case "NOUNPOSS": - // title = l10n.nounPossTitle; - // description = l10n.nounPossDesc; - // break; - // case "NOUNINFL": - // title = l10n.nounInflTitle; - // description = l10n.nounInflDesc; - // break; - // case "NOUNNUM": - // title = l10n.nounNumTitle; - // description = l10n.nounNumDesc; - // break; - case "OTHER": - default: - _setDefaults(); - break; - } - } catch (err, stack) { - debugger(when: kDebugMode); - ErrorHandler.logError( - e: err, - s: stack, - data: { - "match": match.toJson(), - }, - ); - _setDefaults(); - } - } -} diff --git a/lib/pangea/choreographer/utils/match_style_util.dart b/lib/pangea/choreographer/utils/match_style_util.dart new file mode 100644 index 000000000..2dd6c0a71 --- /dev/null +++ b/lib/pangea/choreographer/utils/match_style_util.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/choreographer/constants/match_rule_ids.dart'; +import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; + +class MatchStyleUtil { + static TextStyle underlineStyle(Color color) => TextStyle( + decoration: TextDecoration.underline, + decorationColor: color, + decorationThickness: 5, + ); + + static Color _underlineColor(PangeaMatch match) { + if (match.status == PangeaMatchStatus.automatic) { + return const Color.fromARGB(187, 132, 96, 224); + } + + switch (match.match.rule?.id ?? "unknown") { + case MatchRuleIds.interactiveTranslation: + return const Color.fromARGB(187, 132, 96, 224); + case MatchRuleIds.tokenNeedsTranslation: + case MatchRuleIds.tokenSpanNeedsTranslation: + return const Color.fromARGB(186, 255, 132, 0); + default: + return const Color.fromARGB(149, 255, 17, 0); + } + } + + static TextStyle textStyle( + PangeaMatch match, + TextStyle? existingStyle, + bool isOpenMatch, + ) { + double opacityFactor = 1.0; + if (!isOpenMatch) { + opacityFactor = 0.2; + } + + final alpha = (255 * opacityFactor).round(); + final style = underlineStyle(_underlineColor(match).withAlpha(alpha)); + return existingStyle?.merge(style) ?? style; + } +} diff --git a/lib/pangea/choreographer/widgets/igc/autocorrect_span.dart b/lib/pangea/choreographer/widgets/igc/autocorrect_span.dart new file mode 100644 index 000000000..f9e3a82fa --- /dev/null +++ b/lib/pangea/choreographer/widgets/igc/autocorrect_span.dart @@ -0,0 +1,45 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/choreographer/widgets/igc/autocorrect_popup.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class AutocorrectSpan extends WidgetSpan { + AutocorrectSpan({ + required String transformTargetId, + required String currentText, + required String originalText, + required VoidCallback onUndo, + required TextStyle style, + }) : super( + alignment: PlaceholderAlignment.middle, + child: CompositedTransformTarget( + link: MatrixState.pAnyState.layerLinkAndKey(transformTargetId).link, + child: Builder( + builder: (context) { + return RichText( + key: MatrixState.pAnyState + .layerLinkAndKey(transformTargetId) + .key, + text: TextSpan( + text: currentText, + style: style, + recognizer: TapGestureRecognizer() + ..onTap = () { + OverlayUtil.showOverlay( + context: context, + child: AutocorrectPopup( + originalText: originalText, + onUndo: onUndo, + ), + transformTargetId: transformTargetId, + ); + }, + ), + ); + }, + ), + ), + ); +} diff --git a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart index 11b397ddf..98384a32a 100644 --- a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart @@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; -import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/choreographer/utils/match_style_util.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; @@ -144,7 +144,7 @@ class PangeaTextController extends TextEditingController { return TextSpan( text: text, style: style?.merge( - IGCTextData.underlineStyle( + MatchStyleUtil.underlineStyle( const Color.fromARGB(187, 132, 96, 224), ), ),