toolbar working
parent
57957eac9e
commit
2cb77732e3
@ -1,159 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ShowDefintionUtil {
|
||||
String messageText;
|
||||
final String langCode;
|
||||
final String targetId;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final Room room;
|
||||
String? textSelection;
|
||||
bool inCooldown = false;
|
||||
double? dx;
|
||||
double? dy;
|
||||
|
||||
ShowDefintionUtil({
|
||||
required this.targetId,
|
||||
required this.room,
|
||||
required this.langCode,
|
||||
required this.messageText,
|
||||
});
|
||||
|
||||
void onTextSelection({
|
||||
required BuildContext context,
|
||||
TextSelection? selectedText,
|
||||
SelectedContent? selectedContent,
|
||||
SelectionChangedCause? cause,
|
||||
}) {
|
||||
if ((selectedText == null && selectedContent == null) ||
|
||||
selectedText?.isCollapsed == true) {
|
||||
clearTextSelection();
|
||||
return;
|
||||
}
|
||||
textSelection = selectedText != null
|
||||
? selectedText.textInside(messageText)
|
||||
: selectedContent!.plainText;
|
||||
|
||||
if (BrowserContextMenu.enabled && kIsWeb) {
|
||||
BrowserContextMenu.disableContextMenu();
|
||||
}
|
||||
|
||||
if (kIsWeb && cause != SelectionChangedCause.tap) {
|
||||
handleToolbar(context);
|
||||
}
|
||||
}
|
||||
|
||||
void clearTextSelection() {
|
||||
textSelection = null;
|
||||
if (kIsWeb && !BrowserContextMenu.enabled) {
|
||||
BrowserContextMenu.enableContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void handleToolbar(BuildContext context) async {
|
||||
if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return;
|
||||
inCooldown = true;
|
||||
Timer(const Duration(milliseconds: 750), () => inCooldown = false);
|
||||
await Future.delayed(const Duration(milliseconds: 750));
|
||||
showToolbar(context);
|
||||
}
|
||||
|
||||
void showDefinition(BuildContext context) {
|
||||
if (textSelection == null) return;
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: WordDataCard(
|
||||
word: textSelection!,
|
||||
wordLang: langCode,
|
||||
fullText: messageText,
|
||||
fullTextLang: langCode,
|
||||
hasInfo: false,
|
||||
room: room,
|
||||
),
|
||||
cardSize: const Size(300, 300),
|
||||
transformTargetId: targetId,
|
||||
backDropToDismiss: false,
|
||||
);
|
||||
}
|
||||
|
||||
// web toolbar
|
||||
Future<dynamic> showToolbar(BuildContext context) async {
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(targetId);
|
||||
|
||||
final RenderObject? targetRenderBox =
|
||||
layerLinkAndKey.key.currentContext!.findRenderObject();
|
||||
final Offset transformTargetOffset =
|
||||
(targetRenderBox as RenderBox).localToGlobal(Offset.zero);
|
||||
|
||||
if (dx != null && dx! > MediaQuery.of(context).size.width - 130) {
|
||||
dx = MediaQuery.of(context).size.width - 130;
|
||||
}
|
||||
final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0;
|
||||
final double yOffset =
|
||||
dy != null ? dy! - transformTargetOffset.dy + 10 : 10;
|
||||
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () {
|
||||
showDefinition(context);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context)!.showDefinition,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
size: const Size(130, 45),
|
||||
transformTargetId: targetId,
|
||||
offset: Offset(xOffset, yOffset),
|
||||
);
|
||||
}
|
||||
|
||||
void onMouseRegionUpdate(PointerEvent event) {
|
||||
dx = event.position.dx;
|
||||
dy = event.position.dy;
|
||||
}
|
||||
|
||||
Widget contextMenuOverride({
|
||||
required BuildContext context,
|
||||
EditableTextState? textSelection,
|
||||
SelectableRegionState? contentSelection,
|
||||
}) {
|
||||
if (textSelection == null && contentSelection == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: textSelection?.contextMenuAnchors ??
|
||||
contentSelection!.contextMenuAnchors,
|
||||
buttonItems: [
|
||||
if (textSelection != null) ...textSelection.contextMenuButtonItems,
|
||||
if (contentSelection != null)
|
||||
...contentSelection.contextMenuButtonItems,
|
||||
ContextMenuButtonItem(
|
||||
label: L10n.of(context)!.showDefinition,
|
||||
onPressed: () {
|
||||
showDefinition(context);
|
||||
focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,303 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
enum MessageMode { translation, play, definition, image, spellCheck }
|
||||
|
||||
class MessageOverlayController {
|
||||
OverlayEntry? _overlayEntry;
|
||||
final BuildContext _context;
|
||||
final GlobalKey _targetKey;
|
||||
MessageMode? _currentMode;
|
||||
AnimationController? _animationController;
|
||||
|
||||
MessageOverlayController(this._context, this._targetKey) {
|
||||
_animationController = AnimationController(
|
||||
vsync: Navigator.of(_context), // Using the Navigator's TickerProvider
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
|
||||
void showOverlay() {
|
||||
final RenderBox renderBox =
|
||||
_targetKey.currentContext?.findRenderObject() as RenderBox;
|
||||
final Offset offset = renderBox.localToGlobal(Offset.zero);
|
||||
final Size size = renderBox.size;
|
||||
final double screenWidth = MediaQuery.of(_context).size.width;
|
||||
|
||||
// Determines if there is more room above or below the RenderBox
|
||||
final bool isBottomRoomAvailable =
|
||||
MediaQuery.of(_context).size.height - (offset.dy + size.height) >=
|
||||
size.height;
|
||||
final double topPosition = isBottomRoomAvailable
|
||||
? offset.dy + size.height
|
||||
: offset.dy - size.height;
|
||||
|
||||
// Ensure the overlay does not overflow the screen horizontally
|
||||
double leftPosition = offset.dx + size.width / 2 - screenWidth / 2;
|
||||
leftPosition = leftPosition < 0 ? 0 : leftPosition;
|
||||
final double rightPosition =
|
||||
leftPosition + screenWidth > MediaQuery.of(_context).size.width
|
||||
? MediaQuery.of(_context).size.width - leftPosition - screenWidth
|
||||
: leftPosition;
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
left: leftPosition,
|
||||
right: rightPosition,
|
||||
top: isBottomRoomAvailable ? topPosition : null,
|
||||
bottom: isBottomRoomAvailable
|
||||
? null
|
||||
: MediaQuery.of(_context).size.height -
|
||||
topPosition -
|
||||
size.height,
|
||||
child: AnimatedSize(
|
||||
curve: Curves.easeInOut,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values.map((mode) {
|
||||
return IconButton(
|
||||
icon: Icon(_getIconData(mode)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_currentMode = mode;
|
||||
});
|
||||
_animationController?.forward();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
SizeTransition(
|
||||
sizeFactor: CurvedAnimation(
|
||||
parent: _animationController!,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
),
|
||||
axisAlignment: -1.0,
|
||||
child: _buildModeContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Overlay.of(_context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void hideOverlay() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
_animationController?.reverse();
|
||||
}
|
||||
|
||||
Widget _buildModeContent() {
|
||||
switch (_currentMode) {
|
||||
case MessageMode.translation:
|
||||
return const Text('Translation Mode');
|
||||
case MessageMode.play:
|
||||
return const Text('Play Mode');
|
||||
case MessageMode.definition:
|
||||
return const Text('Definition Mode');
|
||||
case MessageMode.image:
|
||||
return const Text('Image Mode');
|
||||
case MessageMode.spellCheck:
|
||||
return const Text('SpellCheck Mode');
|
||||
default:
|
||||
return const SizedBox
|
||||
.shrink(); // Empty container for the default case, meaning no content
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getIconData(MessageMode mode) {
|
||||
switch (mode) {
|
||||
case MessageMode.translation:
|
||||
return Icons.g_translate;
|
||||
case MessageMode.play:
|
||||
return Icons.play_arrow;
|
||||
case MessageMode.definition:
|
||||
return Icons.book;
|
||||
case MessageMode.image:
|
||||
return Icons.image;
|
||||
case MessageMode.spellCheck:
|
||||
return Icons.spellcheck;
|
||||
default:
|
||||
return Icons.error; // Icon to indicate an error or unsupported mode
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_overlayEntry?.dispose();
|
||||
_animationController?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ShowDefintionUtil {
|
||||
String messageText;
|
||||
final String langCode;
|
||||
final String targetId;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final Room room;
|
||||
String? textSelection;
|
||||
bool inCooldown = false;
|
||||
double? dx;
|
||||
double? dy;
|
||||
|
||||
ShowDefintionUtil({
|
||||
required this.targetId,
|
||||
required this.room,
|
||||
required this.langCode,
|
||||
required this.messageText,
|
||||
});
|
||||
|
||||
void onTextSelection({
|
||||
required BuildContext context,
|
||||
TextSelection? selectedText,
|
||||
SelectedContent? selectedContent,
|
||||
SelectionChangedCause? cause,
|
||||
}) {
|
||||
if ((selectedText == null && selectedContent == null) ||
|
||||
selectedText?.isCollapsed == true) {
|
||||
clearTextSelection();
|
||||
return;
|
||||
}
|
||||
textSelection = selectedText != null
|
||||
? selectedText.textInside(messageText)
|
||||
: selectedContent!.plainText;
|
||||
|
||||
if (BrowserContextMenu.enabled && kIsWeb) {
|
||||
BrowserContextMenu.disableContextMenu();
|
||||
}
|
||||
|
||||
if (kIsWeb && cause != SelectionChangedCause.tap) {
|
||||
handleToolbar(context);
|
||||
}
|
||||
}
|
||||
|
||||
void clearTextSelection() {
|
||||
textSelection = null;
|
||||
if (kIsWeb && !BrowserContextMenu.enabled) {
|
||||
BrowserContextMenu.enableContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void handleToolbar(BuildContext context) async {
|
||||
if (inCooldown || OverlayUtil.isOverlayOpen || !kIsWeb) return;
|
||||
inCooldown = true;
|
||||
Timer(const Duration(milliseconds: 750), () => inCooldown = false);
|
||||
await Future.delayed(const Duration(milliseconds: 750));
|
||||
showToolbar(context);
|
||||
}
|
||||
|
||||
void showDefinition(BuildContext context) {
|
||||
if (textSelection == null) return;
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: WordDataCard(
|
||||
word: textSelection!,
|
||||
wordLang: langCode,
|
||||
fullText: messageText,
|
||||
fullTextLang: langCode,
|
||||
hasInfo: false,
|
||||
room: room,
|
||||
),
|
||||
cardSize: const Size(300, 300),
|
||||
transformTargetId: targetId,
|
||||
backDropToDismiss: false,
|
||||
);
|
||||
}
|
||||
|
||||
// web toolbar
|
||||
Future<dynamic> showToolbar(BuildContext context) async {
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(targetId);
|
||||
|
||||
final RenderObject? targetRenderBox =
|
||||
layerLinkAndKey.key.currentContext!.findRenderObject();
|
||||
final Offset transformTargetOffset =
|
||||
(targetRenderBox as RenderBox).localToGlobal(Offset.zero);
|
||||
|
||||
if (dx != null && dx! > MediaQuery.of(context).size.width - 130) {
|
||||
dx = MediaQuery.of(context).size.width - 130;
|
||||
}
|
||||
final double xOffset = dx != null ? dx! - transformTargetOffset.dx : 0;
|
||||
final double yOffset =
|
||||
dy != null ? dy! - transformTargetOffset.dy + 10 : 10;
|
||||
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: Size.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () {
|
||||
showDefinition(context);
|
||||
},
|
||||
child: Text(
|
||||
L10n.of(context)!.showDefinition,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
size: const Size(130, 45),
|
||||
transformTargetId: targetId,
|
||||
offset: Offset(xOffset, yOffset),
|
||||
);
|
||||
}
|
||||
|
||||
void onMouseRegionUpdate(PointerEvent event) {
|
||||
dx = event.position.dx;
|
||||
dy = event.position.dy;
|
||||
}
|
||||
|
||||
Widget contextMenuOverride({
|
||||
required BuildContext context,
|
||||
EditableTextState? textSelection,
|
||||
SelectableRegionState? contentSelection,
|
||||
}) {
|
||||
if (textSelection == null && contentSelection == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: textSelection?.contextMenuAnchors ??
|
||||
contentSelection!.contextMenuAnchors,
|
||||
buttonItems: [
|
||||
if (textSelection != null) ...textSelection.contextMenuButtonItems,
|
||||
if (contentSelection != null)
|
||||
...contentSelection.contextMenuButtonItems,
|
||||
ContextMenuButtonItem(
|
||||
label: L10n.of(context)!.showDefinition,
|
||||
onPressed: () {
|
||||
showDefinition(context);
|
||||
focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageAudioCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
|
||||
const MessageAudioCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageAudioCardState createState() => MessageAudioCardState();
|
||||
}
|
||||
|
||||
class MessageAudioCardState extends State<MessageAudioCard> {
|
||||
// RepresentationEvent? repEvent;
|
||||
bool _isLoading = false;
|
||||
Event? localAudioEvent;
|
||||
// String langCode = "en";
|
||||
|
||||
// void setLangCode() {
|
||||
// final String? l2Code =
|
||||
// MatrixState.pangeaController.languageController.activeL2Code(
|
||||
// roomID: widget.messageEvent.room.id,
|
||||
// );
|
||||
// setState(() => langCode = l2Code ?? "en");
|
||||
// }
|
||||
|
||||
// void fetchRepresentation(BuildContext context) {
|
||||
// repEvent = widget.messageEvent.representationByLanguage(
|
||||
// langCode,
|
||||
// );
|
||||
|
||||
// if (repEvent == null) {
|
||||
// setState(() => _isLoading = true);
|
||||
// widget.messageEvent
|
||||
// .representationByLanguageGlobal(
|
||||
// context: context,
|
||||
// langCode: langCode,
|
||||
// )
|
||||
// .onError((error, stackTrace) => ErrorHandler.logError())
|
||||
// .then(((RepresentationEvent? event) => repEvent = event))
|
||||
// .whenComplete(
|
||||
// () => setState(() => _isLoading = false),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
void fetchAudio() {
|
||||
if (!mounted) return;
|
||||
// final String? text = widget.messageEvent.displayMessageText;
|
||||
// if (text == null || text.isEmpty) return;
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
widget.messageEvent
|
||||
.getAudioGlobal(widget.messageEvent.messageDisplayLangCode)
|
||||
.then((Event? event) {
|
||||
localAudioEvent = event;
|
||||
}).catchError((e) {
|
||||
if (!mounted) return null;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.errorGettingAudio),
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}).whenComplete(() {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.messageEvent
|
||||
.getDisplayRepresentation(context)
|
||||
.then((_) => fetchAudio());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final playButton = InkWell(
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
onTap: () => widget.messageEvent
|
||||
.getDisplayRepresentation(context)
|
||||
.then((event) => event == null ? null : fetchAudio),
|
||||
child: Material(
|
||||
color: AppConfig.primaryColor.withAlpha(64),
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
child: const Icon(
|
||||
// Change the icon based on some condition. If you have an audio player state, use it here.
|
||||
Icons.play_arrow_outlined,
|
||||
color: AppConfig.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _isLoading
|
||||
? SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: localAudioEvent != null
|
||||
? Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 250,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AudioPlayerWidget(
|
||||
localAudioEvent!,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
:
|
||||
// Opacity(
|
||||
// opacity: widget.messageEvent.getDisplayRepresentation().then((event) => event == null ? ) == null
|
||||
// ? 0.5
|
||||
// : 1,
|
||||
// // child: SizedBox(
|
||||
// // width: 44,
|
||||
// // height: 36,
|
||||
// child:
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: playButton,
|
||||
),
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MessageContextMenu {
|
||||
static List<ContextMenuButtonItem> customToolbarOptions(
|
||||
BuildContext context,
|
||||
void Function()? onDefine,
|
||||
void Function()? onListen,
|
||||
) {
|
||||
return [
|
||||
ContextMenuButtonItem(
|
||||
label: L10n.of(context)!.define,
|
||||
onPressed: onDefine,
|
||||
),
|
||||
ContextMenuButtonItem(
|
||||
label: L10n.of(context)!.listen,
|
||||
onPressed: onListen,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<ContextMenuButtonItem> toolbarOptions(
|
||||
EditableTextState? textSelection,
|
||||
SelectableRegionState? contentSelection,
|
||||
BuildContext context,
|
||||
void Function()? onDefine,
|
||||
void Function()? onListen,
|
||||
) {
|
||||
final List<ContextMenuButtonItem> menuItems =
|
||||
textSelection?.contextMenuButtonItems ??
|
||||
contentSelection?.contextMenuButtonItems ??
|
||||
[];
|
||||
menuItems.sort((a, b) {
|
||||
if (a.type == ContextMenuButtonType.copy) return -1;
|
||||
if (b.type == ContextMenuButtonType.copy) return 1;
|
||||
return 0;
|
||||
});
|
||||
return MessageContextMenu.customToolbarOptions(
|
||||
context, onDefine, onListen) +
|
||||
menuItems;
|
||||
}
|
||||
|
||||
static Widget contextMenuOverride({
|
||||
required BuildContext context,
|
||||
EditableTextState? textSelection,
|
||||
SelectableRegionState? contentSelection,
|
||||
void Function()? onDefine,
|
||||
void Function()? onListen,
|
||||
}) {
|
||||
if (textSelection == null && contentSelection == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final List<ContextMenuButtonItem> menuItems =
|
||||
MessageContextMenu.toolbarOptions(
|
||||
textSelection,
|
||||
contentSelection,
|
||||
context,
|
||||
onDefine,
|
||||
onListen,
|
||||
);
|
||||
|
||||
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||
anchors: textSelection?.contextMenuAnchors ??
|
||||
contentSelection!.contextMenuAnchors,
|
||||
buttonItems: menuItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class MessageTextSelection {
|
||||
String? selectedText;
|
||||
String messageText = "";
|
||||
|
||||
void setMessageText(String text) {
|
||||
messageText = text;
|
||||
}
|
||||
|
||||
void onTextSelection(TextSelection selection) => selection.isCollapsed == true
|
||||
? clearTextSelection()
|
||||
: setTextSelection(selection);
|
||||
|
||||
void setTextSelection(TextSelection selection) {
|
||||
selectedText = selection.textInside(messageText);
|
||||
if (BrowserContextMenu.enabled && kIsWeb) {
|
||||
BrowserContextMenu.disableContextMenu();
|
||||
}
|
||||
// selectionStream.add(selectedText);
|
||||
}
|
||||
|
||||
void clearTextSelection() {
|
||||
selectedText = null;
|
||||
if (kIsWeb && !BrowserContextMenu.enabled) {
|
||||
BrowserContextMenu.enableContextMenu();
|
||||
}
|
||||
// selectionStream.add(selectedText);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,303 @@
|
||||
import 'dart:async';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
enum MessageMode { translation, play, definition }
|
||||
|
||||
class ToolbarDisplayController {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final String targetId;
|
||||
final bool immersionMode;
|
||||
final ChatController controller;
|
||||
|
||||
MessageToolbar? toolbar;
|
||||
String? overlayId;
|
||||
double? messageWidth;
|
||||
|
||||
final toolbarModeStream = StreamController<MessageMode>.broadcast();
|
||||
|
||||
ToolbarDisplayController({
|
||||
required this.pangeaMessageEvent,
|
||||
required this.targetId,
|
||||
required this.immersionMode,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
void setToolbar() {
|
||||
toolbar ??= MessageToolbar(
|
||||
textSelection: MessageTextSelection(),
|
||||
room: pangeaMessageEvent.room,
|
||||
toolbarModeStream: toolbarModeStream,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
controller: controller,
|
||||
);
|
||||
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(targetId);
|
||||
final targetRenderBox =
|
||||
layerLinkAndKey.key.currentContext?.findRenderObject();
|
||||
if (targetRenderBox == null) return;
|
||||
final Size transformTargetSize = (targetRenderBox as RenderBox).size;
|
||||
messageWidth = transformTargetSize.width;
|
||||
}
|
||||
|
||||
void showToolbar(BuildContext context, {MessageMode? mode}) {
|
||||
if (highlighted) return;
|
||||
if (controller.selectMode) {
|
||||
controller.clearSelectedEvents();
|
||||
}
|
||||
focusNode.unfocus();
|
||||
Widget overlayEntry;
|
||||
try {
|
||||
overlayEntry = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: pangeaMessageEvent.ownMessage
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
toolbar!,
|
||||
OverlayMessage(
|
||||
pangeaMessageEvent.event,
|
||||
timeline: pangeaMessageEvent.timeline,
|
||||
immersionMode: immersionMode,
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
toolbarController: this,
|
||||
width: messageWidth,
|
||||
),
|
||||
],
|
||||
);
|
||||
} catch (err) {
|
||||
ErrorHandler.logError(e: err, s: StackTrace.current);
|
||||
return;
|
||||
}
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: overlayEntry,
|
||||
transformTargetId: targetId,
|
||||
targetAnchor: pangeaMessageEvent.ownMessage
|
||||
? Alignment.bottomRight
|
||||
: Alignment.bottomLeft,
|
||||
followerAnchor: pangeaMessageEvent.ownMessage
|
||||
? Alignment.bottomRight
|
||||
: Alignment.bottomLeft,
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(164),
|
||||
);
|
||||
|
||||
if (MatrixState.pAnyState.overlay != null) {
|
||||
overlayId = MatrixState.pAnyState.overlay.hashCode.toString();
|
||||
}
|
||||
|
||||
if (mode != null) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() => toolbarModeStream.add(mode),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool get highlighted =>
|
||||
MatrixState.pAnyState.overlay.hashCode.toString() == overlayId;
|
||||
}
|
||||
|
||||
class MessageToolbar extends StatefulWidget {
|
||||
final MessageTextSelection textSelection;
|
||||
final Room room;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final StreamController<MessageMode> toolbarModeStream;
|
||||
final bool immersionMode;
|
||||
final ChatController controller;
|
||||
|
||||
const MessageToolbar({
|
||||
super.key,
|
||||
required this.textSelection,
|
||||
required this.room,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.toolbarModeStream,
|
||||
required this.immersionMode,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageToolbarState createState() => MessageToolbarState();
|
||||
}
|
||||
|
||||
class MessageToolbarState extends State<MessageToolbar> {
|
||||
Widget? child;
|
||||
MessageMode? _currentMode;
|
||||
late StreamSubscription<String?> _selectionStream;
|
||||
late StreamSubscription<MessageMode> _toolbarModeStream;
|
||||
|
||||
IconData _getIconData(MessageMode mode) {
|
||||
switch (mode) {
|
||||
case MessageMode.translation:
|
||||
return Icons.g_translate;
|
||||
case MessageMode.play:
|
||||
return Icons.play_arrow;
|
||||
case MessageMode.definition:
|
||||
return Icons.book;
|
||||
default:
|
||||
return Icons.error; // Icon to indicate an error or unsupported mode
|
||||
}
|
||||
}
|
||||
|
||||
bool _enabledButton(MessageMode mode) {
|
||||
switch (mode) {
|
||||
case MessageMode.translation:
|
||||
return true;
|
||||
case MessageMode.play:
|
||||
return true;
|
||||
case MessageMode.definition:
|
||||
debugPrint("checking");
|
||||
return widget.textSelection.selectedText != null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void updateMode(MessageMode newMode) {
|
||||
debugPrint("updating toolbar mode");
|
||||
setState(() => _currentMode = newMode);
|
||||
switch (_currentMode) {
|
||||
case MessageMode.translation:
|
||||
showTranslation();
|
||||
break;
|
||||
case MessageMode.play:
|
||||
playAudio();
|
||||
break;
|
||||
case MessageMode.definition:
|
||||
showDefinition();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void showTranslation() {
|
||||
debugPrint("show translation");
|
||||
child = MessageTranslationCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
immersionMode: widget.immersionMode,
|
||||
);
|
||||
}
|
||||
|
||||
void playAudio() {
|
||||
debugPrint("play audio");
|
||||
child = MessageAudioCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
);
|
||||
}
|
||||
|
||||
void showDefinition() {
|
||||
if (widget.textSelection.selectedText == null ||
|
||||
widget.textSelection.selectedText!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
child = WordDataCard(
|
||||
word: widget.textSelection.selectedText!,
|
||||
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
fullText: widget.textSelection.messageText,
|
||||
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
hasInfo: false,
|
||||
room: widget.room,
|
||||
);
|
||||
}
|
||||
|
||||
void showImage() {}
|
||||
|
||||
void spellCheck() {}
|
||||
|
||||
void showMore() {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
widget.controller.onSelectMessage(widget.pangeaMessageEvent.event);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) {
|
||||
updateMode(mode);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectionStream.cancel();
|
||||
_toolbarModeStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(25),
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 300,
|
||||
minWidth: 300,
|
||||
maxHeight: 300,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: Column(
|
||||
children: [
|
||||
child ?? const SizedBox(),
|
||||
SizedBox(height: child == null ? 0 : 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values.map((mode) {
|
||||
return IconButton(
|
||||
icon: Icon(_getIconData(mode)),
|
||||
onPressed:
|
||||
_enabledButton(mode) ? () => updateMode(mode) : null,
|
||||
);
|
||||
}).toList() +
|
||||
[
|
||||
IconButton(
|
||||
icon: Icon(Icons.adaptive.more_outlined),
|
||||
onPressed: showMore,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class MessageTranslationCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final bool immersionMode;
|
||||
|
||||
const MessageTranslationCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
required this.immersionMode,
|
||||
});
|
||||
|
||||
@override
|
||||
MessageTranslationCardState createState() => MessageTranslationCardState();
|
||||
}
|
||||
|
||||
class MessageTranslationCardState extends State<MessageTranslationCard> {
|
||||
RepresentationEvent? repEvent;
|
||||
bool _fetchingRepresentation = false;
|
||||
|
||||
String? translationLangCode() {
|
||||
final String? l1Code =
|
||||
MatrixState.pangeaController.languageController.activeL1Code(
|
||||
roomID: widget.messageEvent.room.id,
|
||||
);
|
||||
if (widget.immersionMode) return l1Code;
|
||||
|
||||
final String? l2Code =
|
||||
MatrixState.pangeaController.languageController.activeL2Code(
|
||||
roomID: widget.messageEvent.room.id,
|
||||
);
|
||||
final String? originalWrittenCode =
|
||||
widget.messageEvent.originalWritten?.content.langCode;
|
||||
return l1Code == originalWrittenCode ? l2Code : l1Code;
|
||||
}
|
||||
|
||||
void fetchRepresentation(BuildContext context) {
|
||||
final String? langCode = translationLangCode();
|
||||
if (langCode == null) return;
|
||||
|
||||
repEvent = widget.messageEvent.representationByLanguage(
|
||||
langCode,
|
||||
);
|
||||
|
||||
if (repEvent == null && mounted) {
|
||||
setState(() => _fetchingRepresentation = true);
|
||||
widget.messageEvent
|
||||
.representationByLanguageGlobal(
|
||||
context: context,
|
||||
langCode: langCode,
|
||||
)
|
||||
.onError(
|
||||
(error, stackTrace) => ErrorHandler.logError(
|
||||
e: error,
|
||||
s: stackTrace,
|
||||
),
|
||||
)
|
||||
.then((RepresentationEvent? event) => repEvent = event)
|
||||
.whenComplete(
|
||||
() => setState(() => _fetchingRepresentation = false),
|
||||
);
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetchRepresentation(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: _fetchingRepresentation
|
||||
? SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: repEvent != null
|
||||
? Text(
|
||||
repEvent!.text,
|
||||
style: BotStyle.text(context),
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context)!.oopsSomethingWentWrong,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_content.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../../config/app_config.dart';
|
||||
|
||||
class OverlayMessage extends StatelessWidget {
|
||||
final Event event;
|
||||
final bool selected;
|
||||
final Timeline timeline;
|
||||
// #Pangea
|
||||
// final LanguageModel? selectedDisplayLang;
|
||||
final bool immersionMode;
|
||||
// final bool definitions;
|
||||
final bool ownMessage;
|
||||
final ToolbarDisplayController toolbarController;
|
||||
final double? width;
|
||||
// Pangea#
|
||||
|
||||
const OverlayMessage(
|
||||
this.event, {
|
||||
this.selected = false,
|
||||
required this.timeline,
|
||||
// #Pangea
|
||||
// required this.selectedDisplayLang,
|
||||
required this.immersionMode,
|
||||
// required this.definitions,
|
||||
required this.ownMessage,
|
||||
required this.toolbarController,
|
||||
this.width,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (event.type != EventTypes.Message ||
|
||||
event.messageType == EventTypes.KeyVerificationRequest) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
var color = Theme.of(context).colorScheme.surfaceVariant;
|
||||
final textColor = ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
: Theme.of(context).colorScheme.onBackground;
|
||||
|
||||
final borderRadius = BorderRadius.only(
|
||||
topLeft: !ownMessage
|
||||
? const Radius.circular(4)
|
||||
: const Radius.circular(AppConfig.borderRadius),
|
||||
topRight: const Radius.circular(AppConfig.borderRadius),
|
||||
bottomLeft: const Radius.circular(AppConfig.borderRadius),
|
||||
bottomRight: ownMessage
|
||||
? const Radius.circular(4)
|
||||
: const Radius.circular(AppConfig.borderRadius),
|
||||
);
|
||||
final noBubble = {
|
||||
MessageTypes.Video,
|
||||
MessageTypes.Image,
|
||||
MessageTypes.Sticker,
|
||||
}.contains(event.messageType) &&
|
||||
!event.redacted;
|
||||
final noPadding = {
|
||||
MessageTypes.File,
|
||||
MessageTypes.Audio,
|
||||
}.contains(event.messageType);
|
||||
|
||||
if (ownMessage) {
|
||||
color = Theme.of(context).colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
// #Pangea
|
||||
final pangeaMessageEvent = PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: ownMessage,
|
||||
);
|
||||
// Pangea#
|
||||
|
||||
return Material(
|
||||
color: noBubble ? Colors.transparent : color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: noBubble || noPadding
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: width ?? FluffyThemes.columnWidth * 1.25,
|
||||
),
|
||||
child: MessageContent(
|
||||
event,
|
||||
textColor: textColor,
|
||||
borderRadius: borderRadius,
|
||||
selected: selected,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
// selectedDisplayLang: selectedDisplayLang,
|
||||
immersionMode: immersionMode,
|
||||
// definitions: definitions,
|
||||
toolbarController: toolbarController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue