full refactor of all chore-related controllers
This commit is contained in:
parent
749517fafb
commit
d945959ba0
37 changed files with 1564 additions and 1818 deletions
|
|
@ -40,12 +40,16 @@ import 'package:fluffychat/pangea/bot/utils/bot_name.dart';
|
|||
import 'package:fluffychat/pangea/chat/utils/unlocked_morphs_snackbar.dart';
|
||||
import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.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/controllers/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/language_mismatch_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/language_mismatch_popup.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/message_analytics_feedback.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
|
@ -371,7 +375,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
if (evt is KeyDownEvent) {
|
||||
// #Pangea
|
||||
// send();
|
||||
choreographer.send(context);
|
||||
onInputBarSubmitted('');
|
||||
// Pangea#
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
|
|
@ -776,7 +780,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
inputFocus.removeListener(_inputFocusListener);
|
||||
onFocusSub?.cancel();
|
||||
//#Pangea
|
||||
choreographer.stateStream.close();
|
||||
choreographer.dispose();
|
||||
MatrixState.pAnyState.closeAllOverlays(force: true);
|
||||
showToolbarStream.close();
|
||||
|
|
@ -1694,7 +1697,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
|
||||
void onSelectMessage(Event event) {
|
||||
// #Pangea
|
||||
if (choreographer.itController.willOpen) {
|
||||
if (choreographer.isITOpen) {
|
||||
return;
|
||||
}
|
||||
// Pangea#
|
||||
|
|
@ -1741,10 +1744,22 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
}
|
||||
|
||||
// #Pangea
|
||||
void onInputBarSubmitted(String _, BuildContext context) {
|
||||
// void onInputBarSubmitted(_) {
|
||||
// void onInputBarSubmitted(String _) {
|
||||
Future<void> onInputBarSubmitted(String _) async {
|
||||
// send();
|
||||
choreographer.send(context);
|
||||
try {
|
||||
await choreographer.send();
|
||||
} on ShowPaywallException {
|
||||
PaywallCard.show(context, choreographer.inputTransformTargetKey);
|
||||
} on OpenMatchesException {
|
||||
if (choreographer.firstIGCMatch != null) {
|
||||
OverlayUtil.showIGCMatch(
|
||||
choreographer.firstIGCMatch!,
|
||||
choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Pangea#
|
||||
FocusScope.of(context).requestFocus(inputFocus);
|
||||
}
|
||||
|
|
@ -2193,11 +2208,14 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
context: context,
|
||||
cardToShow: LanguageMismatchPopup(
|
||||
targetLanguage: targetLanguage,
|
||||
choreographer: choreographer,
|
||||
onUpdate: () async {
|
||||
await choreographer.getLanguageHelp(manual: true);
|
||||
if (choreographer.igc.canShowFirstMatch) {
|
||||
choreographer.igc.showFirstMatch(context);
|
||||
final igcMatch = await choreographer.requestLanguageAssistance();
|
||||
if (igcMatch != null) {
|
||||
OverlayUtil.showIGCMatch(
|
||||
igcMatch,
|
||||
choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -278,11 +278,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
AppConfig.sendOnEnter == true && PlatformInfos.isMobile
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
// #Pangea
|
||||
// onSubmitted: controller.onInputBarSubmitted,
|
||||
onSubmitted: (_) =>
|
||||
controller.onInputBarSubmitted(_, context),
|
||||
// Pangea#
|
||||
onSubmitted: controller.onInputBarSubmitted,
|
||||
onSubmitImage: controller.sendImageFromClipBoard,
|
||||
focusNode: controller.inputFocus,
|
||||
controller: controller.sendController,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:slugify/slugify.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.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/controllers/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
|
||||
import 'package:fluffychat/utils/markdown_context_builder.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
|
|
@ -421,6 +426,41 @@ class InputBar extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
void onInputTap(BuildContext context, {required FocusNode fNode}) {
|
||||
fNode.requestFocus();
|
||||
|
||||
// show the paywall if appropriate
|
||||
final choreographer = controller!.choreographer;
|
||||
if (MatrixState
|
||||
.pangeaController.subscriptionController.subscriptionStatus ==
|
||||
SubscriptionStatus.shouldShowPaywall &&
|
||||
controller!.text.isNotEmpty) {
|
||||
PaywallCard.show(context, choreographer.inputTransformTargetKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no igc text data, then don't do anything
|
||||
if (!choreographer.hasIGCTextData) return;
|
||||
|
||||
final selection = controller!.selection;
|
||||
if (selection.baseOffset >= controller!.text.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final match = choreographer.getMatchByOffset(
|
||||
selection.baseOffset,
|
||||
);
|
||||
if (match == null) return;
|
||||
|
||||
// if autoplay on and it start then just start it
|
||||
if (match.updatedMatch.isITStart) {
|
||||
return choreographer.openIT(match);
|
||||
}
|
||||
OverlayUtil.showIGCMatch(match, choreographer, context);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// #Pangea
|
||||
|
|
@ -486,7 +526,7 @@ class InputBar extends StatelessWidget {
|
|||
? const TextStyle(color: Colors.red)
|
||||
: null,
|
||||
onTap: () {
|
||||
controller?.onInputTap(
|
||||
onInputTap(
|
||||
context,
|
||||
fNode: focusNode,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
|
||||
|
||||
class ActivityRoleTooltip extends StatefulWidget {
|
||||
class ActivityRoleTooltip extends StatelessWidget {
|
||||
final Choreographer choreographer;
|
||||
|
||||
const ActivityRoleTooltip({
|
||||
|
|
@ -16,48 +15,32 @@ class ActivityRoleTooltip extends StatefulWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ActivityRoleTooltip> createState() => ActivityRoleTooltipState();
|
||||
}
|
||||
|
||||
class ActivityRoleTooltipState extends State<ActivityRoleTooltip> {
|
||||
Room get room => widget.choreographer.chatController.room;
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_choreoSub = widget.choreographer.stateStream.stream.listen((event) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
Room get room => choreographer.chatController.room;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!room.showActivityChatUI ||
|
||||
room.ownRole?.goal == null ||
|
||||
widget.choreographer.itController.willOpen) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return ListenableBuilder(
|
||||
listenable: choreographer,
|
||||
builder: (context, _) {
|
||||
if (!room.showActivityChatUI ||
|
||||
room.ownRole?.goal == null ||
|
||||
choreographer.isITOpen) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return InlineTooltip(
|
||||
message: room.ownRole!.goal!,
|
||||
isClosed: room.hasDismissedGoalTooltip,
|
||||
onClose: () async {
|
||||
await room.dismissGoalTooltip();
|
||||
if (mounted) setState(() {});
|
||||
return InlineTooltip(
|
||||
message: room.ownRole!.goal!,
|
||||
isClosed: room.hasDismissedGoalTooltip,
|
||||
onClose: () async {
|
||||
await room.dismissGoalTooltip();
|
||||
},
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
top: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
);
|
||||
},
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16.0,
|
||||
top: 16.0,
|
||||
right: 16.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart';
|
||||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
|
|
@ -36,7 +37,6 @@ class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
|
|||
widget.controller.room,
|
||||
);
|
||||
showPermissionsError = !itEnabled || !igcEnabled;
|
||||
debugPrint("showPermissionsError: $showPermissionsError");
|
||||
|
||||
if (showPermissionsError) {
|
||||
Future.delayed(
|
||||
|
|
@ -46,12 +46,6 @@ class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Rebuild the widget each time there's an update from choreo (i.e., an error).
|
||||
_choreoSub = widget.controller.choreographer.stateStream.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
@ -74,19 +68,25 @@ class ChatFloatingActionButtonState extends State<ChatFloatingActionButton> {
|
|||
child: const Icon(Icons.arrow_downward_outlined),
|
||||
);
|
||||
}
|
||||
if (widget.controller.choreographer.errorService.error != null &&
|
||||
!widget.controller.choreographer.itController.willOpen) {
|
||||
return ChoreographerHasErrorButton(
|
||||
widget.controller.choreographer.errorService.error!,
|
||||
widget.controller.choreographer,
|
||||
);
|
||||
}
|
||||
|
||||
return showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: widget.controller.choreographer,
|
||||
roomID: widget.controller.roomId,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
return ListenableBuilder(
|
||||
listenable: widget.controller.choreographer,
|
||||
builder: (context, _) {
|
||||
if (widget.controller.choreographer.errorService.error != null &&
|
||||
!widget.controller.choreographer.isITOpen) {
|
||||
return ChoreographerHasErrorButton(
|
||||
widget.controller.choreographer.errorService.error!,
|
||||
widget.controller.choreographer,
|
||||
);
|
||||
}
|
||||
|
||||
return showPermissionsError
|
||||
? LanguagePermissionsButtons(
|
||||
choreographer: widget.controller.choreographer,
|
||||
roomID: widget.controller.roomId,
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,41 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
|
||||
class ChatViewBackground extends StatefulWidget {
|
||||
class ChatViewBackground extends StatelessWidget {
|
||||
final Choreographer choreographer;
|
||||
const ChatViewBackground(this.choreographer, {super.key});
|
||||
|
||||
@override
|
||||
ChatViewBackgroundState createState() => ChatViewBackgroundState();
|
||||
}
|
||||
|
||||
class ChatViewBackgroundState extends State<ChatViewBackground> {
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from choreo
|
||||
_choreoSub = widget.choreographer.stateStream.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.choreographer.itController.willOpen
|
||||
? Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Material(
|
||||
borderOnForeground: false,
|
||||
color: const Color.fromRGBO(0, 0, 0, 1).withAlpha(150),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
color: Colors.transparent,
|
||||
return ListenableBuilder(
|
||||
listenable: choreographer,
|
||||
builder: (context, _) {
|
||||
return choreographer.isITOpen
|
||||
? Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Material(
|
||||
borderOnForeground: false,
|
||||
color: const Color.fromRGBO(0, 0, 0, 1).withAlpha(150),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
|
|
@ -9,13 +7,15 @@ import 'package:fluffychat/config/themes.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/input_bar.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/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
class PangeaChatInputRow extends StatefulWidget {
|
||||
class PangeaChatInputRow extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
|
||||
const PangeaChatInputRow({
|
||||
|
|
@ -23,37 +23,13 @@ class PangeaChatInputRow extends StatefulWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PangeaChatInputRow> createState() => PangeaChatInputRowState();
|
||||
}
|
||||
|
||||
class PangeaChatInputRowState extends State<PangeaChatInputRow> {
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from choreo
|
||||
_choreoSub = widget.controller.choreographer.stateStream.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ChatController get _controller => widget.controller;
|
||||
|
||||
LanguageModel? get activel1 =>
|
||||
_controller.pangeaController.languageController.activeL1Model();
|
||||
controller.pangeaController.languageController.activeL1Model();
|
||||
LanguageModel? get activel2 =>
|
||||
_controller.pangeaController.languageController.activeL2Model();
|
||||
controller.pangeaController.languageController.activeL2Model();
|
||||
|
||||
String hintText() {
|
||||
if (_controller.choreographer.itController.willOpen) {
|
||||
String hintText(BuildContext context) {
|
||||
if (controller.choreographer.isITOpen) {
|
||||
return L10n.of(context).buildTranslation;
|
||||
}
|
||||
return activel1 != null &&
|
||||
|
|
@ -72,202 +48,207 @@ class PangeaChatInputRowState extends State<PangeaChatInputRow> {
|
|||
final theme = Theme.of(context);
|
||||
const height = 48.0;
|
||||
|
||||
if (widget.controller.selectMode) {
|
||||
if (controller.selectMode) {
|
||||
return const SizedBox(height: height);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
CompositedTransformTarget(
|
||||
link: _controller.choreographer.inputLayerLinkAndKey.link,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8.0),
|
||||
return ListenableBuilder(
|
||||
listenable: controller.choreographer,
|
||||
builder: (context, _) {
|
||||
return Column(
|
||||
children: [
|
||||
CompositedTransformTarget(
|
||||
link: controller.choreographer.inputLayerLinkAndKey.link,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8.0),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
key: controller.choreographer.inputLayerLinkAndKey.key,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
const SizedBox(width: 4),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: height,
|
||||
width:
|
||||
controller.sendController.text.isEmpty ? height : 0,
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: PopupMenuButton<String>(
|
||||
useRootNavigator: true,
|
||||
icon: const Icon(Icons.add_outlined),
|
||||
onSelected: controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'file',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.attachment_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendFile),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'image',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.image_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendImage),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.purple,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).openCamera),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera-video',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.videocam_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).openVideoCamera),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'location',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.brown,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.gps_fixed_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).shareLocation),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (FluffyThemes.isColumnMode(context))
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context).emojis,
|
||||
icon: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
controller.showEmojiPicker
|
||||
? Icons.keyboard
|
||||
: Icons.add_reaction_outlined,
|
||||
key: ValueKey(controller.showEmojiPicker),
|
||||
),
|
||||
),
|
||||
onPressed: controller.emojiPickerAction,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
child: InputBar(
|
||||
room: controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
autofocus: !PlatformInfos.isMobile,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: AppConfig.sendOnEnter == true &&
|
||||
PlatformInfos.isMobile
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted: controller.onInputBarSubmitted,
|
||||
onSubmitImage: controller.sendImageFromClipBoard,
|
||||
focusNode: controller.inputFocus,
|
||||
controller: controller.sendController,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
top: 3.0,
|
||||
),
|
||||
disabledBorder: InputBorder.none,
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
filled: false,
|
||||
),
|
||||
onChanged: controller.onInputBarChanged,
|
||||
hintText: hintText(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
StartIGCButton(
|
||||
controller: controller,
|
||||
),
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: PlatformInfos.platformCanRecord &&
|
||||
controller.sendController.text.isEmpty &&
|
||||
!controller.choreographer.isITOpen
|
||||
? FloatingActionButton.small(
|
||||
tooltip: L10n.of(context).voiceMessage,
|
||||
onPressed: controller.voiceMessageAction,
|
||||
elevation: 0,
|
||||
heroTag: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(height),
|
||||
),
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
child: const Icon(Icons.mic_none_outlined),
|
||||
)
|
||||
: ChoreographerSendButton(controller: controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
key: _controller.choreographer.inputLayerLinkAndKey.key,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
const SizedBox(width: 4),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: height,
|
||||
width: _controller.sendController.text.isEmpty ? height : 0,
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: PopupMenuButton<String>(
|
||||
useRootNavigator: true,
|
||||
icon: const Icon(Icons.add_outlined),
|
||||
onSelected: _controller.onAddPopupMenuButtonSelected,
|
||||
itemBuilder: (BuildContext context) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'file',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.attachment_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendFile),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
value: 'image',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.image_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).sendImage),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.purple,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.camera_alt_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).openCamera),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'camera-video',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.videocam_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).openVideoCamera),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
if (PlatformInfos.isMobile)
|
||||
PopupMenuItem<String>(
|
||||
value: 'location',
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.brown,
|
||||
foregroundColor: Colors.white,
|
||||
child: Icon(Icons.gps_fixed_outlined),
|
||||
),
|
||||
title: Text(L10n.of(context).shareLocation),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (FluffyThemes.isColumnMode(context))
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
tooltip: L10n.of(context).emojis,
|
||||
icon: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.scaled,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
_controller.showEmojiPicker
|
||||
? Icons.keyboard
|
||||
: Icons.add_reaction_outlined,
|
||||
key: ValueKey(_controller.showEmojiPicker),
|
||||
),
|
||||
),
|
||||
onPressed: _controller.emojiPickerAction,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0.0),
|
||||
child: InputBar(
|
||||
room: _controller.room,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
autofocus: !PlatformInfos.isMobile,
|
||||
keyboardType: TextInputType.multiline,
|
||||
textInputAction: AppConfig.sendOnEnter == true &&
|
||||
PlatformInfos.isMobile
|
||||
? TextInputAction.send
|
||||
: null,
|
||||
onSubmitted: (String value) =>
|
||||
_controller.onInputBarSubmitted(value, context),
|
||||
onSubmitImage: _controller.sendImageFromClipBoard,
|
||||
focusNode: _controller.inputFocus,
|
||||
controller: _controller.sendController,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 6.0,
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
top: 3.0,
|
||||
),
|
||||
disabledBorder: InputBorder.none,
|
||||
hintMaxLines: 1,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
filled: false,
|
||||
),
|
||||
onChanged: _controller.onInputBarChanged,
|
||||
hintText: hintText(),
|
||||
),
|
||||
),
|
||||
),
|
||||
StartIGCButton(
|
||||
controller: _controller,
|
||||
),
|
||||
Container(
|
||||
height: height,
|
||||
width: height,
|
||||
alignment: Alignment.center,
|
||||
child: PlatformInfos.platformCanRecord &&
|
||||
_controller.sendController.text.isEmpty &&
|
||||
!_controller.choreographer.itController.willOpen
|
||||
? FloatingActionButton.small(
|
||||
tooltip: L10n.of(context).voiceMessage,
|
||||
onPressed: _controller.voiceMessageAction,
|
||||
elevation: 0,
|
||||
heroTag: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(height),
|
||||
),
|
||||
backgroundColor: theme.bubbleColor,
|
||||
foregroundColor: theme.onBubbleColor,
|
||||
child: const Icon(Icons.mic_none_outlined),
|
||||
)
|
||||
: ChoreographerSendButton(controller: _controller),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@ class ChoreoConstants {
|
|||
static const green = Colors.green;
|
||||
static const yellow = Color.fromARGB(255, 206, 152, 2);
|
||||
static const red = Colors.red;
|
||||
static const int msBeforeIGCStart = 10000;
|
||||
static const int maxLength = 1000;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +1,180 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.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/igc_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/utils/input_paste_listener.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import '../../../widgets/matrix.dart';
|
||||
import 'error_service.dart';
|
||||
import 'it_controller.dart';
|
||||
|
||||
enum ChoreoMode { igc, it }
|
||||
class OpenMatchesException implements Exception {}
|
||||
|
||||
class Choreographer {
|
||||
PangeaController pangeaController;
|
||||
ChatController chatController;
|
||||
late PangeaTextController _textController;
|
||||
class ShowPaywallException implements Exception {}
|
||||
|
||||
class Choreographer extends ChangeNotifier {
|
||||
final PangeaController pangeaController;
|
||||
final ChatController chatController;
|
||||
|
||||
late PangeaTextController textController;
|
||||
late ITController itController;
|
||||
late IgcController igc;
|
||||
late ErrorService errorService;
|
||||
|
||||
bool isFetching = false;
|
||||
ChoreoRecord? _choreoRecord;
|
||||
|
||||
bool _isFetching = false;
|
||||
int _timesClicked = 0;
|
||||
|
||||
final int msBeforeIGCStart = 10000;
|
||||
|
||||
Timer? debounceTimer;
|
||||
ChoreoRecord? choreoRecord;
|
||||
// last checked by IGC or translation
|
||||
Timer? _debounceTimer;
|
||||
String? _lastChecked;
|
||||
ChoreoMode choreoMode = ChoreoMode.igc;
|
||||
ChoreoMode _choreoMode = ChoreoMode.igc;
|
||||
String? _sourceText;
|
||||
|
||||
final StreamController stateStream = StreamController.broadcast();
|
||||
StreamSubscription? _languageStream;
|
||||
StreamSubscription? _settingsUpdateStream;
|
||||
late AssistanceState _currentAssistanceState;
|
||||
|
||||
String? translatedText;
|
||||
|
||||
Choreographer(this.pangeaController, this.chatController) {
|
||||
_initialize();
|
||||
}
|
||||
_initialize() {
|
||||
_textController = PangeaTextController(choreographer: this);
|
||||
InputPasteListener(_textController, onPaste);
|
||||
|
||||
int get timesClicked => _timesClicked;
|
||||
bool get isFetching => _isFetching;
|
||||
ChoreoMode get choreoMode => _choreoMode;
|
||||
|
||||
String? get sourceText => _sourceText;
|
||||
String get currentText => textController.text;
|
||||
|
||||
void _initialize() {
|
||||
textController = PangeaTextController(choreographer: this);
|
||||
|
||||
itController = ITController(this);
|
||||
igc = IgcController(this);
|
||||
errorService = ErrorService(this);
|
||||
_textController.addListener(_onChangeListener);
|
||||
|
||||
errorService = ErrorService();
|
||||
errorService.addListener(notifyListeners);
|
||||
|
||||
textController.addListener(_onChange);
|
||||
|
||||
_languageStream =
|
||||
pangeaController.userController.languageStream.stream.listen((update) {
|
||||
clear();
|
||||
setState();
|
||||
notifyListeners();
|
||||
});
|
||||
|
||||
_settingsUpdateStream =
|
||||
pangeaController.userController.settingsUpdateStream.stream.listen((_) {
|
||||
setState();
|
||||
notifyListeners();
|
||||
});
|
||||
_currentAssistanceState = assistanceState;
|
||||
clear();
|
||||
}
|
||||
|
||||
void send(BuildContext context) {
|
||||
debugPrint("can send message: $canSendMessage");
|
||||
void clear() {
|
||||
_choreoMode = ChoreoMode.igc;
|
||||
_lastChecked = null;
|
||||
_timesClicked = 0;
|
||||
_isFetching = false;
|
||||
_choreoRecord = null;
|
||||
_sourceText = null;
|
||||
itController.clear();
|
||||
igc.clear();
|
||||
_resetDebounceTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
errorService.dispose();
|
||||
textController.dispose();
|
||||
_languageStream?.cancel();
|
||||
_settingsUpdateStream?.cancel();
|
||||
TtsController.stop();
|
||||
}
|
||||
|
||||
void onPaste(value) {
|
||||
_initChoreoRecord();
|
||||
_choreoRecord!.pastedStrings.add(value);
|
||||
}
|
||||
|
||||
void onClickSend() {
|
||||
if (assistanceState == AssistanceState.fetched) {
|
||||
_timesClicked++;
|
||||
|
||||
// if user is doing IT, call closeIT here to
|
||||
// ensure source text is replaced when needed
|
||||
if (isITOpen && _timesClicked > 1) {
|
||||
closeIT();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setChoreoMode(ChoreoMode mode) {
|
||||
_choreoMode = mode;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _resetDebounceTimer() {
|
||||
if (_debounceTimer != null) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _initChoreoRecord() {
|
||||
_choreoRecord ??= ChoreoRecord(
|
||||
originalText: textController.text,
|
||||
choreoSteps: [],
|
||||
openMatches: [],
|
||||
);
|
||||
}
|
||||
|
||||
void _startLoading() {
|
||||
_lastChecked = textController.text;
|
||||
_isFetching = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _stopLoading() {
|
||||
_isFetching = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<PangeaMatchState?> requestLanguageAssistance() async {
|
||||
await _getLanguageAssistance(manual: true);
|
||||
if (igc.canShowFirstMatch) {
|
||||
return igc.onShowFirstMatch();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> send() 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) return;
|
||||
|
||||
if (igc.canShowFirstMatch) {
|
||||
igc.showFirstMatch(context);
|
||||
return;
|
||||
throw OpenMatchesException();
|
||||
} else if (isRunningIT) {
|
||||
// If the user is in the middle of IT, don't send the message.
|
||||
// If they've already clicked the send button once, this will
|
||||
|
|
@ -101,16 +182,14 @@ class Choreographer {
|
|||
return;
|
||||
}
|
||||
|
||||
final isSubscribed = pangeaController.subscriptionController.isSubscribed;
|
||||
if (isSubscribed != null && !isSubscribed) {
|
||||
// don't want to run IGC if user isn't subscribed, so either
|
||||
// show the paywall if applicable or just send the message
|
||||
final status = pangeaController.subscriptionController.subscriptionStatus;
|
||||
status == SubscriptionStatus.shouldShowPaywall
|
||||
? PaywallCard.show(context, chatController)
|
||||
: chatController.send(
|
||||
message: chatController.sendController.text,
|
||||
);
|
||||
final subscriptionStatus =
|
||||
pangeaController.subscriptionController.subscriptionStatus;
|
||||
|
||||
if (subscriptionStatus != SubscriptionStatus.subscribed) {
|
||||
if (subscriptionStatus == SubscriptionStatus.shouldShowPaywall) {
|
||||
throw ShowPaywallException();
|
||||
}
|
||||
chatController.send(message: chatController.sendController.text);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -119,23 +198,79 @@ class Choreographer {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!igc.hasRelevantIGCTextData && !itController.dismissed) {
|
||||
getLanguageHelp().then((value) => _sendWithIGC(context));
|
||||
if (!igc.hasIGCTextData && !itController.dismissed) {
|
||||
await _getLanguageAssistance();
|
||||
await send();
|
||||
} else {
|
||||
_sendWithIGC(context);
|
||||
_sendWithIGC();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendWithIGC(BuildContext context) async {
|
||||
if (!canSendMessage) {
|
||||
// It's possible that the reason user can't send message is because they're in the middle of IT. If this is the case,
|
||||
// do nothing (there's no matches to show). The user can click the send button again to override this.
|
||||
if (!isRunningIT) {
|
||||
igc.showFirstMatch(context);
|
||||
}
|
||||
/// Handles any changes to the text input
|
||||
void _onChange() {
|
||||
if (_lastChecked != null && _lastChecked == textController.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastChecked = textController.text;
|
||||
|
||||
if (textController.editType == EditType.igc ||
|
||||
textController.editType == EditType.itDismissed) {
|
||||
textController.editType = EditType.keyboard;
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any open IGC/IT overlays
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
if (errorService.isError) return;
|
||||
|
||||
igc.clear();
|
||||
_resetDebounceTimer();
|
||||
|
||||
if (textController.editType == EditType.it) {
|
||||
_getLanguageAssistance();
|
||||
} else {
|
||||
_sourceText = null;
|
||||
_debounceTimer ??= Timer(
|
||||
const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart),
|
||||
() => _getLanguageAssistance(),
|
||||
);
|
||||
}
|
||||
|
||||
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
|
||||
//a change being from the keyboard unless explicitly set to one of the other
|
||||
//types when that action happens (e.g. an it/igc choice is selected)
|
||||
textController.editType = EditType.keyboard;
|
||||
}
|
||||
|
||||
/// Fetches the language help for the current text, including grammar correction, language detection,
|
||||
/// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
|
||||
/// or if autoIGC is not enabled and the user has not manually requested it.
|
||||
/// [onlyTokensAndLanguageDetection] will
|
||||
Future<void> _getLanguageAssistance({
|
||||
bool manual = false,
|
||||
}) async {
|
||||
if (errorService.isError) return;
|
||||
final SubscriptionStatus canSendStatus =
|
||||
pangeaController.subscriptionController.subscriptionStatus;
|
||||
|
||||
if (canSendStatus != SubscriptionStatus.subscribed ||
|
||||
l2Lang == null ||
|
||||
l1Lang == null ||
|
||||
(!igcEnabled && !itEnabled) ||
|
||||
(!isAutoIGCEnabled && !manual && _choreoMode != ChoreoMode.it)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_resetDebounceTimer();
|
||||
_initChoreoRecord();
|
||||
|
||||
_startLoading();
|
||||
await (isRunningIT ? itController.continueIT() : igc.getIGCTextData());
|
||||
_stopLoading();
|
||||
}
|
||||
|
||||
Future<void> _sendWithIGC() async {
|
||||
if (chatController.sendController.text.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -143,10 +278,10 @@ class Choreographer {
|
|||
final message = chatController.sendController.text;
|
||||
final fakeEventId = chatController.sendFakeMessage();
|
||||
final PangeaRepresentation? originalWritten =
|
||||
choreoRecord?.includedIT == true && translatedText != null
|
||||
_choreoRecord?.includedIT == true && _sourceText != null
|
||||
? PangeaRepresentation(
|
||||
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
|
||||
text: translatedText!,
|
||||
text: _sourceText!,
|
||||
originalWritten: true,
|
||||
originalSent: false,
|
||||
)
|
||||
|
|
@ -192,7 +327,7 @@ class Choreographer {
|
|||
"currentText": message,
|
||||
"l1LangCode": l1LangCode,
|
||||
"l2LangCode": l2LangCode,
|
||||
"choreoRecord": choreoRecord?.toJson(),
|
||||
"choreoRecord": _choreoRecord?.toJson(),
|
||||
},
|
||||
level: e is TimeoutException ? SentryLevel.warning : SentryLevel.error,
|
||||
);
|
||||
|
|
@ -202,522 +337,174 @@ class Choreographer {
|
|||
originalSent: originalSent,
|
||||
originalWritten: originalWritten,
|
||||
tokensSent: tokensSent,
|
||||
choreo: choreoRecord,
|
||||
choreo: _choreoRecord,
|
||||
tempEventId: fakeEventId,
|
||||
);
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
_resetDebounceTimer() {
|
||||
if (debounceTimer != null) {
|
||||
debounceTimer?.cancel();
|
||||
debounceTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _initChoreoRecord() {
|
||||
choreoRecord ??= ChoreoRecord(
|
||||
originalText: textController.text,
|
||||
choreoSteps: [],
|
||||
openMatches: [],
|
||||
);
|
||||
}
|
||||
|
||||
void onITStart(PangeaMatchState itMatch) {
|
||||
void openIT(PangeaMatchState itMatch) {
|
||||
if (!itMatch.updatedMatch.isITStart) {
|
||||
throw Exception("this isn't an itStart match!");
|
||||
throw Exception("Attempted to open IT with a non-IT start match");
|
||||
}
|
||||
choreoMode = ChoreoMode.it;
|
||||
itController.initializeIT(
|
||||
ITStartData(_textController.text, null),
|
||||
);
|
||||
|
||||
translatedText = _textController.text;
|
||||
_choreoMode = ChoreoMode.it;
|
||||
_sourceText = textController.text;
|
||||
itController.openIT();
|
||||
|
||||
igc.clear();
|
||||
_textController.setSystemText("", EditType.itStart);
|
||||
textController.setSystemText("", EditType.it);
|
||||
|
||||
_initChoreoRecord();
|
||||
itMatch.setStatus(PangeaMatchStatus.accepted);
|
||||
choreoRecord!.addRecord(
|
||||
_textController.text,
|
||||
_choreoRecord!.addRecord(
|
||||
textController.text,
|
||||
match: itMatch.updatedMatch,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Handles any changes to the text input
|
||||
_onChangeListener() {
|
||||
// Rebuild the IGC button if the state has changed.
|
||||
// This accounts for user typing after initial IGC has completed
|
||||
if (_currentAssistanceState != assistanceState) {
|
||||
setState();
|
||||
}
|
||||
|
||||
if (_noChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastChecked = _textController.text;
|
||||
|
||||
if (_textController.editType == EditType.igc ||
|
||||
_textController.editType == EditType.itDismissed) {
|
||||
_textController.editType = EditType.keyboard;
|
||||
return;
|
||||
}
|
||||
|
||||
// not sure if this is necessary now
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
|
||||
if (errorService.isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
igc.clear();
|
||||
|
||||
_resetDebounceTimer();
|
||||
|
||||
// we store translated text in the choreographer to save at the original written
|
||||
// text, but if the user edits the text after the translation, reset it, since the
|
||||
// sent text may not be an exact translation of the original text
|
||||
if (_textController.editType == EditType.keyboard) {
|
||||
translatedText = null;
|
||||
}
|
||||
|
||||
if (editTypeIsKeyboard) {
|
||||
debounceTimer ??= Timer(
|
||||
Duration(milliseconds: msBeforeIGCStart),
|
||||
() => getLanguageHelp(),
|
||||
);
|
||||
} else {
|
||||
getLanguageHelp();
|
||||
}
|
||||
|
||||
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
|
||||
//a change being from the keyboard unless explicitly set to one of the other
|
||||
//types when that action happens (e.g. an it/igc choice is selected)
|
||||
textController.editType = EditType.keyboard;
|
||||
void closeIT() {
|
||||
itController.closeIT();
|
||||
errorService.resetError();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Fetches the language help for the current text, including grammar correction, language detection,
|
||||
/// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
|
||||
/// or if autoIGC is not enabled and the user has not manually requested it.
|
||||
/// [onlyTokensAndLanguageDetection] will
|
||||
Future<void> getLanguageHelp({
|
||||
bool manual = false,
|
||||
}) async {
|
||||
try {
|
||||
if (errorService.isError) return;
|
||||
final SubscriptionStatus canSendStatus =
|
||||
pangeaController.subscriptionController.subscriptionStatus;
|
||||
|
||||
if (canSendStatus != SubscriptionStatus.subscribed ||
|
||||
l2Lang == null ||
|
||||
l1Lang == null ||
|
||||
(!igcEnabled && !itEnabled) ||
|
||||
(!isAutoIGCEnabled && !manual && choreoMode != ChoreoMode.it)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_resetDebounceTimer();
|
||||
startLoading();
|
||||
_initChoreoRecord();
|
||||
|
||||
// if getting language assistance after finishing IT,
|
||||
// reset the itController
|
||||
if (choreoMode == ChoreoMode.it && itController.isTranslationDone) {
|
||||
itController.clear();
|
||||
}
|
||||
|
||||
await (isRunningIT
|
||||
? itController.getTranslationData(_useCustomInput)
|
||||
: igc.getIGCTextData());
|
||||
} catch (err, stack) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {
|
||||
"l2Lang": l2Lang?.toJson(),
|
||||
"l1Lang": l1Lang?.toJson(),
|
||||
"choreoMode": choreoMode,
|
||||
"igcEnabled": igcEnabled,
|
||||
"itEnabled": itEnabled,
|
||||
"isAutoIGCEnabled": isAutoIGCEnabled,
|
||||
"isTranslationDone": itController.isTranslationDone,
|
||||
"useCustomInput": _useCustomInput,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
stopLoading();
|
||||
}
|
||||
Continuance onSelectContinuance(int index) {
|
||||
final continuance = itController.onSelectContinuance(index);
|
||||
notifyListeners();
|
||||
return continuance;
|
||||
}
|
||||
|
||||
void onITChoiceSelect(ITStep step) {
|
||||
_textController.setSystemText(
|
||||
_textController.text + step.continuances[step.chosen!].text,
|
||||
step.continuances[step.chosen!].gold
|
||||
? EditType.itGold
|
||||
: EditType.itStandard,
|
||||
void onAcceptContinuance(int index) {
|
||||
final step = itController.getAcceptedITStep(index);
|
||||
textController.setSystemText(
|
||||
textController.text + step.continuances[step.chosen].text,
|
||||
EditType.it,
|
||||
);
|
||||
textController.selection = TextSelection.collapsed(
|
||||
offset: textController.text.length,
|
||||
);
|
||||
_textController.selection =
|
||||
TextSelection.collapsed(offset: _textController.text.length);
|
||||
|
||||
_initChoreoRecord();
|
||||
choreoRecord!.addRecord(_textController.text, step: step);
|
||||
|
||||
giveInputFocus();
|
||||
_choreoRecord!.addRecord(textController.text, step: step);
|
||||
chatController.inputFocus.requestFocus();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> onAcceptReplacement({
|
||||
void setSourceText(String? text) {
|
||||
_sourceText = text;
|
||||
}
|
||||
|
||||
void setEditingSourceText(bool value) {
|
||||
itController.setEditing(value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void submitSourceTextEdits(String text) {
|
||||
_sourceText = text;
|
||||
itController.onSubmitEdits();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
PangeaMatchState? getMatchByOffset(int offset) =>
|
||||
igc.getMatchByOffset(offset);
|
||||
|
||||
void clearMatches(Object error) {
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
igc.clearMatches();
|
||||
errorService.setError(ChoreoError(raw: error));
|
||||
}
|
||||
|
||||
Future<void> fetchSpanDetails({
|
||||
required PangeaMatchState match,
|
||||
}) async {
|
||||
try {
|
||||
if (igc.igcTextData == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "onReplacementSelect with null igcTextData",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
return;
|
||||
}
|
||||
if (match.updatedMatch.match.selectedChoice == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "onReplacementSelect with null selectedChoice",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
final isNormalizationError =
|
||||
match.updatedMatch.match.isNormalizationError();
|
||||
|
||||
final updatedMatch = igc.igcTextData!.acceptReplacement(
|
||||
match,
|
||||
PangeaMatchStatus.accepted,
|
||||
bool force = false,
|
||||
}) =>
|
||||
igc.fetchSpanDetails(
|
||||
match: match,
|
||||
force: force,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.currentText,
|
||||
EditType.igc,
|
||||
);
|
||||
void onAcceptReplacement({
|
||||
required PangeaMatchState match,
|
||||
}) {
|
||||
final updatedMatch = igc.acceptReplacement(
|
||||
match,
|
||||
PangeaMatchStatus.accepted,
|
||||
);
|
||||
|
||||
//if it's the right choice, replace in text
|
||||
if (!isNormalizationError) {
|
||||
_initChoreoRecord();
|
||||
choreoRecord!.addRecord(
|
||||
_textController.text,
|
||||
match: updatedMatch,
|
||||
);
|
||||
}
|
||||
textController.setSystemText(
|
||||
igc.currentText!,
|
||||
EditType.igc,
|
||||
);
|
||||
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
setState();
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
"match": match.toJson(),
|
||||
},
|
||||
if (!updatedMatch.match.isNormalizationError()) {
|
||||
_initChoreoRecord();
|
||||
_choreoRecord!.addRecord(
|
||||
textController.text,
|
||||
match: updatedMatch,
|
||||
);
|
||||
igc.clear();
|
||||
} finally {
|
||||
setState();
|
||||
}
|
||||
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void onUndoReplacement(PangeaMatchState match) {
|
||||
try {
|
||||
igc.igcTextData?.undoReplacement(match);
|
||||
choreoRecord?.choreoSteps.removeWhere(
|
||||
(step) => step.acceptedOrIgnoredMatch == match.updatedMatch,
|
||||
);
|
||||
igc.undoReplacement(match);
|
||||
_choreoRecord?.choreoSteps.removeWhere(
|
||||
(step) => step.acceptedOrIgnoredMatch == match.updatedMatch,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.currentText,
|
||||
EditType.igc,
|
||||
textController.setSystemText(
|
||||
igc.currentText!,
|
||||
EditType.igc,
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void onIgnoreMatch({required PangeaMatchState match}) {
|
||||
final updatedMatch = igc.ignoreReplacement(match);
|
||||
if (!updatedMatch.match.isNormalizationError()) {
|
||||
_initChoreoRecord();
|
||||
_choreoRecord!.addRecord(
|
||||
textController.text,
|
||||
match: updatedMatch,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
setState();
|
||||
}
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void acceptNormalizationMatches() {
|
||||
final normalizationsMatches = igc.igcTextData!.openNormalizationMatches;
|
||||
if (normalizationsMatches.isEmpty) return;
|
||||
final normalizationsMatches = igc.openNormalizationMatches;
|
||||
if (normalizationsMatches?.isEmpty ?? true) return;
|
||||
|
||||
_initChoreoRecord();
|
||||
for (final match in normalizationsMatches) {
|
||||
for (final match in normalizationsMatches!) {
|
||||
match.selectChoice(
|
||||
match.updatedMatch.match.choices!.indexWhere(
|
||||
(c) => c.isBestCorrection,
|
||||
),
|
||||
);
|
||||
|
||||
final updatedMatch = igc.igcTextData!.acceptReplacement(
|
||||
final updatedMatch = igc.acceptReplacement(
|
||||
match,
|
||||
PangeaMatchStatus.automatic,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.currentText,
|
||||
textController.setSystemText(
|
||||
igc.currentText!,
|
||||
EditType.igc,
|
||||
);
|
||||
|
||||
choreoRecord!.addRecord(
|
||||
_choreoRecord!.addRecord(
|
||||
currentText,
|
||||
match: updatedMatch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onIgnoreMatch({required PangeaMatchState match}) {
|
||||
try {
|
||||
if (igc.igcTextData == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "should not be in onIgnoreMatch with null igcTextData",
|
||||
s: StackTrace.current,
|
||||
data: {},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final updatedMatch = igc.igcTextData!.ignoreReplacement(match);
|
||||
igc.onIgnoreMatch(updatedMatch);
|
||||
|
||||
if (!updatedMatch.match.isNormalizationError()) {
|
||||
_initChoreoRecord();
|
||||
choreoRecord!.addRecord(
|
||||
_textController.text,
|
||||
match: updatedMatch,
|
||||
);
|
||||
}
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
data: {
|
||||
"igcTextData": igc.igcTextData?.toJson(),
|
||||
"match": match.toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
},
|
||||
);
|
||||
igc.clear();
|
||||
} finally {
|
||||
setState();
|
||||
}
|
||||
}
|
||||
|
||||
void giveInputFocus() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
chatController.inputFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
String get currentText => _textController.text;
|
||||
|
||||
PangeaTextController get textController => _textController;
|
||||
|
||||
String get accessToken => pangeaController.userController.accessToken;
|
||||
|
||||
clear() {
|
||||
choreoMode = ChoreoMode.igc;
|
||||
_lastChecked = null;
|
||||
_timesClicked = 0;
|
||||
isFetching = false;
|
||||
choreoRecord = null;
|
||||
translatedText = null;
|
||||
itController.clear();
|
||||
igc.clear();
|
||||
_resetDebounceTimer();
|
||||
}
|
||||
|
||||
Future<void> onPaste(value) async {
|
||||
_initChoreoRecord();
|
||||
choreoRecord!.pastedStrings.add(value);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
_textController.dispose();
|
||||
_languageStream?.cancel();
|
||||
_settingsUpdateStream?.cancel();
|
||||
stateStream.close();
|
||||
TtsController.stop();
|
||||
}
|
||||
|
||||
LanguageModel? get l2Lang {
|
||||
return pangeaController.languageController.activeL2Model();
|
||||
}
|
||||
|
||||
String? get l2LangCode => l2Lang?.langCode;
|
||||
|
||||
LanguageModel? get l1Lang =>
|
||||
pangeaController.languageController.activeL1Model();
|
||||
|
||||
String? get l1LangCode => l1Lang?.langCode;
|
||||
|
||||
String? get userId => pangeaController.userController.userId;
|
||||
|
||||
bool get _noChange =>
|
||||
_lastChecked != null && _lastChecked == _textController.text;
|
||||
|
||||
bool get isRunningIT =>
|
||||
choreoMode == ChoreoMode.it && !itController.isTranslationDone;
|
||||
|
||||
void startLoading() {
|
||||
_lastChecked = _textController.text;
|
||||
isFetching = true;
|
||||
setState();
|
||||
}
|
||||
|
||||
void stopLoading() {
|
||||
isFetching = false;
|
||||
setState();
|
||||
}
|
||||
|
||||
void incrementTimesClicked() {
|
||||
if (assistanceState == AssistanceState.fetched) {
|
||||
_timesClicked++;
|
||||
|
||||
// if user is doing IT, call closeIT here to
|
||||
// ensure source text is replaced when needed
|
||||
if (itController.isOpen && _timesClicked > 1) {
|
||||
itController.closeIT();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get roomId => chatController.roomId;
|
||||
|
||||
bool get _useCustomInput => [
|
||||
EditType.keyboard,
|
||||
EditType.igc,
|
||||
EditType.alternativeTranslation,
|
||||
].contains(_textController.editType);
|
||||
|
||||
bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType;
|
||||
|
||||
setState() {
|
||||
if (!stateStream.isClosed) {
|
||||
stateStream.add(0);
|
||||
}
|
||||
_currentAssistanceState = assistanceState;
|
||||
}
|
||||
|
||||
LayerLinkAndKey get itBarLinkAndKey =>
|
||||
MatrixState.pAnyState.layerLinkAndKey(itBarTransformTargetKey);
|
||||
|
||||
String get itBarTransformTargetKey => 'it_bar$roomId';
|
||||
|
||||
LayerLinkAndKey get inputLayerLinkAndKey =>
|
||||
MatrixState.pAnyState.layerLinkAndKey(inputTransformTargetKey);
|
||||
|
||||
String get inputTransformTargetKey => 'input$roomId';
|
||||
|
||||
LayerLinkAndKey get itBotLayerLinkAndKey =>
|
||||
MatrixState.pAnyState.layerLinkAndKey(itBotTransformTargetKey);
|
||||
|
||||
String get itBotTransformTargetKey => 'itBot$roomId';
|
||||
|
||||
bool get igcEnabled => pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveGrammar,
|
||||
chatController.room,
|
||||
);
|
||||
|
||||
bool get itEnabled => pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveTranslator,
|
||||
chatController.room,
|
||||
);
|
||||
|
||||
bool get isAutoIGCEnabled =>
|
||||
pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.autoIGC,
|
||||
chatController.room,
|
||||
);
|
||||
|
||||
AssistanceState get assistanceState {
|
||||
final isSubscribed = pangeaController.subscriptionController.isSubscribed;
|
||||
if (isSubscribed != null && !isSubscribed) {
|
||||
return AssistanceState.noSub;
|
||||
}
|
||||
|
||||
if (currentText.isEmpty && itController.sourceText == null) {
|
||||
return AssistanceState.noMessage;
|
||||
}
|
||||
|
||||
if ((igc.igcTextData?.hasOpenMatches ?? false) || isRunningIT) {
|
||||
return AssistanceState.fetched;
|
||||
}
|
||||
|
||||
if (isFetching) {
|
||||
return AssistanceState.fetching;
|
||||
}
|
||||
|
||||
if (igc.igcTextData == null) {
|
||||
return AssistanceState.notFetched;
|
||||
}
|
||||
|
||||
return AssistanceState.complete;
|
||||
}
|
||||
|
||||
bool get canSendMessage {
|
||||
// if there's an error, let them send. we don't want to block them from sending in this case
|
||||
if (errorService.isError ||
|
||||
l2Lang == null ||
|
||||
l1Lang == null ||
|
||||
_timesClicked > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if they're in IT mode, don't let them send
|
||||
if (itEnabled && isRunningIT) return false;
|
||||
|
||||
// if they've turned off IGC then let them send the message when they want
|
||||
if (!isAutoIGCEnabled) return true;
|
||||
|
||||
// if we're in the middle of fetching results, don't let them send
|
||||
if (isFetching) return false;
|
||||
|
||||
// they're supposed to run IGC but haven't yet, don't let them send
|
||||
if (igc.igcTextData == null) {
|
||||
return itController.dismissed;
|
||||
}
|
||||
|
||||
// if they have relevant matches, don't let them send
|
||||
final hasITMatches = igc.igcTextData!.hasOpenITMatches;
|
||||
final hasIGCMatches = igc.igcTextData!.hasOpenIGCMatches;
|
||||
if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// otherwise, let them send
|
||||
return true;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import '../../common/utils/error_handler.dart';
|
||||
|
||||
class ChoreoError {
|
||||
|
|
@ -16,17 +15,14 @@ class ChoreoError {
|
|||
IconData get icon => Icons.error_outline;
|
||||
}
|
||||
|
||||
class ErrorService {
|
||||
class ErrorService extends ChangeNotifier {
|
||||
ChoreoError? _error;
|
||||
int coolDownSeconds = 0;
|
||||
final Choreographer controller;
|
||||
|
||||
ErrorService(this.controller);
|
||||
ErrorService();
|
||||
|
||||
bool get isError => _error != null;
|
||||
|
||||
ChoreoError? get error => _error;
|
||||
|
||||
Duration get defaultCooldown {
|
||||
coolDownSeconds += 3;
|
||||
return Duration(seconds: coolDownSeconds);
|
||||
|
|
@ -34,7 +30,7 @@ class ErrorService {
|
|||
|
||||
final List<String> _errorCache = [];
|
||||
|
||||
setError(ChoreoError? error, {Duration? duration}) {
|
||||
void setError(ChoreoError? error) {
|
||||
if (_errorCache.contains(error?.raw.toString())) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -44,25 +40,21 @@ class ErrorService {
|
|||
}
|
||||
|
||||
_error = error;
|
||||
Future.delayed(duration ?? defaultCooldown, () {
|
||||
Future.delayed(defaultCooldown, () {
|
||||
clear();
|
||||
_setState();
|
||||
notifyListeners();
|
||||
});
|
||||
_setState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setErrorAndLock(ChoreoError? error) {
|
||||
void setErrorAndLock(ChoreoError? error) {
|
||||
_error = error;
|
||||
_setState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
resetError() {
|
||||
void resetError() {
|
||||
clear();
|
||||
_setState();
|
||||
}
|
||||
|
||||
void _setState() {
|
||||
controller.setState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
|
||||
|
||||
extension ChoregrapherUserSettingsExtension on Choreographer {
|
||||
LanguageModel? get l2Lang =>
|
||||
pangeaController.languageController.activeL2Model();
|
||||
String? get l2LangCode => l2Lang?.langCode;
|
||||
LanguageModel? get l1Lang =>
|
||||
pangeaController.languageController.activeL1Model();
|
||||
String? get l1LangCode => l1Lang?.langCode;
|
||||
|
||||
bool get igcEnabled => pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveGrammar,
|
||||
chatController.room,
|
||||
);
|
||||
bool get itEnabled => pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.interactiveTranslator,
|
||||
chatController.room,
|
||||
);
|
||||
bool get isAutoIGCEnabled =>
|
||||
pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.autoIGC,
|
||||
chatController.room,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
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/enums/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
|
||||
extension ChoregrapherUserSettingsExtension on Choreographer {
|
||||
bool get isITOpen => itController.open;
|
||||
bool get isEditingSourceText => itController.editing;
|
||||
bool get isITDone => itController.isTranslationDone;
|
||||
bool get isRunningIT => choreoMode == ChoreoMode.it && !isITDone;
|
||||
List<Continuance>? get itStepContinuances => itController.continuances;
|
||||
|
||||
String? get currentIGCText => igc.currentText;
|
||||
PangeaMatchState? get openIGCMatch => igc.openMatch;
|
||||
PangeaMatchState? get firstIGCMatch => igc.firstOpenMatch;
|
||||
List<PangeaMatchState>? get openIGCMatches => igc.openMatches;
|
||||
List<PangeaMatchState>? get closedIGCMatches => igc.closedMatches;
|
||||
bool get canShowFirstIGCMatch => igc.canShowFirstMatch;
|
||||
bool get hasIGCTextData => igc.hasIGCTextData;
|
||||
|
||||
AssistanceState get assistanceState {
|
||||
final isSubscribed = pangeaController.subscriptionController.isSubscribed;
|
||||
if (isSubscribed == false) return AssistanceState.noSub;
|
||||
if (currentText.isEmpty && sourceText == null) {
|
||||
return AssistanceState.noMessage;
|
||||
}
|
||||
|
||||
if (igc.hasOpenMatches || isRunningIT) {
|
||||
return AssistanceState.fetched;
|
||||
}
|
||||
|
||||
if (isFetching) return AssistanceState.fetching;
|
||||
if (!igc.hasIGCTextData) return AssistanceState.notFetched;
|
||||
return AssistanceState.complete;
|
||||
}
|
||||
|
||||
bool get canSendMessage {
|
||||
// if there's an error, let them send. we don't want to block them from sending in this case
|
||||
if (errorService.isError ||
|
||||
l2Lang == null ||
|
||||
l1Lang == null ||
|
||||
timesClicked > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if they're in IT mode, don't let them send
|
||||
if (itEnabled && isRunningIT) return false;
|
||||
|
||||
// if they've turned off IGC then let them send the message when they want
|
||||
if (!isAutoIGCEnabled) return true;
|
||||
|
||||
// if we're in the middle of fetching results, don't let them send
|
||||
if (isFetching) return false;
|
||||
|
||||
// they're supposed to run IGC but haven't yet, don't let them send
|
||||
if (!igc.hasIGCTextData) {
|
||||
return itController.dismissed;
|
||||
}
|
||||
|
||||
// if they have relevant matches, don't let them send
|
||||
final hasITMatches = igc.hasOpenITMatches;
|
||||
final hasIGCMatches = igc.hasOpenIGCMatches;
|
||||
if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// otherwise, let them send
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
extension ChoregrapherUserSettingsExtension on Choreographer {
|
||||
LayerLinkAndKey get itBarLinkAndKey =>
|
||||
MatrixState.pAnyState.layerLinkAndKey(itBarTransformTargetKey);
|
||||
String get itBarTransformTargetKey => 'it_bar${chatController.roomId}';
|
||||
LayerLinkAndKey get inputLayerLinkAndKey =>
|
||||
MatrixState.pAnyState.layerLinkAndKey(inputTransformTargetKey);
|
||||
String get inputTransformTargetKey => 'input${chatController.roomId}';
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:matrix/matrix.dart' hide Result;
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.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/models/pangea_match_state.dart';
|
||||
|
|
@ -16,37 +17,103 @@ import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_request.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../common/utils/error_handler.dart';
|
||||
import '../../common/utils/overlay.dart';
|
||||
|
||||
class IgcController {
|
||||
Choreographer choreographer;
|
||||
IGCTextData? igcTextData;
|
||||
final Choreographer _choreographer;
|
||||
IGCTextData? _igcTextData;
|
||||
|
||||
IgcController(this.choreographer);
|
||||
IgcController(this._choreographer);
|
||||
|
||||
String? get currentText => _igcTextData?.currentText;
|
||||
bool get hasOpenMatches => _igcTextData?.hasOpenMatches == true;
|
||||
bool get hasOpenITMatches => _igcTextData?.hasOpenITMatches == true;
|
||||
bool get hasOpenIGCMatches => _igcTextData?.hasOpenIGCMatches == true;
|
||||
|
||||
PangeaMatchState? get openMatch => _igcTextData?.openMatch;
|
||||
PangeaMatchState? get firstOpenMatch => _igcTextData?.firstOpenMatch;
|
||||
List<PangeaMatchState>? get openMatches => _igcTextData?.openMatches;
|
||||
List<PangeaMatchState>? get closedMatches => _igcTextData?.closedMatches;
|
||||
List<PangeaMatchState>? get openNormalizationMatches =>
|
||||
_igcTextData?.openNormalizationMatches;
|
||||
|
||||
bool get canShowFirstMatch => _igcTextData?.firstOpenMatch != null;
|
||||
bool get hasIGCTextData {
|
||||
if (_igcTextData == null) return false;
|
||||
return _igcTextData!.currentText == _choreographer.currentText;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_igcTextData = null;
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
}
|
||||
|
||||
void clearMatches() => _igcTextData?.clearMatches();
|
||||
|
||||
PangeaMatchState? onShowFirstMatch() {
|
||||
if (!canShowFirstMatch) {
|
||||
throw "should not be calling showFirstMatch with this igcTextData.";
|
||||
}
|
||||
|
||||
final match = _igcTextData!.firstOpenMatch!;
|
||||
if (match.updatedMatch.isITStart && _igcTextData != null) {
|
||||
_choreographer.openIT(match);
|
||||
return null;
|
||||
}
|
||||
|
||||
_choreographer.chatController.inputFocus.unfocus();
|
||||
return match;
|
||||
}
|
||||
|
||||
PangeaMatchState? getMatchByOffset(int offset) =>
|
||||
_igcTextData?.getMatchByOffset(offset);
|
||||
|
||||
PangeaMatch acceptReplacement(
|
||||
PangeaMatchState match,
|
||||
PangeaMatchStatus status,
|
||||
) {
|
||||
if (_igcTextData == null) {
|
||||
throw "acceptReplacement called with null igcTextData";
|
||||
}
|
||||
return _igcTextData!.acceptReplacement(match, status);
|
||||
}
|
||||
|
||||
PangeaMatch ignoreReplacement(PangeaMatchState match) {
|
||||
IgcRepo.ignore(match.updatedMatch);
|
||||
if (_igcTextData == null) {
|
||||
throw "should not be in onIgnoreMatch with null igcTextData";
|
||||
}
|
||||
return _igcTextData!.ignoreReplacement(match);
|
||||
}
|
||||
|
||||
void undoReplacement(PangeaMatchState match) {
|
||||
if (_igcTextData == null) {
|
||||
throw "undoReplacement called with null igcTextData";
|
||||
}
|
||||
_igcTextData!.undoReplacement(match);
|
||||
}
|
||||
|
||||
Future<void> getIGCTextData() async {
|
||||
if (choreographer.currentText.isEmpty) return clear();
|
||||
debugPrint('getIGCTextData called with ${choreographer.currentText}');
|
||||
if (_choreographer.currentText.isEmpty) return clear();
|
||||
debugPrint('getIGCTextData called with ${_choreographer.currentText}');
|
||||
|
||||
final IGCRequestModel reqBody = IGCRequestModel(
|
||||
fullText: choreographer.currentText,
|
||||
userId: choreographer.pangeaController.userController.userId!,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC:
|
||||
choreographer.igcEnabled && choreographer.choreoMode != ChoreoMode.it,
|
||||
enableIT:
|
||||
choreographer.itEnabled && choreographer.choreoMode != ChoreoMode.it,
|
||||
fullText: _choreographer.currentText,
|
||||
userId: _choreographer.pangeaController.userController.userId!,
|
||||
userL1: _choreographer.l1LangCode!,
|
||||
userL2: _choreographer.l2LangCode!,
|
||||
enableIGC: _choreographer.igcEnabled &&
|
||||
_choreographer.choreoMode != ChoreoMode.it,
|
||||
enableIT: _choreographer.itEnabled &&
|
||||
_choreographer.choreoMode != ChoreoMode.it,
|
||||
prevMessages: _prevMessages(),
|
||||
);
|
||||
|
||||
final res = await IgcRepo.get(
|
||||
choreographer.accessToken,
|
||||
_choreographer.pangeaController.userController.accessToken,
|
||||
reqBody,
|
||||
).timeout(
|
||||
(const Duration(seconds: 10)),
|
||||
|
|
@ -58,78 +125,80 @@ class IgcController {
|
|||
);
|
||||
|
||||
if (res.isError) {
|
||||
choreographer.errorService.setError(ChoreoError(raw: res.error));
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// this will happen when the user changes the input while igc is fetching results
|
||||
if (res.result!.originalInput.trim() != choreographer.currentText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final response = res.result!;
|
||||
igcTextData = IGCTextData(
|
||||
originalInput: response.originalInput,
|
||||
matches: response.matches,
|
||||
);
|
||||
choreographer.acceptNormalizationMatches();
|
||||
|
||||
for (final match in igcTextData!.openMatches) {
|
||||
setSpanDetails(match: match);
|
||||
}
|
||||
}
|
||||
|
||||
void onIgnoreMatch(PangeaMatch match) {
|
||||
IgcRepo.ignore(match);
|
||||
}
|
||||
|
||||
bool get canShowFirstMatch {
|
||||
return igcTextData?.firstOpenMatch != null;
|
||||
}
|
||||
|
||||
void showFirstMatch(BuildContext context) {
|
||||
if (!canShowFirstMatch) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "should not be calling showFirstMatch with this igcTextData.",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"igcTextData": igcTextData?.toJson(),
|
||||
},
|
||||
_igcTextData = IGCTextData(
|
||||
originalInput: reqBody.fullText,
|
||||
matches: [],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final match = igcTextData!.firstOpenMatch!;
|
||||
if (match.updatedMatch.isITStart && igcTextData != null) {
|
||||
choreographer.onITStart(match);
|
||||
// this will happen when the user changes the input while igc is fetching results
|
||||
if (res.result!.originalInput.trim() != _choreographer.currentText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
choreographer.chatController.inputFocus.unfocus();
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
OverlayUtil.showPositionedCard(
|
||||
overlayKey:
|
||||
"span_card_overlay_${match.updatedMatch.match.offset}_${match.updatedMatch.match.length}",
|
||||
context: context,
|
||||
cardToShow: SpanCard(
|
||||
match: match,
|
||||
choreographer: choreographer,
|
||||
),
|
||||
maxHeight: 325,
|
||||
maxWidth: 325,
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
onDismiss: () => choreographer.setState(),
|
||||
ignorePointer: true,
|
||||
isScrollable: false,
|
||||
final response = res.result!;
|
||||
_igcTextData = IGCTextData(
|
||||
originalInput: response.originalInput,
|
||||
matches: response.matches,
|
||||
);
|
||||
|
||||
try {
|
||||
_choreographer.acceptNormalizationMatches();
|
||||
if (_igcTextData != null) {
|
||||
for (final match in _igcTextData!.openMatches) {
|
||||
fetchSpanDetails(match: match);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"igcResponse": response.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchSpanDetails({
|
||||
required PangeaMatchState match,
|
||||
bool force = false,
|
||||
}) async {
|
||||
final span = match.updatedMatch.match;
|
||||
if (span.isNormalizationError() && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await SpanDataRepo.get(
|
||||
_choreographer.pangeaController.userController.accessToken,
|
||||
request: SpanDetailsRequest(
|
||||
userL1: _choreographer.l1LangCode!,
|
||||
userL2: _choreographer.l2LangCode!,
|
||||
enableIGC: _choreographer.igcEnabled,
|
||||
enableIT: _choreographer.itEnabled,
|
||||
span: span,
|
||||
),
|
||||
).timeout(
|
||||
(const Duration(seconds: 10)),
|
||||
onTimeout: () {
|
||||
return Result.error(
|
||||
TimeoutException('Span details request timed out'),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (response.isError) {
|
||||
_choreographer.clearMatches(response.error!);
|
||||
return;
|
||||
}
|
||||
|
||||
_igcTextData?.setSpanData(match, response.result!.span);
|
||||
}
|
||||
|
||||
/// Get the content of previous text and audio messages in chat.
|
||||
/// Passed to IGC request to add context.
|
||||
List<PreviousMessage> _prevMessages({int numMessages = 5}) {
|
||||
final List<Event> events = choreographer.chatController.visibleEvents
|
||||
final List<Event> events = _choreographer.chatController.visibleEvents
|
||||
.where(
|
||||
(e) =>
|
||||
e.type == EventTypes.Message &&
|
||||
|
|
@ -144,9 +213,9 @@ class IgcController {
|
|||
? event.content.toString()
|
||||
: PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: choreographer.chatController.timeline!,
|
||||
timeline: _choreographer.chatController.timeline!,
|
||||
ownMessage: event.senderId ==
|
||||
choreographer.pangeaController.matrixState.client.userID,
|
||||
_choreographer.pangeaController.matrixState.client.userID,
|
||||
).getSpeechToTextLocal()?.transcript.text.trim(); // trim whitespace
|
||||
if (content == null) continue;
|
||||
messages.add(
|
||||
|
|
@ -162,51 +231,4 @@ class IgcController {
|
|||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
bool get hasRelevantIGCTextData {
|
||||
if (igcTextData == null) return false;
|
||||
|
||||
if (igcTextData!.currentText != choreographer.currentText) {
|
||||
debugPrint(
|
||||
"returning isIGCTextDataRelevant false because text has changed",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
clear() {
|
||||
igcTextData = null;
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
}
|
||||
|
||||
Future<void> setSpanDetails({
|
||||
required PangeaMatchState match,
|
||||
bool force = false,
|
||||
}) async {
|
||||
final span = match.updatedMatch.match;
|
||||
if (span.isNormalizationError() && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await SpanDataRepo.get(
|
||||
choreographer.accessToken,
|
||||
request: SpanDetailsRequest(
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC: choreographer.igcEnabled,
|
||||
enableIT: choreographer.itEnabled,
|
||||
span: span,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.isError) {
|
||||
choreographer.errorService.setError(ChoreoError(raw: response.error));
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
igcTextData?.setSpanData(match, response.result!.span);
|
||||
choreographer.setState();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/it_repo.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../models/it_step.dart';
|
||||
|
|
@ -20,121 +17,180 @@ import '../repo/it_response_model.dart';
|
|||
import 'choreographer.dart';
|
||||
|
||||
class ITController {
|
||||
Choreographer choreographer;
|
||||
final Choreographer _choreographer;
|
||||
|
||||
bool _isOpen = false;
|
||||
bool _willOpen = false;
|
||||
bool _isEditingSourceText = false;
|
||||
bool dismissed = false;
|
||||
ITStep? _currentITStep;
|
||||
final List<Completer<ITStep>> _queue = [];
|
||||
GoldRouteTracker? _goldRouteTracker;
|
||||
|
||||
ITStartData? _itStartData;
|
||||
String? sourceText;
|
||||
List<ITStep> completedITSteps = [];
|
||||
CurrentITStep? currentITStep;
|
||||
Completer<CurrentITStep?>? nextITStep;
|
||||
GoldRouteTracker goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
List<int> payLoadIds = [];
|
||||
bool _open = false;
|
||||
bool _editing = false;
|
||||
bool _dismissed = false;
|
||||
|
||||
ITController(this.choreographer);
|
||||
ITController(this._choreographer);
|
||||
|
||||
void clear() {
|
||||
_isOpen = false;
|
||||
_willOpen = false;
|
||||
MatrixState.pAnyState.closeOverlay("it_feedback_card");
|
||||
bool get open => _open;
|
||||
bool get editing => _editing;
|
||||
bool get dismissed => _dismissed;
|
||||
List<Continuance>? get continuances => _currentITStep?.continuances;
|
||||
bool get isTranslationDone => _currentITStep?.isFinal ?? false;
|
||||
|
||||
_isEditingSourceText = false;
|
||||
dismissed = false;
|
||||
String? get _sourceText => _choreographer.sourceText;
|
||||
|
||||
_itStartData = null;
|
||||
sourceText = null;
|
||||
completedITSteps = [];
|
||||
currentITStep = null;
|
||||
nextITStep = null;
|
||||
goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
payLoadIds = [];
|
||||
|
||||
choreographer.choreoMode = ChoreoMode.igc;
|
||||
choreographer.setState();
|
||||
ITRequestModel _request(String textInput) {
|
||||
assert(_sourceText != null);
|
||||
return ITRequestModel(
|
||||
text: _sourceText!,
|
||||
customInput: textInput,
|
||||
sourceLangCode:
|
||||
MatrixState.pangeaController.languageController.activeL1Code()!,
|
||||
targetLangCode:
|
||||
MatrixState.pangeaController.languageController.activeL2Code()!,
|
||||
userId: _choreographer.chatController.room.client.userID!,
|
||||
roomId: _choreographer.chatController.room.id,
|
||||
goldTranslation: _goldRouteTracker?.fullTranslation,
|
||||
goldContinuances: _goldRouteTracker?.continuances,
|
||||
);
|
||||
}
|
||||
|
||||
Duration get animationSpeed => const Duration(milliseconds: 300);
|
||||
|
||||
Future<void> initializeIT(ITStartData itStartData) async {
|
||||
_willOpen = true;
|
||||
Future.delayed(const Duration(microseconds: 100), () {
|
||||
_isOpen = true;
|
||||
});
|
||||
_itStartData = itStartData;
|
||||
}
|
||||
void openIT() => _open = true;
|
||||
|
||||
void closeIT() {
|
||||
// if the user hasn't gone through any IT steps, reset the text
|
||||
if (completedITSteps.isEmpty && sourceText != null) {
|
||||
choreographer.textController.setSystemText(
|
||||
sourceText!,
|
||||
if (_choreographer.currentText.isEmpty && _sourceText != null) {
|
||||
_choreographer.textController.setSystemText(
|
||||
_sourceText!,
|
||||
EditType.itDismissed,
|
||||
);
|
||||
}
|
||||
clear();
|
||||
choreographer.errorService.resetError();
|
||||
dismissed = true;
|
||||
|
||||
clear(dismissed: true);
|
||||
}
|
||||
|
||||
/// if IGC isn't positive that text is full L1 then translate to L1
|
||||
Future<void> _setSourceText() async {
|
||||
if (_itStartData == null || _itStartData!.text.isEmpty) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "choreo context",
|
||||
data: {
|
||||
"igcTextData": choreographer.igc.igcTextData?.toJson(),
|
||||
"currentText": choreographer.currentText,
|
||||
},
|
||||
),
|
||||
);
|
||||
throw Exception("null _itStartData or empty text in _setSourceText");
|
||||
}
|
||||
debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}");
|
||||
// if (_itStartData!.langCode == choreographer.l1LangCode) {
|
||||
sourceText = _itStartData!.text;
|
||||
choreographer.translatedText = sourceText;
|
||||
return;
|
||||
// }
|
||||
void clear({bool dismissed = false}) {
|
||||
MatrixState.pAnyState.closeOverlay("it_feedback_card");
|
||||
|
||||
// final FullTextTranslationResponseModel res =
|
||||
// await FullTextTranslationRepo.translate(
|
||||
// accessToken: await choreographer.accessToken,
|
||||
// request: FullTextTranslationRequestModel(
|
||||
// text: _itStartData!.text,
|
||||
// tgtLang: choreographer.l1LangCode!,
|
||||
// srcLang: _itStartData!.langCode,
|
||||
// userL1: choreographer.l1LangCode!,
|
||||
// userL2: choreographer.l2LangCode!,
|
||||
// ),
|
||||
// );
|
||||
// sourceText = res.bestTranslation;
|
||||
_open = false;
|
||||
_editing = false;
|
||||
_dismissed = dismissed;
|
||||
_queue.clear();
|
||||
_currentITStep = null;
|
||||
_goldRouteTracker = null;
|
||||
|
||||
_choreographer.setChoreoMode(ChoreoMode.igc);
|
||||
_choreographer.setSourceText(null);
|
||||
}
|
||||
|
||||
// used 1) at very beginning (with custom input = null)
|
||||
// and 2) if they make direct edits to the text field
|
||||
Future<void> getTranslationData(bool useCustomInput) async {
|
||||
final String currentText = choreographer.currentText;
|
||||
void setEditing(bool value) => _editing = value;
|
||||
|
||||
if (sourceText == null) await _setSourceText();
|
||||
void onSubmitEdits() {
|
||||
_editing = false;
|
||||
_queue.clear();
|
||||
_currentITStep = null;
|
||||
_goldRouteTracker = null;
|
||||
continueIT();
|
||||
}
|
||||
|
||||
if (useCustomInput && currentITStep != null) {
|
||||
completedITSteps.add(
|
||||
ITStep(
|
||||
currentITStep!.continuances,
|
||||
customInput: currentText,
|
||||
),
|
||||
);
|
||||
Continuance onSelectContinuance(int index) {
|
||||
if (_currentITStep == null) {
|
||||
throw "onSelectContinuance called with null currentITStep";
|
||||
}
|
||||
|
||||
currentITStep = null;
|
||||
if (index < 0 || index >= _currentITStep!.continuances.length) {
|
||||
throw "onSelectContinuance called with invalid index $index";
|
||||
}
|
||||
|
||||
// During first IT step, next step will not be set
|
||||
if (nextITStep == null) {
|
||||
final step = _currentITStep!.continuances[index];
|
||||
_currentITStep!.continuances[index] = step.copyWith(
|
||||
wasClicked: true,
|
||||
);
|
||||
return _currentITStep!.continuances[index];
|
||||
}
|
||||
|
||||
CompletedITStep getAcceptedITStep(int chosenIndex) {
|
||||
if (_currentITStep == null) {
|
||||
throw "getAcceptedITStep called with null currentITStep";
|
||||
}
|
||||
|
||||
if (chosenIndex < 0 || chosenIndex >= _currentITStep!.continuances.length) {
|
||||
throw "getAcceptedITStep called with invalid index $chosenIndex";
|
||||
}
|
||||
|
||||
return CompletedITStep(
|
||||
_currentITStep!.continuances,
|
||||
chosen: chosenIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> continueIT() async {
|
||||
if (_currentITStep == 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initTranslationData() async {
|
||||
final String currentText = _choreographer.currentText;
|
||||
final res = await ITRepo.get(_request(currentText)).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
return Result.error(
|
||||
TimeoutException("ITRepo.get timed out after 10 seconds"),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (_sourceText == null || !_open) return;
|
||||
if (res.isError || res.result?.goldContinuances == null) {
|
||||
_choreographer.errorService.setErrorAndLock(
|
||||
ChoreoError(raw: res.asError),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = res.result!;
|
||||
_goldRouteTracker = GoldRouteTracker(
|
||||
result.goldContinuances!,
|
||||
_sourceText!,
|
||||
);
|
||||
|
||||
_currentITStep = ITStep(
|
||||
sourceText: _sourceText!,
|
||||
currentText: currentText,
|
||||
responseModel: res.result!,
|
||||
storedGoldContinuances: _goldRouteTracker!.continuances,
|
||||
);
|
||||
|
||||
_fillITStepQueue();
|
||||
}
|
||||
|
||||
Future<void> _fillITStepQueue() async {
|
||||
if (_sourceText == null || _goldRouteTracker!.continuances.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
final sourceText = _sourceText!;
|
||||
String currentText =
|
||||
_choreographer.currentText + _goldRouteTracker!.continuances[0].text;
|
||||
|
||||
for (int i = 1; i < _goldRouteTracker!.continuances.length; i++) {
|
||||
_queue.add(Completer<ITStep>());
|
||||
final res = await ITRepo.get(_request(currentText)).timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
|
|
@ -145,192 +201,29 @@ class ITController {
|
|||
);
|
||||
|
||||
if (res.isError) {
|
||||
if (_willOpen) {
|
||||
choreographer.errorService.setErrorAndLock(
|
||||
ChoreoError(raw: res.asError),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sourceText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = res.result!;
|
||||
if (result.goldContinuances != null &&
|
||||
result.goldContinuances!.isNotEmpty) {
|
||||
goldRouteTracker = GoldRouteTracker(
|
||||
result.goldContinuances!,
|
||||
sourceText!,
|
||||
_queue.last.completeError(res.asError!);
|
||||
break;
|
||||
} else {
|
||||
final step = ITStep(
|
||||
sourceText: sourceText,
|
||||
currentText: currentText,
|
||||
responseModel: res.result!,
|
||||
storedGoldContinuances: _goldRouteTracker!.continuances,
|
||||
);
|
||||
_queue.last.complete(step);
|
||||
}
|
||||
|
||||
currentITStep = CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: currentText,
|
||||
responseModel: result,
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
|
||||
_addPayloadId(result);
|
||||
} else {
|
||||
currentITStep = await nextITStep!.future;
|
||||
}
|
||||
|
||||
if (isTranslationDone) {
|
||||
nextITStep = null;
|
||||
closeIT();
|
||||
} else {
|
||||
nextITStep = Completer<CurrentITStep?>();
|
||||
final nextStep = await _getNextTranslationData();
|
||||
nextITStep?.complete(nextStep);
|
||||
currentText += _goldRouteTracker!.continuances[i].text;
|
||||
}
|
||||
}
|
||||
|
||||
Future<CurrentITStep?> _getNextTranslationData() async {
|
||||
if (sourceText == null) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("sourceText is null in getNextTranslationData"),
|
||||
data: {
|
||||
"sourceText": sourceText,
|
||||
"currentITStepPayloadID": currentITStep?.payloadId,
|
||||
"continuances": goldRouteTracker.continuances.map((e) => e.toJson()),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (completedITSteps.length >= goldRouteTracker.continuances.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String currentText = choreographer.currentText;
|
||||
final String nextText =
|
||||
goldRouteTracker.continuances[completedITSteps.length].text;
|
||||
|
||||
final res = await ITRepo.get(
|
||||
_request(currentText + nextText),
|
||||
);
|
||||
|
||||
if (sourceText == null) return null;
|
||||
if (res.isError) {
|
||||
choreographer.errorService.setErrorAndLock(
|
||||
ChoreoError(raw: res.asError),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: nextText,
|
||||
responseModel: res.result!,
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onEditSourceTextSubmit(String newSourceText) async {
|
||||
try {
|
||||
_isOpen = true;
|
||||
_isEditingSourceText = false;
|
||||
_itStartData = ITStartData(newSourceText, choreographer.l1LangCode);
|
||||
completedITSteps = [];
|
||||
currentITStep = null;
|
||||
nextITStep = null;
|
||||
goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
payLoadIds = [];
|
||||
|
||||
_setSourceText();
|
||||
getTranslationData(false);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
if (err is! http.Response) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: stack,
|
||||
data: {
|
||||
"newSourceText": newSourceText,
|
||||
"l1Lang": choreographer.l1LangCode,
|
||||
},
|
||||
);
|
||||
}
|
||||
choreographer.errorService.setErrorAndLock(
|
||||
ChoreoError(raw: err),
|
||||
);
|
||||
} finally {
|
||||
choreographer.textController.setSystemText(
|
||||
"",
|
||||
EditType.other,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ITRequestModel _request(String textInput) => ITRequestModel(
|
||||
text: sourceText!,
|
||||
customInput: textInput,
|
||||
sourceLangCode: sourceLangCode,
|
||||
targetLangCode: targetLangCode,
|
||||
userId: choreographer.userId!,
|
||||
roomId: choreographer.roomId!,
|
||||
goldTranslation: goldRouteTracker.fullTranslation,
|
||||
goldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
|
||||
//maybe we store IT data in the same format? make a specific kind of match?
|
||||
void selectTranslation(int chosenIndex) {
|
||||
if (currentITStep == null) return;
|
||||
final itStep = ITStep(
|
||||
currentITStep!.continuances,
|
||||
chosen: chosenIndex,
|
||||
);
|
||||
|
||||
completedITSteps.add(itStep);
|
||||
choreographer.onITChoiceSelect(itStep);
|
||||
choreographer.setState();
|
||||
}
|
||||
|
||||
String get uniqueKeyForLayerLink => "itChoices${choreographer.roomId}";
|
||||
|
||||
void _addPayloadId(ITResponseModel res) {
|
||||
payLoadIds.add(res.payloadId);
|
||||
}
|
||||
|
||||
bool get isTranslationDone => currentITStep != null && currentITStep!.isFinal;
|
||||
|
||||
bool get isOpen => _isOpen;
|
||||
|
||||
bool get willOpen => _willOpen;
|
||||
|
||||
String get targetLangCode => choreographer.l2LangCode!;
|
||||
|
||||
String get sourceLangCode => choreographer.l1LangCode!;
|
||||
|
||||
bool get isLoading => choreographer.isFetching;
|
||||
|
||||
void setIsEditingSourceText(bool value) {
|
||||
_isEditingSourceText = value;
|
||||
choreographer.setState();
|
||||
}
|
||||
|
||||
bool get isEditingSourceText => _isEditingSourceText;
|
||||
}
|
||||
|
||||
class ITStartData {
|
||||
String text;
|
||||
String? langCode;
|
||||
|
||||
ITStartData(this.text, this.langCode);
|
||||
}
|
||||
|
||||
class GoldRouteTracker {
|
||||
late String _originalText;
|
||||
List<Continuance> continuances;
|
||||
final String _originalText;
|
||||
final List<Continuance> continuances;
|
||||
|
||||
GoldRouteTracker(this.continuances, String originalText) {
|
||||
_originalText = originalText;
|
||||
}
|
||||
|
||||
static get defaultTracker => GoldRouteTracker([], "");
|
||||
const GoldRouteTracker(this.continuances, String originalText)
|
||||
: _originalText = originalText;
|
||||
|
||||
Continuance? currentContinuance({
|
||||
required String currentText,
|
||||
|
|
@ -362,13 +255,11 @@ class GoldRouteTracker {
|
|||
}
|
||||
}
|
||||
|
||||
class CurrentITStep {
|
||||
class ITStep {
|
||||
late List<Continuance> continuances;
|
||||
late bool isFinal;
|
||||
late String? translationId;
|
||||
late int payloadId;
|
||||
|
||||
CurrentITStep({
|
||||
ITStep({
|
||||
required String sourceText,
|
||||
required String currentText,
|
||||
required ITResponseModel responseModel,
|
||||
|
|
@ -379,8 +270,6 @@ class CurrentITStep {
|
|||
final goldTracker = GoldRouteTracker(gold, sourceText);
|
||||
|
||||
isFinal = responseModel.isFinal;
|
||||
translationId = responseModel.translationId;
|
||||
payloadId = responseModel.payloadId;
|
||||
|
||||
if (responseModel.continuances.isEmpty) {
|
||||
continuances = [];
|
||||
|
|
@ -410,8 +299,4 @@ class CurrentITStep {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get continuance with highest level
|
||||
Continuance get best =>
|
||||
continuances.reduce((a, b) => a.level < b.level ? a : b);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,122 +1,96 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.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/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../common/utils/overlay.dart';
|
||||
import '../enums/edit_type.dart';
|
||||
import 'choreographer.dart';
|
||||
|
||||
class PangeaTextController extends TextEditingController {
|
||||
Choreographer choreographer;
|
||||
|
||||
final Choreographer choreographer;
|
||||
EditType editType = EditType.keyboard;
|
||||
String _currentText = '';
|
||||
|
||||
PangeaTextController({
|
||||
String? text,
|
||||
required this.choreographer,
|
||||
}) {
|
||||
text ??= '';
|
||||
this.text = text;
|
||||
addListener(() {
|
||||
final difference =
|
||||
text.characters.length - _currentText.characters.length;
|
||||
|
||||
if (difference > 1 && editType == EditType.keyboard) {
|
||||
choreographer.onPaste(
|
||||
text.characters
|
||||
.getRange(
|
||||
_currentText.characters.length,
|
||||
text.characters.length,
|
||||
)
|
||||
.join(),
|
||||
);
|
||||
}
|
||||
_currentText = text;
|
||||
});
|
||||
}
|
||||
|
||||
static const int maxLength = 1000;
|
||||
bool get exceededMaxLength => text.length >= maxLength;
|
||||
bool get exceededMaxLength => text.length >= ChoreoConstants.maxLength;
|
||||
|
||||
bool forceKeepOpen = false;
|
||||
TextStyle _underlineStyle(Color color) => TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: color,
|
||||
decorationThickness: 5,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void setSystemText(String text, EditType type) {
|
||||
editType = type;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
void onInputTap(BuildContext context, {required FocusNode fNode}) {
|
||||
fNode.requestFocus();
|
||||
forceKeepOpen = true;
|
||||
if (!context.mounted) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// show the paywall if appropriate
|
||||
if (choreographer
|
||||
.pangeaController.subscriptionController.subscriptionStatus ==
|
||||
SubscriptionStatus.shouldShowPaywall &&
|
||||
!choreographer.isFetching &&
|
||||
text.isNotEmpty) {
|
||||
PaywallCard.show(context, choreographer.chatController);
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no igc text data, then don't do anything
|
||||
if (choreographer.igc.igcTextData == null) return;
|
||||
|
||||
// debugPrint(
|
||||
// "onInputTap matches are ${choreographer.igc.igcTextData?.matches.map((e) => e.match.rule.id).toList().toString()}");
|
||||
|
||||
// if user is just trying to get their cursor into the text input field to add soemthing,
|
||||
// then don't interrupt them
|
||||
if (selection.baseOffset >= text.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final match = choreographer.igc.igcTextData!.getMatchByOffset(
|
||||
selection.baseOffset,
|
||||
);
|
||||
if (match == null) return;
|
||||
|
||||
// if autoplay on and it start then just start it
|
||||
if (match.updatedMatch.isITStart) {
|
||||
return choreographer.onITStart(match);
|
||||
}
|
||||
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
OverlayUtil.showPositionedCard(
|
||||
overlayKey:
|
||||
"span_card_overlay_${match.updatedMatch.match.offset}_${match.updatedMatch.match.length}",
|
||||
context: context,
|
||||
maxHeight: 400,
|
||||
maxWidth: 350,
|
||||
cardToShow: SpanCard(
|
||||
match: match,
|
||||
choreographer: choreographer,
|
||||
),
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
onDismiss: () => choreographer.setState(),
|
||||
ignorePointer: true,
|
||||
isScrollable: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TextSpan buildTextSpan({
|
||||
required BuildContext context,
|
||||
TextStyle? style,
|
||||
required bool withComposing,
|
||||
}) {
|
||||
// If the composing range is out of range for the current text, ignore it to
|
||||
// preserve the tree integrity, otherwise in release mode a RangeError will
|
||||
// be thrown and this EditableText will be built with a broken subtree.
|
||||
// debugPrint("composing? $withComposing");
|
||||
// if (!value.isComposingRangeValid || !withComposing) {
|
||||
// debugPrint("just returning straight text");
|
||||
// // debugger(when: kDebugMode);
|
||||
// return TextSpan(style: style, text: text);
|
||||
// }
|
||||
// if (value.isComposingRangeValid) {
|
||||
// debugPrint("composing before ${value.composing.textBefore(value.text)}");
|
||||
// debugPrint("composing inside ${value.composing.textInside(value.text)}");
|
||||
// debugPrint("composing after ${value.composing.textAfter(value.text)}");
|
||||
// }
|
||||
|
||||
final SubscriptionStatus canSendStatus = choreographer
|
||||
.pangeaController.subscriptionController.subscriptionStatus;
|
||||
if (canSendStatus == SubscriptionStatus.shouldShowPaywall &&
|
||||
|
|
@ -125,33 +99,38 @@ class PangeaTextController extends TextEditingController {
|
|||
return TextSpan(
|
||||
text: text,
|
||||
style: style?.merge(
|
||||
MatchStyleUtil.underlineStyle(
|
||||
_underlineStyle(
|
||||
const Color.fromARGB(187, 132, 96, 224),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (choreographer.igc.igcTextData == null || text.isEmpty) {
|
||||
} else if (!choreographer.hasIGCTextData || text.isEmpty) {
|
||||
return TextSpan(text: text, style: style);
|
||||
} else {
|
||||
final parts = text.split(choreographer.igc.igcTextData!.currentText);
|
||||
|
||||
final parts = text.split(choreographer.currentIGCText!);
|
||||
if (parts.length == 1 || parts.length > 2) {
|
||||
return TextSpan(text: text, style: style);
|
||||
}
|
||||
|
||||
List<InlineSpan> inlineSpans = [];
|
||||
try {
|
||||
inlineSpans = constructTokenSpan(
|
||||
defaultStyle: style,
|
||||
onUndo: choreographer.onUndoReplacement,
|
||||
);
|
||||
} catch (e) {
|
||||
choreographer.errorService.setError(
|
||||
ChoreoError(raw: e),
|
||||
);
|
||||
inlineSpans = [TextSpan(text: text, style: style)];
|
||||
choreographer.igc.clear();
|
||||
}
|
||||
final inlineSpans = constructTokenSpan(
|
||||
defaultStyle: style,
|
||||
onUndo: (match) {
|
||||
try {
|
||||
choreographer.onUndoReplacement(match);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
choreographer.clearMatches(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return TextSpan(
|
||||
style: style,
|
||||
|
|
@ -169,7 +148,7 @@ class PangeaTextController extends TextEditingController {
|
|||
VoidCallback onUndo,
|
||||
) {
|
||||
if (match.updatedMatch.status == PangeaMatchStatus.automatic) {
|
||||
final span = choreographer.igc.igcTextData!.currentText.characters
|
||||
final span = choreographer.currentIGCText!.characters
|
||||
.getRange(
|
||||
match.updatedMatch.match.offset,
|
||||
match.updatedMatch.match.offset + match.updatedMatch.match.length,
|
||||
|
|
@ -193,7 +172,7 @@ class PangeaTextController extends TextEditingController {
|
|||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: choreographer.igc.igcTextData!.currentText.characters
|
||||
text: choreographer.currentIGCText!.characters
|
||||
.getRange(
|
||||
match.updatedMatch.match.offset,
|
||||
match.updatedMatch.match.offset + match.updatedMatch.match.length,
|
||||
|
|
@ -210,20 +189,20 @@ class PangeaTextController extends TextEditingController {
|
|||
required void Function(PangeaMatchState) onUndo,
|
||||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final automaticMatches = choreographer.igc.igcTextData!.closedMatches
|
||||
.where((m) => m.updatedMatch.status == PangeaMatchStatus.automatic)
|
||||
.toList();
|
||||
final automaticMatches = choreographer.closedIGCMatches
|
||||
?.where((m) => m.updatedMatch.status == PangeaMatchStatus.automatic)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
final textSpanMatches = [
|
||||
...choreographer.igc.igcTextData!.openMatches,
|
||||
...choreographer.openIGCMatches ?? [],
|
||||
...automaticMatches,
|
||||
]..sort(
|
||||
(a, b) =>
|
||||
a.updatedMatch.match.offset.compareTo(b.updatedMatch.match.offset),
|
||||
);
|
||||
|
||||
final currentText = choreographer.igc.igcTextData!.currentText;
|
||||
|
||||
final currentText = choreographer.currentIGCText!;
|
||||
final spans = <InlineSpan>[];
|
||||
int cursor = 0;
|
||||
|
||||
|
|
@ -235,9 +214,8 @@ class PangeaTextController extends TextEditingController {
|
|||
spans.add(TextSpan(text: text, style: defaultStyle));
|
||||
}
|
||||
|
||||
final openMatch =
|
||||
choreographer.igc.igcTextData?.openMatch?.updatedMatch.match;
|
||||
final style = MatchStyleUtil.textStyle(
|
||||
final openMatch = choreographer.openIGCMatch?.updatedMatch.match;
|
||||
final style = _textStyle(
|
||||
match.updatedMatch,
|
||||
defaultStyle,
|
||||
openMatch != null &&
|
||||
|
|
|
|||
1
lib/pangea/choreographer/enums/choreo_mode.dart
Normal file
1
lib/pangea/choreographer/enums/choreo_mode.dart
Normal file
|
|
@ -0,0 +1 @@
|
|||
enum ChoreoMode { igc, it }
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
enum EditType {
|
||||
itStandard,
|
||||
igc,
|
||||
keyboard,
|
||||
alternativeTranslation,
|
||||
itGold,
|
||||
itStart,
|
||||
it,
|
||||
itDismissed,
|
||||
keyboard,
|
||||
other,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class ChoreoRecord {
|
|||
return text;
|
||||
}
|
||||
|
||||
void addRecord(String text, {PangeaMatch? match, ITStep? step}) {
|
||||
void addRecord(String text, {PangeaMatch? match, CompletedITStep? step}) {
|
||||
if (match != null && step != null) {
|
||||
throw Exception("match and step should not both be defined");
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ class ChoreoRecordStep {
|
|||
/// last step in list may contain open
|
||||
final PangeaMatch? acceptedOrIgnoredMatch;
|
||||
|
||||
final ITStep? itStep;
|
||||
final CompletedITStep? itStep;
|
||||
|
||||
ChoreoRecordStep({
|
||||
this.edits,
|
||||
|
|
@ -264,7 +264,9 @@ class ChoreoRecordStep {
|
|||
acceptedOrIgnoredMatch: json[_acceptedOrIgnoredMatchKey] != null
|
||||
? PangeaMatch.fromJson(json[_acceptedOrIgnoredMatchKey])
|
||||
: null,
|
||||
itStep: json[_stepKey] != null ? ITStep.fromJson(json[_stepKey]) : null,
|
||||
itStep: json[_stepKey] != null
|
||||
? CompletedITStep.fromJson(json[_stepKey])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ class IGCTextData {
|
|||
List<PangeaMatchState> get openNormalizationMatches =>
|
||||
_state.openNormalizationMatches;
|
||||
|
||||
void clearMatches() => _state.clearMatches();
|
||||
|
||||
void setSpanData(PangeaMatchState match, SpanData spanData) {
|
||||
_state.setSpanData(match, spanData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ class IGCTextState {
|
|||
);
|
||||
}
|
||||
|
||||
void clearMatches() {
|
||||
_openMatches.clear();
|
||||
_closedMatches.clear();
|
||||
}
|
||||
|
||||
void _filterIgnoredMatches() {
|
||||
for (final match in _openMatches) {
|
||||
if (IgcRepo.isIgnored(match.updatedMatch)) {
|
||||
|
|
@ -110,12 +115,13 @@ class IGCTextState {
|
|||
PangeaMatchState match,
|
||||
PangeaMatchStatus status,
|
||||
) {
|
||||
final openMatch = _openMatches.firstWhereOrNull(
|
||||
final openMatch = _openMatches.firstWhere(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
orElse: () => throw "No open match found for acceptReplacement",
|
||||
);
|
||||
|
||||
if (match.updatedMatch.match.selectedChoice == null) {
|
||||
throw "match.match.selectedChoice is null in acceptReplacement";
|
||||
throw "acceptReplacement called with null selectedChoice";
|
||||
}
|
||||
|
||||
match.setStatus(status);
|
||||
|
|
@ -132,8 +138,9 @@ class IGCTextState {
|
|||
}
|
||||
|
||||
PangeaMatch ignoreReplacement(PangeaMatchState match) {
|
||||
final openMatch = _openMatches.firstWhereOrNull(
|
||||
final openMatch = _openMatches.firstWhere(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
orElse: () => throw "No open match found for ignoreReplacement",
|
||||
);
|
||||
|
||||
match.setStatus(PangeaMatchStatus.ignored);
|
||||
|
|
@ -143,8 +150,9 @@ class IGCTextState {
|
|||
}
|
||||
|
||||
void undoReplacement(PangeaMatchState match) {
|
||||
final closedMatch = _closedMatches.firstWhereOrNull(
|
||||
final closedMatch = _closedMatches.firstWhere(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
orElse: () => throw "No closed match found for undoReplacement",
|
||||
);
|
||||
|
||||
_closedMatches.remove(closedMatch);
|
||||
|
|
|
|||
|
|
@ -3,48 +3,35 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import '../constants/choreo_constants.dart';
|
||||
|
||||
class ITStep {
|
||||
class CompletedITStep {
|
||||
final List<Continuance> continuances;
|
||||
final int? chosen;
|
||||
final String? customInput;
|
||||
final bool showAlternativeTranslationOption = false;
|
||||
final int chosen;
|
||||
|
||||
ITStep(
|
||||
const CompletedITStep(
|
||||
this.continuances, {
|
||||
this.chosen,
|
||||
this.customInput,
|
||||
}) {
|
||||
if (chosen == null && customInput == null) {
|
||||
throw Exception("ITStep must have either chosen or customInput");
|
||||
}
|
||||
if (chosen != null && customInput != null) {
|
||||
throw Exception("ITStep must have only chosen or customInput");
|
||||
}
|
||||
}
|
||||
required this.chosen,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['continuances'] = continuances.map((e) => e.toJson(true)).toList();
|
||||
data['chosen'] = chosen;
|
||||
data['custom_input'] = customInput;
|
||||
return data;
|
||||
}
|
||||
|
||||
factory ITStep.fromJson(Map<String, dynamic> json) {
|
||||
factory CompletedITStep.fromJson(Map<String, dynamic> json) {
|
||||
final List<Continuance> continuances = <Continuance>[];
|
||||
for (final Map<String, dynamic> continuance in json['continuances']) {
|
||||
continuances.add(Continuance.fromJson(continuance));
|
||||
}
|
||||
return ITStep(
|
||||
return CompletedITStep(
|
||||
continuances,
|
||||
chosen: json['chosen'],
|
||||
customInput: json['custom_input'],
|
||||
);
|
||||
}
|
||||
|
||||
Continuance? get chosenContinuance {
|
||||
if (chosen == null) return null;
|
||||
return continuances[chosen!];
|
||||
return continuances[chosen];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/utils/text_normalization_util.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import '../enums/span_choice_type.dart';
|
||||
import '../enums/span_data_type.dart';
|
||||
|
||||
|
|
@ -136,8 +137,33 @@ class SpanData {
|
|||
final errorSpan = fullText.characters.skip(offset).take(length).toString();
|
||||
|
||||
return correctChoice != null &&
|
||||
TextNormalizationUtil.normalizeString(correctChoice) ==
|
||||
TextNormalizationUtil.normalizeString(errorSpan);
|
||||
_normalizeString(correctChoice) == _normalizeString(errorSpan);
|
||||
}
|
||||
|
||||
String _normalizeString(String input) {
|
||||
try {
|
||||
// Step 1: Remove diacritics (accents)
|
||||
String normalized = removeDiacritics(input);
|
||||
normalized = normalized.replaceAll(RegExp(r'[^\x00-\x7F]'), '');
|
||||
|
||||
// Step 2: Remove punctuation
|
||||
normalized = normalized.replaceAll(RegExp(r'[^\w\s]'), '');
|
||||
|
||||
// Step 3: Convert to lowercase
|
||||
normalized = normalized.toLowerCase();
|
||||
|
||||
// Step 4: Trim and normalize whitespace
|
||||
normalized = normalized.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
|
||||
return normalized.isEmpty ? input : normalized;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'input': input},
|
||||
);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
|
||||
class InputPasteListener {
|
||||
final PangeaTextController controller;
|
||||
final Function(String) onPaste;
|
||||
|
||||
String _currentText = '';
|
||||
|
||||
InputPasteListener(
|
||||
this.controller,
|
||||
this.onPaste,
|
||||
) {
|
||||
controller.addListener(() {
|
||||
final difference =
|
||||
controller.text.characters.length - _currentText.characters.length;
|
||||
if (difference > 1 && controller.editType == EditType.keyboard) {
|
||||
onPaste(
|
||||
controller.text.characters
|
||||
.getRange(
|
||||
_currentText.characters.length,
|
||||
controller.text.characters.length,
|
||||
)
|
||||
.join(),
|
||||
);
|
||||
}
|
||||
_currentText = controller.text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import 'package:diacritic/diacritic.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
||||
class TextNormalizationUtil {
|
||||
static String normalizeString(String input) {
|
||||
try {
|
||||
// Step 1: Remove diacritics (accents)
|
||||
String normalized = removeDiacritics(input);
|
||||
normalized = normalized.replaceAll(RegExp(r'[^\x00-\x7F]'), '');
|
||||
|
||||
// Step 2: Remove punctuation
|
||||
normalized = normalized.replaceAll(RegExp(r'[^\w\s]'), '');
|
||||
|
||||
// Step 3: Convert to lowercase
|
||||
normalized = normalized.toLowerCase();
|
||||
|
||||
// Step 4: Trim and normalize whitespace
|
||||
normalized = normalized.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||
|
||||
return normalized.isEmpty ? input : normalized;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {'input': input},
|
||||
);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,23 +3,16 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
|
||||
class CardErrorWidget extends StatelessWidget {
|
||||
final String error;
|
||||
final Choreographer? choreographer;
|
||||
final int? offset;
|
||||
final double maxWidth;
|
||||
final double padding;
|
||||
|
||||
const CardErrorWidget({
|
||||
super.key,
|
||||
required this.error,
|
||||
this.choreographer,
|
||||
this.offset,
|
||||
this.maxWidth = 275,
|
||||
this.padding = 8,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -31,7 +24,7 @@ class CardErrorWidget extends StatelessWidget {
|
|||
);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(padding),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -3,20 +3,17 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class LanguageMismatchPopup extends StatelessWidget {
|
||||
final String targetLanguage;
|
||||
final Choreographer choreographer;
|
||||
final VoidCallback onUpdate;
|
||||
|
||||
const LanguageMismatchPopup({
|
||||
super.key,
|
||||
required this.targetLanguage,
|
||||
required this.choreographer,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart';
|
||||
|
|
@ -10,15 +9,13 @@ import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class PaywallCard extends StatelessWidget {
|
||||
final ChatController chatController;
|
||||
const PaywallCard({
|
||||
super.key,
|
||||
required this.chatController,
|
||||
});
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context,
|
||||
ChatController chatController,
|
||||
String targetId,
|
||||
) async {
|
||||
if (!MatrixState
|
||||
.pangeaController.subscriptionController.shouldShowPaywall) {
|
||||
|
|
@ -28,12 +25,10 @@ class PaywallCard extends StatelessWidget {
|
|||
await SubscriptionManagementRepo.setDismissedPaywall();
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: PaywallCard(
|
||||
chatController: chatController,
|
||||
),
|
||||
cardToShow: const PaywallCard(),
|
||||
maxHeight: 325,
|
||||
maxWidth: 325,
|
||||
transformTargetId: chatController.choreographer.inputTransformTargetKey,
|
||||
transformTargetId: targetId,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +86,6 @@ class PaywallCard extends StatelessWidget {
|
|||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
chatController.clearSelectedEvents();
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/span_choice_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../bot/widgets/bot_face_svg.dart';
|
||||
import '../choice_array.dart';
|
||||
import 'why_button.dart';
|
||||
|
||||
// CTODO refactor
|
||||
class SpanCard extends StatefulWidget {
|
||||
final PangeaMatchState match;
|
||||
final Choreographer choreographer;
|
||||
|
|
@ -34,11 +40,6 @@ class SpanCardState extends State<SpanCard> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.match.updatedMatch.isITStart == true) {
|
||||
_onITStart();
|
||||
return;
|
||||
}
|
||||
|
||||
getSpanDetails();
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +61,7 @@ class SpanCardState extends State<SpanCard> {
|
|||
fetchingData = true;
|
||||
});
|
||||
|
||||
await widget.choreographer.igc.setSpanDetails(
|
||||
await widget.choreographer.fetchSpanDetails(
|
||||
match: widget.match,
|
||||
force: force,
|
||||
);
|
||||
|
|
@ -70,12 +71,6 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onITStart() {
|
||||
if (widget.choreographer.itEnabled) {
|
||||
widget.choreographer.onITStart(widget.match);
|
||||
}
|
||||
}
|
||||
|
||||
void _onChoiceSelect(int index) {
|
||||
widget.match.selectChoice(index);
|
||||
setState(
|
||||
|
|
@ -86,25 +81,53 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
|
||||
Future<void> _onAcceptReplacement() async {
|
||||
await widget.choreographer.onAcceptReplacement(
|
||||
match: widget.match,
|
||||
);
|
||||
try {
|
||||
widget.choreographer.onAcceptReplacement(
|
||||
match: widget.match,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"match": widget.match.toJson(),
|
||||
},
|
||||
);
|
||||
widget.choreographer.clearMatches(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_showFirstMatch();
|
||||
}
|
||||
|
||||
void _onIgnoreMatch() {
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() {
|
||||
widget.choreographer.onIgnoreMatch(match: widget.match);
|
||||
_showFirstMatch();
|
||||
},
|
||||
);
|
||||
try {
|
||||
widget.choreographer.onIgnoreMatch(match: widget.match);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"match": widget.match.toJson(),
|
||||
},
|
||||
);
|
||||
widget.choreographer.clearMatches(e);
|
||||
return;
|
||||
}
|
||||
|
||||
_showFirstMatch();
|
||||
}
|
||||
|
||||
void _showFirstMatch() {
|
||||
if (widget.choreographer.igc.canShowFirstMatch) {
|
||||
widget.choreographer.igc.showFirstMatch(context);
|
||||
if (widget.choreographer.canShowFirstIGCMatch) {
|
||||
final igcMatch = widget.choreographer.igc.onShowFirstMatch();
|
||||
OverlayUtil.showIGCMatch(
|
||||
igcMatch!,
|
||||
widget.choreographer,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
|
|
@ -18,23 +17,19 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import 'card_error_widget.dart';
|
||||
|
||||
class WordDataCard extends StatefulWidget {
|
||||
final bool hasInfo;
|
||||
final String word;
|
||||
final String fullText;
|
||||
final String? choiceFeedback;
|
||||
final String wordLang;
|
||||
final String fullTextLang;
|
||||
final Room room;
|
||||
|
||||
const WordDataCard({
|
||||
super.key,
|
||||
required this.word,
|
||||
required this.wordLang,
|
||||
required this.hasInfo,
|
||||
required this.fullText,
|
||||
required this.fullTextLang,
|
||||
required this.room,
|
||||
this.choiceFeedback,
|
||||
required this.wordLang,
|
||||
required this.fullTextLang,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ import 'dart:developer';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
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/it_controller.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';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/word_data_card.dart';
|
||||
|
|
@ -30,11 +34,7 @@ class ITBar extends StatefulWidget {
|
|||
}
|
||||
|
||||
class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
||||
ITController get itController => widget.choreographer.itController;
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
bool showedClickInstruction = false;
|
||||
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
bool wasOpen = false;
|
||||
|
|
@ -44,24 +44,28 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
super.initState();
|
||||
|
||||
// Rebuild the widget each time there's an update from choreo.
|
||||
_choreoSub = widget.choreographer.stateStream.stream.listen((_) {
|
||||
if (itController.willOpen != wasOpen) {
|
||||
itController.willOpen ? _controller.forward() : _controller.reverse();
|
||||
widget.choreographer.addListener(() {
|
||||
if (widget.choreographer.isITOpen != wasOpen) {
|
||||
widget.choreographer.isITOpen
|
||||
? _controller.forward()
|
||||
: _controller.reverse();
|
||||
}
|
||||
wasOpen = itController.willOpen;
|
||||
wasOpen = widget.choreographer.isITOpen;
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
wasOpen = itController.willOpen;
|
||||
wasOpen = widget.choreographer.isITOpen;
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: itController.animationSpeed,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
|
||||
|
||||
// Start in the correct state
|
||||
itController.willOpen ? _controller.forward() : _controller.reverse();
|
||||
widget.choreographer.isITOpen
|
||||
? _controller.forward()
|
||||
: _controller.reverse();
|
||||
}
|
||||
|
||||
bool get showITInstructionsTooltip {
|
||||
|
|
@ -75,18 +79,10 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
bool get showTranslationsChoicesTooltip {
|
||||
return !showedClickInstruction &&
|
||||
!showITInstructionsTooltip &&
|
||||
!itController.choreographer.isFetching &&
|
||||
!itController.isLoading &&
|
||||
!itController.isEditingSourceText &&
|
||||
!itController.isTranslationDone &&
|
||||
itController.currentITStep != null &&
|
||||
itController.currentITStep!.continuances.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
!widget.choreographer.isFetching &&
|
||||
!widget.choreographer.isEditingSourceText &&
|
||||
!widget.choreographer.isITDone &&
|
||||
widget.choreographer.itStepContinuances?.isNotEmpty == true;
|
||||
}
|
||||
|
||||
final double iconDimension = 36;
|
||||
|
|
@ -130,7 +126,7 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (itController.isEditingSourceText)
|
||||
if (widget.choreographer.isEditingSourceText)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
|
@ -140,14 +136,14 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
),
|
||||
child: TextField(
|
||||
controller: TextEditingController(
|
||||
text: itController.sourceText,
|
||||
text: widget.choreographer.sourceText,
|
||||
),
|
||||
autofocus: true,
|
||||
enableSuggestions: false,
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted:
|
||||
itController.onEditSourceTextSubmit,
|
||||
widget.choreographer.submitSourceTextEdits,
|
||||
obscureText: false,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
|
|
@ -155,24 +151,21 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!itController.isEditingSourceText &&
|
||||
itController.sourceText != null)
|
||||
if (!widget.choreographer.isEditingSourceText &&
|
||||
widget.choreographer.sourceText != null)
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
child: IconButton(
|
||||
iconSize: iconSize,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () {
|
||||
if (itController.nextITStep != null) {
|
||||
itController.setIsEditingSourceText(true);
|
||||
}
|
||||
},
|
||||
onPressed: () => widget.choreographer
|
||||
.setEditingSourceText(true),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
// iconSize: 20,
|
||||
),
|
||||
),
|
||||
if (!itController.isEditingSourceText)
|
||||
if (!widget.choreographer.isEditingSourceText)
|
||||
SizedBox(
|
||||
width: iconDimension,
|
||||
height: iconDimension,
|
||||
|
|
@ -195,22 +188,23 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
color: Theme.of(context).colorScheme.primary,
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () {
|
||||
itController.isEditingSourceText
|
||||
? itController.setIsEditingSourceText(false)
|
||||
: itController.closeIT();
|
||||
widget.choreographer.isEditingSourceText
|
||||
? widget.choreographer
|
||||
.setEditingSourceText(false)
|
||||
: widget.choreographer.closeIT();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!itController.isEditingSourceText)
|
||||
if (!widget.choreographer.isEditingSourceText)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: !itController.willOpen
|
||||
child: !widget.choreographer.isITOpen
|
||||
? const SizedBox()
|
||||
: itController.sourceText != null
|
||||
: widget.choreographer.sourceText != null
|
||||
? Text(
|
||||
itController.sourceText!,
|
||||
widget.choreographer.sourceText!,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
: const LinearProgressIndicator(),
|
||||
|
|
@ -220,13 +214,15 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
child: AnimatedSize(
|
||||
duration: itController.animationSpeed,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Center(
|
||||
child: itController.choreographer.errorService.isError
|
||||
? ITError(controller: itController)
|
||||
: itController.isTranslationDone
|
||||
child: widget.choreographer.errorService.isError
|
||||
? ITError(choreographer: widget.choreographer)
|
||||
: widget.choreographer.isITDone
|
||||
? const SizedBox()
|
||||
: ITChoices(controller: itController),
|
||||
: ITChoices(
|
||||
choreographer: widget.choreographer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -242,39 +238,19 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
|
|||
}
|
||||
|
||||
class ITChoices extends StatelessWidget {
|
||||
final Choreographer choreographer;
|
||||
const ITChoices({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.choreographer,
|
||||
});
|
||||
|
||||
// final choices = [
|
||||
// "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.",
|
||||
// "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.",
|
||||
// "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.",
|
||||
// "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.",
|
||||
// "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.",
|
||||
// "we need a really long translation to see what's going to happen with that. it should probably have multiple sentences so that we can see what happens there.",
|
||||
// ];
|
||||
|
||||
final ITController controller;
|
||||
|
||||
String? get sourceText {
|
||||
if ((controller.sourceText == null || controller.sourceText!.isEmpty)) {
|
||||
ErrorHandler.logError(
|
||||
m: "null source text in ItChoices",
|
||||
data: {},
|
||||
);
|
||||
}
|
||||
return controller.sourceText;
|
||||
}
|
||||
|
||||
void showCard(
|
||||
BuildContext context,
|
||||
int index, [
|
||||
Color? borderColor,
|
||||
String? choiceFeedback,
|
||||
]) {
|
||||
if (controller.currentITStep == null) {
|
||||
if (choreographer.itStepContinuances == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "currentITStep is null in showCard",
|
||||
s: StackTrace.current,
|
||||
|
|
@ -285,41 +261,34 @@ class ITChoices extends StatelessWidget {
|
|||
return;
|
||||
}
|
||||
|
||||
controller.choreographer.chatController.inputFocus.unfocus();
|
||||
final text = choreographer.itStepContinuances![index].text;
|
||||
choreographer.chatController.inputFocus.unfocus();
|
||||
MatrixState.pAnyState.closeOverlay("it_feedback_card");
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: choiceFeedback == null
|
||||
? WordDataCard(
|
||||
word: controller.currentITStep!.continuances[index].text,
|
||||
wordLang: controller.targetLangCode,
|
||||
fullText: sourceText ?? controller.choreographer.currentText,
|
||||
fullTextLang: sourceText != null
|
||||
? controller.sourceLangCode
|
||||
: controller.targetLangCode,
|
||||
// IMPORTANT COMMENT TO KEEP: We're going to forace hasInfo to false for now
|
||||
// because we don't want to show the word data card for correct choices and the contextual definition
|
||||
// for incorrect choices. This gives away the answer (if you're Kel at least).
|
||||
// The reason hasInfo is false for incorrect choices is that we're not includng the tokens for distractors.
|
||||
// Correct choices will have the tokens, but we don't want to show something different for them.
|
||||
// hasInfo: controller.currentITStep!.continuances[index].hasInfo,
|
||||
hasInfo: false,
|
||||
word: text,
|
||||
wordLang: choreographer.l2LangCode!,
|
||||
fullText: choreographer.sourceText ?? choreographer.currentText,
|
||||
fullTextLang: choreographer.sourceText != null
|
||||
? choreographer.l1LangCode!
|
||||
: choreographer.l2LangCode!,
|
||||
choiceFeedback: choiceFeedback,
|
||||
room: controller.choreographer.chatController.room,
|
||||
)
|
||||
: ITFeedbackCard(
|
||||
req: FullTextTranslationRequestModel(
|
||||
text: controller.currentITStep!.continuances[index].text,
|
||||
tgtLang: controller.sourceLangCode,
|
||||
userL1: controller.sourceLangCode,
|
||||
userL2: controller.targetLangCode,
|
||||
text: text,
|
||||
tgtLang: choreographer.l2LangCode!,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
),
|
||||
choiceFeedback: choiceFeedback,
|
||||
),
|
||||
maxHeight: 300,
|
||||
maxWidth: 300,
|
||||
borderColor: borderColor,
|
||||
transformTargetId: controller.choreographer.itBarTransformTargetKey,
|
||||
transformTargetId: choreographer.itBarTransformTargetKey,
|
||||
isScrollable: choiceFeedback == null,
|
||||
overlayKey: "it_feedback_card",
|
||||
ignorePointer: true,
|
||||
|
|
@ -328,12 +297,41 @@ class ITChoices extends StatelessWidget {
|
|||
|
||||
void selectContinuance(int index, BuildContext context) {
|
||||
MatrixState.pAnyState.closeOverlay("it_feedback_card");
|
||||
final Continuance continuance =
|
||||
controller.currentITStep!.continuances[index];
|
||||
Continuance continuance;
|
||||
try {
|
||||
continuance = choreographer.onSelectContinuance(index);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"index": index,
|
||||
},
|
||||
);
|
||||
choreographer.closeIT();
|
||||
return;
|
||||
}
|
||||
|
||||
if (continuance.level == 1) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => controller.selectTranslation(index),
|
||||
() {
|
||||
try {
|
||||
choreographer.onAcceptContinuance(index);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
level: SentryLevel.warning,
|
||||
data: {
|
||||
"index": index,
|
||||
},
|
||||
);
|
||||
choreographer.closeIT();
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
showCard(
|
||||
|
|
@ -343,20 +341,16 @@ class ITChoices extends StatelessWidget {
|
|||
continuance.feedbackText(context),
|
||||
);
|
||||
}
|
||||
controller.currentITStep!.continuances[index] = continuance.copyWith(
|
||||
wasClicked: true,
|
||||
);
|
||||
controller.choreographer.setState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
if (controller.isEditingSourceText) {
|
||||
if (choreographer.isEditingSourceText) {
|
||||
return const SizedBox();
|
||||
}
|
||||
if (controller.currentITStep == null) {
|
||||
return controller.willOpen
|
||||
if (choreographer.itStepContinuances == null) {
|
||||
return choreographer.isITOpen
|
||||
? CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
|
|
@ -364,11 +358,11 @@ class ITChoices extends StatelessWidget {
|
|||
: const SizedBox();
|
||||
}
|
||||
return ChoicesArray(
|
||||
id: controller.currentITStep.hashCode.toString(),
|
||||
isLoading: controller.isLoading ||
|
||||
controller.choreographer.isFetching ||
|
||||
controller.currentITStep == null,
|
||||
choices: controller.currentITStep!.continuances.map((e) {
|
||||
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(),
|
||||
|
|
@ -383,8 +377,8 @@ class ITChoices extends StatelessWidget {
|
|||
onPressed: (value, index) => selectContinuance(index, context),
|
||||
onLongPress: (value, index) => showCard(context, index),
|
||||
selectedChoiceIndex: null,
|
||||
langCode: controller.choreographer.pangeaController.languageController
|
||||
.activeL2Code(),
|
||||
langCode:
|
||||
choreographer.pangeaController.languageController.activeL2Code(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -394,8 +388,11 @@ class ITChoices extends StatelessWidget {
|
|||
}
|
||||
|
||||
class ITError extends StatelessWidget {
|
||||
final ITController controller;
|
||||
const ITError({super.key, required this.controller});
|
||||
final Choreographer choreographer;
|
||||
const ITError({
|
||||
super.key,
|
||||
required this.choreographer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -413,10 +410,7 @@ class ITError extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.closeIT();
|
||||
controller.choreographer.errorService.resetError();
|
||||
},
|
||||
onPressed: choreographer.closeIT,
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.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/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +1,65 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.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/enums/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import '../../../pages/chat/chat.dart';
|
||||
|
||||
class ChoreographerSendButton extends StatefulWidget {
|
||||
class ChoreographerSendButton extends StatelessWidget {
|
||||
const ChoreographerSendButton({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
final ChatController controller;
|
||||
|
||||
@override
|
||||
State<ChoreographerSendButton> createState() =>
|
||||
ChoreographerSendButtonState();
|
||||
}
|
||||
|
||||
class ChoreographerSendButtonState extends State<ChoreographerSendButton> {
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from
|
||||
// choreo. This keeps the spin up-to-date.
|
||||
_choreoSub = widget.controller.choreographer.stateStream.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
Future<void> _onPressed(BuildContext context) async {
|
||||
controller.choreographer.onClickSend();
|
||||
try {
|
||||
await controller.choreographer.send();
|
||||
} on ShowPaywallException {
|
||||
PaywallCard.show(
|
||||
context,
|
||||
controller.choreographer.inputTransformTargetKey,
|
||||
);
|
||||
} on OpenMatchesException {
|
||||
if (controller.choreographer.firstIGCMatch != null) {
|
||||
OverlayUtil.showIGCMatch(
|
||||
controller.choreographer.firstIGCMatch!,
|
||||
controller.choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
color:
|
||||
widget.controller.choreographer.assistanceState.stateColor(context),
|
||||
onPressed: widget.controller.choreographer.isFetching
|
||||
? null
|
||||
: () {
|
||||
widget.controller.choreographer.incrementTimesClicked();
|
||||
widget.controller.choreographer.send(context);
|
||||
},
|
||||
tooltip: L10n.of(context).send,
|
||||
),
|
||||
return ListenableBuilder(
|
||||
listenable: controller.choreographer,
|
||||
builder: (context, _) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: controller.choreographer.textController,
|
||||
builder: (context, _, __) {
|
||||
return Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
color: controller.choreographer.assistanceState
|
||||
.stateColor(context),
|
||||
onPressed: controller.choreographer.isFetching
|
||||
? null
|
||||
: () => _onPressed(context),
|
||||
tooltip: L10n.of(context).send,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.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/enums/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/learning_settings/pages/settings_learning.dart';
|
||||
import '../../../pages/chat/chat.dart';
|
||||
|
||||
|
|
@ -27,7 +30,6 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
AssistanceState get assistanceState =>
|
||||
widget.controller.choreographer.assistanceState;
|
||||
AnimationController? _controller;
|
||||
StreamSubscription? _choreoListener;
|
||||
AssistanceState? _prevState;
|
||||
|
||||
@override
|
||||
|
|
@ -36,19 +38,17 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
_choreoListener = widget.controller.choreographer.stateStream.stream
|
||||
.listen(_updateSpinnerState);
|
||||
widget.controller.choreographer.addListener(_updateSpinnerState);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
_choreoListener?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateSpinnerState(_) {
|
||||
void _updateSpinnerState() {
|
||||
if (_prevState != AssistanceState.fetching &&
|
||||
assistanceState == AssistanceState.fetching) {
|
||||
_controller?.repeat();
|
||||
|
|
@ -62,8 +62,14 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
}
|
||||
|
||||
void _showFirstMatch() {
|
||||
if (widget.controller.choreographer.igc.canShowFirstMatch) {
|
||||
widget.controller.choreographer.igc.showFirstMatch(context);
|
||||
if (widget.controller.choreographer.canShowFirstIGCMatch) {
|
||||
final match = widget.controller.choreographer.igc.onShowFirstMatch();
|
||||
if (match == null) return;
|
||||
OverlayUtil.showIGCMatch(
|
||||
match,
|
||||
widget.controller.choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +85,10 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
Future<void> _onTap() async {
|
||||
switch (assistanceState) {
|
||||
case AssistanceState.noSub:
|
||||
await PaywallCard.show(context, widget.controller);
|
||||
await PaywallCard.show(
|
||||
context,
|
||||
widget.controller.choreographer.inputTransformTargetKey,
|
||||
);
|
||||
return;
|
||||
case AssistanceState.noMessage:
|
||||
showDialog(
|
||||
|
|
@ -92,8 +101,16 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
if (widget.controller.shouldShowLanguageMismatchPopup) {
|
||||
widget.controller.showLanguageMismatchPopup();
|
||||
} else {
|
||||
await widget.controller.choreographer.getLanguageHelp(manual: true);
|
||||
_showFirstMatch();
|
||||
final igcMatch =
|
||||
await widget.controller.choreographer.requestLanguageAssistance();
|
||||
|
||||
if (igcMatch != null) {
|
||||
OverlayUtil.showIGCMatch(
|
||||
igcMatch,
|
||||
widget.controller.choreographer,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case AssistanceState.fetched:
|
||||
|
|
@ -120,66 +137,70 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = Icon(
|
||||
size: 36,
|
||||
Icons.autorenew_rounded,
|
||||
color: assistanceState.stateColor(context),
|
||||
);
|
||||
|
||||
return Tooltip(
|
||||
message: _enableFeedback ? L10n.of(context).check : "",
|
||||
child: Material(
|
||||
elevation: _enableFeedback ? 4.0 : 0.0,
|
||||
borderRadius: BorderRadius.circular(99.0),
|
||||
shadowColor: Theme.of(context).colorScheme.surface.withAlpha(128),
|
||||
child: InkWell(
|
||||
enableFeedback: _enableFeedback,
|
||||
onTap: _enableFeedback ? _onTap : null,
|
||||
customBorder: const CircleBorder(),
|
||||
onLongPress: _enableFeedback
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (c) => const SettingsLearning(),
|
||||
barrierDismissible: false,
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _backgroundColor,
|
||||
),
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: widget.controller.choreographer.textController,
|
||||
builder: (context, _, __) {
|
||||
final icon = Icon(
|
||||
size: 36,
|
||||
Icons.autorenew_rounded,
|
||||
color: assistanceState.stateColor(context),
|
||||
);
|
||||
return Tooltip(
|
||||
message: _enableFeedback ? L10n.of(context).check : "",
|
||||
child: Material(
|
||||
elevation: _enableFeedback ? 4.0 : 0.0,
|
||||
borderRadius: BorderRadius.circular(99.0),
|
||||
shadowColor: Theme.of(context).colorScheme.surface.withAlpha(128),
|
||||
child: InkWell(
|
||||
enableFeedback: _enableFeedback,
|
||||
onTap: _enableFeedback ? _onTap : null,
|
||||
customBorder: const CircleBorder(),
|
||||
onLongPress: _enableFeedback
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (c) => const SettingsLearning(),
|
||||
barrierDismissible: false,
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _backgroundColor,
|
||||
),
|
||||
),
|
||||
_controller != null
|
||||
? RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: math.pi * 2)
|
||||
.animate(_controller!),
|
||||
child: icon,
|
||||
)
|
||||
: icon,
|
||||
AnimatedContainer(
|
||||
width: 20,
|
||||
height: 20,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _backgroundColor,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.check,
|
||||
color: assistanceState.stateColor(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
_controller != null
|
||||
? RotationTransition(
|
||||
turns: Tween(begin: 0.0, end: math.pi * 2)
|
||||
.animate(_controller!),
|
||||
child: icon,
|
||||
)
|
||||
: icon,
|
||||
AnimatedContainer(
|
||||
width: 20,
|
||||
height: 20,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _backgroundColor,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
size: 16,
|
||||
Icons.check,
|
||||
color: assistanceState.stateColor(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import 'dart:developer';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_ui_extension.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/anchored_overlay_widget.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/overlay_container.dart';
|
||||
|
|
@ -217,6 +221,28 @@ class OverlayUtil {
|
|||
}
|
||||
}
|
||||
|
||||
static void showIGCMatch(
|
||||
PangeaMatchState match,
|
||||
Choreographer choreographer,
|
||||
BuildContext context,
|
||||
) {
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
showPositionedCard(
|
||||
overlayKey:
|
||||
"span_card_overlay_${match.updatedMatch.match.offset}_${match.updatedMatch.match.length}",
|
||||
context: context,
|
||||
cardToShow: SpanCard(
|
||||
match: match,
|
||||
choreographer: choreographer,
|
||||
),
|
||||
maxHeight: 325,
|
||||
maxWidth: 325,
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
ignorePointer: true,
|
||||
isScrollable: false,
|
||||
);
|
||||
}
|
||||
|
||||
static void showTutorialOverlay(
|
||||
BuildContext context, {
|
||||
required Widget overlayContent,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import 'package:fluffychat/pangea/subscription/utils/subscription_app_id.dart';
|
|||
import 'package:fluffychat/pangea/subscription/widgets/subscription_paywall.dart';
|
||||
import 'package:fluffychat/pangea/user/controllers/user_controller.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
enum SubscriptionStatus {
|
||||
loading,
|
||||
|
|
@ -267,6 +268,8 @@ class SubscriptionController extends BaseController {
|
|||
return;
|
||||
}
|
||||
if (isSubscribed == null || isSubscribed!) return;
|
||||
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
await showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: !PlatformInfos.isMobile,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue