refactor IT bar

This commit is contained in:
ggurdin 2025-11-03 13:02:03 -05:00
parent 2b522b6dd7
commit 978d70822f
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
5 changed files with 459 additions and 423 deletions

View file

@ -303,13 +303,15 @@ class ChatController extends State<ChatPageWithRoom>
return;
}
if (!scrollController.hasClients) return;
if (timeline?.allowNewEvent == false ||
scrollController.position.pixels > 0 && _scrolledUp == false) {
setState(() => _scrolledUp = true);
} else if (scrollController.position.pixels <= 0 && _scrolledUp == true) {
setState(() => _scrolledUp = false);
setReadMarker();
}
// #Pangea
// if (timeline?.allowNewEvent == false ||
// scrollController.position.pixels > 0 && _scrolledUp == false) {
// setState(() => _scrolledUp = true);
// } else if (scrollController.position.pixels <= 0 && _scrolledUp == true) {
// setState(() => _scrolledUp = false);
// setReadMarker();
// }
// Pangea#
if (scrollController.position.pixels == 0 ||
scrollController.position.pixels == 64) {
@ -789,6 +791,8 @@ class ChatController extends State<ChatPageWithRoom>
_botAudioSubscription?.cancel();
_router.routeInformationProvider.removeListener(_onRouteChanged);
carouselController.dispose();
scrollController.dispose();
inputFocus.dispose();
TokensUtil.clearNewTokenCache();
//Pangea#
super.dispose();
@ -867,6 +871,7 @@ class ChatController extends State<ChatPageWithRoom>
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
);
inputFocus.unfocus();
sendController.setSystemText("", EditType.other);
setState(() => _fakeEventIDs.add(eventID));
@ -1697,7 +1702,7 @@ class ChatController extends State<ChatPageWithRoom>
void onSelectMessage(Event event) {
// #Pangea
if (choreographer.isITOpen) {
if (choreographer.itController.open.value) {
return;
}
// Pangea#
@ -1751,14 +1756,20 @@ class ChatController extends State<ChatPageWithRoom>
await choreographer.send();
} on ShowPaywallException {
PaywallCard.show(context, choreographer.inputTransformTargetKey);
return;
} on OpenMatchesException {
if (choreographer.firstIGCMatch != null) {
OverlayUtil.showIGCMatch(
choreographer.firstIGCMatch!,
choreographer,
context,
);
if (choreographer.firstOpenMatch != null) {
if (choreographer.firstOpenMatch!.updatedMatch.isITStart) {
choreographer.openIT(choreographer.firstOpenMatch!);
} else {
OverlayUtil.showIGCMatch(
choreographer.firstOpenMatch!,
choreographer,
context,
);
}
}
return;
}
// Pangea#
FocusScope.of(context).requestFocus(inputFocus);
@ -2064,13 +2075,6 @@ class ChatController extends State<ChatPageWithRoom>
});
}
double inputBarHeight = 64;
void updateInputBarHeight(double height) {
if (mounted && height != inputBarHeight) {
setState(() => inputBarHeight = height);
}
}
bool get displayChatDetailsColumn {
try {
return _displayChatDetailsColumn.value;
@ -2207,15 +2211,27 @@ class ChatController extends State<ChatPageWithRoom>
OverlayUtil.showPositionedCard(
context: context,
cardToShow: LanguageMismatchPopup(
targetLanguage: targetLanguage,
onUpdate: () async {
final igcMatch = await choreographer.requestLanguageAssistance();
if (igcMatch != null) {
OverlayUtil.showIGCMatch(
igcMatch,
choreographer,
context,
);
onConfirm: () async {
await MatrixState.pangeaController.userController.updateProfile(
(profile) {
profile.userSettings.targetLanguage = targetLanguage;
return profile;
},
waitForDataInSync: true,
);
await choreographer.requestLanguageAssistance();
final openMatch = choreographer.firstOpenMatch;
if (openMatch != null) {
if (openMatch.updatedMatch.isITStart) {
choreographer.openIT(openMatch);
} else {
OverlayUtil.showIGCMatch(
openMatch,
choreographer,
context,
);
}
}
},
),
@ -2246,9 +2262,10 @@ class ChatController extends State<ChatPageWithRoom>
targetAnchor: Alignment.topRight,
context: context,
child: MessageAnalyticsFeedback(
overlayId: "msg_analytics_feedback_$eventId",
newGrammarConstructs: newGrammarConstructs,
newVocabConstructs: newVocabConstructs,
close: () => MatrixState.pAnyState
.closeOverlay("msg_analytics_feedback_$eventId"),
),
transformTargetId: eventId,
ignorePointer: true,

View file

@ -44,13 +44,13 @@ class Choreographer extends ChangeNotifier {
ChoreoRecord? _choreoRecord;
bool _isFetching = false;
final ValueNotifier<bool> _isFetching = ValueNotifier(false);
int _timesClicked = 0;
Timer? _debounceTimer;
String? _lastChecked;
ChoreoMode _choreoMode = ChoreoMode.igc;
String? _sourceText;
final ValueNotifier<String?> _sourceText = ValueNotifier(null);
StreamSubscription? _languageStream;
StreamSubscription? _settingsUpdateStream;
@ -60,10 +60,10 @@ class Choreographer extends ChangeNotifier {
}
int get timesClicked => _timesClicked;
bool get isFetching => _isFetching;
ValueNotifier<bool> get isFetching => _isFetching;
ChoreoMode get choreoMode => _choreoMode;
String? get sourceText => _sourceText;
ValueNotifier<String?> get sourceText => _sourceText;
String get currentText => textController.text;
void _initialize() {
@ -94,9 +94,9 @@ class Choreographer extends ChangeNotifier {
_choreoMode = ChoreoMode.igc;
_lastChecked = null;
_timesClicked = 0;
_isFetching = false;
_isFetching.value = false;
_choreoRecord = null;
_sourceText = null;
_sourceText.value = null;
itController.clear();
igc.clear();
_resetDebounceTimer();
@ -109,6 +109,8 @@ class Choreographer extends ChangeNotifier {
textController.dispose();
_languageStream?.cancel();
_settingsUpdateStream?.cancel();
_debounceTimer?.cancel();
_isFetching.dispose();
TtsController.stop();
}
@ -123,7 +125,7 @@ class Choreographer extends ChangeNotifier {
// if user is doing IT, call closeIT here to
// ensure source text is replaced when needed
if (isITOpen && _timesClicked > 1) {
if (itController.open.value && _timesClicked > 1) {
closeIT();
}
}
@ -151,27 +153,42 @@ class Choreographer extends ChangeNotifier {
void _startLoading() {
_lastChecked = textController.text;
_isFetching = true;
_isFetching.value = true;
notifyListeners();
}
void _stopLoading() {
_isFetching = false;
_isFetching.value = false;
notifyListeners();
}
Future<PangeaMatchState?> requestLanguageAssistance() async {
await _getLanguageAssistance(manual: true);
if (igc.canShowFirstMatch) {
return igc.onShowFirstMatch();
}
return null;
}
Future<void> requestLanguageAssistance() =>
_getLanguageAssistance(manual: true);
Future<void> send() async {
Future<void> send([int recurrence = 0]) async {
// if isFetching, already called to getLanguageHelp and hasn't completed yet
// could happen if user clicked send button multiple times in a row
if (_isFetching) return;
if (_isFetching.value) return;
if (errorService.isError) {
await _sendWithIGC();
return;
}
if (recurrence > 1) {
ErrorHandler.logError(
e: Exception("Choreographer send exceeded max recurrences"),
level: SentryLevel.warning,
data: {
"currentText": chatController.sendController.text,
"l1LangCode": l1LangCode,
"l2LangCode": l2LangCode,
"choreoRecord": _choreoRecord?.toJson(),
},
);
await _sendWithIGC();
return;
}
if (igc.canShowFirstMatch) {
throw OpenMatchesException();
@ -200,9 +217,12 @@ class Choreographer extends ChangeNotifier {
if (!igc.hasIGCTextData && !itController.dismissed) {
await _getLanguageAssistance();
await send();
// it's possible for this not to be true, i.e. if IGC has an error
if (igc.hasIGCTextData) {
await send(recurrence + 1);
}
} else {
_sendWithIGC();
await _sendWithIGC();
}
}
@ -230,7 +250,7 @@ class Choreographer extends ChangeNotifier {
if (textController.editType == EditType.it) {
_getLanguageAssistance();
} else {
_sourceText = null;
_sourceText.value = null;
_debounceTimer ??= Timer(
const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart),
() => _getLanguageAssistance(),
@ -278,10 +298,10 @@ class Choreographer extends ChangeNotifier {
final message = chatController.sendController.text;
final fakeEventId = chatController.sendFakeMessage();
final PangeaRepresentation? originalWritten =
_choreoRecord?.includedIT == true && _sourceText != null
_choreoRecord?.includedIT == true && _sourceText.value != null
? PangeaRepresentation(
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
text: _sourceText!,
text: _sourceText.value!,
originalWritten: true,
originalSent: false,
)
@ -348,21 +368,21 @@ class Choreographer extends ChangeNotifier {
if (!itMatch.updatedMatch.isITStart) {
throw Exception("Attempted to open IT with a non-IT start match");
}
chatController.inputFocus.unfocus();
_choreoMode = ChoreoMode.it;
_sourceText = textController.text;
itController.openIT();
igc.clear();
setChoreoMode(ChoreoMode.it);
_sourceText.value = textController.text;
textController.setSystemText("", EditType.it);
itController.openIT();
igc.clear();
_initChoreoRecord();
itMatch.setStatus(PangeaMatchStatus.accepted);
_choreoRecord!.addRecord(
textController.text,
"",
match: itMatch.updatedMatch,
);
notifyListeners();
}
void closeIT() {
@ -394,7 +414,7 @@ class Choreographer extends ChangeNotifier {
}
void setSourceText(String? text) {
_sourceText = text;
_sourceText.value = text;
}
void setEditingSourceText(bool value) {
@ -403,7 +423,8 @@ class Choreographer extends ChangeNotifier {
}
void submitSourceTextEdits(String text) {
_sourceText = text;
_sourceText.value = text;
textController.setSystemText("", EditType.it);
itController.onSubmitEdits();
notifyListeners();
}

View file

@ -19,28 +19,27 @@ import 'choreographer.dart';
class ITController {
final Choreographer _choreographer;
ITStep? _currentITStep;
ValueNotifier<ITStep?> _currentITStep = ValueNotifier(null);
final List<Completer<ITStep>> _queue = [];
GoldRouteTracker? _goldRouteTracker;
bool _open = false;
bool _editing = false;
final ValueNotifier<bool> _open = ValueNotifier(false);
final ValueNotifier<bool> _editing = ValueNotifier(false);
bool _dismissed = false;
ITController(this._choreographer);
bool get open => _open;
bool get editing => _editing;
ValueNotifier<bool> get open => _open;
ValueNotifier<bool> get editing => _editing;
bool get dismissed => _dismissed;
List<Continuance>? get continuances => _currentITStep?.continuances;
bool get isTranslationDone => _currentITStep?.isFinal ?? false;
ValueNotifier<ITStep?> get currentITStep => _currentITStep;
String? get _sourceText => _choreographer.sourceText;
ValueNotifier<String?> get _sourceText => _choreographer.sourceText;
ITRequestModel _request(String textInput) {
assert(_sourceText != null);
assert(_sourceText.value != null);
return ITRequestModel(
text: _sourceText!,
text: _sourceText.value!,
customInput: textInput,
sourceLangCode:
MatrixState.pangeaController.languageController.activeL1Code()!,
@ -53,13 +52,15 @@ class ITController {
);
}
void openIT() => _open = true;
void openIT() {
_open.value = true;
}
void closeIT() {
// if the user hasn't gone through any IT steps, reset the text
if (_choreographer.currentText.isEmpty && _sourceText != null) {
if (_choreographer.currentText.isEmpty && _sourceText.value != null) {
_choreographer.textController.setSystemText(
_sourceText!,
_sourceText.value!,
EditType.itDismissed,
);
}
@ -70,77 +71,81 @@ class ITController {
void clear({bool dismissed = false}) {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
_open = false;
_editing = false;
_open.value = false;
_editing.value = false;
_dismissed = dismissed;
_queue.clear();
_currentITStep = null;
_currentITStep = ValueNotifier(null);
_goldRouteTracker = null;
_choreographer.setChoreoMode(ChoreoMode.igc);
_choreographer.setSourceText(null);
}
void setEditing(bool value) => _editing = value;
void setEditing(bool value) {
_editing.value = value;
}
void onSubmitEdits() {
_editing = false;
_editing.value = false;
_queue.clear();
_currentITStep = null;
_currentITStep = ValueNotifier(null);
_goldRouteTracker = null;
continueIT();
}
Continuance onSelectContinuance(int index) {
if (_currentITStep == null) {
throw "onSelectContinuance called with null currentITStep";
if (_currentITStep.value == null) {
throw "onSelectContinuance called when _currentITStep is null";
}
if (index < 0 || index >= _currentITStep!.continuances.length) {
if (index < 0 || index >= _currentITStep.value!.continuances.length) {
throw "onSelectContinuance called with invalid index $index";
}
final step = _currentITStep!.continuances[index];
_currentITStep!.continuances[index] = step.copyWith(
final currentStep = _currentITStep.value!;
currentStep.continuances[index] = currentStep.continuances[index].copyWith(
wasClicked: true,
);
return _currentITStep!.continuances[index];
_currentITStep.value = _currentITStep.value!.copyWith(
continuances: currentStep.continuances,
);
return _currentITStep.value!.continuances[index];
}
CompletedITStep getAcceptedITStep(int chosenIndex) {
if (_currentITStep == null) {
throw "getAcceptedITStep called with null currentITStep";
if (_currentITStep.value == null) {
throw "getAcceptedITStep called when _currentITStep is null";
}
if (chosenIndex < 0 || chosenIndex >= _currentITStep!.continuances.length) {
if (chosenIndex < 0 ||
chosenIndex >= _currentITStep.value!.continuances.length) {
throw "getAcceptedITStep called with invalid index $chosenIndex";
}
return CompletedITStep(
_currentITStep!.continuances,
_currentITStep.value!.continuances,
chosen: chosenIndex,
);
}
Future<void> continueIT() async {
if (_currentITStep == null) {
if (_currentITStep.value == null) {
await _initTranslationData();
return;
}
if (_queue.isEmpty) {
_choreographer.closeIT();
return;
}
final nextStepCompleter = _queue.removeAt(0);
try {
_currentITStep = await nextStepCompleter.future;
} catch (e) {
if (_open) {
_choreographer.errorService.setErrorAndLock(
ChoreoError(raw: e),
);
} else {
try {
final nextStepCompleter = _queue.removeAt(0);
_currentITStep.value = await nextStepCompleter.future;
} catch (e) {
if (_open.value) {
_choreographer.errorService.setErrorAndLock(
ChoreoError(raw: e),
);
}
}
}
}
@ -156,7 +161,7 @@ class ITController {
},
);
if (_sourceText == null || !_open) return;
if (_sourceText.value == null || !_open.value) return;
if (res.isError || res.result?.goldContinuances == null) {
_choreographer.errorService.setErrorAndLock(
ChoreoError(raw: res.asError),
@ -167,11 +172,11 @@ class ITController {
final result = res.result!;
_goldRouteTracker = GoldRouteTracker(
result.goldContinuances!,
_sourceText!,
_sourceText.value!,
);
_currentITStep = ITStep(
sourceText: _sourceText!,
_currentITStep.value = ITStep.fromResponse(
sourceText: _sourceText.value!,
currentText: currentText,
responseModel: res.result!,
storedGoldContinuances: _goldRouteTracker!.continuances,
@ -181,11 +186,13 @@ class ITController {
}
Future<void> _fillITStepQueue() async {
if (_sourceText == null || _goldRouteTracker!.continuances.length < 2) {
if (_sourceText.value == null ||
_goldRouteTracker!.continuances.length < 2) {
return;
}
final sourceText = _sourceText!;
final sourceText = _sourceText.value!;
final goldContinuances = _goldRouteTracker!.continuances;
String currentText =
_choreographer.currentText + _goldRouteTracker!.continuances[0].text;
@ -199,21 +206,22 @@ class ITController {
);
},
);
if (_queue.isEmpty) break;
if (res.isError) {
_queue.last.completeError(res.asError!);
break;
} else {
final step = ITStep(
final step = ITStep.fromResponse(
sourceText: sourceText,
currentText: currentText,
responseModel: res.result!,
storedGoldContinuances: _goldRouteTracker!.continuances,
storedGoldContinuances: goldContinuances,
);
_queue.last.complete(step);
}
currentText += _goldRouteTracker!.continuances[i].text;
currentText += goldContinuances[i].text;
}
}
}
@ -259,7 +267,9 @@ class ITStep {
late List<Continuance> continuances;
late bool isFinal;
ITStep({
ITStep({this.continuances = const [], this.isFinal = false});
factory ITStep.fromResponse({
required String sourceText,
required String currentText,
required ITResponseModel responseModel,
@ -269,8 +279,8 @@ class ITStep {
storedGoldContinuances ?? responseModel.goldContinuances ?? [];
final goldTracker = GoldRouteTracker(gold, sourceText);
isFinal = responseModel.isFinal;
final isFinal = responseModel.isFinal;
List<Continuance> continuances;
if (responseModel.continuances.isEmpty) {
continuances = [];
} else {
@ -298,5 +308,20 @@ class ITStep {
continuances = List<Continuance>.from(responseModel.continuances);
}
}
return ITStep(
continuances: continuances,
isFinal: isFinal,
);
}
ITStep copyWith({
List<Continuance>? continuances,
bool? isFinal,
}) {
return ITStep(
continuances: continuances ?? this.continuances,
isFinal: isFinal ?? this.isFinal,
);
}
}

View file

@ -94,7 +94,7 @@ class PangeaTextController extends TextEditingController {
final SubscriptionStatus canSendStatus = choreographer
.pangeaController.subscriptionController.subscriptionStatus;
if (canSendStatus == SubscriptionStatus.shouldShowPaywall &&
!choreographer.isFetching &&
!choreographer.isFetching.value &&
text.isNotEmpty) {
return TextSpan(
text: text,
@ -214,7 +214,7 @@ class PangeaTextController extends TextEditingController {
spans.add(TextSpan(text: text, style: defaultStyle));
}
final openMatch = choreographer.openIGCMatch?.updatedMatch.match;
final openMatch = choreographer.openMatch?.updatedMatch.match;
final style = _textStyle(
match.updatedMatch,
defaultStyle,

View file

@ -1,7 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
@ -10,7 +8,6 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_ui_extension.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_request_model.dart';
@ -34,223 +31,49 @@ class ITBar extends StatefulWidget {
}
class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
bool showedClickInstruction = false;
late AnimationController _controller;
late Animation<double> _animation;
bool wasOpen = false;
final TextEditingController _sourceTextController = TextEditingController();
Timer? _successTimer;
@override
void initState() {
super.initState();
// Rebuild the widget each time there's an update from choreo.
widget.choreographer.addListener(() {
if (widget.choreographer.isITOpen != wasOpen) {
widget.choreographer.isITOpen
? _controller.forward()
: _controller.reverse();
}
wasOpen = widget.choreographer.isITOpen;
setState(() {});
});
wasOpen = widget.choreographer.isITOpen;
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
// Start in the correct state
widget.choreographer.isITOpen
? _controller.forward()
: _controller.reverse();
_open.value ? _controller.forward() : _controller.reverse();
_open.addListener(() {
final nextText = _sourceText.value ?? widget.choreographer.currentText;
if (_sourceTextController.text != nextText) {
_sourceTextController.text = nextText;
}
_open.value ? _controller.forward() : _controller.reverse();
});
}
bool get showITInstructionsTooltip {
final toggledOff = InstructionsEnum.clickBestOption.isToggledOff;
if (!toggledOff) {
setState(() => showedClickInstruction = true);
}
return !toggledOff;
}
bool get showTranslationsChoicesTooltip {
return !showedClickInstruction &&
!showITInstructionsTooltip &&
!widget.choreographer.isFetching &&
!widget.choreographer.isEditingSourceText &&
!widget.choreographer.isITDone &&
widget.choreographer.itStepContinuances?.isNotEmpty == true;
}
final double iconDimension = 36;
final double iconSize = 20;
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
axisAlignment: -1.0,
child: CompositedTransformTarget(
link: widget.choreographer.itBarLinkAndKey.link,
child: Column(
spacing: 8.0,
children: [
if (showITInstructionsTooltip)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickBestOption,
animate: false,
),
if (showTranslationsChoicesTooltip)
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.translationChoices,
animate: false,
),
Container(
key: widget.choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer,
),
padding: const EdgeInsets.all(3),
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (widget.choreographer.isEditingSourceText)
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 10,
top: 10,
),
child: TextField(
controller: TextEditingController(
text: widget.choreographer.sourceText,
),
autofocus: true,
enableSuggestions: false,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted:
widget.choreographer.submitSourceTextEdits,
obscureText: false,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
),
if (!widget.choreographer.isEditingSourceText &&
widget.choreographer.sourceText != null)
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
onPressed: () => widget.choreographer
.setEditingSourceText(true),
icon: const Icon(Icons.edit_outlined),
// iconSize: 20,
),
),
if (!widget.choreographer.isEditingSourceText)
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.settings_outlined),
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
),
),
SizedBox(
width: iconDimension,
height: iconDimension,
child: IconButton(
iconSize: iconSize,
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: () {
widget.choreographer.isEditingSourceText
? widget.choreographer
.setEditingSourceText(false)
: widget.choreographer.closeIT();
},
),
),
],
),
if (!widget.choreographer.isEditingSourceText)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: !widget.choreographer.isITOpen
? const SizedBox()
: widget.choreographer.sourceText != null
? Text(
widget.choreographer.sourceText!,
textAlign: TextAlign.center,
)
: const LinearProgressIndicator(),
),
const SizedBox(height: 8.0),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
constraints: const BoxConstraints(minHeight: 80),
child: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: Center(
child: widget.choreographer.errorService.isError
? ITError(choreographer: widget.choreographer)
: widget.choreographer.isITDone
? const SizedBox()
: ITChoices(
choreographer: widget.choreographer,
),
),
),
),
],
),
),
),
],
),
),
);
void dispose() {
_controller.dispose();
_sourceTextController.dispose();
_successTimer?.cancel();
super.dispose();
}
}
class ITChoices extends StatelessWidget {
final Choreographer choreographer;
const ITChoices({
super.key,
required this.choreographer,
});
ValueNotifier<String?> get _sourceText => widget.choreographer.sourceText;
ValueNotifier<bool> get _open => widget.choreographer.itController.open;
void showCard(
BuildContext context,
void _showFeedbackCard(
int index, [
Color? borderColor,
String? choiceFeedback,
]) {
if (choreographer.itStepContinuances == null) {
final currentStep = widget.choreographer.itController.currentITStep.value;
if (currentStep == null) {
ErrorHandler.logError(
m: "currentITStep is null in showCard",
s: StackTrace.current,
@ -261,45 +84,40 @@ class ITChoices extends StatelessWidget {
return;
}
final text = choreographer.itStepContinuances![index].text;
choreographer.chatController.inputFocus.unfocus();
final text = currentStep.continuances[index].text;
MatrixState.pAnyState.closeOverlay("it_feedback_card");
OverlayUtil.showPositionedCard(
context: context,
cardToShow: choiceFeedback == null
? WordDataCard(
word: text,
wordLang: choreographer.l2LangCode!,
fullText: choreographer.sourceText ?? choreographer.currentText,
fullTextLang: choreographer.sourceText != null
? choreographer.l1LangCode!
: choreographer.l2LangCode!,
choiceFeedback: choiceFeedback,
wordLang: widget.choreographer.l2LangCode!,
fullText: _sourceText.value ?? widget.choreographer.currentText,
fullTextLang: widget.choreographer.l1LangCode!,
)
: ITFeedbackCard(
req: FullTextTranslationRequestModel(
FullTextTranslationRequestModel(
text: text,
tgtLang: choreographer.l2LangCode!,
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,
tgtLang: widget.choreographer.l1LangCode!,
userL1: widget.choreographer.l1LangCode!,
userL2: widget.choreographer.l2LangCode!,
),
choiceFeedback: choiceFeedback,
),
maxHeight: 300,
maxWidth: 300,
borderColor: borderColor,
transformTargetId: choreographer.itBarTransformTargetKey,
transformTargetId: widget.choreographer.itBarTransformTargetKey,
isScrollable: choiceFeedback == null,
overlayKey: "it_feedback_card",
ignorePointer: true,
);
}
void selectContinuance(int index, BuildContext context) {
void _selectContinuance(int index) {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
Continuance continuance;
try {
continuance = choreographer.onSelectContinuance(index);
continuance = widget.choreographer.onSelectContinuance(index);
} catch (e, s) {
ErrorHandler.logError(
e: e,
@ -309,33 +127,15 @@ class ITChoices extends StatelessWidget {
"index": index,
},
);
choreographer.closeIT();
widget.choreographer.closeIT();
return;
}
if (continuance.level == 1) {
Future.delayed(
const Duration(milliseconds: 500),
() {
try {
choreographer.onAcceptContinuance(index);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
level: SentryLevel.warning,
data: {
"index": index,
},
);
choreographer.closeIT();
return;
}
},
);
// CTODO doesn't always go to next continuance
_onCorrectSelection(index);
} else {
showCard(
context,
_showFeedbackCard(
index,
continuance.level == 2 ? ChoreoConstants.yellow : ChoreoConstants.red,
continuance.feedbackText(context),
@ -343,81 +143,254 @@ class ITChoices extends StatelessWidget {
}
}
@override
Widget build(BuildContext context) {
try {
if (choreographer.isEditingSourceText) {
return const SizedBox();
void _onCorrectSelection(int index) {
_successTimer?.cancel();
_successTimer = Timer(const Duration(milliseconds: 500), () {
if (!mounted) return;
try {
widget.choreographer.onAcceptContinuance(index);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
level: SentryLevel.warning,
data: {
"index": index,
},
);
widget.choreographer.closeIT();
}
if (choreographer.itStepContinuances == null) {
return choreographer.isITOpen
? CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox();
}
return ChoicesArray(
id: Object.hashAll(choreographer.itStepContinuances!).toString(),
isLoading: choreographer.isFetching ||
choreographer.itStepContinuances == null,
choices: choreographer.itStepContinuances!.map((e) {
debugPrint("WAS CLICKED: ${e.wasClicked}");
try {
return Choice(
text: e.text.trim(),
color: e.color,
isGold: e.description == "best",
);
} catch (e) {
debugger(when: kDebugMode);
return Choice(text: "error", color: Colors.red);
}
}).toList(),
onPressed: (value, index) => selectContinuance(index, context),
onLongPress: (value, index) => showCard(context, index),
selectedChoiceIndex: null,
langCode:
choreographer.pangeaController.languageController.activeL2Code(),
);
} catch (e) {
debugger(when: kDebugMode);
return const SizedBox();
}
});
}
}
class ITError extends StatelessWidget {
final Choreographer choreographer;
const ITError({
super.key,
required this.choreographer,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
ErrorIndicator(
message: L10n.of(context).translationError,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
return AnimatedBuilder(
animation: _animation,
builder: (context, child) => SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
child: child,
),
child: CompositedTransformTarget(
link: widget.choreographer.itBarLinkAndKey.link,
child: Column(
children: [
if (!InstructionsEnum.clickBestOption.isToggledOff) ...[
const InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickBestOption,
animate: false,
),
const SizedBox(height: 8.0),
],
Container(
key: widget.choreographer.itBarLinkAndKey.key,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer,
),
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 12.0,
children: [
_ITBarHeader(
onClose: widget.choreographer.closeIT,
setEditing: widget.choreographer.itController.setEditing,
editing: widget.choreographer.itController.editing,
sourceTextController: _sourceTextController,
sourceText: _sourceText,
onSubmitEdits: widget.choreographer.submitSourceTextEdits,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
constraints: const BoxConstraints(minHeight: 80),
child: Center(
child: widget.choreographer.errorService.isError
? Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
ErrorIndicator(
message: L10n.of(context).translationError,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
),
),
IconButton(
onPressed: widget.choreographer.closeIT,
icon: const Icon(
Icons.close,
size: 20,
),
),
],
)
: ValueListenableBuilder(
valueListenable: widget
.choreographer.itController.currentITStep,
builder: (context, step, __) {
return step == null
? CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(context)
.colorScheme
.primary,
)
: _ITChoices(
continuances: step.continuances,
onPressed: _selectContinuance,
onLongPressed: _showFeedbackCard,
);
},
),
),
),
],
),
),
),
IconButton(
onPressed: choreographer.closeIT,
icon: const Icon(
Icons.close,
size: 20,
),
),
],
],
),
),
);
}
}
class _ITBarHeader extends StatelessWidget {
final VoidCallback onClose;
final Function(String) onSubmitEdits;
final Function(bool) setEditing;
final ValueNotifier<bool> editing;
final TextEditingController sourceTextController;
final ValueNotifier<String?> sourceText;
const _ITBarHeader({
required this.onClose,
required this.setEditing,
required this.editing,
required this.onSubmitEdits,
required this.sourceTextController,
required this.sourceText,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: editing,
builder: (context, isEditing, __) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedCrossFade(
duration: const Duration(milliseconds: 200),
crossFadeState: isEditing
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Row(
spacing: 12.0,
children: [
Expanded(
child: TextField(
controller: sourceTextController,
autofocus: true,
enableSuggestions: false,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: onSubmitEdits,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: () => setEditing(false),
),
],
),
secondChild: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
color: Theme.of(context).colorScheme.primary,
onPressed: () => setEditing(true),
icon: const Icon(Icons.edit_outlined),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.settings_outlined),
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: onClose,
),
],
),
),
isEditing
? const SizedBox(height: 24.0)
: ValueListenableBuilder(
valueListenable: sourceText,
builder: (context, text, __) {
return Container(
padding: const EdgeInsets.only(top: 8.0),
constraints: const BoxConstraints(minHeight: 24.0),
child: sourceText.value != null
? Text(
sourceText.value!,
textAlign: TextAlign.center,
)
: const SizedBox(),
);
},
),
],
);
},
);
}
}
class _ITChoices extends StatelessWidget {
final List<Continuance> continuances;
final Function(int) onPressed;
final Function(int) onLongPressed;
const _ITChoices({
required this.continuances,
required this.onPressed,
required this.onLongPressed,
});
@override
Widget build(BuildContext context) {
return ChoicesArray(
id: Object.hashAll(continuances).toString(),
isLoading: false,
choices: [
...continuances.map(
(e) => Choice(
text: e.text.trim(),
color: e.color,
isGold: e.description == "best",
),
),
],
onPressed: (value, index) => onPressed(index),
onLongPress: (value, index) => onLongPressed(index),
selectedChoiceIndex: null,
langCode: MatrixState.pangeaController.languageController.activeL2Code(),
);
}
}