From 2b522b6dd7a2fb3d0f65351842975b08486e432b Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 3 Nov 2025 12:52:22 -0500 Subject: [PATCH] widgets refactor --- lib/l10n/intl_en.arb | 3 +- lib/pages/chat/chat_view.dart | 301 ++++++--------- lib/pages/chat/events/old.dart | 1 - .../activity_role_tooltip.dart | 3 +- .../widgets/chat_floating_action_button.dart | 90 ++--- lib/pangea/chat/widgets/chat_input_bar.dart | 100 +---- .../chat/widgets/chat_view_background.dart | 3 +- .../chat/widgets/pangea_chat_input_row.dart | 5 +- .../choreographer_state_extension.dart | 24 +- .../controllers/igc_controller.dart | 36 +- .../enums/assistance_state_enum.dart | 17 + .../choreographer/widgets/choice_array.dart | 54 +-- .../widgets/igc/autocorrect_popup.dart | 3 +- .../widgets/igc/card_error_widget.dart | 25 +- .../widgets/igc/card_header.dart | 63 ++-- .../widgets/igc/language_mismatch_popup.dart | 36 +- .../igc/message_analytics_feedback.dart | 234 +++++------- .../widgets/igc/paywall_card.dart | 89 ++--- .../choreographer/widgets/igc/span_card.dart | 354 +++++++++--------- .../widgets/igc/word_data_card.dart | 154 +++----- .../widgets/it_feedback_card.dart | 139 ++++--- .../choreographer/widgets/it_shimmer.dart | 108 ++---- .../language_permissions_warning_buttons.dart | 122 ------ .../choreographer/widgets/send_button.dart | 51 +-- .../widgets/start_igc_button.dart | 41 +- lib/pangea/common/utils/feedback_model.dart | 29 ++ .../instructions/instructions_show_popup.dart | 73 ---- lib/pangea/login/pages/new_course_page.dart | 7 + .../phonetic_transcription_widget.dart | 112 +++--- .../toolbar/controllers/tts_controller.dart | 33 +- .../toolbar/enums/message_mode_enum.dart | 16 - .../reading_assistance_input_bar.dart | 11 - .../widgets/message_meaning_button.dart | 43 --- .../widgets/message_mode_locked_card.dart | 32 -- .../widgets/message_selection_overlay.dart | 11 - .../widgets/message_translation_card.dart | 103 ----- .../multiple_choice_activity.dart | 2 +- .../practice_activity_card.dart | 5 +- .../word_zoom_activity_button.dart | 5 - .../toolbar/widgets/toolbar_button.dart | 4 - 40 files changed, 873 insertions(+), 1669 deletions(-) delete mode 100644 lib/pages/chat/events/old.dart delete mode 100644 lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart create mode 100644 lib/pangea/common/utils/feedback_model.dart delete mode 100644 lib/pangea/instructions/instructions_show_popup.dart delete mode 100644 lib/pangea/toolbar/widgets/message_meaning_button.dart delete mode 100644 lib/pangea/toolbar/widgets/message_mode_locked_card.dart delete mode 100644 lib/pangea/toolbar/widgets/message_translation_card.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0e9a1ae4d..524cfbf17 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5317,5 +5317,6 @@ "feedbackDialogDesc": "I make mistakes too! Anything to help me improve?", "getStartedFriendsButton": "Invite a friend", "contactHasBeenInvitedToTheCourse": "Contact has been invited to the course", - "inviteFriends": "Invite friends" + "inviteFriends": "Invite friends", + "failedToLoadFeedback": "Failed to load feedback." } \ No newline at end of file diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index dcaa14006..443de4427 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -14,12 +14,10 @@ import 'package:fluffychat/pages/chat/chat_app_bar_title.dart'; import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_finished_status_message.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_stats_menu.dart'; -import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/load_activity_summary_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart'; +import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart'; import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart'; -import 'package:fluffychat/pangea/chat/widgets/chat_input_bar_header.dart'; import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; @@ -206,9 +204,7 @@ class ChatView extends StatelessWidget { return Scaffold( appBar: AppBar( // #Pangea - // actionsIconTheme: - // IconThemeData( - // #Pangea + // actionsIconTheme: IconThemeData( // color: controller.selectedEvents.isEmpty // ? null // : theme.colorScheme.onTertiaryContainer, @@ -244,11 +240,15 @@ class ChatView extends StatelessWidget { ), // #Pangea // builder: (context, _) => UnreadRoomsBadge( + // filter: (r) => r.id != controller.roomId, + // badgePosition: + // BadgePosition.topEnd(end: 8, top: 4), + // child: const Center(child: BackButton()), + // ), builder: (context, _) => Center( child: SizedBox( height: kToolbarHeight, child: UnreadRoomsBadge( - // Pangea# filter: (r) => r.id != controller.roomId, badgePosition: BadgePosition.topEnd( end: 8, @@ -258,6 +258,7 @@ class ChatView extends StatelessWidget { ), ), ), + // Pangea# ), titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0, title: ChatAppBarTitle(controller), @@ -267,13 +268,6 @@ class ChatView extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // #Pangea - if (!controller.showActivityDropdown) - Divider( - height: 1, - color: theme.dividerColor, - ), - // Pangea# PinnedEvents(controller), if (scrollUpBannerEventId != null) ChatAppBarListTile( @@ -318,13 +312,16 @@ class ChatView extends StatelessWidget { // ), // ) // : null, + floatingActionButton: Padding( + padding: const EdgeInsets.only(bottom: 56.0), + child: ChatFloatingActionButton(controller: controller), + ), // body: DropTarget( // onDragDone: controller.onDragDone, // onDragEntered: controller.onDragEntered, // onDragExited: controller.onDragExited, // child: Stack( body: Stack( - // Pangea# children: [ if (accountConfig.wallpaperUrl != null) Opacity( @@ -338,106 +335,116 @@ class ChatView extends StatelessWidget { cacheKey: accountConfig.wallpaperUrl.toString(), uri: accountConfig.wallpaperUrl, fit: BoxFit.cover, - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, + height: MediaQuery.sizeOf(context).height, + width: MediaQuery.sizeOf(context).width, isThumbnail: false, placeholder: (_) => Container(), ), ), ), SafeArea( - // #Pangea - // child: Column( - child: Stack( - children: [ - Column( - // Pangea# - children: [ - Expanded( - child: GestureDetector( - onTap: controller.clearSingleSelectedEvent, - child: ChatEventList(controller: controller), - ), + child: Column( + children: [ + Expanded( + child: GestureDetector( + onTap: controller.clearSingleSelectedEvent, + // #Pangea + // child: ChatEventList(controller: controller), + child: Stack( + children: [ + ChatEventList(controller: controller), + ChatViewBackground(controller.choreographer), + ], ), - // #Pangea - // if (controller.showScrollDownButton) - // Divider( - // height: 1, - // color: theme.dividerColor, - // ), // Pangea# - if (controller.room.isExtinct) - Container( - margin: EdgeInsets.all(bottomSheetPadding), - width: double.infinity, - child: ElevatedButton.icon( - icon: const Icon(Icons.chevron_right), - label: Text(L10n.of(context).enterNewChat), - onPressed: controller.goToNewRoomAction, - ), - ) - // #Pangea - // else if (controller.room.canSendDefaultMessages && - // controller.room.membership == Membership.join) - else if (controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join && - controller.room.isAbandonedDMRoom == true) - // Pangea# - Container( - margin: EdgeInsets.all(bottomSheetPadding), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, - ), - alignment: Alignment.center, - child: Material( - clipBehavior: Clip.hardEdge, - color: controller.selectedEvents.isNotEmpty - ? theme.colorScheme.tertiaryContainer - : theme.colorScheme.surfaceContainerHigh, - borderRadius: const BorderRadius.all( - Radius.circular(24), - ), - child: controller.room.isAbandonedDMRoom == - true - ? Row( - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - foregroundColor: - theme.colorScheme.error, - ), - icon: const Icon( - Icons.archive_outlined, - ), - onPressed: controller.leaveChat, - label: Text( - L10n.of(context).leave, - ), + ), + ), + // #Pangea + // if (controller.showScrollDownButton) + // Divider( + // height: 1, + // color: theme.dividerColor, + // ), + ListenableBuilder( + listenable: controller.scrollController, + builder: (context, _) { + if (controller.scrollController.hasClients && + controller.scrollController.position.pixels > + 0) { + return Divider( + height: 1, + color: theme.dividerColor, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + // Pangea# + if (controller.room.isExtinct) + Container( + margin: EdgeInsets.all(bottomSheetPadding), + width: double.infinity, + child: ElevatedButton.icon( + icon: const Icon(Icons.chevron_right), + label: Text(L10n.of(context).enterNewChat), + onPressed: controller.goToNewRoomAction, + ), + ) + else if (controller.room.canSendDefaultMessages && + controller.room.membership == Membership.join) + Container( + margin: EdgeInsets.all(bottomSheetPadding), + constraints: const BoxConstraints( + maxWidth: FluffyThemes.maxTimelineWidth, + ), + alignment: Alignment.center, + child: Material( + clipBehavior: Clip.hardEdge, + color: controller.selectedEvents.isNotEmpty + ? theme.colorScheme.tertiaryContainer + : theme.colorScheme.surfaceContainerHigh, + borderRadius: const BorderRadius.all( + Radius.circular(24), + ), + child: controller.room.isAbandonedDMRoom == true + ? Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, ), - TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.all( - 16, - ), - ), - icon: const Icon( - Icons.forum_outlined, - ), - onPressed: - controller.recreateChat, - label: Text( - L10n.of(context).reopenChat, - ), + foregroundColor: + theme.colorScheme.error, + ), + icon: const Icon( + Icons.archive_outlined, + ), + onPressed: controller.leaveChat, + label: Text( + L10n.of(context).leave, + ), + ), + TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.all( + 16, ), - ], - ) - // #Pangea - : null, + ), + icon: const Icon( + Icons.forum_outlined, + ), + onPressed: controller.recreateChat, + label: Text( + L10n.of(context).reopenChat, + ), + ), + ], + ) + // #Pangea // : Column( // mainAxisSize: MainAxisSize.min, // children: [ @@ -446,85 +453,25 @@ class ChatView extends StatelessWidget { // ChatEmojiPicker(controller), // ], // ), - // Pangea# - ), - ), - // #Pangea - // Keep messages above minimum input bar height - if (!controller.room.isAbandonedDMRoom && - controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join && - (controller.room.activityPlan == null || - !controller.room.showActivityChatUI || - controller.room.isActiveInActivity)) - AnimatedSize( - duration: const Duration(milliseconds: 200), - child: SizedBox( - height: controller.inputBarHeight, - ), - ), - if (controller.room.isActivityFinished) - LoadActivitySummaryWidget( - room: controller.room, - ), + : ChatInputBar( + controller: controller, + padding: bottomSheetPadding, + ), - ActivityFinishedStatusMessage( - controller: controller, - ), - // Pangea# - ], - ), - // #Pangea - ChatViewBackground(controller.choreographer), - if (!controller.room.isAbandonedDMRoom && - controller.room.canSendDefaultMessages && - controller.room.membership == Membership.join && - (controller.room.activityPlan == null || - !controller.room.showActivityChatUI || - controller.room.isActiveInActivity)) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ChatInputBarHeader( - controller: controller, - padding: bottomSheetPadding, - ), - if (controller.showScrollDownButton) - Divider( - height: 1, - color: Theme.of(context).dividerColor, - ), - Container( - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.surface, - ), - child: ChatInputBar( - controller: controller, - padding: bottomSheetPadding, - ), - ), - ], + // Pangea# ), ), - ActivityStatsMenu(controller), - if (controller.room.activitySummary?.summary != null && - controller.hasRainedConfetti == false) - StarRainWidget( - showBlast: true, - onFinished: () => - controller.setHasRainedConfetti(true), - ), - // Pangea# ], ), ), // #Pangea + ActivityStatsMenu(controller), + if (controller.room.activitySummary?.summary != null && + controller.hasRainedConfetti == false) + StarRainWidget( + showBlast: true, + onFinished: () => controller.setHasRainedConfetti(true), + ), // if (controller.dragging) // Container( // color: theme.scaffoldBackgroundColor.withAlpha(230), diff --git a/lib/pages/chat/events/old.dart b/lib/pages/chat/events/old.dart deleted file mode 100644 index 8b1378917..000000000 --- a/lib/pages/chat/events/old.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart index 14b55ecd8..9583d02fc 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart @@ -4,7 +4,6 @@ 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 StatelessWidget { @@ -24,7 +23,7 @@ class ActivityRoleTooltip extends StatelessWidget { builder: (context, _) { if (!room.showActivityChatUI || room.ownRole?.goal == null || - choreographer.isITOpen) { + choreographer.itController.open.value) { return const SizedBox(); } diff --git a/lib/pangea/chat/widgets/chat_floating_action_button.dart b/lib/pangea/chat/widgets/chat_floating_action_button.dart index d720535c1..00103f36e 100644 --- a/lib/pangea/chat/widgets/chat_floating_action_button.dart +++ b/lib/pangea/chat/widgets/chat_floating_action_button.dart @@ -1,91 +1,47 @@ -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'; -class ChatFloatingActionButton extends StatefulWidget { +class ChatFloatingActionButton extends StatelessWidget { final ChatController controller; const ChatFloatingActionButton({ super.key, required this.controller, }); - @override - ChatFloatingActionButtonState createState() => - ChatFloatingActionButtonState(); -} - -class ChatFloatingActionButtonState extends State { - bool showPermissionsError = false; - StreamSubscription? _choreoSub; - - @override - void initState() { - final permissionsController = - widget.controller.pangeaController.permissionsController; - final itEnabled = permissionsController.isToolEnabled( - ToolSetting.interactiveTranslator, - widget.controller.room, - ); - final igcEnabled = permissionsController.isToolEnabled( - ToolSetting.interactiveGrammar, - widget.controller.room, - ); - showPermissionsError = !itEnabled || !igcEnabled; - - if (showPermissionsError) { - Future.delayed( - const Duration(seconds: 5), - () { - if (mounted) setState(() => showPermissionsError = false); - }, - ); - } - super.initState(); - } - - @override - void dispose() { - _choreoSub?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { - if (widget.controller.selectedEvents.isNotEmpty) { + if (controller.selectedEvents.isNotEmpty) { return const SizedBox.shrink(); } - if (widget.controller.showScrollDownButton) { - return FloatingActionButton( - onPressed: widget.controller.scrollDown, - heroTag: null, - mini: true, - child: const Icon(Icons.arrow_downward_outlined), - ); - } return ListenableBuilder( - listenable: widget.controller.choreographer, + listenable: Listenable.merge( + [ + controller.choreographer, + controller.scrollController, + ], + ), builder: (context, _) { - if (widget.controller.choreographer.errorService.error != null && - !widget.controller.choreographer.isITOpen) { - return ChoreographerHasErrorButton( - widget.controller.choreographer.errorService.error!, - widget.controller.choreographer, + if (controller.scrollController.hasClients && + controller.scrollController.position.pixels > 0) { + return FloatingActionButton( + onPressed: controller.scrollDown, + heroTag: null, + mini: true, + child: const Icon(Icons.arrow_downward_outlined), ); } - return showPermissionsError - ? LanguagePermissionsButtons( - choreographer: widget.controller.choreographer, - roomID: widget.controller.roomId, - ) - : const SizedBox.shrink(); + if (controller.choreographer.errorService.error != null && + !controller.choreographer.itController.open.value) { + return ChoreographerHasErrorButton( + controller.choreographer.errorService.error!, + controller.choreographer, + ); + } + return const SizedBox.shrink(); }, ); } diff --git a/lib/pangea/chat/widgets/chat_input_bar.dart b/lib/pangea/chat/widgets/chat_input_bar.dart index e001edd09..dc3ec5cff 100644 --- a/lib/pangea/chat/widgets/chat_input_bar.dart +++ b/lib/pangea/chat/widgets/chat_input_bar.dart @@ -1,8 +1,5 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_emoji_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; @@ -10,7 +7,7 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activi import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; -class ChatInputBar extends StatefulWidget { +class ChatInputBar extends StatelessWidget { final ChatController controller; final double padding; @@ -20,92 +17,21 @@ class ChatInputBar extends StatefulWidget { super.key, }); - @override - State createState() => ChatInputBarState(); -} - -class ChatInputBarState extends State { - Timer? _debounceTimer; - - void _updateHeight() { - _debounceTimer = Timer(const Duration(milliseconds: 100), () { - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null || !renderBox.hasSize) return; - widget.controller.updateInputBarHeight(renderBox.size.height); - }); - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight()); - } - - @override - void dispose() { - _debounceTimer?.cancel(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return NotificationListener( - onNotification: (SizeChangedLayoutNotification notification) { - WidgetsBinding.instance.addPostFrameCallback((_) => _updateHeight()); - return true; - }, - child: SizeChangedLayoutNotifier( - child: Column( - children: [ - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, - ), - child: ActivityRoleTooltip( - choreographer: widget.controller.choreographer, - ), - ), - Container( - padding: EdgeInsets.all( - widget.padding, - ), - constraints: const BoxConstraints( - maxWidth: FluffyThemes.maxTimelineWidth, - ), - alignment: Alignment.center, - child: Material( - clipBehavior: Clip.hardEdge, - type: MaterialType.transparency, - borderRadius: const BorderRadius.all( - Radius.circular(24), - ), - child: Column( - children: [ - ITBar(choreographer: widget.controller.choreographer), - DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest, - ), - child: Column( - children: [ - if (!widget.controller.obscureText) - ReplyDisplay(widget.controller), - PangeaChatInputRow( - controller: widget.controller, - ), - ChatEmojiPicker(widget.controller), - ], - ), - ), - ], - ), - ), - ), - ], + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ActivityRoleTooltip( + choreographer: controller.choreographer, ), - ), + ITBar(choreographer: controller.choreographer), + if (!controller.obscureText) ReplyDisplay(controller), + PangeaChatInputRow( + controller: controller, + ), + ChatEmojiPicker(controller), + ], ); } } diff --git a/lib/pangea/chat/widgets/chat_view_background.dart b/lib/pangea/chat/widgets/chat_view_background.dart index 5568f9cb9..7802605e7 100644 --- a/lib/pangea/chat/widgets/chat_view_background.dart +++ b/lib/pangea/chat/widgets/chat_view_background.dart @@ -3,7 +3,6 @@ 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 StatelessWidget { final Choreographer choreographer; @@ -14,7 +13,7 @@ class ChatViewBackground extends StatelessWidget { return ListenableBuilder( listenable: choreographer, builder: (context, _) { - return choreographer.isITOpen + return choreographer.itController.open.value ? Positioned( left: 0, right: 0, diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index 42f509a7a..4aec816ae 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -7,7 +7,6 @@ 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'; @@ -29,7 +28,7 @@ class PangeaChatInputRow extends StatelessWidget { controller.pangeaController.languageController.activeL2Model(); String hintText(BuildContext context) { - if (controller.choreographer.isITOpen) { + if (controller.choreographer.itController.open.value) { return L10n.of(context).buildTranslation; } return activel1 != null && @@ -227,7 +226,7 @@ class PangeaChatInputRow extends StatelessWidget { alignment: Alignment.center, child: PlatformInfos.platformCanRecord && controller.sendController.text.isEmpty && - !controller.choreographer.isITOpen + !controller.choreographer.itController.open.value ? FloatingActionButton.small( tooltip: L10n.of(context).voiceMessage, onPressed: controller.voiceMessageAction, diff --git a/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart b/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart index 35a1a5886..9c97fdf0e 100644 --- a/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart +++ b/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart @@ -2,19 +2,17 @@ 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? get itStepContinuances => itController.continuances; + bool get isRunningIT { + return choreoMode == ChoreoMode.it && + itController.currentITStep.value?.isFinal != true; + } String? get currentIGCText => igc.currentText; - PangeaMatchState? get openIGCMatch => igc.openMatch; - PangeaMatchState? get firstIGCMatch => igc.firstOpenMatch; + PangeaMatchState? get openMatch => igc.openMatch; + PangeaMatchState? get firstOpenMatch => igc.firstOpenMatch; List? get openIGCMatches => igc.openMatches; List? get closedIGCMatches => igc.closedMatches; bool get canShowFirstIGCMatch => igc.canShowFirstMatch; @@ -23,15 +21,19 @@ extension ChoregrapherUserSettingsExtension on Choreographer { AssistanceState get assistanceState { final isSubscribed = pangeaController.subscriptionController.isSubscribed; if (isSubscribed == false) return AssistanceState.noSub; - if (currentText.isEmpty && sourceText == null) { + if (currentText.isEmpty && sourceText.value == null) { return AssistanceState.noMessage; } + if (errorService.isError) { + return AssistanceState.error; + } + if (igc.hasOpenMatches || isRunningIT) { return AssistanceState.fetched; } - if (isFetching) return AssistanceState.fetching; + if (isFetching.value) return AssistanceState.fetching; if (!igc.hasIGCTextData) return AssistanceState.notFetched; return AssistanceState.complete; } @@ -52,7 +54,7 @@ extension ChoregrapherUserSettingsExtension on Choreographer { if (!isAutoIGCEnabled) return true; // if we're in the middle of fetching results, don't let them send - if (isFetching) return false; + if (isFetching.value) return false; // they're supposed to run IGC but haven't yet, don't let them send if (!igc.hasIGCTextData) { diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 1dffb4a1b..f506fd735 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -7,6 +7,7 @@ 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'; @@ -53,21 +54,6 @@ class IgcController { 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); @@ -125,10 +111,10 @@ class IgcController { ); if (res.isError) { - _igcTextData = IGCTextData( - originalInput: reqBody.fullText, - matches: [], + _choreographer.errorService.setErrorAndLock( + ChoreoError(raw: res.asError), ); + clear(); return; } @@ -145,11 +131,6 @@ class IgcController { try { _choreographer.acceptNormalizationMatches(); - if (_igcTextData != null) { - for (final match in _igcTextData!.openMatches) { - fetchSpanDetails(match: match); - } - } } catch (e, s) { ErrorHandler.logError( e: e, @@ -160,6 +141,12 @@ class IgcController { }, ); } + + if (_igcTextData != null) { + for (final match in _igcTextData!.openMatches) { + fetchSpanDetails(match: match).catchError((e) {}); + } + } } Future fetchSpanDetails({ @@ -190,8 +177,7 @@ class IgcController { ); if (response.isError) { - _choreographer.clearMatches(response.error!); - return; + throw response.error!; } _igcTextData?.setSpanData(match, response.result!.span); diff --git a/lib/pangea/choreographer/enums/assistance_state_enum.dart b/lib/pangea/choreographer/enums/assistance_state_enum.dart index 2dda0c897..e81d94675 100644 --- a/lib/pangea/choreographer/enums/assistance_state_enum.dart +++ b/lib/pangea/choreographer/enums/assistance_state_enum.dart @@ -13,6 +13,7 @@ enum AssistanceState { fetching, fetched, complete, + error, } extension AssistanceStateExtension on AssistanceState { @@ -21,6 +22,7 @@ extension AssistanceStateExtension on AssistanceState { case AssistanceState.noSub: case AssistanceState.noMessage: case AssistanceState.fetched: + case AssistanceState.error: return Theme.of(context).disabledColor; case AssistanceState.notFetched: case AssistanceState.fetching: @@ -29,4 +31,19 @@ extension AssistanceStateExtension on AssistanceState { return AppConfig.success; } } + + Color sendButtonColor(context) { + switch (this) { + case AssistanceState.noMessage: + case AssistanceState.fetched: + return Theme.of(context).disabledColor; + case AssistanceState.noSub: + case AssistanceState.error: + case AssistanceState.notFetched: + case AssistanceState.fetching: + return Theme.of(context).colorScheme.primary; + case AssistanceState.complete: + return AppConfig.success; + } + } } diff --git a/lib/pangea/choreographer/widgets/choice_array.dart b/lib/pangea/choreographer/widgets/choice_array.dart index bb672d4c9..c5e26af1e 100644 --- a/lib/pangea/choreographer/widgets/choice_array.dart +++ b/lib/pangea/choreographer/widgets/choice_array.dart @@ -13,14 +13,9 @@ import 'package:fluffychat/widgets/matrix.dart'; import '../../bot/utils/bot_style.dart'; import 'it_shimmer.dart'; +// CTODO refactor typedef ChoiceCallback = void Function(String value, int index); -enum OverflowMode { - wrap, - horizontalScroll, - verticalScroll, -} - class ChoicesArray extends StatefulWidget { final bool isLoading; final List? choices; @@ -38,7 +33,7 @@ class ChoicesArray extends StatefulWidget { final String? id; /// some uses of this widget want to disable clicking of the choices - final bool isActive; + final bool enabled; final String Function(String)? getDisplayCopy; @@ -46,10 +41,6 @@ class ChoicesArray extends StatefulWidget { /// select choices once the correct choice has been selected final bool enableMultiSelect; - final double? fontSize; - - final OverflowMode overflowMode; - const ChoicesArray({ super.key, required this.isLoading, @@ -58,13 +49,11 @@ class ChoicesArray extends StatefulWidget { required this.selectedChoiceIndex, this.enableAudio = true, this.langCode, - this.isActive = true, + this.enabled = true, this.onLongPress, this.getDisplayCopy, this.id, this.enableMultiSelect = false, - this.fontSize, - this.overflowMode = OverflowMode.wrap, }); @override @@ -99,8 +88,8 @@ class ChoicesArrayState extends State { .mapIndexed( (index, entry) => ChoiceItem( theme: theme, - onLongPress: widget.isActive ? widget.onLongPress : null, - onPressed: widget.isActive + onLongPress: widget.enabled ? widget.onLongPress : null, + onPressed: widget.enabled ? (String value, int index) { widget.onPressed(value, index); // TODO - what to pass here as eventID? @@ -122,39 +111,18 @@ class ChoicesArrayState extends State { isSelected: widget.selectedChoiceIndex == index, id: widget.id, getDisplayCopy: widget.getDisplayCopy, - fontSize: widget.fontSize, ), ) .toList(); return widget.isLoading && (widget.choices == null || widget.choices!.length <= 1) - ? ItShimmer( - fontSize: widget.fontSize ?? - Theme.of(context).textTheme.bodyMedium?.fontSize ?? - 16, - ) - : widget.overflowMode == OverflowMode.wrap - ? Wrap( - alignment: WrapAlignment.center, - spacing: 4.0, - children: choices, - ) - : widget.overflowMode == OverflowMode.horizontalScroll - ? SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: choices, - ), - ) - : SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: choices, - ), - ); + ? const ItShimmer() + : Wrap( + alignment: WrapAlignment.center, + spacing: 4.0, + children: choices, + ); } } diff --git a/lib/pangea/choreographer/widgets/igc/autocorrect_popup.dart b/lib/pangea/choreographer/widgets/igc/autocorrect_popup.dart index bc9fef219..dc49d053a 100644 --- a/lib/pangea/choreographer/widgets/igc/autocorrect_popup.dart +++ b/lib/pangea/choreographer/widgets/igc/autocorrect_popup.dart @@ -12,13 +12,12 @@ class AutocorrectPopup extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Material( type: MaterialType.transparency, child: Container( padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: theme.colorScheme.surface.withAlpha(200), + color: Theme.of(context).colorScheme.surface.withAlpha(200), borderRadius: BorderRadius.circular(8.0), ), child: Row( diff --git a/lib/pangea/choreographer/widgets/igc/card_error_widget.dart b/lib/pangea/choreographer/widgets/igc/card_error_widget.dart index 234c3f2f8..43f7c107a 100644 --- a/lib/pangea/choreographer/widgets/igc/card_error_widget.dart +++ b/lib/pangea/choreographer/widgets/igc/card_error_widget.dart @@ -3,49 +3,38 @@ 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/common/utils/error_handler.dart'; class CardErrorWidget extends StatelessWidget { final String error; - final double maxWidth; - - const CardErrorWidget({ + const CardErrorWidget( + this.error, { super.key, - required this.error, - this.maxWidth = 275, }); @override Widget build(BuildContext context) { - final ErrorCopy errorCopy = ErrorCopy( - context, - title: L10n.of(context).oopsSomethingWentWrong, - body: error, - ); - - return Container( + return Padding( padding: const EdgeInsets.all(8.0), - constraints: BoxConstraints(maxWidth: maxWidth), child: Column( + spacing: 6.0, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - errorCopy.title, + L10n.of(context).oopsSomethingWentWrong, style: BotStyle.text(context), softWrap: true, ), - const SizedBox(height: 6.0), Row( + spacing: 12.0, mainAxisSize: MainAxisSize.min, children: [ const BotFace( width: 50.0, expression: BotExpression.addled, ), - const SizedBox(width: 12.0), Flexible( child: Text( - errorCopy.body, + error, style: BotStyle.text(context), softWrap: true, ), diff --git a/lib/pangea/choreographer/widgets/igc/card_header.dart b/lib/pangea/choreographer/widgets/igc/card_header.dart index f70a971a5..a1ed7e680 100644 --- a/lib/pangea/choreographer/widgets/igc/card_header.dart +++ b/lib/pangea/choreographer/widgets/igc/card_header.dart @@ -5,52 +5,41 @@ import 'package:fluffychat/widgets/matrix.dart'; import '../../../bot/widgets/bot_face_svg.dart'; class CardHeader extends StatelessWidget { - const CardHeader({ + const CardHeader( + this.text, { super.key, - required this.text, - required this.botExpression, - this.onClose, }); - final BotExpression botExpression; final String text; - final void Function()? onClose; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 5.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Row( - children: [ - BotFace( - width: 50.0, - expression: botExpression, + return Row( + spacing: 12.0, + children: [ + Expanded( + child: Row( + spacing: 12.0, + children: [ + const BotFace( + width: 50.0, + expression: BotExpression.addled, + ), + Expanded( + child: Text( + text, + style: BotStyle.text(context), + softWrap: true, ), - const SizedBox(width: 12.0), - Flexible( - child: Text( - text, - style: BotStyle.text(context), - softWrap: true, - ), - ), - ], - ), + ), + ], ), - const SizedBox(width: 5.0), - IconButton( - icon: const Icon(Icons.close_outlined), - onPressed: () { - if (onClose != null) onClose!(); - MatrixState.pAnyState.closeOverlay(); - }, - ), - ], - ), + ), + IconButton( + icon: const Icon(Icons.close_outlined), + onPressed: MatrixState.pAnyState.closeOverlay, + ), + ], ); } } diff --git a/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart b/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart index 3fde694c5..0e3a510c9 100644 --- a/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart +++ b/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart @@ -2,30 +2,13 @@ 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/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 VoidCallback onUpdate; - - const LanguageMismatchPopup({ - super.key, - required this.targetLanguage, - required this.onUpdate, - }); - - Future _onConfirm(BuildContext context) async { - await MatrixState.pangeaController.userController.updateProfile( - (profile) { - profile.userSettings.targetLanguage = targetLanguage; - return profile; - }, - waitForDataInSync: true, - ); - } + final Future Function() onConfirm; + const LanguageMismatchPopup({super.key, required this.onConfirm}); @override Widget build(BuildContext context) { @@ -33,10 +16,7 @@ class LanguageMismatchPopup extends StatelessWidget { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - CardHeader( - text: L10n.of(context).languageMismatchTitle, - botExpression: BotExpression.addled, - ), + CardHeader(L10n.of(context).languageMismatchTitle), Padding( padding: const EdgeInsets.all(8), child: Column( @@ -54,15 +34,13 @@ class LanguageMismatchPopup extends StatelessWidget { onPressed: () async { await showFutureLoadingDialog( context: context, - future: () => _onConfirm(context), + future: onConfirm, ); MatrixState.pAnyState.closeOverlay(); - onUpdate(); }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - (Theme.of(context).colorScheme.primary).withAlpha(25), - ), + style: TextButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primary.withAlpha(25), ), child: Text(L10n.of(context).confirm), ), diff --git a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart index da458b4c9..f1070f9f1 100644 --- a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart +++ b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart @@ -6,17 +6,16 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; -import 'package:fluffychat/widgets/matrix.dart'; class MessageAnalyticsFeedback extends StatefulWidget { - final String overlayId; final int newGrammarConstructs; final int newVocabConstructs; + final VoidCallback close; const MessageAnalyticsFeedback({ - required this.overlayId, required this.newGrammarConstructs, required this.newVocabConstructs, + required this.close, super.key, }); @@ -27,36 +26,27 @@ class MessageAnalyticsFeedback extends StatefulWidget { class MessageAnalyticsFeedbackState extends State with TickerProviderStateMixin { - late AnimationController _vocabController; - late AnimationController _grammarController; + late AnimationController _numbersController; late AnimationController _bubbleController; + late AnimationController _tickerController; - late Animation _vocabOpacity; - late Animation _grammarOpacity; - late Animation _scaleAnimation; - late Animation _opacityAnimation; + late Animation _numbersOpacityAnimation; + late Animation _bubbleScaleAnimation; + late Animation _bubbleOpacityAnimation; - static const counterDelay = Duration(milliseconds: 400); + Animation? _grammarTickerAnimation; + Animation? _vocabTickerAnimation; @override void initState() { super.initState(); - _grammarController = AnimationController( + _numbersController = AnimationController( vsync: this, duration: FluffyThemes.animationDuration, ); - _grammarOpacity = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut), - ); - - _vocabController = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _vocabOpacity = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut), + _numbersOpacityAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _numbersController, curve: Curves.easeInOut), ); _bubbleController = AnimationController( @@ -64,51 +54,67 @@ class MessageAnalyticsFeedbackState extends State duration: FluffyThemes.animationDuration, ); - _scaleAnimation = Tween(begin: 0.0, end: 1.0).animate( + _bubbleScaleAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut), ); - _opacityAnimation = Tween(begin: 0.0, end: 0.9).animate( + _bubbleOpacityAnimation = Tween(begin: 0.0, end: 0.9).animate( CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut), ); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _bubbleController.forward(); + _tickerController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); - Future.delayed(counterDelay, () { - if (mounted) { - _vocabController.forward(); - _grammarController.forward(); - } - }); + _numbersOpacityAnimation.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _startTickerAnimations(); + } + }); - Future.delayed(const Duration(milliseconds: 4000), () { - if (!mounted) return; - _bubbleController.reverse().then((_) { - MatrixState.pAnyState.closeOverlay(widget.overlayId); - }); - }); + _bubbleController.forward(); + Future.delayed( + const Duration(milliseconds: 400), + _numbersController.forward, + ); + Future.delayed(const Duration(milliseconds: 4000), () async { + await _bubbleController.reverse(); + if (mounted) widget.close(); }); } @override void dispose() { - _vocabController.dispose(); - _grammarController.dispose(); + _numbersController.dispose(); _bubbleController.dispose(); + _tickerController.dispose(); super.dispose(); } - void _showAnalyticsDialog(ConstructTypeEnum? type) { - switch (type) { - case ConstructTypeEnum.morph: - context.go("/rooms/analytics/${ConstructTypeEnum.morph.string}"); - break; - case ConstructTypeEnum.vocab: - default: - context.go("/rooms/analytics/${ConstructTypeEnum.vocab.string}"); - break; - } + void _startTickerAnimations() { + _vocabTickerAnimation = IntTween( + begin: 0, + end: widget.newVocabConstructs, + ).animate( + CurvedAnimation( + parent: _tickerController, + curve: Curves.easeOutCubic, + ), + ); + + _grammarTickerAnimation = IntTween( + begin: 0, + end: widget.newGrammarConstructs, + ).animate( + CurvedAnimation( + parent: _tickerController, + curve: Curves.easeOutCubic, + ), + ); + + setState(() {}); + _tickerController.forward(); } @override @@ -116,22 +122,24 @@ class MessageAnalyticsFeedbackState extends State if (widget.newVocabConstructs <= 0 && widget.newGrammarConstructs <= 0) { return const SizedBox.shrink(); } + // CTODO check if working - final theme = Theme.of(context); return Material( type: MaterialType.transparency, child: InkWell( - onTap: () => _showAnalyticsDialog(null), + onTap: () => context.go("/rooms/analytics"), child: ScaleTransition( - scale: _scaleAnimation, + scale: _bubbleScaleAnimation, alignment: Alignment.bottomRight, child: AnimatedBuilder( animation: _bubbleController, builder: (context, child) { return Container( decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest - .withAlpha((_opacityAnimation.value * 255).round()), + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withAlpha((_bubbleOpacityAnimation.value * 255).round()), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16.0), topRight: Radius.circular(16.0), @@ -147,26 +155,18 @@ class MessageAnalyticsFeedbackState extends State mainAxisSize: MainAxisSize.min, children: [ if (widget.newVocabConstructs > 0) - NewConstructsBadge( - controller: _vocabController, - opacityAnimation: _vocabOpacity, - newConstructs: widget.newVocabConstructs, + _NewConstructsBadge( + opacityAnimation: _numbersOpacityAnimation, + tickerAnimation: _vocabTickerAnimation, type: ConstructTypeEnum.vocab, tooltip: L10n.of(context).newVocab, - onTap: () => _showAnalyticsDialog( - ConstructTypeEnum.vocab, - ), ), if (widget.newGrammarConstructs > 0) - NewConstructsBadge( - controller: _grammarController, - opacityAnimation: _grammarOpacity, - newConstructs: widget.newGrammarConstructs, + _NewConstructsBadge( + opacityAnimation: _numbersOpacityAnimation, + tickerAnimation: _grammarTickerAnimation, type: ConstructTypeEnum.morph, tooltip: L10n.of(context).newGrammar, - onTap: () => _showAnalyticsDialog( - ConstructTypeEnum.morph, - ), ), ], ), @@ -179,33 +179,27 @@ class MessageAnalyticsFeedbackState extends State } } -class NewConstructsBadge extends StatelessWidget { - final AnimationController controller; +class _NewConstructsBadge extends StatelessWidget { final Animation opacityAnimation; - - final int newConstructs; + final Animation? tickerAnimation; final ConstructTypeEnum type; final String tooltip; - final VoidCallback onTap; - const NewConstructsBadge({ - required this.controller, + const _NewConstructsBadge({ required this.opacityAnimation, - required this.newConstructs, + required this.tickerAnimation, required this.type, required this.tooltip, - required this.onTap, - super.key, }); @override Widget build(BuildContext context) { return InkWell( - onTap: onTap, + onTap: () => context.go("/rooms/analytics/${type.string}"), child: Tooltip( message: tooltip, child: AnimatedBuilder( - animation: controller, + animation: opacityAnimation, builder: (context, child) { return Opacity( opacity: opacityAnimation.value, @@ -220,10 +214,9 @@ class NewConstructsBadge extends StatelessWidget { size: 24, ), const SizedBox(width: 4.0), - AnimatedCounter( + _AnimatedCounter( key: ValueKey("$type-counter"), - endValue: newConstructs, - startAnimation: opacityAnimation.value > 0.9, + animation: tickerAnimation, style: TextStyle( color: type.indicator.color(context), fontWeight: FontWeight.bold, @@ -240,76 +233,31 @@ class NewConstructsBadge extends StatelessWidget { } } -class AnimatedCounter extends StatefulWidget { - final int endValue; +class _AnimatedCounter extends StatelessWidget { + final Animation? animation; final TextStyle? style; - final bool startAnimation; - const AnimatedCounter({ + const _AnimatedCounter({ super.key, - required this.endValue, + required this.animation, this.style, - this.startAnimation = true, }); - @override - State createState() => _AnimatedCounterState(); -} - -class _AnimatedCounterState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _animation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: FluffyThemes.animationDuration, - ); - - _animation = IntTween( - begin: 0, - end: widget.endValue, - ).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeOutCubic, - ), - ); - - if (widget.startAnimation) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) _controller.forward(); - }); - } - } - - @override - void didUpdateWidget(AnimatedCounter oldWidget) { - super.didUpdateWidget(oldWidget); - if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) { - _controller.forward(); - } - } - - bool get _hasAnimated => _controller.isCompleted || _controller.isAnimating; - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + if (animation == null) { + return Text( + "+ 0", + style: style, + ); + } + return AnimatedBuilder( - animation: _animation, + animation: animation!, builder: (context, child) { return Text( - "+ ${_animation.value}", - style: widget.style, + "+ ${animation!.value}", + style: style, ); }, ); diff --git a/lib/pangea/choreographer/widgets/igc/paywall_card.dart b/lib/pangea/choreographer/widgets/igc/paywall_card.dart index d27c3d5e0..7a34824fc 100644 --- a/lib/pangea/choreographer/widgets/igc/paywall_card.dart +++ b/lib/pangea/choreographer/widgets/igc/paywall_card.dart @@ -2,16 +2,13 @@ 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/widgets/igc/card_header.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/subscription/repo/subscription_management_repo.dart'; import 'package:fluffychat/widgets/matrix.dart'; class PaywallCard extends StatelessWidget { - const PaywallCard({ - super.key, - }); + const PaywallCard({super.key}); static Future show( BuildContext context, @@ -34,71 +31,33 @@ class PaywallCard extends StatelessWidget { @override Widget build(BuildContext context) { - final bool inTrialWindow = - MatrixState.pangeaController.userController.inTrialWindow(); - return Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, + spacing: 12.0, children: [ - CardHeader( - text: L10n.of(context).clickMessageTitle, - botExpression: BotExpression.addled, - ), - Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - L10n.of(context).subscribedToUnlockTools, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ), - // if (inTrialWindow) - // Text( - // L10n.of(context).noPaymentInfo, - // style: BotStyle.text(context), - // textAlign: TextAlign.center, - // ), - if (inTrialWindow) ...[ - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: TextButton( - onPressed: () { - MatrixState.pangeaController.subscriptionController - .activateNewUserTrial(); - MatrixState.pAnyState.closeOverlay(); - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - (Theme.of(context).colorScheme.primary).withAlpha(25), - ), - ), - child: Text(L10n.of(context).activateTrial), - ), - ), - ], - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: TextButton( - onPressed: () { - MatrixState.pangeaController.subscriptionController - .showPaywall(context); - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - (Theme.of(context).colorScheme.primary).withAlpha(25), - ), - ), - child: Text(L10n.of(context).getAccess), + CardHeader(L10n.of(context).clickMessageTitle), + Column( + spacing: 12.0, + children: [ + Text( + L10n.of(context).subscribedToUnlockTools, + style: BotStyle.text(context), + textAlign: TextAlign.center, + ), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () { + MatrixState.pangeaController.subscriptionController + .showPaywall(context); + }, + style: TextButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primary.withAlpha(25), ), + child: Text(L10n.of(context).getAccess), ), - ], - ), + ), + ], ), ], ); diff --git a/lib/pangea/choreographer/widgets/igc/span_card.dart b/lib/pangea/choreographer/widgets/igc/span_card.dart index 267045fbf..ffe88a4d5 100644 --- a/lib/pangea/choreographer/widgets/igc/span_card.dart +++ b/lib/pangea/choreographer/widgets/igc/span_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -11,14 +12,13 @@ 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/feedback_model.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; +import 'package:fluffychat/pangea/common/widgets/error_indicator.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,57 +34,103 @@ class SpanCard extends StatefulWidget { } class SpanCardState extends State { - bool fetchingData = false; + bool _loadingChoices = true; + final _feedbackModel = FeedbackModel(); + + SpanChoice? _latestSelectedChoice; + final ScrollController scrollController = ScrollController(); @override void initState() { super.initState(); - getSpanDetails(); + _fetchChoices(); } @override void dispose() { - TtsController.stop(); + _feedbackModel.dispose(); scrollController.dispose(); super.dispose(); } - SpanChoice? get selectedChoice => - widget.match.updatedMatch.match.selectedChoice; + List? get _choices => widget.match.updatedMatch.match.choices; - Future getSpanDetails({bool force = false}) async { - if (widget.match.updatedMatch.isITStart) return; + SpanChoice? get _selectedChoice => + widget.match.updatedMatch.match.selectedChoice ?? + widget.match.updatedMatch.match.choices?.firstWhereOrNull( + (c) => c.value == _latestSelectedChoice?.value, + ); - if (!mounted) return; - setState(() { - fetchingData = true; - }); + String? get _selectedFeedback => _selectedChoice?.feedback; - await widget.choreographer.fetchSpanDetails( - match: widget.match, - force: force, - ); + Future _fetchChoices() async { + if (_choices != null && _choices!.length > 1) { + setState(() => _loadingChoices = false); + return; + } - if (mounted) { - setState(() => fetchingData = false); + try { + setState(() => _loadingChoices = true); + await widget.choreographer.fetchSpanDetails( + match: widget.match, + ); + } catch (e) { + widget.choreographer.clearMatches(e); + } finally { + if (_choices == null || _choices!.isEmpty) { + widget.choreographer.clearMatches( + 'No choices available for span ${widget.match.updatedMatch.match.message}', + ); + } + setState(() => _loadingChoices = false); + } + } + + Future _fetchFeedback() async { + if (_selectedFeedback != null) { + _feedbackModel.setState(FeedbackLoaded(_selectedFeedback!)); + return; + } + + try { + _feedbackModel.setState(FeedbackLoading()); + await widget.choreographer.fetchSpanDetails( + match: widget.match, + force: true, + ); + } finally { + if (mounted) { + if (_selectedFeedback == null) { + _feedbackModel.setState( + FeedbackError( + L10n.of(context).failedToLoadFeedback, + ), + ); + } else { + _feedbackModel.setState( + FeedbackLoaded(_selectedFeedback!), + ); + } + } } } void _onChoiceSelect(int index) { + final selected = _choices![index]; widget.match.selectChoice(index); - setState( - () => (selectedChoice!.isBestCorrection - ? BotExpression.gold - : BotExpression.surprised), + _latestSelectedChoice = selected; + _feedbackModel.setState( + selected.feedback != null + ? FeedbackLoaded(selected.feedback!) + : FeedbackIdle(), ); + setState(() {}); } - Future _onAcceptReplacement() async { + void _onMatchUpdate(VoidCallback updateFunc) async { try { - widget.choreographer.onAcceptReplacement( - match: widget.match, - ); + updateFunc(); } catch (e, s) { ErrorHandler.logError( e: e, @@ -97,34 +143,21 @@ class SpanCardState extends State { widget.choreographer.clearMatches(e); return; } - _showFirstMatch(); } - void _onIgnoreMatch() { - 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; - } + void _onAcceptReplacement() => _onMatchUpdate(() { + widget.choreographer.onAcceptReplacement(match: widget.match); + }); - _showFirstMatch(); - } + void _onIgnoreMatch() => _onMatchUpdate(() { + widget.choreographer.onIgnoreMatch(match: widget.match); + }); void _showFirstMatch() { if (widget.choreographer.canShowFirstIGCMatch) { - final igcMatch = widget.choreographer.igc.onShowFirstMatch(); OverlayUtil.showIGCMatch( - igcMatch!, + widget.choreographer.igc.firstOpenMatch!, widget.choreographer, context, ); @@ -135,29 +168,6 @@ class SpanCardState extends State { @override Widget build(BuildContext context) { - return WordMatchContent( - controller: this, - scrollController: scrollController, - ); - } -} - -class WordMatchContent extends StatelessWidget { - final SpanCardState controller; - final ScrollController scrollController; - - const WordMatchContent({ - required this.controller, - required this.scrollController, - super.key, - }); - - @override - Widget build(BuildContext context) { - if (controller.widget.match.updatedMatch.isITStart) { - return const SizedBox(); - } - return SizedBox( height: 300.0, child: Column( @@ -167,34 +177,81 @@ class WordMatchContent extends StatelessWidget { controller: scrollController, child: SingleChildScrollView( controller: scrollController, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 8), - ChoicesArray( - isLoading: controller.fetchingData, - choices: - controller.widget.match.updatedMatch.match.choices - ?.map( - (e) => Choice( - text: e.value, - color: e.selected ? e.type.color : null, - isGold: e.type.name == 'bestCorrection', - ), - ) - .toList(), - onPressed: (value, index) => - controller._onChoiceSelect(index), - selectedChoiceIndex: controller - .widget.match.updatedMatch.match.selectedChoiceIndex, - id: controller.widget.match.hashCode.toString(), - langCode: MatrixState.pangeaController.languageController - .activeL2Code(), - ), - const SizedBox(height: 12), - PromptAndFeedback(controller: controller), - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Column( + spacing: 12.0, + children: [ + ChoicesArray( + isLoading: _loadingChoices, + choices: widget.match.updatedMatch.match.choices + ?.map( + (e) => Choice( + text: e.value, + color: e.selected ? e.type.color : null, + isGold: e.type.name == 'bestCorrection', + ), + ) + .toList(), + onPressed: (value, index) => _onChoiceSelect(index), + selectedChoiceIndex: + widget.match.updatedMatch.match.selectedChoiceIndex, + id: widget.match.hashCode.toString(), + langCode: MatrixState + .pangeaController.languageController + .activeL2Code(), + ), + ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 100.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ListenableBuilder( + listenable: _feedbackModel, + builder: (context, _) { + if (_loadingChoices) { + return const SizedBox( + width: 24.0, + height: 24.0, + child: CircularProgressIndicator(), + ); + } + + final state = _feedbackModel.state; + return switch (state) { + FeedbackIdle() => + _selectedChoice == null + ? Text( + widget.match.updatedMatch.match.type + .typeName + .defaultPrompt(context), + style: + BotStyle.text(context).copyWith( + fontStyle: FontStyle.italic, + ), + ) + : WhyButton( + onPress: _fetchFeedback, + loading: false, + ), + FeedbackLoading() => WhyButton( + onPress: _fetchFeedback, + loading: true, + ), + FeedbackError(:final error) => + ErrorIndicator(message: error.toString()), + FeedbackLoaded(:final value) => + Text(value, style: BotStyle.text(context)), + }; + }, + ), + ], + ), + ), + ], + ), ), ), ), @@ -203,7 +260,7 @@ class WordMatchContent extends StatelessWidget { decoration: BoxDecoration( color: Theme.of(context).cardColor, ), - padding: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 12.0), child: Row( spacing: 10.0, children: [ @@ -211,12 +268,11 @@ class WordMatchContent extends StatelessWidget { child: Opacity( opacity: 0.8, child: TextButton( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - Theme.of(context).colorScheme.primary.withAlpha(25), - ), + style: TextButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primary.withAlpha(25), ), - onPressed: controller._onIgnoreMatch, + onPressed: _onIgnoreMatch, child: Center( child: Text(L10n.of(context).ignoreInThisText), ), @@ -225,26 +281,19 @@ class WordMatchContent extends StatelessWidget { ), Expanded( child: Opacity( - opacity: controller.selectedChoice != null ? 1.0 : 0.5, + opacity: _selectedChoice != null ? 1.0 : 0.5, child: TextButton( - onPressed: controller.selectedChoice != null - ? controller._onAcceptReplacement - : null, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - (controller.selectedChoice != null - ? controller.selectedChoice!.color - : Theme.of(context).colorScheme.primary) - .withAlpha(50), - ), - // Outline if Replace button enabled - side: controller.selectedChoice != null - ? WidgetStateProperty.all( - BorderSide( - color: controller.selectedChoice!.color, - style: BorderStyle.solid, - width: 2.0, - ), + onPressed: + _selectedChoice != null ? _onAcceptReplacement : null, + style: TextButton.styleFrom( + backgroundColor: (_selectedChoice?.color ?? + Theme.of(context).colorScheme.primary) + .withAlpha(50), + side: _selectedChoice != null + ? BorderSide( + color: _selectedChoice!.color, + style: BorderStyle.solid, + width: 2.0, ) : null, ), @@ -260,60 +309,3 @@ class WordMatchContent extends StatelessWidget { ); } } - -class PromptAndFeedback extends StatelessWidget { - const PromptAndFeedback({ - super.key, - required this.controller, - }); - - final SpanCardState controller; - - @override - Widget build(BuildContext context) { - return Container( - constraints: controller.widget.match.updatedMatch.isITStart - ? null - : const BoxConstraints(minHeight: 75.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (controller.selectedChoice == null && controller.fetchingData) - const Center( - child: SizedBox( - width: 24.0, - height: 24.0, - child: CircularProgressIndicator(), - ), - ), - if (controller.selectedChoice != null) ...[ - if (controller.selectedChoice?.feedback != null) - Text( - controller.selectedChoice!.feedbackToDisplay(context), - style: BotStyle.text(context), - ), - const SizedBox(height: 8), - if (controller.selectedChoice?.feedback == null) - WhyButton( - onPress: () { - if (!controller.fetchingData) { - controller.getSpanDetails(force: true); - } - }, - loading: controller.fetchingData, - ), - ], - if (!controller.fetchingData && controller.selectedChoice == null) - Text( - controller.widget.match.updatedMatch.match.type.typeName - .defaultPrompt(context), - style: BotStyle.text(context).copyWith( - fontStyle: FontStyle.italic, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pangea/choreographer/widgets/igc/word_data_card.dart b/lib/pangea/choreographer/widgets/igc/word_data_card.dart index 2ec66cb32..b1c67eb60 100644 --- a/lib/pangea/choreographer/widgets/igc/word_data_card.dart +++ b/lib/pangea/choreographer/widgets/igc/word_data_card.dart @@ -1,25 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:http/http.dart'; +import 'package:async/async.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/bot/utils/bot_style.dart'; import 'package:fluffychat/pangea/choreographer/repo/contextual_definition_repo.dart'; import 'package:fluffychat/pangea/choreographer/repo/contextual_definition_request_model.dart'; -import 'package:fluffychat/pangea/choreographer/repo/contextual_definition_response_model.dart'; -import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/common/utils/feedback_model.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/toolbar/widgets/toolbar_content_loading_indicator.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'card_error_widget.dart'; class WordDataCard extends StatefulWidget { final String word; final String fullText; - final String? choiceFeedback; final String wordLang; final String fullTextLang; @@ -27,7 +23,6 @@ class WordDataCard extends StatefulWidget { super.key, required this.word, required this.fullText, - this.choiceFeedback, required this.wordLang, required this.fullTextLang, }); @@ -37,137 +32,88 @@ class WordDataCard extends StatefulWidget { } class WordDataCardController extends State { - final PangeaController controller = MatrixState.pangeaController; - - bool isLoadingContextualDefinition = false; - ContextualDefinitionResponseModel? contextualDefinitionRes; - - Object? definitionError; - LanguageModel? activeL1; - LanguageModel? activeL2; - - Response get noLanguages => Response("", 405); + final FeedbackModel _feedbackModel = FeedbackModel(); @override void initState() { - if (!mounted) return; - activeL1 = controller.languageController.activeL1Model()!; - activeL2 = controller.languageController.activeL2Model()!; - if (activeL1 == null || activeL2 == null) { - definitionError = noLanguages; - } else { - getContextualDefinition(); - } super.initState(); + _getContextualDefinition(); } @override void didUpdateWidget(covariant WordDataCard oldWidget) { - // debugger(when: kDebugMode); if (oldWidget.word != widget.word) { - getContextualDefinition(); + _getContextualDefinition(); } super.didUpdateWidget(oldWidget); } - Future getContextualDefinition() async { - final ContextualDefinitionRequestModel req = - ContextualDefinitionRequestModel( - fullText: widget.fullText, - word: widget.word, - feedbackLang: activeL1?.langCode ?? LanguageKeys.defaultLanguage, - fullTextLang: widget.fullTextLang, - wordLang: widget.wordLang, - ); - if (!mounted) return; + @override + void dispose() { + _feedbackModel.dispose(); + super.dispose(); + } - setState(() { - contextualDefinitionRes = null; - definitionError = null; - isLoadingContextualDefinition = true; - }); + ContextualDefinitionRequestModel get _request => + ContextualDefinitionRequestModel( + fullText: widget.fullText, + word: widget.word, + fullTextLang: widget.fullTextLang, + wordLang: widget.wordLang, + feedbackLang: + MatrixState.pangeaController.languageController.activeL1Code() ?? + LanguageKeys.defaultLanguage, + ); + Future _getContextualDefinition() async { + _feedbackModel.setState(FeedbackLoading()); final resp = await ContextualDefinitionRepo.get( MatrixState.pangeaController.userController.accessToken, - req, + _request, + ).timeout( + const Duration(seconds: 10), + onTimeout: () { + return Result.error("Timeout getting definition"); + }, ); + if (!mounted) return; if (resp.isError) { - definitionError = Exception("Error getting definition"); - } - - if (mounted) { - setState(() => isLoadingContextualDefinition = false); + _feedbackModel.setState( + const FeedbackError("Error getting definition"), + ); + } else { + _feedbackModel.setState(FeedbackLoaded(resp.result!.text)); } } - void handleGetDefinitionButtonPress() { - if (isLoadingContextualDefinition) return; - getContextualDefinition(); - } - - @override - Widget build(BuildContext context) => WordDataCardView(controller: this); -} - -class WordDataCardView extends StatelessWidget { - const WordDataCardView({ - super.key, - required this.controller, - }); - - final WordDataCardController controller; - @override Widget build(BuildContext context) { - if (controller.activeL1 == null || controller.activeL2 == null) { - ErrorHandler.logError( - m: "should not be here", - data: { - "activeL1": controller.activeL1?.toJson(), - "activeL2": controller.activeL2?.toJson(), - }, - ); - return CardErrorWidget( - error: L10n.of(context).noLanguagesSet, - maxWidth: AppConfig.toolbarMinWidth, - ); - } - return ConstrainedBox( constraints: const BoxConstraints( maxWidth: AppConfig.toolbarMinWidth, maxHeight: AppConfig.toolbarMaxHeight, ), - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - if (controller.widget.choiceFeedback != null) - Text( - controller.widget.choiceFeedback!, - style: BotStyle.text(context), - ), - const SizedBox(height: 5.0), - if (controller.isLoadingContextualDefinition) + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: ListenableBuilder( + listenable: _feedbackModel, + builder: (context, _) { + final state = _feedbackModel.state; + return switch (state) { + FeedbackIdle() => const SizedBox.shrink(), + FeedbackLoading() => const ToolbarContentLoadingIndicator(), - if (controller.contextualDefinitionRes != null) - Text( - controller.contextualDefinitionRes!.text, - style: BotStyle.text(context), - textAlign: TextAlign.center, - ), - if (controller.definitionError != null) - Text( + FeedbackError() => Text( L10n.of(context).sorryNoResults, style: BotStyle.text(context), textAlign: TextAlign.center, ), - ], - ), + FeedbackLoaded(:final value) => + Text(value, style: BotStyle.text(context)), + }; + }, ), ), ), diff --git a/lib/pangea/choreographer/widgets/it_feedback_card.dart b/lib/pangea/choreographer/widgets/it_feedback_card.dart index 3849c27d4..3bd2600e0 100644 --- a/lib/pangea/choreographer/widgets/it_feedback_card.dart +++ b/lib/pangea/choreographer/widgets/it_feedback_card.dart @@ -2,27 +2,24 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:http/http.dart'; +import 'package:async/async.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_request_model.dart'; -import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_response_model.dart'; +import 'package:fluffychat/pangea/common/utils/feedback_model.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import '../../../widgets/matrix.dart'; import '../../bot/utils/bot_style.dart'; -import '../../common/controllers/pangea_controller.dart'; import 'igc/card_error_widget.dart'; class ITFeedbackCard extends StatefulWidget { final FullTextTranslationRequestModel req; - final String choiceFeedback; - const ITFeedbackCard({ + const ITFeedbackCard( + this.req, { super.key, - required this.req, - required this.choiceFeedback, }); @override @@ -30,92 +27,84 @@ class ITFeedbackCard extends StatefulWidget { } class ITFeedbackCardController extends State { - final PangeaController controller = MatrixState.pangeaController; - - Object? error; - bool isLoadingFeedback = false; - bool isTranslating = false; - FullTextTranslationResponseModel? res; - String? translatedFeedback; - - Response get noLanguages => Response("", 405); + final FeedbackModel _feedbackModel = FeedbackModel(); @override void initState() { - if (!mounted) return; - //any setup? super.initState(); - getFeedback(); + _getFeedback(); } - Future getFeedback() async { - setState(() { - isLoadingFeedback = true; - }); + @override + void dispose() { + _feedbackModel.dispose(); + super.dispose(); + } + Future _getFeedback() async { + _feedbackModel.setState(FeedbackLoading()); final result = await FullTextTranslationRepo.get( - controller.userController.accessToken, + MatrixState.pangeaController.userController.accessToken, widget.req, + ).timeout( + const Duration(seconds: 10), + onTimeout: () { + return Result.error("Timeout getting translation"); + }, ); - res = result.result; - if (result.isError) error = result.error; - if (mounted) { - setState(() { - isLoadingFeedback = false; - }); + if (!mounted) return; + if (result.isError) { + _feedbackModel.setState( + FeedbackError(result.error.toString()), + ); + } else { + _feedbackModel.setState( + FeedbackLoaded(result.result!.bestTranslation), + ); } } - @override - Widget build(BuildContext context) => error == null - ? ITFeedbackCardView(controller: this) - : CardErrorWidget( - error: L10n.of(context).errorFetchingDefinition, - ); -} - -class ITFeedbackCardView extends StatelessWidget { - const ITFeedbackCardView({ - super.key, - required this.controller, - }); - - final ITFeedbackCardController controller; - @override Widget build(BuildContext context) { - const characterWidth = 10.0; + return ListenableBuilder( + listenable: _feedbackModel, + builder: (context, _) { + final state = _feedbackModel.state; + if (state is FeedbackError) { + return CardErrorWidget(L10n.of(context).errorFetchingDefinition); + } - return Container( - constraints: const BoxConstraints(maxWidth: 300), - alignment: Alignment.center, - child: Wrap( - alignment: WrapAlignment.center, - children: [ - Text( - controller.widget.req.text, - style: BotStyle.text(context), + return Container( + constraints: const BoxConstraints(maxWidth: 300), + alignment: Alignment.center, + child: Wrap( + spacing: 10, + alignment: WrapAlignment.center, + children: [ + Text( + widget.req.text, + style: BotStyle.text(context), + ), + Text( + "≈", + style: BotStyle.text(context), + ), + _feedbackModel.state is FeedbackLoaded + ? Text( + (state as FeedbackLoaded).value, + style: BotStyle.text(context), + ) + : TextLoadingShimmer( + width: min( + 140, + 10.0 * widget.req.text.length, + ), + ), + ], ), - const SizedBox(width: 10), - Text( - "≈", - style: BotStyle.text(context), - ), - const SizedBox(width: 10), - controller.res?.bestTranslation != null - ? Text( - controller.res!.bestTranslation, - style: BotStyle.text(context), - ) - : TextLoadingShimmer( - width: min( - 140, - characterWidth * controller.widget.req.text.length, - ), - ), - ], - ), + ); + }, ); } } diff --git a/lib/pangea/choreographer/widgets/it_shimmer.dart b/lib/pangea/choreographer/widgets/it_shimmer.dart index 2ad0616f7..93c32e633 100644 --- a/lib/pangea/choreographer/widgets/it_shimmer.dart +++ b/lib/pangea/choreographer/widgets/it_shimmer.dart @@ -3,89 +3,51 @@ import 'dart:ui'; import 'package:flutter/material.dart'; class ItShimmer extends StatelessWidget { - const ItShimmer({ - super.key, - required this.fontSize, - }); + const ItShimmer({super.key}); - final double fontSize; - - Iterable renderShimmerIfListEmpty( - BuildContext context, { - int noOfBars = 3, - }) { + @override + Widget build(BuildContext context) { final List dummyStrings = []; - for (int i = 0; i < noOfBars; i++) { + for (int i = 0; i < 3; i++) { dummyStrings.add(" " * 10); } - return dummyStrings.map( - (e) => ITShimmerElement( - text: e, - fontSize: fontSize, - ), - ); - } - - // PTODO - bring this back, make it shimmer - @override - Widget build(BuildContext context) { return Wrap( alignment: WrapAlignment.center, - children: [...renderShimmerIfListEmpty(context, noOfBars: 3)], - ); - } -} - -class ITShimmerElement extends StatelessWidget { - const ITShimmerElement({ - super.key, - required this.text, - required this.fontSize, - }); - - final String text; - final double fontSize; - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(minWidth: 50), - margin: const EdgeInsets.all(2), - padding: EdgeInsets.zero, - // decoration: BoxDecoration( - // borderRadius: const BorderRadius.all(Radius.circular(10)), - // border: Border.all( - // color: Theme.of(context).colorScheme.primary, - // style: BorderStyle.solid, - // width: 2.0, - // ), - // ), - child: ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: TextButton( - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(horizontal: 7), - ), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + children: [ + ...dummyStrings.map( + (e) => Container( + constraints: const BoxConstraints(minWidth: 50), + margin: const EdgeInsets.all(2), + padding: EdgeInsets.zero, + child: ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: TextButton( + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(horizontal: 7), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + backgroundColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.primary.withAlpha(50), + ), + ), + onPressed: null, + child: Text( + e, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Colors.transparent, + fontSize: 16, + ), + ), ), ), - backgroundColor: WidgetStateProperty.all( - Theme.of(context).colorScheme.primary.withAlpha(50), - ), - ), - onPressed: null, - child: Text( - text, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.transparent, fontSize: fontSize), ), ), - ), + ], ); } } diff --git a/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart b/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart deleted file mode 100644 index 1866bee74..000000000 --- a/lib/pangea/choreographer/widgets/language_permissions_warning_buttons.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -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'; - -class ErrorCopy { - final String title; - final String? description; - - ErrorCopy(this.title, [this.description]); -} - -class LanguagePermissionsButtons extends StatelessWidget { - final String? roomID; - final Choreographer choreographer; - - const LanguagePermissionsButtons({ - super.key, - required this.roomID, - required this.choreographer, - }); - - @override - Widget build(BuildContext context) { - if (roomID == null) return const SizedBox.shrink(); - final ErrorCopy? copy = getCopy(context); - if (copy == null) return const SizedBox.shrink(); - - final Widget text = RichText( - text: TextSpan( - children: [ - TextSpan( - text: copy.title, - style: TextStyle( - color: Theme.of(context).brightness == Brightness.light - ? Colors.white - : Colors.black, - ), - ), - if (copy.description != null) - TextSpan( - text: copy.description, - style: TextStyle(color: Theme.of(context).colorScheme.primary), - recognizer: TapGestureRecognizer() - ..onTap = () { - showDialog( - context: context, - builder: (c) => const SettingsLearning(), - barrierDismissible: false, - ); - }, - ), - ], - ), - ); - - return FloatingActionButton( - mini: true, - child: const Icon(Icons.history_edu_outlined), - onPressed: () => showMessage(context, text), - ); - } - - ErrorCopy? getCopy(BuildContext context) { - final bool itDisabled = !choreographer.itEnabled; - final bool igcDisabled = !choreographer.igcEnabled; - if (roomID == null) { - ErrorHandler.logError( - e: Exception("Room ID is null in language permissions"), - data: {}, - ); - return null; - } - - if (igcDisabled && itDisabled) { - return ErrorCopy( - L10n.of(context).errorDisableLanguageAssistance, - " ${L10n.of(context).errorDisableLanguageAssistanceUserDesc}", - ); - } - - if (itDisabled) { - return ErrorCopy( - L10n.of(context).errorDisableIT, - " ${L10n.of(context).errorDisableITUserDesc}", - ); - } - - if (igcDisabled) { - return ErrorCopy( - L10n.of(context).errorDisableIGC, - " ${L10n.of(context).errorDisableIGCUserDesc}", - ); - } - - debugger(when: kDebugMode); - ErrorHandler.logError( - e: Exception("Unhandled case in language permissions"), - data: { - "roomID": roomID, - }, - ); - return null; - } - - void showMessage(BuildContext context, Widget text) { - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 10), - content: text, - ), - ); - } -} diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index 8452bfa15..9507ecc00 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -26,12 +26,17 @@ class ChoreographerSendButton extends StatelessWidget { controller.choreographer.inputTransformTargetKey, ); } on OpenMatchesException { - if (controller.choreographer.firstIGCMatch != null) { - OverlayUtil.showIGCMatch( - controller.choreographer.firstIGCMatch!, - controller.choreographer, - context, - ); + if (controller.choreographer.firstOpenMatch != null) { + if (controller.choreographer.firstOpenMatch!.updatedMatch.isITStart) { + controller.choreographer + .openIT(controller.choreographer.firstOpenMatch!); + } else { + OverlayUtil.showIGCMatch( + controller.choreographer.firstOpenMatch!, + controller.choreographer, + context, + ); + } } } } @@ -39,25 +44,23 @@ class ChoreographerSendButton extends StatelessWidget { @override Widget build(BuildContext context) { return ListenableBuilder( - listenable: controller.choreographer, + listenable: Listenable.merge([ + controller.choreographer.textController, + controller.choreographer.isFetching, + ]), 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, - ), - ); - }, + return Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + icon: const Icon(Icons.send_outlined), + color: controller.choreographer.assistanceState + .sendButtonColor(context), + onPressed: controller.choreographer.isFetching.value + ? null + : () => _onPressed(context), + tooltip: L10n.of(context).send, + ), ); }, ); diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index dc0b174d9..3162e9557 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -63,13 +63,17 @@ class StartIGCButtonState extends State void _showFirstMatch() { if (widget.controller.choreographer.canShowFirstIGCMatch) { - final match = widget.controller.choreographer.igc.onShowFirstMatch(); + final match = widget.controller.choreographer.igc.firstOpenMatch; if (match == null) return; - OverlayUtil.showIGCMatch( - match, - widget.controller.choreographer, - context, - ); + if (match.updatedMatch.isITStart) { + widget.controller.choreographer.openIT(match); + } else { + OverlayUtil.showIGCMatch( + match, + widget.controller.choreographer, + context, + ); + } } } @@ -79,6 +83,8 @@ class StartIGCButtonState extends State AssistanceState.fetched, AssistanceState.complete, AssistanceState.noMessage, + AssistanceState.noSub, + AssistanceState.error, ].contains(assistanceState); } @@ -101,15 +107,18 @@ class StartIGCButtonState extends State if (widget.controller.shouldShowLanguageMismatchPopup) { widget.controller.showLanguageMismatchPopup(); } else { - final igcMatch = - await widget.controller.choreographer.requestLanguageAssistance(); - - if (igcMatch != null) { - OverlayUtil.showIGCMatch( - igcMatch, - widget.controller.choreographer, - context, - ); + await widget.controller.choreographer.requestLanguageAssistance(); + final openMatch = widget.controller.choreographer.firstOpenMatch; + if (openMatch != null) { + if (openMatch.updatedMatch.isITStart) { + widget.controller.choreographer.openIT(openMatch); + } else { + OverlayUtil.showIGCMatch( + openMatch, + widget.controller.choreographer, + context, + ); + } } } return; @@ -118,6 +127,7 @@ class StartIGCButtonState extends State return; case AssistanceState.complete: case AssistanceState.fetching: + case AssistanceState.error: return; } } @@ -128,6 +138,7 @@ class StartIGCButtonState extends State case AssistanceState.noMessage: case AssistanceState.fetched: case AssistanceState.complete: + case AssistanceState.error: return Theme.of(context).colorScheme.surfaceContainerHighest; case AssistanceState.notFetched: case AssistanceState.fetching: diff --git a/lib/pangea/common/utils/feedback_model.dart b/lib/pangea/common/utils/feedback_model.dart new file mode 100644 index 000000000..da5975335 --- /dev/null +++ b/lib/pangea/common/utils/feedback_model.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +sealed class FeedbackState { + const FeedbackState(); +} + +class FeedbackIdle extends FeedbackState {} + +class FeedbackLoading extends FeedbackState {} + +class FeedbackLoaded extends FeedbackState { + final T value; + const FeedbackLoaded(this.value); +} + +class FeedbackError extends FeedbackState { + final Object error; + const FeedbackError(this.error); +} + +class FeedbackModel extends ChangeNotifier { + FeedbackState _state = FeedbackIdle(); + FeedbackState get state => _state; + + void setState(FeedbackState newState) { + _state = newState; + notifyListeners(); + } +} diff --git a/lib/pangea/instructions/instructions_show_popup.dart b/lib/pangea/instructions/instructions_show_popup.dart deleted file mode 100644 index 65b53ac87..000000000 --- a/lib/pangea/instructions/instructions_show_popup.dart +++ /dev/null @@ -1,73 +0,0 @@ -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/widgets/igc/card_header.dart'; -import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -import 'package:fluffychat/pangea/instructions/instructions_toggle.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -/// Instruction Card gives users tips on -/// how to use Pangea Chat's features -Future instructionsShowPopup( - BuildContext context, - InstructionsEnum key, - String transformTargetKey, { - bool showToggle = true, - Widget? customContent, - bool forceShow = false, -}) async { - final bool userLangsSet = - await MatrixState.pangeaController.userController.areUserLanguagesSet; - if (!userLangsSet) { - return; - } - - // if ((_instructionsShown[key.toString()] ?? false) && !forceShow) { - // return; - // } - // _instructionsShown[key.toString()] = true; - - if (key.isToggledOff && !forceShow) { - return; - } - - final botStyle = BotStyle.text(context); - Future.delayed( - const Duration(seconds: 1), - () { - if (!context.mounted) return; - OverlayUtil.showPositionedCard( - context: context, - backDropToDismiss: false, - cardToShow: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CardHeader( - text: key.title(L10n.of(context)), - botExpression: BotExpression.idle, - // onClose: () => {_instructionsClosed[key.toString()] = true}, - ), - const SizedBox(height: 10.0), - Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - key.body(L10n.of(context)), - style: botStyle, - ), - ), - if (customContent != null) customContent, - if (showToggle) InstructionsToggle(instructionsKey: key), - ], - ), - maxHeight: 300, - maxWidth: 300, - transformTargetId: transformTargetKey, - closePrevOverlay: false, - overlayKey: key.toString(), - ); - }, - ); -} diff --git a/lib/pangea/login/pages/new_course_page.dart b/lib/pangea/login/pages/new_course_page.dart index 01a3bff97..0b4e0d91d 100644 --- a/lib/pangea/login/pages/new_course_page.dart +++ b/lib/pangea/login/pages/new_course_page.dart @@ -55,6 +55,13 @@ class NewCoursePageState extends State { _loadCourses(); } + @override + void dispose() { + _courses.dispose(); + _targetLanguageFilter.dispose(); + super.dispose(); + } + CourseFilter get _filter { return CourseFilter( targetLanguage: _targetLanguageFilter.value, diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index 59d71d1d2..98484a536 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -119,7 +119,7 @@ class _PhoneticTranscriptionWidgetState } } - Future _handleAudioTap(BuildContext context) async { + Future _handleAudioTap() async { if (_isPlaying) { await TtsController.stop(); setState(() => _isPlaying = false); @@ -146,7 +146,7 @@ class _PhoneticTranscriptionWidgetState child: HoverBuilder( builder: (context, hovering) { return GestureDetector( - onTap: () => _handleAudioTap(context), + onTap: _handleAudioTap, child: AnimatedContainer( duration: const Duration(milliseconds: 150), decoration: BoxDecoration( @@ -156,58 +156,66 @@ class _PhoneticTranscriptionWidgetState borderRadius: BorderRadius.circular(6), ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_error != null) - _error is UnsubscribedException - ? ErrorIndicator( - message: L10n.of(context) - .subscribeToUnlockTranscriptions, - onTap: () { - MatrixState - .pangeaController.subscriptionController - .showPaywall(context); - }, - ) - : ErrorIndicator( - message: - L10n.of(context).failedToFetchTranscription, - ) - else if (_isLoading || _transcription == null) - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(), - ) - else - Flexible( - child: Text( - _transcription!, - textScaler: TextScaler.noScaling, - style: widget.style ?? - Theme.of(context).textTheme.bodyMedium, + child: CompositedTransformTarget( + link: MatrixState.pAnyState + .layerLinkAndKey("phonetic-transcription-${widget.text}") + .link, + child: Row( + key: MatrixState.pAnyState + .layerLinkAndKey("phonetic-transcription-${widget.text}") + .key, + mainAxisSize: MainAxisSize.min, + children: [ + if (_error != null) + _error is UnsubscribedException + ? ErrorIndicator( + message: L10n.of(context) + .subscribeToUnlockTranscriptions, + onTap: () { + MatrixState + .pangeaController.subscriptionController + .showPaywall(context); + }, + ) + : ErrorIndicator( + message: + L10n.of(context).failedToFetchTranscription, + ) + else if (_isLoading || _transcription == null) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(), + ) + else + Flexible( + child: Text( + _transcription!, + textScaler: TextScaler.noScaling, + style: widget.style ?? + Theme.of(context).textTheme.bodyMedium, + ), ), - ), - if (_transcription != null && - _error == null && - widget.enabled) - const SizedBox(width: 8), - if (_transcription != null && - _error == null && - widget.enabled) - Tooltip( - message: _isPlaying - ? L10n.of(context).stop - : L10n.of(context).playAudio, - child: Icon( - _isPlaying ? Icons.pause_outlined : Icons.volume_up, - size: widget.iconSize ?? 24, - color: widget.iconColor ?? - Theme.of(context).iconTheme.color, + if (_transcription != null && + _error == null && + widget.enabled) + const SizedBox(width: 8), + if (_transcription != null && + _error == null && + widget.enabled) + Tooltip( + message: _isPlaying + ? L10n.of(context).stop + : L10n.of(context).playAudio, + child: Icon( + _isPlaying ? Icons.pause_outlined : Icons.volume_up, + size: widget.iconSize ?? 24, + color: widget.iconColor ?? + Theme.of(context).iconTheme.color, + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/pangea/toolbar/controllers/tts_controller.dart b/lib/pangea/toolbar/controllers/tts_controller.dart index 1550dc5bc..a088faead 100644 --- a/lib/pangea/toolbar/controllers/tts_controller.dart +++ b/lib/pangea/toolbar/controllers/tts_controller.dart @@ -11,11 +11,14 @@ import 'package:just_audio/just_audio.dart'; import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pangea/bot/utils/bot_style.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart'; +import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -import 'package:fluffychat/pangea/instructions/instructions_show_popup.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -334,11 +337,27 @@ class TtsController { BuildContext context, String targetID, ) async => - instructionsShowPopup( - context, - InstructionsEnum.ttsDisabled, - targetID, - showToggle: false, - forceShow: true, + OverlayUtil.showPositionedCard( + context: context, + backDropToDismiss: false, + cardToShow: Column( + spacing: 12.0, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CardHeader(InstructionsEnum.ttsDisabled.title(L10n.of(context))), + Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + InstructionsEnum.ttsDisabled.body(L10n.of(context)), + style: BotStyle.text(context), + ), + ), + ], + ), + maxHeight: 300, + maxWidth: 300, + transformTargetId: targetID, + closePrevOverlay: false, + overlayKey: InstructionsEnum.ttsDisabled.toString(), ); } diff --git a/lib/pangea/toolbar/enums/message_mode_enum.dart b/lib/pangea/toolbar/enums/message_mode_enum.dart index b2d1d8f2a..9c80b5761 100644 --- a/lib/pangea/toolbar/enums/message_mode_enum.dart +++ b/lib/pangea/toolbar/enums/message_mode_enum.dart @@ -25,7 +25,6 @@ enum MessageMode { messageMeaning, listening, messageSpeechToText, - messageTranslation, // message not selected noneSelected, @@ -34,8 +33,6 @@ enum MessageMode { extension MessageModeExtension on MessageMode { IconData get icon { switch (this) { - case MessageMode.messageTranslation: - return Icons.translate; case MessageMode.listening: return Icons.volume_up; case MessageMode.messageSpeechToText: @@ -58,8 +55,6 @@ extension MessageModeExtension on MessageMode { String title(BuildContext context) { switch (this) { - case MessageMode.messageTranslation: - return L10n.of(context).translations; case MessageMode.listening: return L10n.of(context).messageAudio; case MessageMode.messageSpeechToText: @@ -83,8 +78,6 @@ extension MessageModeExtension on MessageMode { String tooltip(BuildContext context) { switch (this) { - case MessageMode.messageTranslation: - return L10n.of(context).translationTooltip; case MessageMode.listening: return L10n.of(context).listen; case MessageMode.messageSpeechToText: @@ -121,8 +114,6 @@ extension MessageModeExtension on MessageMode { return InstructionsEnum.chooseEmoji; case MessageMode.noneSelected: return InstructionsEnum.readingAssistanceOverview; - case MessageMode.messageTranslation: - return InstructionsEnum.completeActivitiesToUnlock; case MessageMode.messageMeaning: case MessageMode.wordZoom: case MessageMode.practiceActivity: @@ -142,7 +133,6 @@ extension MessageModeExtension on MessageMode { return 0.5; case MessageMode.listening: return 0.3; - case MessageMode.messageTranslation: case MessageMode.messageSpeechToText: case MessageMode.wordZoom: case MessageMode.wordEmoji: @@ -156,8 +146,6 @@ extension MessageModeExtension on MessageMode { MessageOverlayController overlayController, ) { switch (this) { - case MessageMode.messageTranslation: - return overlayController.isTranslationUnlocked; case MessageMode.practiceActivity: case MessageMode.listening: case MessageMode.messageSpeechToText: @@ -175,8 +163,6 @@ extension MessageModeExtension on MessageMode { bool isModeDone(MessageOverlayController overlayController) { switch (this) { - case MessageMode.messageTranslation: - return overlayController.isTotallyDone; case MessageMode.listening: return overlayController.isListeningDone; case MessageMode.wordEmoji: @@ -230,7 +216,6 @@ extension MessageModeExtension on MessageMode { case MessageMode.noneSelected: case MessageMode.messageMeaning: - case MessageMode.messageTranslation: case MessageMode.wordZoom: case MessageMode.messageSpeechToText: case MessageMode.practiceActivity: @@ -290,7 +275,6 @@ extension MessageModeExtension on MessageMode { case MessageMode.noneSelected: case MessageMode.messageMeaning: - case MessageMode.messageTranslation: case MessageMode.wordZoom: case MessageMode.messageSpeechToText: case MessageMode.practiceActivity: diff --git a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart index a7e83de6e..9e55bfa4b 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/reading_assistance_input_bar.dart @@ -3,9 +3,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/toolbar/widgets/practice_mode_buttons.dart'; @@ -73,15 +71,6 @@ class ReadingAssistanceInputBarState extends State { textAlign: TextAlign.center, ); - case MessageMode.messageTranslation: - if (overlayController.isTranslationUnlocked) { - content = MessageTranslationCard( - messageEvent: overlayController.pangeaMessageEvent, - ); - } else { - content = MessageModeLockedCard(controller: overlayController); - } - case MessageMode.wordEmoji: case MessageMode.wordMeaning: case MessageMode.listening: diff --git a/lib/pangea/toolbar/widgets/message_meaning_button.dart b/lib/pangea/toolbar/widgets/message_meaning_button.dart deleted file mode 100644 index 044d913a8..000000000 --- a/lib/pangea/toolbar/widgets/message_meaning_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart'; - -class MessageMeaningButton extends StatelessWidget { - final MessageOverlayController overlayController; - final double buttonSize; - - const MessageMeaningButton({ - super.key, - required this.overlayController, - required this.buttonSize, - }); - - @override - Widget build(BuildContext context) { - return AnimatedCrossFade( - crossFadeState: overlayController.isPracticeComplete - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - duration: FluffyThemes.animationDuration, - firstChild: ToolbarButton( - mode: MessageMode.messageMeaning, - overlayController: overlayController, - buttonSize: buttonSize, - ), - secondChild: Container( - width: buttonSize, - height: buttonSize, - alignment: Alignment.center, - child: Icon( - MessageMode.messageMeaning.icon, - color: AppConfig.gold, - size: buttonSize, - ), - ), - ); - } -} diff --git a/lib/pangea/toolbar/widgets/message_mode_locked_card.dart b/lib/pangea/toolbar/widgets/message_mode_locked_card.dart deleted file mode 100644 index 1ca6f1519..000000000 --- a/lib/pangea/toolbar/widgets/message_mode_locked_card.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; - -class MessageModeLockedCard extends StatelessWidget { - final MessageOverlayController controller; - - const MessageModeLockedCard({super.key, required this.controller}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Icon( - Icons.lock_outline, - size: 40, - color: Theme.of(context).colorScheme.primary, - ), - // if (!InstructionsEnum.completeActivitiesToUnlock.isToggledOff) ...[ - // const SizedBox(height: 8), - // const InstructionsInlineTooltip( - // instructionsEnum: InstructionsEnum.completeActivitiesToUnlock, - // bold: true, - // ), - // ], - ], - ); - } -} diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index fe928f2a9..5c5893fe8 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -337,8 +337,6 @@ class MessageOverlayController extends State bool get hideWordCardContent => readingAssistanceMode == ReadingAssistanceMode.practiceMode; - bool get isPracticeComplete => isTranslationUnlocked; - bool isPracticeActivityDone(ActivityTypeEnum activityType) => practiceSelection?.activities(activityType).every((a) => a.isComplete) == true; @@ -353,15 +351,6 @@ class MessageOverlayController extends State bool get isMorphDone => isPracticeActivityDone(ActivityTypeEnum.morphId); - /// you have to complete one of the mode mini-games to unlock translation - bool get isTranslationUnlocked => - pangeaMessageEvent.ownMessage == true || - !messageInUserL2 || - isEmojiDone || - isMeaningDone || - isListeningDone || - isMorphDone; - bool get isTotallyDone => isEmojiDone && isMeaningDone && isListeningDone && isMorphDone; diff --git a/lib/pangea/toolbar/widgets/message_translation_card.dart b/lib/pangea/toolbar/widgets/message_translation_card.dart deleted file mode 100644 index 76fce80d6..000000000 --- a/lib/pangea/toolbar/widgets/message_translation_card.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; -import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; -import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -class MessageTranslationCard extends StatefulWidget { - final PangeaMessageEvent messageEvent; - - const MessageTranslationCard({ - super.key, - required this.messageEvent, - }); - - @override - MessageTranslationCardState createState() => MessageTranslationCardState(); -} - -class MessageTranslationCardState extends State { - PangeaRepresentation? repEvent; - bool _fetchingTranslation = false; - - @override - void initState() { - super.initState(); - loadTranslation(); - } - - Future loadTranslation() async { - if (!mounted) return; - try { - setState(() => _fetchingTranslation = true); - repEvent = await widget.messageEvent.l1Respresentation(); - } catch (err) { - ErrorHandler.logError( - e: err, - data: {}, - ); - } finally { - if (mounted) { - setState(() => _fetchingTranslation = false); - } - } - } - - String? get l1Code => - MatrixState.pangeaController.languageController.activeL1Code(); - String? get l2Code => - MatrixState.pangeaController.languageController.activeL2Code(); - - /// Show warning if message's language code is user's L1 - /// or if translated text is same as original text. - /// Warning does not show if was previously closed - bool get notGoingToTranslate { - final bool isWrittenInL1 = - l1Code != null && widget.messageEvent.originalSent?.langCode == l1Code; - - return isWrittenInL1; - } - - @override - Widget build(BuildContext context) { - debugPrint('MessageTranslationCard build'); - if (!_fetchingTranslation && repEvent == null) { - return CardErrorWidget( - error: L10n.of(context).errorFetchingTranslation, - maxWidth: AppConfig.toolbarMinWidth, - ); - } - - final loadingTranslation = repEvent == null; - - if (_fetchingTranslation || loadingTranslation) { - return const ToolbarContentLoadingIndicator(); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - repEvent!.text, - style: AppConfig.messageTextStyle( - widget.messageEvent.event, - Theme.of(context).colorScheme.primary, - ), - textAlign: TextAlign.center, - ), - if (notGoingToTranslate) - const InstructionsInlineTooltip( - instructionsEnum: InstructionsEnum.l1Translation, - ), - ], - ); - } -} diff --git a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart index 5e2a7d290..5d5b37b7e 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/multiple_choice_activity.dart @@ -237,7 +237,7 @@ class MultipleChoiceActivityState extends State { onPressed: updateChoice, selectedChoiceIndex: selectedChoiceIndex, choices: choices(context), - isActive: true, + enabled: true, id: currentRecordModel?.hashCode.toString(), enableAudio: practiceActivity.activityType.includeTTSOnClick, langCode: diff --git a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart index 3728d3d1b..8fd39f622 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart @@ -296,10 +296,7 @@ class PracticeActivityCardState extends State { Widget build(BuildContext context) { if (_error != null || (!fetchingActivity && currentActivity == null)) { debugger(when: kDebugMode); - return CardErrorWidget( - error: L10n.of(context).errorFetchingActivity, - maxWidth: 500, - ); + return CardErrorWidget(L10n.of(context).errorFetchingActivity); } return Column( diff --git a/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart b/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart index 9628df870..67f47d261 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart @@ -42,11 +42,6 @@ class WordZoomActivityButton extends StatelessWidget { iconSize: 24, // Keep this constant as scaling handles the size change color: isSelected ? Theme.of(context).colorScheme.primary : null, visualDensity: VisualDensity.compact, - // style: IconButton.styleFrom( - // backgroundColor: isSelected - // ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.25) - // : Colors.transparent, - // ), ), ), ); diff --git a/lib/pangea/toolbar/widgets/toolbar_button.dart b/lib/pangea/toolbar/widgets/toolbar_button.dart index de632f39c..4da87573b 100644 --- a/lib/pangea/toolbar/widgets/toolbar_button.dart +++ b/lib/pangea/toolbar/widgets/toolbar_button.dart @@ -22,10 +22,6 @@ class ToolbarButton extends StatelessWidget { overlayController, ); - bool get enabled => mode == MessageMode.messageTranslation - ? overlayController.isTranslationUnlocked - : true; - @override Widget build(BuildContext context) { return Tooltip(