2507 still requesting unlocking translation faster (#2549)
* initial work for new reading assistance modes * feat: added select mode buttons * chore: highlight audio tokens, always open selection mode first
This commit is contained in:
parent
6a9e4350d1
commit
22f46caf80
10 changed files with 505 additions and 110 deletions
|
|
@ -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) +
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
final GlobalKey<ReadingAssistanceContentState> wordZoomKey = GlobalKey();
|
||||
|
||||
ReadingAssistanceMode? readingAssistanceMode; // default mode
|
||||
bool showTranslation = false;
|
||||
String? translationText;
|
||||
|
||||
double maxWidth = AppConfig.toolbarMinWidth;
|
||||
|
||||
|
|
@ -289,7 +291,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
/// 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<MessageSelectionOverlay>
|
|||
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<MessageSelectionOverlay>
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
/////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -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<MessageSelectionPositioner>
|
|||
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<MessageSelectionPositioner>
|
|||
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<MessageSelectionPositioner>
|
|||
_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<MessageSelectionPositioner>
|
|||
|
||||
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<Offset>(
|
||||
begin: _currentOffset,
|
||||
end: _adjustedOriginalMessageOffset,
|
||||
|
|
@ -208,7 +206,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
setState(() => _currentOffset = _overlayOffsetAnimation?.value);
|
||||
}
|
||||
});
|
||||
} else if (mode == ReadingAssistanceMode.messageMode) {
|
||||
} else if (mode == ReadingAssistanceMode.practiceMode) {
|
||||
_overlayOffsetAnimation = Tween<Offset>(
|
||||
begin: _currentOffset,
|
||||
end: _centeredMessageOffset!,
|
||||
|
|
@ -266,10 +264,10 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
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<MessageSelectionPositioner>
|
|||
}
|
||||
|
||||
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<MessageSelectionPositioner>
|
|||
if (!hasHeaderOverflow && !hasFooterOverflow) {
|
||||
return Offset(
|
||||
_ownMessage ? _messageRightOffset : _messageLeftOffset,
|
||||
_originalMessageBottomOffset - _reactionsHeight,
|
||||
bottomOffset,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -427,13 +428,9 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
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<MessageSelectionPositioner>
|
|||
|
||||
// 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<MessageSelectionPositioner>
|
|||
|
||||
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<MessageSelectionPositioner>
|
|||
),
|
||||
Opacity(
|
||||
opacity: _readingAssistanceMode ==
|
||||
ReadingAssistanceMode.messageMode
|
||||
ReadingAssistanceMode.practiceMode
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: OverlayCenterContent(
|
||||
|
|
@ -601,7 +608,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
|
|||
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<MessageSelectionPositioner>
|
|||
),
|
||||
],
|
||||
),
|
||||
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<MessageSelectionPositioner>
|
|||
_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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
312
lib/pangea/toolbar/widgets/select_mode_buttons.dart
Normal file
312
lib/pangea/toolbar/widgets/select_mode_buttons.dart
Normal file
|
|
@ -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<SelectModeButtons> createState() => SelectModeButtonsState();
|
||||
}
|
||||
|
||||
class SelectModeButtonsState extends State<SelectModeButtons> {
|
||||
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<void> _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<void> _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<void> _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<void> _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<void> _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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ class LemmaMeaningWidgetState extends State<LemmaMeaningWidget> {
|
|||
widget.token!,
|
||||
) &&
|
||||
widget.controller!.readingAssistanceMode ==
|
||||
ReadingAssistanceMode.messageMode) {
|
||||
ReadingAssistanceMode.practiceMode) {
|
||||
return WordZoomActivityButton(
|
||||
icon: const Icon(Symbols.dictionary),
|
||||
isSelected: widget.controller?.toolbarMode == MessageMode.wordMeaning,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue