diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index d897abb6a..88a28ea97 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -33,11 +33,9 @@ abstract class AppConfig { static const double readingAssistanceInputBarHeight = 140.0; static const double reactionsPickerHeight = 48.0; static const double chatInputRowOverlayPadding = 8.0; - static const double tokenModeInputBarHeight = reactionsPickerHeight + - toolbarButtonsHeight + - (chatInputRowOverlayPadding * 2) + - toolbarSpacing; - static const double messageModeInputBarHeight = + static const double selectModeInputBarHeight = + reactionsPickerHeight + (chatInputRowOverlayPadding * 2) + toolbarSpacing; + static const double practiceModeInputBarHeight = readingAssistanceInputBarHeight + toolbarButtonsHeight + (chatInputRowOverlayPadding * 2) + diff --git a/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart b/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart index f0ef61617..3d684b4a4 100644 --- a/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart +++ b/lib/pangea/toolbar/enums/reading_assistance_mode_enum.dart @@ -1,9 +1,9 @@ enum ReadingAssistanceMode { /// Overlay message is directly over the original message - tokenMode, + selectMode, /// Overlay message is centered and larger than the original message - messageMode, + practiceMode, /// Overlay message is moving to the center of the screen transitionMode, diff --git a/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart b/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart index 4dc09068e..8f7b0ec62 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/overlay_footer.dart @@ -6,7 +6,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.dart'; import 'package:fluffychat/pangea/toolbar/enums/reading_assistance_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; -import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button_column.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/practice_mode_buttons.dart'; class OverlayFooter extends StatelessWidget { final ChatController controller; @@ -33,16 +33,16 @@ class OverlayFooter extends StatelessWidget { left: bottomSheetPadding, right: bottomSheetPadding, ), - height: readingAssistanceMode == ReadingAssistanceMode.messageMode || + height: readingAssistanceMode == ReadingAssistanceMode.practiceMode || readingAssistanceMode == ReadingAssistanceMode.transitionMode - ? AppConfig.messageModeInputBarHeight - : AppConfig.tokenModeInputBarHeight, + ? AppConfig.practiceModeInputBarHeight + : AppConfig.selectModeInputBarHeight, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ if (showToolbarButtons) - ToolbarButtonRow(overlayController: overlayController), + PracticeModeButtons(overlayController: overlayController), Material( clipBehavior: Clip.hardEdge, color: Colors.transparent, diff --git a/lib/pangea/toolbar/utils/token_rendering_util.dart b/lib/pangea/toolbar/utils/token_rendering_util.dart index 1ce3f8109..d6d2cc27f 100644 --- a/lib/pangea/toolbar/utils/token_rendering_util.dart +++ b/lib/pangea/toolbar/utils/token_rendering_util.dart @@ -76,11 +76,11 @@ class TokenRenderingUtil { } switch (readingAssistanceMode!) { - case ReadingAssistanceMode.tokenMode: + case ReadingAssistanceMode.selectMode: return isTransitionAnimation; case ReadingAssistanceMode.transitionMode: return false; - case ReadingAssistanceMode.messageMode: + case ReadingAssistanceMode.practiceMode: return !isTransitionAnimation; } } diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 12280a52b..0372c4115 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -90,6 +90,8 @@ class MessageOverlayController extends State final GlobalKey wordZoomKey = GlobalKey(); ReadingAssistanceMode? readingAssistanceMode; // default mode + bool showTranslation = false; + String? translationText; double maxWidth = AppConfig.toolbarMinWidth; @@ -289,7 +291,7 @@ class MessageOverlayController extends State } /// Update [selectedSpan] - void updateSelectedSpan(PangeaTokenText selectedSpan, [bool force = false]) { + void updateSelectedSpan(PangeaTokenText? selectedSpan, [bool force = false]) { if (selectedMorph != null) { selectedMorph = null; } @@ -407,7 +409,7 @@ class MessageOverlayController extends State pangeaMessageEvent!.event.messageType == MessageTypes.Text; bool get hideWordCardContent => - readingAssistanceMode == ReadingAssistanceMode.messageMode; + readingAssistanceMode == ReadingAssistanceMode.practiceMode; bool get isPracticeComplete => isTranslationUnlocked; @@ -575,6 +577,18 @@ class MessageOverlayController extends State } } + void setShowTranslation(bool show, String? translation) { + if (showTranslation == show) return; + if (show && translation == null) return; + + if (mounted) { + setState(() { + showTranslation = show; + translationText = show ? translation : null; + }); + } + } + ///////////////////////////////////// /// Build ///////////////////////////////////// diff --git a/lib/pangea/toolbar/widgets/message_selection_positioner.dart b/lib/pangea/toolbar/widgets/message_selection_positioner.dart index 9a9a39ae4..a2e29cca2 100644 --- a/lib/pangea/toolbar/widgets/message_selection_positioner.dart +++ b/lib/pangea/toolbar/widgets/message_selection_positioner.dart @@ -21,6 +21,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/measure_render_box.dart'; import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart'; import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/select_mode_buttons.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -112,14 +113,14 @@ class MessageSelectionPositionerState extends State setState(() { _currentOffset = Offset( _ownMessage ? _messageRightOffset : _messageLeftOffset, - _originalMessageBottomOffset - _reactionsHeight, + _originalMessageBottomOffset - + _reactionsHeight - + _selectionButtonsHeight, ); }); _setReadingAssistanceMode( - widget.initialSelectedToken == null - ? ReadingAssistanceMode.messageMode - : ReadingAssistanceMode.tokenMode, + ReadingAssistanceMode.selectMode, ); }); } @@ -129,9 +130,6 @@ class MessageSelectionPositionerState extends State super.didUpdateWidget(oldWidget); final mode = widget.overlayController.toolbarMode; if (mode != _currentMode) { - if (_currentMode == MessageMode.noneSelected) { - _setReadingAssistanceMode(ReadingAssistanceMode.messageMode); - } setState(() => _currentMode = mode); } } @@ -151,12 +149,12 @@ class MessageSelectionPositionerState extends State _centeredMessageOffset = Offset( offset.dx - _columnWidth - _horizontalPadding - 2.0, _mediaQuery!.size.height - - offset.dy - + (offset.dy - + ((AppConfig.practiceModeInputBarHeight - + AppConfig.selectModeInputBarHeight) * + 0.75)) - renderBox.size.height - - _reactionsHeight + - ((AppConfig.messageModeInputBarHeight - - AppConfig.tokenModeInputBarHeight) * - 0.75), + _reactionsHeight, ); setState(() {}); @@ -182,19 +180,19 @@ class MessageSelectionPositionerState extends State await _centeredMessageCompleter.future; - if (mode == ReadingAssistanceMode.messageMode) { + if (mode == ReadingAssistanceMode.practiceMode) { setState( () => widget.overlayController.readingAssistanceMode = ReadingAssistanceMode.transitionMode, ); - } else if (mode == ReadingAssistanceMode.tokenMode) { + } else if (mode == ReadingAssistanceMode.selectMode) { setState( () => widget.overlayController.readingAssistanceMode = - ReadingAssistanceMode.tokenMode, + ReadingAssistanceMode.selectMode, ); } - if (mode == ReadingAssistanceMode.tokenMode) { + if (mode == ReadingAssistanceMode.selectMode) { _overlayOffsetAnimation = Tween( begin: _currentOffset, end: _adjustedOriginalMessageOffset, @@ -208,7 +206,7 @@ class MessageSelectionPositionerState extends State setState(() => _currentOffset = _overlayOffsetAnimation?.value); } }); - } else if (mode == ReadingAssistanceMode.messageMode) { + } else if (mode == ReadingAssistanceMode.practiceMode) { _overlayOffsetAnimation = Tween( begin: _currentOffset, end: _centeredMessageOffset!, @@ -266,10 +264,10 @@ class MessageSelectionPositionerState extends State widget.overlayController.readingAssistanceMode; double get _inputBarSize => - _readingAssistanceMode == ReadingAssistanceMode.messageMode || + _readingAssistanceMode == ReadingAssistanceMode.practiceMode || _readingAssistanceMode == ReadingAssistanceMode.transitionMode - ? AppConfig.messageModeInputBarHeight - : AppConfig.tokenModeInputBarHeight; + ? AppConfig.practiceModeInputBarHeight + : AppConfig.selectModeInputBarHeight; bool get _showDetails => (Matrix.of(context).store.getBool(SettingKeys.displayChatDetailsColumn) ?? @@ -399,7 +397,10 @@ class MessageSelectionPositionerState extends State } final topOffset = _originalMessageOffset.dy; - final bottomOffset = _originalMessageBottomOffset; + final bottomOffset = _originalMessageBottomOffset - + _reactionsHeight - + _selectionButtonsHeight; + final hasHeaderOverflow = topOffset < (_headerHeight + AppConfig.toolbarSpacing); final hasFooterOverflow = @@ -408,7 +409,7 @@ class MessageSelectionPositionerState extends State if (!hasHeaderOverflow && !hasFooterOverflow) { return Offset( _ownMessage ? _messageRightOffset : _messageLeftOffset, - _originalMessageBottomOffset - _reactionsHeight, + bottomOffset, ); } @@ -427,13 +428,9 @@ class MessageSelectionPositionerState extends State newBottomOffset, ); } else { - final difference = - bottomOffset - (_footerHeight + AppConfig.toolbarSpacing); return Offset( _ownMessage ? _messageRightOffset : _messageLeftOffset, - _mediaQuery!.size.height - - (_originalMessageOffset.dy + difference) - - _originalMessageSize.height, + _footerHeight + AppConfig.toolbarSpacing, ); } } @@ -487,10 +484,20 @@ class MessageSelectionPositionerState extends State // measurement for items in the toolbar - bool get showToolbarButtons => + bool get showPracticeButtons => (widget.pangeaMessageEvent?.shouldShowToolbar ?? false) && widget.pangeaMessageEvent?.event.messageType == MessageTypes.Text && - (widget.pangeaMessageEvent?.messageDisplayLangIsL2 ?? false); + (widget.pangeaMessageEvent?.messageDisplayLangIsL2 ?? false) && + widget.overlayController.readingAssistanceMode == + ReadingAssistanceMode.practiceMode; + + bool get showSelectionButtons => + widget.overlayController.readingAssistanceMode == + ReadingAssistanceMode.selectMode; + + double get _selectionButtonsHeight { + return AppConfig.toolbarButtonsHeight; + } bool get _hasReactions { final reactionsEvents = widget.event.aggregatedEvents( @@ -507,10 +514,10 @@ class MessageSelectionPositionerState extends State double get _readingAssistanceModeOpacity { switch (_readingAssistanceMode) { - case ReadingAssistanceMode.messageMode: + case ReadingAssistanceMode.practiceMode: case ReadingAssistanceMode.transitionMode: return 0.8; - case ReadingAssistanceMode.tokenMode: + case ReadingAssistanceMode.selectMode: case null: return 0.4; } @@ -564,7 +571,7 @@ class MessageSelectionPositionerState extends State ), Opacity( opacity: _readingAssistanceMode == - ReadingAssistanceMode.messageMode + ReadingAssistanceMode.practiceMode ? 1.0 : 0.0, child: OverlayCenterContent( @@ -601,7 +608,7 @@ class MessageSelectionPositionerState extends State OverlayFooter( controller: widget.chatController, overlayController: widget.overlayController, - showToolbarButtons: showToolbarButtons, + showToolbarButtons: showPracticeButtons, readingAssistanceMode: _readingAssistanceMode, ), SizedBox(height: _mediaQuery?.padding.bottom ?? 0), @@ -616,7 +623,9 @@ class MessageSelectionPositionerState extends State ), ], ), - if (_readingAssistanceMode != ReadingAssistanceMode.messageMode) + if (_readingAssistanceMode != + ReadingAssistanceMode.practiceMode && + _readingAssistanceMode != null) AnimatedBuilder( animation: _overlayOffsetAnimation ?? _animationController, builder: (context, child) { @@ -630,30 +639,50 @@ class MessageSelectionPositionerState extends State _messageRightOffset : null, bottom: (_overlayOffsetAnimation?.value)?.dy ?? - _originalMessageBottomOffset - _reactionsHeight, - child: OverlayCenterContent( - event: widget.event, - messageHeight: _originalMessageSize.height, - messageWidth: _originalMessageSize.width, - maxWidth: widget.overlayController.maxWidth, - overlayController: widget.overlayController, - chatController: widget.chatController, - pangeaMessageEvent: widget.pangeaMessageEvent, - nextEvent: widget.nextEvent, - prevEvent: widget.prevEvent, - hasReactions: _hasReactions, - sizeAnimation: _messageSizeAnimation, - isTransitionAnimation: true, - maxHeight: _mediaQuery!.size.height - - _headerHeight - - _footerHeight - - AppConfig.toolbarSpacing * 2, - readingAssistanceMode: _readingAssistanceMode, + _originalMessageBottomOffset - + _reactionsHeight - + _selectionButtonsHeight, + child: Column( + crossAxisAlignment: _ownMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + OverlayCenterContent( + event: widget.event, + messageHeight: _originalMessageSize.height, + messageWidth: _originalMessageSize.width, + maxWidth: widget.overlayController.maxWidth, + overlayController: widget.overlayController, + chatController: widget.chatController, + pangeaMessageEvent: widget.pangeaMessageEvent, + nextEvent: widget.nextEvent, + prevEvent: widget.prevEvent, + hasReactions: _hasReactions, + sizeAnimation: _messageSizeAnimation, + isTransitionAnimation: true, + maxHeight: _mediaQuery!.size.height - + _headerHeight - + _footerHeight - + AppConfig.toolbarSpacing * 2, + readingAssistanceMode: _readingAssistanceMode, + ), + if (showSelectionButtons) + SelectModeButtons( + overlayController: widget.overlayController, + lauchPractice: () { + _setReadingAssistanceMode( + ReadingAssistanceMode.practiceMode, + ); + widget.overlayController + .updateSelectedSpan(null); + }, + ), + ], ), ); }, ), - if (showToolbarButtons) + if (showPracticeButtons) Positioned( top: 0, child: IgnorePointer( diff --git a/lib/pangea/toolbar/widgets/overlay_center_content.dart b/lib/pangea/toolbar/widgets/overlay_center_content.dart index d618848a8..4908fc1c3 100644 --- a/lib/pangea/toolbar/widgets/overlay_center_content.dart +++ b/lib/pangea/toolbar/widgets/overlay_center_content.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/events/message_reactions.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; @@ -53,9 +55,11 @@ class OverlayCenterContent extends StatelessWidget { @override Widget build(BuildContext context) { + final showTranslation = overlayController.showTranslation && + overlayController.translationText != null; return IgnorePointer( ignoring: !isTransitionAnimation && - readingAssistanceMode != ReadingAssistanceMode.messageMode, + readingAssistanceMode != ReadingAssistanceMode.practiceMode, child: Container( constraints: BoxConstraints(maxWidth: maxWidth), child: Material( @@ -66,31 +70,80 @@ class OverlayCenterContent extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - MeasureRenderBox( - onChange: onChangeMessageSize, - child: OverlayMessage( - event, - pangeaMessageEvent: pangeaMessageEvent, - immersionMode: chatController.choreographer.immersionMode, - controller: chatController, - overlayController: overlayController, - nextEvent: nextEvent, - prevEvent: prevEvent, - timeline: chatController.timeline!, - sizeAnimation: sizeAnimation, - // there's a split seconds between when the transition animation starts and - // when the sizeAnimation is set when the original dimensions need to be enforced - messageWidth: (sizeAnimation == null && isTransitionAnimation) - ? messageWidth - : null, - messageHeight: - (sizeAnimation == null && isTransitionAnimation) - ? messageHeight - : null, - maxHeight: maxHeight, - isTransitionAnimation: isTransitionAnimation, - readingAssistanceMode: readingAssistanceMode, - ), + Stack( + alignment: Alignment.topCenter, + children: [ + if (overlayController.readingAssistanceMode == + ReadingAssistanceMode.selectMode) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: Theme.of(context).colorScheme.primaryContainer, + ), + padding: EdgeInsets.all( + showTranslation ? 8.0 : 0.0, + ), + constraints: BoxConstraints( + maxWidth: messageWidth ?? maxWidth, + ), + child: Column( + children: [ + AnimatedContainer( + duration: FluffyThemes.animationDuration, + height: showTranslation ? messageHeight : 0, + width: showTranslation ? messageWidth : 0, + ), + AnimatedSize( + duration: FluffyThemes.animationDuration, + child: SizedBox( + width: messageWidth, + child: showTranslation + ? Text( + overlayController.translationText!, + style: AppConfig.messageTextStyle( + event, + Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + textAlign: TextAlign.center, + ) + : const SizedBox(), + ), + ), + ], + ), + ), + MeasureRenderBox( + onChange: onChangeMessageSize, + child: OverlayMessage( + event, + pangeaMessageEvent: pangeaMessageEvent, + immersionMode: chatController.choreographer.immersionMode, + controller: chatController, + overlayController: overlayController, + nextEvent: nextEvent, + prevEvent: prevEvent, + timeline: chatController.timeline!, + sizeAnimation: sizeAnimation, + // there's a split seconds between when the transition animation starts and + // when the sizeAnimation is set when the original dimensions need to be enforced + messageWidth: + (sizeAnimation == null && isTransitionAnimation) + ? messageWidth + : null, + messageHeight: + (sizeAnimation == null && isTransitionAnimation) + ? messageHeight + : null, + maxHeight: maxHeight, + isTransitionAnimation: isTransitionAnimation, + readingAssistanceMode: readingAssistanceMode, + ), + ), + ], ), if (hasReactions) Padding( diff --git a/lib/pangea/toolbar/widgets/toolbar_button_column.dart b/lib/pangea/toolbar/widgets/practice_mode_buttons.dart similarity index 83% rename from lib/pangea/toolbar/widgets/toolbar_button_column.dart rename to lib/pangea/toolbar/widgets/practice_mode_buttons.dart index e3d0f63a6..77ec386ec 100644 --- a/lib/pangea/toolbar/widgets/toolbar_button_column.dart +++ b/lib/pangea/toolbar/widgets/practice_mode_buttons.dart @@ -4,10 +4,10 @@ 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 ToolbarButtonRow extends StatelessWidget { +class PracticeModeButtons extends StatelessWidget { final MessageOverlayController overlayController; - const ToolbarButtonRow({ + const PracticeModeButtons({ required this.overlayController, super.key, }); @@ -48,17 +48,6 @@ class ToolbarButtonRow extends StatelessWidget { buttonSize: buttonSize, ), ), - Container( - width: buttonSize + 4, - height: buttonSize + 4, - alignment: Alignment.center, - child: ToolbarButton( - mode: MessageMode.messageTranslation, - overlayController: overlayController, - onPressed: overlayController.updateToolbarMode, - buttonSize: buttonSize, - ), - ), Container( width: buttonSize + 4, height: buttonSize + 4, diff --git a/lib/pangea/toolbar/widgets/select_mode_buttons.dart b/lib/pangea/toolbar/widgets/select_mode_buttons.dart new file mode 100644 index 000000000..3c0f3cc82 --- /dev/null +++ b/lib/pangea/toolbar/widgets/select_mode_buttons.dart @@ -0,0 +1,312 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:matrix/matrix.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat/events/audio_player.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart'; +import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +enum SelectMode { + audio(Icons.volume_up), + translate(Icons.translate), + practice(Symbols.fitness_center); + + final IconData icon; + const SelectMode(this.icon); + + String tooltip(BuildContext context) { + final l10n = L10n.of(context); + switch (this) { + case SelectMode.audio: + return l10n.playAudio; + case SelectMode.translate: + return l10n.translationTooltip; + case SelectMode.practice: + return l10n.practice; + } + } +} + +class SelectModeButtons extends StatefulWidget { + final VoidCallback lauchPractice; + final MessageOverlayController overlayController; + + const SelectModeButtons({ + required this.lauchPractice, + required this.overlayController, + super.key, + }); + + @override + State createState() => SelectModeButtonsState(); +} + +class SelectModeButtonsState extends State { + static const double iconWidth = 36.0; + static const double buttonSize = 40.0; + + SelectMode? _selectedMode; + + final AudioPlayer _audioPlayer = AudioPlayer(); + bool _isLoadingAudio = false; + PangeaAudioFile? _audioFile; + + StreamSubscription? _onPlayerStateChanged; + StreamSubscription? _onAudioPositionChanged; + + bool _isLoadingTranslation = false; + PangeaRepresentation? _repEvent; + + @override + void initState() { + super.initState(); + _onPlayerStateChanged = _audioPlayer.playerStateStream.listen((state) { + if (state.processingState == ProcessingState.completed) { + _audioPlayer.stop(); + _audioPlayer.seek(null); + } + setState(() {}); + }); + _onAudioPositionChanged ??= _audioPlayer.positionStream.listen((state) { + if (_audioFile != null) { + widget.overlayController.highlightCurrentText( + state.inMilliseconds, + _audioFile!.tokens, + ); + } + }); + } + + @override + void dispose() { + _audioPlayer.dispose(); + _onPlayerStateChanged?.cancel(); + _onAudioPositionChanged?.cancel(); + super.dispose(); + } + + PangeaMessageEvent? get messageEvent => + widget.overlayController.pangeaMessageEvent; + + String? get l1Code => + MatrixState.pangeaController.languageController.activeL1Code(); + String? get l2Code => + MatrixState.pangeaController.languageController.activeL2Code(); + + Future _updateMode(SelectMode mode) async { + widget.overlayController.updateSelectedSpan(null); + + if (_selectedMode == SelectMode.translate) { + widget.overlayController.setShowTranslation(false, null); + await Future.delayed(FluffyThemes.animationDuration); + } + + setState( + () => _selectedMode = + _selectedMode == mode && mode != SelectMode.audio ? null : mode, + ); + + if (_selectedMode == SelectMode.audio) { + _playAudio(); + return; + } else { + _audioPlayer.stop(); + _audioPlayer.seek(null); + } + + if (_selectedMode == SelectMode.practice) { + widget.lauchPractice(); + return; + } + + if (_selectedMode == SelectMode.translate) { + await _loadTranslation(); + if (_repEvent == null) return; + widget.overlayController.setShowTranslation( + true, + _repEvent!.text, + ); + } + } + + Future _fetchAudio() async { + if (!mounted || messageEvent == null) return; + setState(() => _isLoadingAudio = true); + + try { + final String langCode = messageEvent!.messageDisplayLangCode; + final Event? localEvent = messageEvent!.getTextToSpeechLocal( + langCode, + messageEvent!.messageDisplayText, + ); + + if (localEvent != null) { + _audioFile = await localEvent.getPangeaAudioFile(); + } else { + _audioFile = await messageEvent!.getMatrixAudioFile( + langCode, + ); + } + + if (mounted) setState(() => _isLoadingAudio = false); + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError( + e: e, + s: s, + m: 'something wrong getting audio in MessageAudioCardState', + data: { + 'widget.messageEvent.messageDisplayLangCode': + messageEvent?.messageDisplayLangCode, + }, + ); + if (mounted) setState(() => _isLoadingAudio = false); + } + } + + Future _playAudio() async { + if (_audioPlayer.playerState.playing) { + await _audioPlayer.pause(); + return; + } else if (_audioPlayer.position != Duration.zero) { + await _audioPlayer.play(); + return; + } + + if (_audioFile == null) { + await _fetchAudio(); + } + + if (_audioFile == null) return; + await _audioPlayer.setAudioSource( + BytesAudioSource( + _audioFile!.bytes, + _audioFile!.mimeType, + ), + ); + _audioPlayer.play(); + } + + Future _fetchRepresentation() async { + if (l1Code == null || messageEvent == null || _repEvent != null) { + return; + } + + _repEvent = messageEvent!.representationByLanguage(l1Code!)?.content; + if (_repEvent == null && mounted) { + _repEvent = await messageEvent?.representationByLanguageGlobal( + langCode: l1Code!, + ); + } + } + + Future _loadTranslation() async { + if (!mounted) return; + setState(() => _isLoadingTranslation = true); + + try { + await _fetchRepresentation(); + } catch (err) { + ErrorHandler.logError( + e: err, + data: {}, + ); + } + + if (mounted) { + setState(() => _isLoadingTranslation = false); + } + } + + Widget icon(SelectMode mode) { + if (mode == SelectMode.audio) { + if (_isLoadingAudio) { + return const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator.adaptive(), + ); + } else { + return Icon( + _audioPlayer.playerState.playing == true + ? Icons.pause_outlined + : Icons.play_arrow, + size: 20, + color: mode == _selectedMode ? Colors.white : null, + ); + } + } + + if (mode == SelectMode.translate) { + if (_isLoadingTranslation) { + return const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator.adaptive(), + ); + } else if (_repEvent != null) { + return Icon( + mode.icon, + size: 20, + color: mode == _selectedMode ? Colors.white : null, + ); + } + } + + return Icon( + mode.icon, + size: 20, + color: mode == _selectedMode ? Colors.white : null, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + height: AppConfig.toolbarButtonsHeight, + alignment: Alignment.bottomCenter, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + spacing: 4.0, + children: [ + for (final mode in SelectMode.values) + Tooltip( + message: mode.tooltip(context), + child: PressableButton( + depressed: mode == _selectedMode, + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primaryContainer, + onPressed: () => _updateMode(mode), + playSound: true, + child: Container( + height: buttonSize, + width: buttonSize, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + child: icon(mode), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart index 9e1b04fb6..64e4ab679 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget.dart @@ -127,7 +127,7 @@ class LemmaMeaningWidgetState extends State { widget.token!, ) && widget.controller!.readingAssistanceMode == - ReadingAssistanceMode.messageMode) { + ReadingAssistanceMode.practiceMode) { return WordZoomActivityButton( icon: const Icon(Symbols.dictionary), isSelected: widget.controller?.toolbarMode == MessageMode.wordMeaning,