diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 9b0e5883f..cd2db75db 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,10 +1,12 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/widgets/chat/pangea_reaction_picker.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -274,48 +276,54 @@ class ChatInputRow extends StatelessWidget { ], ), ), - Container( - height: height, - width: height, - alignment: Alignment.center, - child: - // #Pangea - // KeyBoardShortcuts( - // keysToPress: { - // LogicalKeyboardKey.altLeft, - // LogicalKeyboardKey.keyE, - // }, - // onKeysPressed: controller.emojiPickerAction, - // helpLabel: L10n.of(context)!.emojis, - // child: - // Pangea# - IconButton( - tooltip: L10n.of(context)!.emojis, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); - }, - child: Icon( - controller.showEmojiPicker - ? Icons.keyboard - : Icons.add_reaction_outlined, - key: ValueKey(controller.showEmojiPicker), - ), - ), - onPressed: controller.emojiPickerAction, - ), - ), // #Pangea + kIsWeb + ? + // Pangea# + Container( + height: height, + width: height, + alignment: Alignment.center, + child: + // #Pangea + // KeyBoardShortcuts( + // keysToPress: { + // LogicalKeyboardKey.altLeft, + // LogicalKeyboardKey.keyE, + // }, + // onKeysPressed: controller.emojiPickerAction, + // helpLabel: L10n.of(context)!.emojis, + // child: + // Pangea# + IconButton( + tooltip: L10n.of(context)!.emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: + SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.add_reaction_outlined, + key: ValueKey(controller.showEmojiPicker), + ), + ), + onPressed: controller.emojiPickerAction, + ), + ) + // #Pangea + : const SizedBox(width: 10), // if (Matrix.of(context).isMultiAccount && // Matrix.of(context).hasComplexBundles && // Matrix.of(context).currentBundle!.length > 1) @@ -369,6 +377,11 @@ class ChatInputRow extends StatelessWidget { ), ), ), + // #Pangea + StartIGCButton( + controller: controller, + ), + // Pangea# Container( height: height, width: height, diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index ba1ba5219..8d3fc747e 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -8,7 +8,6 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; @@ -456,26 +455,18 @@ class ChatView extends StatelessWidget { maxWidth: FluffyThemes.columnWidth * 2.4, ), child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.end, children: [ - StartIGCButton( - controller: controller, + PointsGainedAnimation( + gainColor: Theme.of(context) + .colorScheme + .onPrimary, + origin: + AnalyticsUpdateOrigin.sendMessage, ), - Row( - children: [ - PointsGainedAnimation( - gainColor: Theme.of(context) - .colorScheme - .onPrimary, - origin: AnalyticsUpdateOrigin - .sendMessage, - ), - const SizedBox(width: 100), - ChatFloatingActionButton( - controller: controller, - ), - ], + const SizedBox(width: 100), + ChatFloatingActionButton( + controller: controller, ), ], ), @@ -492,23 +483,38 @@ class ChatView extends StatelessWidget { alignment: Alignment.center, child: Material( clipBehavior: Clip.hardEdge, - color: Theme.of(context) - .colorScheme - .surfaceContainerHighest, + // #Pangea + // color: Theme.of(context) + // .colorScheme + // .surfaceContainerHighest, + type: MaterialType.transparency, + // Pangea# borderRadius: const BorderRadius.all( Radius.circular(24), ), + child: Column( children: [ const ConnectionStatusHeader(), ITBar( choreographer: controller.choreographer, ), - ReplyDisplay(controller), - ChatInputRowWrapper( - controller: controller, + DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + ), + child: Column( + children: [ + ReplyDisplay(controller), + ChatInputRowWrapper( + controller: controller, + ), + ChatEmojiPicker(controller), + ], + ), ), - ChatEmojiPicker(controller), ], ), ), diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 1fdcad5a2..33a5a6974 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -42,6 +42,8 @@ class Choreographer { late ErrorService errorService; bool isFetching = false; + int _timesClicked = 0; + Timer? debounceTimer; ChoreoRecord choreoRecord = ChoreoRecord.newRecord; // last checked by IGC or translation @@ -447,6 +449,7 @@ class Choreographer { clear() { choreoMode = ChoreoMode.igc; _lastChecked = null; + _timesClicked = 0; isFetching = false; choreoRecord = ChoreoRecord.newRecord; itController.clear(); @@ -506,6 +509,18 @@ class Choreographer { setState(); } + void incrementTimesClicked() { + if (assistanceState == AssistanceState.fetched) { + _timesClicked++; + + // if user is doing IT, call closeIT here to + // ensure source text is replaced when needed + if (itController.isOpen && _timesClicked > 1) { + itController.closeIT(); + } + } + } + get roomId => chatController.roomId; bool get _useCustomInput => [ @@ -602,7 +617,12 @@ class Choreographer { bool get canSendMessage { // if there's an error, let them send. we don't want to block them from sending in this case - if (errorService.isError || l2Lang == null || l1Lang == null) return true; + if (errorService.isError || + l2Lang == null || + l1Lang == null || + _timesClicked > 1) { + return true; + } // if they're in IT mode, don't let them send if (itEnabled && isRunningIT) return false; diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 6b6af84d8..e39b34615 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -10,13 +10,13 @@ import 'package:fluffychat/pangea/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; +import 'package:fluffychat/pangea/pages/settings_learning/settings_learning.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../../../config/app_config.dart'; import '../../controllers/it_feedback_controller.dart'; import '../../models/it_response_model.dart'; import '../../utils/overlay.dart'; @@ -31,19 +31,39 @@ class ITBar extends StatefulWidget { ITBarState createState() => ITBarState(); } -class ITBarState extends State { +class ITBarState extends State with SingleTickerProviderStateMixin { ITController get itController => widget.choreographer.itController; StreamSubscription? _choreoSub; bool showedClickInstruction = false; + late AnimationController _controller; + late Animation _animation; + bool wasOpen = false; + @override void initState() { + super.initState(); + // Rebuild the widget each time there's an update from choreo. _choreoSub = widget.choreographer.stateListener.stream.listen((_) { + if (itController.willOpen != wasOpen) { + itController.willOpen ? _controller.forward() : _controller.reverse(); + } + wasOpen = itController.willOpen; setState(() {}); }); - super.initState(); + + wasOpen = itController.willOpen; + + _controller = AnimationController( + duration: itController.animationSpeed, + vsync: this, + ); + _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); + + // Start in the correct state + itController.willOpen ? _controller.forward() : _controller.reverse(); } bool get showITInstructionsTooltip { @@ -72,129 +92,165 @@ class ITBarState extends State { super.dispose(); } + final double iconDimension = 36; + final double iconSize = 20; + @override Widget build(BuildContext context) { - return AnimatedSize( - duration: itController.animationSpeed, - curve: Curves.fastOutSlowIn, - clipBehavior: Clip.none, - child: !itController.willOpen - ? const SizedBox() - : CompositedTransformTarget( - link: widget.choreographer.itBarLinkAndKey.link, - child: AnimatedOpacity( - duration: itController.animationSpeed, - opacity: itController.willOpen ? 1.0 : 0.0, - child: Container( - key: widget.choreographer.itBarLinkAndKey.key, - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.light - ? Colors.white - : Colors.black, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(AppConfig.borderRadius), - topRight: Radius.circular(AppConfig.borderRadius), - ), - ), - width: double.infinity, - padding: const EdgeInsets.fromLTRB(0, 3, 3, 3), - child: Stack( - alignment: Alignment.topCenter, - children: [ - const Positioned( - top: 60, - child: PointsGainedAnimation( - origin: AnalyticsUpdateOrigin.it, - ), - ), - SingleChildScrollView( - child: Column( - children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // // Row( - // // mainAxisAlignment: MainAxisAlignment.start, - // // crossAxisAlignment: CrossAxisAlignment.start, - // // children: [ - // // CounterDisplay( - // // correct: controller.correctChoices, - // // custom: controller.customChoices, - // // incorrect: controller.incorrectChoices, - // // yellow: controller.wildcardChoices, - // // ), - // // CompositedTransformTarget( - // // link: choreographer.itBotLayerLinkAndKey.link, - // // child: ITBotButton( - // // key: choreographer.itBotLayerLinkAndKey.key, - // // choreographer: choreographer, - // // ), - // // ), - // // ], - // // ), - // ITCloseButton(choreographer: choreographer), - // ], - // ), - // const SizedBox(height: 40.0), - OriginalText(controller: itController), - const SizedBox(height: 7.0), - if (showITInstructionsTooltip) - InlineTooltip( - instructionsEnum: - InstructionsEnum.clickBestOption, - onClose: () => setState(() {}), + return SizeTransition( + sizeFactor: _animation, + axis: Axis.vertical, + axisAlignment: -1.0, + child: CompositedTransformTarget( + link: widget.choreographer.itBarLinkAndKey.link, + child: Container( + key: widget.choreographer.itBarLinkAndKey.key, + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : Colors.black, + ), + padding: const EdgeInsets.fromLTRB(0, 3, 3, 3), + child: Stack( + alignment: Alignment.topCenter, + children: [ + SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (!itController.isEditingSourceText && + itController.sourceText != null) + SizedBox(width: iconDimension * 3), + if (!itController.isEditingSourceText) + Expanded( + child: itController.sourceText != null + ? Text( + itController.sourceText!, + textAlign: TextAlign.center, + ) + : const LinearProgressIndicator(), + ), + if (itController.isEditingSourceText) + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 20, + right: 10, + top: 10, ), - if (showTranslationsChoicesTooltip) - InlineTooltip( - instructionsEnum: - InstructionsEnum.translationChoices, - onClose: () => setState(() {}), - ), - IntrinsicHeight( - child: Container( - constraints: - const BoxConstraints(minHeight: 80), - width: double.infinity, - padding: - const EdgeInsets.symmetric(horizontal: 4.0), - child: Center( - child: itController - .choreographer.errorService.isError - ? ITError( - error: itController.choreographer - .errorService.error!, - controller: itController, - ) - : itController.showChoiceFeedback - ? ChoiceFeedbackText( - controller: itController, - ) - : itController.isTranslationDone - ? TranslationFeedback( - controller: itController, - ) - : ITChoices( - controller: itController, - ), + child: TextField( + controller: TextEditingController( + text: itController.sourceText, + ), + autofocus: true, + enableSuggestions: false, + maxLines: null, + textInputAction: TextInputAction.send, + onSubmitted: + itController.onEditSourceTextSubmit, + obscureText: false, + decoration: const InputDecoration( + border: OutlineInputBorder(), ), ), ), - ], + ), + if (!itController.isEditingSourceText && + itController.sourceText != null) + SizedBox( + width: iconDimension, + height: iconDimension, + child: IconButton( + iconSize: iconSize, + color: Theme.of(context).colorScheme.primary, + onPressed: () { + if (itController.nextITStep != null) { + itController.setIsEditingSourceText(true); + } + }, + icon: const Icon(Icons.edit_outlined), + // iconSize: 20, + ), + ), + if (!itController.isEditingSourceText) + SizedBox( + width: iconDimension, + height: iconDimension, + child: IconButton( + iconSize: iconSize, + color: Theme.of(context).colorScheme.primary, + icon: const Icon(Icons.settings_outlined), + onPressed: () => showDialog( + context: context, + builder: (c) => const SettingsLearning(), + ), + ), + ), + SizedBox( + width: iconDimension, + height: iconDimension, + child: IconButton( + iconSize: iconSize, + color: Theme.of(context).colorScheme.primary, + icon: const Icon(Icons.close_outlined), + onPressed: () { + itController.isEditingSourceText + ? itController.setIsEditingSourceText(false) + : itController.closeIT(); + }, + ), + ), + ], + ), + const SizedBox(height: 8.0), + if (showITInstructionsTooltip) + const InlineTooltip( + instructionsEnum: InstructionsEnum.clickBestOption, + ), + if (showTranslationsChoicesTooltip) + const InlineTooltip( + instructionsEnum: InstructionsEnum.translationChoices, + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + constraints: const BoxConstraints(minHeight: 80), + child: AnimatedSize( + duration: itController.animationSpeed, + child: Center( + child: itController.choreographer.errorService.isError + ? ITError( + error: itController + .choreographer.errorService.error!, + controller: itController, + ) + : itController.showChoiceFeedback + ? ChoiceFeedbackText( + controller: itController, + ) + : itController.isTranslationDone + ? TranslationFeedback( + controller: itController, + ) + : ITChoices(controller: itController), ), ), - Positioned( - top: 0.0, - right: 0.0, - child: - ITCloseButton(choreographer: widget.choreographer), - ), - ], - ), + ), + ], ), ), - // ), - ), + const Positioned( + top: 60, + child: PointsGainedAnimation( + origin: AnalyticsUpdateOrigin.it, + ), + ), + ], + ), + ), + ), ); } } @@ -228,70 +284,6 @@ class ChoiceFeedbackText extends StatelessWidget { } } -class OriginalText extends StatelessWidget { - const OriginalText({ - super.key, - required this.controller, - }); - - final ITController controller; - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints(minHeight: 50), - padding: const EdgeInsets.only(left: 60.0, right: 40.0), - decoration: const BoxDecoration( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(AppConfig.borderRadius), - topRight: Radius.circular(AppConfig.borderRadius), - ), - ), - child: Row( - //PTODO - does this already update after reset or we need to setState? - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!controller.isEditingSourceText) - controller.sourceText != null - ? Flexible(child: Text(controller.sourceText!)) - : const LinearProgressIndicator(), - const SizedBox(width: 4), - if (controller.isEditingSourceText) - Expanded( - child: TextField( - controller: TextEditingController(text: controller.sourceText), - autofocus: true, - enableSuggestions: false, - maxLines: null, - textInputAction: TextInputAction.send, - onSubmitted: controller.onEditSourceTextSubmit, - obscureText: false, - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - ), - ), - if (!controller.isEditingSourceText && controller.sourceText != null) - AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: controller.nextITStep != null ? 0.7 : 0.0, - child: IconButton( - onPressed: () => { - if (controller.nextITStep != null) - { - controller.setIsEditingSourceText(true), - }, - }, - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ), - ], - ), - ); - } -} - class ITChoices extends StatelessWidget { const ITChoices({ super.key, diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index f5e358a31..7a4c33f13 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -40,26 +40,29 @@ class ChoreographerSendButtonState extends State { @override Widget build(BuildContext context) { - return widget.controller.choreographer.isFetching && - widget.controller.choreographer.isAutoIGCEnabled - ? Container( - height: 56, - width: 56, - padding: const EdgeInsets.all(13), - child: const CircularProgressIndicator(), - ) - : Container( - height: 56, - alignment: Alignment.center, - child: IconButton( - icon: const Icon(Icons.send_outlined), - color: widget.controller.choreographer.assistanceState - .stateColor(context), - onPressed: () { - widget.controller.choreographer.send(context); - }, - tooltip: L10n.of(context)!.send, - ), - ); + return + // widget.controller.choreographer.isFetching && + // widget.controller.choreographer.isAutoIGCEnabled + // ? Container( + // height: 56, + // width: 56, + // padding: const EdgeInsets.all(13), + // child: const CircularProgressIndicator(), + // ) + // : + Container( + height: 56, + alignment: Alignment.center, + child: IconButton( + icon: const Icon(Icons.send_outlined), + color: + widget.controller.choreographer.assistanceState.stateColor(context), + onPressed: () { + widget.controller.choreographer.incrementTimesClicked(); + widget.controller.choreographer.send(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 f9782e763..9045757e0 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -1,12 +1,9 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/enum/assistance_state_enum.dart'; import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../../../pages/chat/chat.dart'; @@ -27,8 +24,8 @@ class StartIGCButtonState extends State AssistanceState get assistanceState => widget.controller.choreographer.assistanceState; AnimationController? _controller; - StreamSubscription? choreoListener; - AssistanceState? prevState; + StreamSubscription? _choreoListener; + AssistanceState? _prevState; @override void initState() { @@ -36,117 +33,96 @@ class StartIGCButtonState extends State vsync: this, duration: const Duration(seconds: 2), ); - choreoListener = widget.controller.choreographer.stateListener.stream - .listen(updateSpinnerState); + _choreoListener = widget.controller.choreographer.stateListener.stream + .listen(_updateSpinnerState); super.initState(); } @override void dispose() { _controller?.dispose(); - choreoListener?.cancel(); + _choreoListener?.cancel(); super.dispose(); } - void updateSpinnerState(_) { - if (prevState != AssistanceState.fetching && + void _updateSpinnerState(_) { + if (_prevState != AssistanceState.fetching && assistanceState == AssistanceState.fetching) { _controller?.repeat(); - } else if (prevState == AssistanceState.fetching && + } else if (_prevState == AssistanceState.fetching && assistanceState != AssistanceState.fetching) { _controller?.reset(); } if (mounted) { - setState(() => prevState = assistanceState); + setState(() => _prevState = assistanceState); } } - bool get itEnabled => widget.controller.choreographer.itEnabled; - bool get igcEnabled => widget.controller.choreographer.igcEnabled; + void _showFirstMatch() { + final igcData = widget.controller.choreographer.igc.igcTextData; + if (igcData != null && igcData.matches.isNotEmpty) { + widget.controller.choreographer.igc.showFirstMatch(context); + } + } - SubscriptionStatus get subscriptionStatus => widget - .controller.pangeaController.subscriptionController.subscriptionStatus; - - bool get grammarCorrectionEnabled => - (itEnabled || igcEnabled) && - subscriptionStatus == SubscriptionStatus.subscribed; + Future _onTap() async { + switch (assistanceState) { + case AssistanceState.notFetched: + await widget.controller.choreographer.getLanguageHelp( + onlyTokensAndLanguageDetection: false, + manual: true, + ); + _showFirstMatch(); + return; + case AssistanceState.fetched: + _showFirstMatch(); + return; + case AssistanceState.complete: + case AssistanceState.fetching: + case AssistanceState.noMessage: + return; + } + } @override Widget build(BuildContext context) { - if (!grammarCorrectionEnabled || - widget.controller.choreographer.isAutoIGCEnabled || - widget.controller.choreographer.choreoMode == ChoreoMode.it) { - return const SizedBox.shrink(); - } - - final Widget icon = Icon( - Icons.autorenew_rounded, - size: 46, - color: assistanceState.stateColor(context), - ); - return SizedBox( - height: 50, - width: 50, child: InkWell( + onTap: _onTap, customBorder: const CircleBorder(), onLongPress: () => pLanguageDialog(context, () {}), - child: FloatingActionButton( - tooltip: assistanceState.tooltip( - L10n.of(context)!, - ), - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - disabledElevation: 0, - shape: const CircleBorder(), - onPressed: () { - if (assistanceState != AssistanceState.fetching) { - widget.controller.choreographer - .getLanguageHelp( - onlyTokensAndLanguageDetection: false, - manual: true, - ) - .then((_) { - if (widget.controller.choreographer.igc.igcTextData != null && - widget.controller.choreographer.igc.igcTextData!.matches - .isNotEmpty) { - widget.controller.choreographer.igc.showFirstMatch(context); - } - }); - } - }, - child: Stack( - alignment: Alignment.center, - children: [ - _controller != null - ? RotationTransition( - turns: Tween(begin: 0.0, end: math.pi * 2) - .animate(_controller!), - child: icon, - ) - : icon, - Container( - width: 26, - height: 26, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).scaffoldBackgroundColor, - ), + child: Stack( + alignment: Alignment.center, + children: [ + _controller != null + ? RotationTransition( + turns: Tween(begin: 0.0, end: math.pi * 2) + .animate(_controller!), + child: Icon( + size: 36, + Icons.autorenew_rounded, + color: assistanceState.stateColor(context), + ), + ) + : Icon( + size: 36, + Icons.autorenew_rounded, + color: assistanceState.stateColor(context), + ), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), - Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: assistanceState.stateColor(context), - ), - ), - Icon( - size: 16, - Icons.check, - color: Theme.of(context).scaffoldBackgroundColor, - ), - ], - ), + ), + Icon( + size: 16, + Icons.check, + color: assistanceState.stateColor(context), + ), + ], ), ), ); diff --git a/lib/pangea/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart index f96cc05bb..88681a00d 100644 --- a/lib/pangea/utils/inline_tooltip.dart +++ b/lib/pangea/utils/inline_tooltip.dart @@ -1,75 +1,112 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -class InlineTooltip extends StatelessWidget { +class InlineTooltip extends StatefulWidget { final InstructionsEnum instructionsEnum; - final VoidCallback onClose; const InlineTooltip({ super.key, required this.instructionsEnum, - required this.onClose, }); @override - Widget build(BuildContext context) { - if (instructionsEnum.toggledOff()) { - return const SizedBox(); - } + InlineTooltipState createState() => InlineTooltipState(); +} - return Padding( - padding: const EdgeInsets.all(8.0), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - color: Theme.of(context).colorScheme.primary.withAlpha(20), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - // Lightbulb icon on the left - Icon( - Icons.lightbulb, - size: 20, - color: Theme.of(context).colorScheme.onSurface, - ), - const SizedBox(width: 8), - // Text in the middle - Flexible( - child: Center( - child: Text( - instructionsEnum.body(L10n.of(context)!), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - height: 1.5, - ), - textAlign: TextAlign.left, - ), - ), - ), - // Close button on the right - IconButton( - constraints: const BoxConstraints(), - icon: Icon( - Icons.close_outlined, - size: 20, +class InlineTooltipState extends State + with SingleTickerProviderStateMixin { + bool _isToggledOff = true; + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _isToggledOff = widget.instructionsEnum.toggledOff(); + + // Initialize AnimationController and Animation + _controller = AnimationController( + duration: FluffyThemes.animationDuration, + vsync: this, + ); + + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ); + + // Start in correct state + if (!_isToggledOff) _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _closeTooltip() { + MatrixState.pangeaController.instructions.setToggledOff( + widget.instructionsEnum, + true, + ); + setState(() { + _isToggledOff = true; + _controller.reverse(); + }); + } + + @override + Widget build(BuildContext context) { + return SizeTransition( + sizeFactor: _animation, + axisAlignment: -1.0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + color: Theme.of(context).colorScheme.primary.withAlpha(20), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.lightbulb, + size: _isToggledOff ? 0 : 20, color: Theme.of(context).colorScheme.onSurface, ), - onPressed: () { - MatrixState.pangeaController.instructions.setToggledOff( - instructionsEnum, - true, - ); - onClose(); - }, - ), - ], + const SizedBox(width: 8), + Flexible( + child: Center( + child: Text( + widget.instructionsEnum.body(L10n.of(context)!), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + height: 1.5, + ), + textAlign: TextAlign.left, + ), + ), + ), + IconButton( + constraints: const BoxConstraints(), + icon: Icon( + Icons.close_outlined, + size: _isToggledOff ? 0 : 20, + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: _closeTooltip, + ), + ], + ), ), ), ), diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index cf61c3d49..c55a0942c 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -187,9 +187,8 @@ class MessageSpeechToTextCardState extends State { ), ], ), - InlineTooltip( + const InlineTooltip( instructionsEnum: InstructionsEnum.speechToText, - onClose: () => setState(() => {}), ), ], ), diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 0f95a3e55..7e33f9b07 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -156,15 +156,26 @@ class MessageTranslationCardState extends State { style: BotStyle.text(context), textAlign: TextAlign.center, ), - if (notGoingToTranslate && widget.selection == null) - InlineTooltip( - instructionsEnum: InstructionsEnum.l1Translation, - onClose: () => setState(() {}), + if (notGoingToTranslate && + widget.selection == null && + !InstructionsEnum.l1Translation.toggledOff()) + const Row( + mainAxisSize: MainAxisSize.min, + children: [ + InlineTooltip( + instructionsEnum: InstructionsEnum.l1Translation, + ), + ], ), - if (widget.selection != null) - InlineTooltip( - instructionsEnum: InstructionsEnum.clickAgainToDeselect, - onClose: () => setState(() {}), + if (widget.selection != null && + !InstructionsEnum.clickAgainToDeselect.toggledOff()) + const Row( + mainAxisSize: MainAxisSize.min, + children: [ + InlineTooltip( + instructionsEnum: InstructionsEnum.clickAgainToDeselect, + ), + ], ), ], ), diff --git a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart index 3877038d8..dfefb0276 100644 --- a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart +++ b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart @@ -67,25 +67,19 @@ class _StarAnimationWidgetState extends State } class GamifiedTextWidget extends StatelessWidget { - final VoidCallback onCloseTooltip; - - const GamifiedTextWidget({ - required this.onCloseTooltip, - super.key, - }); + const GamifiedTextWidget({super.key}); @override Widget build(BuildContext context) { - return SizedBox( + return const SizedBox( width: AppConfig.toolbarMinWidth, child: Padding( - padding: const EdgeInsets.fromLTRB(16, 20, 16, 16), + padding: EdgeInsets.fromLTRB(16, 20, 16, 16), child: Column( children: [ - const StarAnimationWidget(), + StarAnimationWidget(), InlineTooltip( instructionsEnum: InstructionsEnum.unlockedLanguageTools, - onClose: onCloseTooltip, ), ], ), diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 1aee0602a..b82ff2b1a 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -320,14 +320,10 @@ class PracticeActivityCardState extends State { } } - void _closeTooltip() => setState(() {}); - @override Widget build(BuildContext context) { if (!fetchingActivity && currentActivity == null) { - return GamifiedTextWidget( - onCloseTooltip: _closeTooltip, - ); + return const GamifiedTextWidget(); } return Stack(