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/message_unsubscribed_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:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; enum MessageMode { translation, play, definition } class ToolbarDisplayController { final PangeaMessageEvent pangeaMessageEvent; final String targetId; final bool immersionMode; final ChatController controller; final FocusNode focusNode = FocusNode(); MessageToolbar? toolbar; String? overlayId; double? messageWidth; final toolbarModeStream = StreamController.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, ); } void showToolbar(BuildContext context, {MessageMode? mode}) { if (highlighted) return; if (controller.selectMode) { controller.clearSelectedEvents(); } focusNode.requestFocus(); final LayerLinkAndKey layerLinkAndKey = MatrixState.pAnyState.layerLinkAndKey(targetId); final targetRenderBox = layerLinkAndKey.key.currentContext?.findRenderObject(); if (targetRenderBox != null) { final Size transformTargetSize = (targetRenderBox as RenderBox).size; messageWidth = transformTargetSize.width; } WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Widget overlayEntry; if (toolbar == null) return; try { overlayEntry = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: pangeaMessageEvent.ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ toolbar!, const SizedBox(height: 6), 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(100), ); 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 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 { Widget? child; MessageMode? currentMode; bool updatingMode = false; late StreamSubscription selectionStream; late StreamSubscription 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 } } String getModeTitle(MessageMode mode) { switch (mode) { case MessageMode.translation: return L10n.of(context)!.translations; case MessageMode.play: return L10n.of(context)!.messageAudio; case MessageMode.definition: return L10n.of(context)!.definitions; default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode } } String getModeTooltip(MessageMode mode) { switch (mode) { case MessageMode.translation: return L10n.of(context)!.translationTooltip; case MessageMode.play: return L10n.of(context)!.audioTooltip; case MessageMode.definition: return L10n.of(context)!.define; default: return L10n.of(context)! .oopsSomethingWentWrong; // Title to indicate an error or unsupported mode } } void updateMode(MessageMode newMode) { if (updatingMode) return; debugPrint("updating toolbar mode"); final bool subscribed = MatrixState.pangeaController.subscriptionController.isSubscribed; setState(() { currentMode = newMode; updatingMode = true; }); if (!subscribed) { child = MessageUnsubscribedCard( languageTool: getModeTitle(newMode), mode: newMode, toolbarModeStream: widget.toolbarModeStream, ); } else { switch (currentMode) { case MessageMode.translation: showTranslation(); break; case MessageMode.play: playAudio(); break; case MessageMode.definition: showDefinition(); break; default: break; } } setState(() { updatingMode = false; }); } void showTranslation() { debugPrint("show translation"); child = MessageTranslationCard( messageEvent: widget.pangeaMessageEvent, immersionMode: widget.immersionMode, selection: widget.textSelection, ); } void playAudio() { debugPrint("play audio"); child = MessageAudioCard( messageEvent: widget.pangeaMessageEvent, ); } void showDefinition() { if (widget.textSelection.selectedText == null || widget.textSelection.selectedText!.isEmpty) { child = const SelectToDefine(); return; } child = WordDataCard( word: widget.textSelection.selectedText!, wordLang: widget.pangeaMessageEvent.messageDisplayLangCode, fullText: widget.textSelection.messageText, fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode, hasInfo: true, room: widget.room, ); } void showImage() {} void spellCheck() {} void showMore() { MatrixState.pAnyState.closeOverlay(); widget.controller.onSelectMessage(widget.pangeaMessageEvent.event); } @override void initState() { super.initState(); widget.textSelection.selectedText = null; toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) { updateMode(mode); }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { updateMode(MessageMode.play); }); Timer? timer; selectionStream = widget.textSelection.selectionStream.stream.listen((value) { timer?.cancel(); timer = Timer(const Duration(milliseconds: 500), () { if (currentMode != null || value != null && value.isNotEmpty) { updateMode(currentMode ?? MessageMode.translation); } }); }); } @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 Tooltip( message: getModeTooltip(mode), child: IconButton( icon: Icon(getIconData(mode)), color: currentMode == mode ? Theme.of(context).colorScheme.primary : null, onPressed: () => updateMode(mode), ), ); }).toList() + [ Tooltip( message: L10n.of(context)!.more, child: IconButton( icon: const Icon(Icons.add_reaction_outlined), onPressed: showMore, ), ), ], ), ], ), ), ); } }