You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fluffychat/lib/pangea/widgets/chat/message_toolbar.dart

430 lines
13 KiB
Dart

2 years ago
import 'dart:async';
import 'dart:developer';
2 years ago
2 years ago
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
2 years ago
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_speech_to_text_card.dart';
2 years ago
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
2 years ago
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
2 years ago
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
import 'package:fluffychat/pangea/widgets/practice_activity_card/message_practice_activity_card.dart';
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
2 years ago
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
2 years ago
import 'package:flutter/material.dart';
2 years ago
import 'package:flutter_gen/gen_l10n/l10n.dart';
2 years ago
import 'package:matrix/matrix.dart';
class ToolbarDisplayController {
final PangeaMessageEvent pangeaMessageEvent;
final String targetId;
final bool immersionMode;
final ChatController controller;
2 years ago
final FocusNode focusNode = FocusNode();
Event? nextEvent;
Event? previousEvent;
2 years ago
MessageToolbar? toolbar;
String? overlayId;
double? messageWidth;
final toolbarModeStream = StreamController<MessageMode>.broadcast();
ToolbarDisplayController({
required this.pangeaMessageEvent,
required this.targetId,
required this.immersionMode,
required this.controller,
this.nextEvent,
this.previousEvent,
2 years ago
});
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();
}
if (!MatrixState.pangeaController.languageController.languagesSet) {
pLanguageDialog(context, () {});
return;
}
2 years ago
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;
}
2 years ago
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Widget overlayEntry;
if (toolbar == null) return;
2 years ago
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,
nextEvent: nextEvent,
previousEvent: previousEvent,
2 years ago
),
],
);
} catch (err) {
debugger(when: kDebugMode);
2 years ago
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),
2 years ago
);
2 years ago
if (MatrixState.pAnyState.overlay != null) {
overlayId = MatrixState.pAnyState.overlay.hashCode.toString();
}
2 years ago
2 years ago
if (mode != null) {
Future.delayed(
const Duration(milliseconds: 100),
() => toolbarModeStream.add(mode),
);
}
});
2 years ago
}
bool get highlighted {
if (overlayId == null) return false;
if (MatrixState.pAnyState.overlay == null) overlayId = null;
return MatrixState.pAnyState.overlay.hashCode.toString() == overlayId;
}
2 years ago
}
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? toolbarContent;
2 years ago
MessageMode? currentMode;
bool updatingMode = false;
2 years ago
late StreamSubscription<String?> selectionStream;
late StreamSubscription<MessageMode> toolbarModeStream;
2 years ago
void updateMode(MessageMode newMode) {
if (updatingMode) return;
2 years ago
debugPrint("updating toolbar mode");
2 years ago
final bool subscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
if (!newMode.isValidMode(widget.pangeaMessageEvent.event)) {
ErrorHandler.logError(
e: "Invalid mode for event",
s: StackTrace.current,
data: {
"newMode": newMode,
"event": widget.pangeaMessageEvent.event,
},
);
return;
}
// if there is an uncompleted activity, then show that
// we don't want the user to user the tools to get the answer :P
if (widget.pangeaMessageEvent.hasUncompletedActivity) {
newMode = MessageMode.practiceActivity;
}
if (mounted) {
setState(() {
currentMode = newMode;
updatingMode = true;
});
}
2 years ago
if (!subscribed) {
toolbarContent = MessageUnsubscribedCard(
languageTool: newMode.title(context),
mode: newMode,
toolbarModeStream: widget.toolbarModeStream,
2 years ago
);
} else {
switch (currentMode) {
case MessageMode.translation:
showTranslation();
break;
case MessageMode.textToSpeech:
showTextToSpeech();
break;
case MessageMode.speechToText:
showSpeechToText();
2 years ago
break;
case MessageMode.definition:
showDefinition();
break;
case MessageMode.practiceActivity:
showPracticeActivity();
break;
2 years ago
default:
ErrorHandler.logError(
e: "Invalid toolbar mode",
s: StackTrace.current,
data: {"newMode": newMode},
);
2 years ago
break;
}
2 years ago
}
if (mounted) {
setState(() {
updatingMode = false;
});
}
2 years ago
}
void showTranslation() {
debugPrint("show translation");
toolbarContent = MessageTranslationCard(
2 years ago
messageEvent: widget.pangeaMessageEvent,
immersionMode: widget.immersionMode,
selection: widget.textSelection,
2 years ago
);
}
void showTextToSpeech() {
debugPrint("show text to speech");
toolbarContent = MessageAudioCard(
messageEvent: widget.pangeaMessageEvent,
);
}
void showSpeechToText() {
debugPrint("show speech to text");
toolbarContent = MessageSpeechToTextCard(
messageEvent: widget.pangeaMessageEvent,
);
2 years ago
}
void showDefinition() {
debugPrint("show definition");
2 years ago
if (widget.textSelection.selectedText == null ||
widget.textSelection.selectedText!.isEmpty) {
toolbarContent = const SelectToDefine();
2 years ago
return;
}
toolbarContent = WordDataCard(
2 years ago
word: widget.textSelection.selectedText!,
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
fullText: widget.textSelection.messageText,
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
hasInfo: true,
2 years ago
room: widget.room,
);
}
void showPracticeActivity() {
toolbarContent =
PracticeActivityCard(pangeaMessageEvent: widget.pangeaMessageEvent);
}
2 years ago
void showImage() {}
void spellCheck() {}
void showMore() {
MatrixState.pAnyState.closeOverlay();
widget.controller.onSelectMessage(widget.pangeaMessageEvent.event);
}
@override
void initState() {
super.initState();
2 years ago
widget.textSelection.selectedText = null;
2 years ago
toolbarModeStream = widget.toolbarModeStream.stream.listen((mode) {
2 years ago
updateMode(mode);
});
2 years ago
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final bool autoplay = MatrixState.pangeaController.pStoreService.read(
PLocalKey.autoPlayMessages,
) ??
false;
if (widget.pangeaMessageEvent.isAudioMessage) {
updateMode(MessageMode.speechToText);
return;
}
autoplay
? updateMode(MessageMode.textToSpeech)
: updateMode(MessageMode.translation);
2 years ago
});
Timer? timer;
2 years ago
selectionStream =
widget.textSelection.selectionStream.stream.listen((value) {
timer?.cancel();
timer = Timer(const Duration(milliseconds: 500), () {
if (value != null && value.isNotEmpty) {
final MessageMode newMode = currentMode == MessageMode.definition
? MessageMode.definition
: MessageMode.translation;
updateMode(newMode);
} else if (currentMode != null) {
updateMode(currentMode!);
}
});
});
2 years ago
}
@override
void dispose() {
2 years ago
selectionStream.cancel();
toolbarModeStream.cancel();
2 years ago
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: [
Padding(
padding: const EdgeInsets.all(8.0),
child: toolbarContent ?? const SizedBox(),
),
SizedBox(height: toolbarContent == null ? 0 : 20),
2 years ago
],
),
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: MessageMode.values.map((mode) {
if ([
MessageMode.definition,
MessageMode.textToSpeech,
MessageMode.translation,
].contains(mode) &&
widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
if (mode == MessageMode.speechToText &&
!widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox.shrink();
}
return Tooltip(
message: mode.tooltip(context),
child: IconButton(
icon: Icon(mode.icon),
color: currentMode == mode
? Theme.of(context).colorScheme.primary
: null,
onPressed: () => updateMode(mode),
),
2 years ago
);
}).toList() +
[
Tooltip(
message: L10n.of(context)!.more,
child: IconButton(
2 years ago
icon: const Icon(Icons.add_reaction_outlined),
onPressed: showMore,
),
2 years ago
),
],
),
],
),
),
);
}
}