refactor: simplify igc text data model, fixes autocorrection highligh… (#4504)
* refactor: simplify igc text data model, fixes autocorrection highlighting issue * add util for match text styles
This commit is contained in:
parent
c962c56cb7
commit
fc78da4731
7 changed files with 294 additions and 659 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> json) {
|
||||
return IGCTextData(
|
||||
matches: json[_matchesKey] != null
|
||||
? (json[_matchesKey] as Iterable)
|
||||
matches: json["matches"] != null
|
||||
? (json["matches"] as Iterable)
|
||||
.map<PangeaMatch>(
|
||||
(e) {
|
||||
return PangeaMatch.fromJson(e as Map<String, dynamic>);
|
||||
|
|
@ -60,58 +53,151 @@ class IGCTextData {
|
|||
);
|
||||
}
|
||||
|
||||
factory IGCTextData.fromRepresentationEvent(
|
||||
RepresentationEvent event,
|
||||
String userL1,
|
||||
String userL2,
|
||||
) {
|
||||
final PangeaRepresentation content = event.content;
|
||||
final List<PangeaMatch> matches = event.choreo?.choreoSteps
|
||||
.map((step) => step.acceptedOrIgnoredMatch)
|
||||
.whereType<PangeaMatch>()
|
||||
.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<String, dynamic> 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<int> _matchIndicesByOffset(int offset) {
|
||||
final List<int> 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<InlineSpan> constructTokenSpan({
|
||||
required List<ChoreoRecordStep> choreoSteps,
|
||||
void Function(PangeaMatch)? onUndo,
|
||||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final automaticMatches = choreoSteps
|
||||
.map((s) => s.acceptedOrIgnoredMatch)
|
||||
.whereType<PangeaMatch>()
|
||||
.where((m) => m.status == PangeaMatchStatus.automatic)
|
||||
.toList();
|
||||
|
||||
final textSpanMatches = [...matches, ...automaticMatches]
|
||||
..sort((a, b) => a.match.offset.compareTo(b.match.offset));
|
||||
|
||||
final spans = <InlineSpan>[];
|
||||
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<int> matchIndicesByOffset(int offset) {
|
||||
final List<int> matchesForOffset = [];
|
||||
for (final (index, match) in matches.indexed) {
|
||||
if (match.isOffsetInMatchSpan(offset)) {
|
||||
matchesForOffset.add(index);
|
||||
}
|
||||
}
|
||||
return matchesForOffset;
|
||||
}
|
||||
|
||||
int getTopMatchIndexForOffset(int offset) {
|
||||
final List<int> 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<InlineSpan> constructTokenSpan({
|
||||
required List<ChoreoRecordStep> choreoSteps,
|
||||
void Function(PangeaMatch)? onUndo,
|
||||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final automaticMatches = choreoSteps
|
||||
.where(
|
||||
(step) =>
|
||||
step.acceptedOrIgnoredMatch?.status ==
|
||||
PangeaMatchStatus.automatic,
|
||||
)
|
||||
.map((step) => step.acceptedOrIgnoredMatch)
|
||||
.whereType<PangeaMatch>()
|
||||
.toList();
|
||||
|
||||
final List<PangeaMatch> textSpanMatches = List.from(matches);
|
||||
textSpanMatches.addAll(automaticMatches);
|
||||
|
||||
final List<InlineSpan> items = [];
|
||||
|
||||
if (loading) {
|
||||
return [
|
||||
TextSpan(
|
||||
text: originalInput,
|
||||
style: defaultStyle,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
textSpanMatches.sort((a, b) => a.match.offset.compareTo(b.match.offset));
|
||||
final List<List<int>> 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<PangeaMatch> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic> json) {
|
||||
// try {
|
||||
return PangeaMatch(
|
||||
match: SpanData.fromJson(json[_matchKey] as Map<String, dynamic>),
|
||||
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<String, dynamic> json) {
|
||||
return PangeaMatch(
|
||||
match: SpanData.fromJson(json[_matchKey] as Map<String, dynamic>),
|
||||
status: json[_statusKey] != null
|
||||
? PangeaMatchStatus.fromString(json[_statusKey] as String)
|
||||
: PangeaMatchStatus.open,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
lib/pangea/choreographer/utils/match_style_util.dart
Normal file
43
lib/pangea/choreographer/utils/match_style_util.dart
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
45
lib/pangea/choreographer/widgets/igc/autocorrect_span.dart
Normal file
45
lib/pangea/choreographer/widgets/igc/autocorrect_span.dart
Normal file
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue