experimenting

This commit is contained in:
William Jordan-Cooley 2024-01-27 12:08:19 -05:00
parent 5901de1ab0
commit 57957eac9e
10 changed files with 346 additions and 149 deletions

View file

@ -1,12 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
@ -15,6 +7,13 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/error_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'config/setting_keys.dart';
import 'utils/background_push.dart';
import 'widgets/fluffy_chat_app.dart';

View file

@ -1277,10 +1277,11 @@ class ChatController extends State<ChatPageWithRoom>
}
void onSelectMessage(Event event) {
// #Pangea
// #Pangea
if (choreographer.itController.isOpen) {
return;
}
// Pangea#
if (!event.redacted) {
if (selectedEvents.contains(event)) {

View file

@ -148,7 +148,7 @@ class ChatEventList extends StatelessWidget {
scrollToEventId: (String eventId) =>
controller.scrollToEventId(eventId),
// #Pangea
// longPressSelect: controller.selectedEvents.isEmpty,
longPressSelect: controller.selectedEvents.isNotEmpty,
selectedDisplayLang:
controller.choreographer.messageOptions.selectedDisplayLang,
immersionMode: controller.choreographer.immersionMode,

View file

@ -241,8 +241,14 @@ class Message extends StatelessWidget {
alignment: alignment,
padding: const EdgeInsets.only(left: 8),
child: GestureDetector(
onTap: () => print("got message tap"),
onDoubleTap: () => print("got message double tap"),
onDoubleTapDown: (details) =>
print("got message double tap down"),
onLongPress: longPressSelect
? null
? selected
? null
: () => print('long press')
: () {
onSelect(event);
// Android usually has a vibration effect on long press:

View file

@ -361,6 +361,7 @@ class MessageContent extends StatelessWidget {
cause: cause,
context: context,
),
onTap: () => messageToolbar?.onTextTap(context),
);
},
),

View file

@ -1,17 +1,16 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/models/message_data_models.dart';
import 'package:fluffychat/pangea/repo/tokens_repo.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../constants/pangea_event_types.dart';
import '../enum/use_type.dart';
import '../models/choreo_record.dart';

View file

@ -591,51 +591,51 @@ extension PangeaRoom on Room {
required String parentEventId,
required String type,
}) async {
try {
debugPrint("creating $type child for $parentEventId");
Sentry.addBreadcrumb(Breadcrumb.fromJson(content));
if (parentEventId.contains("web")) {
debugger(when: kDebugMode);
Sentry.addBreadcrumb(
Breadcrumb(
message:
"sendPangeaEvent with likely invalid parentEventId $parentEventId",
),
);
}
final Map<String, dynamic> repContent = {
// what is the functionality of m.reference?
"m.relates_to": {"rel_type": type, "event_id": parentEventId},
type: content,
};
final String? newEventId = await sendEvent(repContent, type: type);
if (newEventId == null) {
debugger(when: kDebugMode);
}
//PTODO - handle the frequent case of a null newEventId
final Event? newEvent = await getEventById(newEventId!);
if (newEvent == null) {
debugger(when: kDebugMode);
}
return newEvent;
} catch (err, stack) {
// try {
debugPrint("creating $type child for $parentEventId");
Sentry.addBreadcrumb(Breadcrumb.fromJson(content));
if (parentEventId.contains("web")) {
debugger(when: kDebugMode);
ErrorHandler.logError(
e: err,
s: stack,
data: {
"type": type,
"parentEventId": parentEventId,
"content": content,
},
Sentry.addBreadcrumb(
Breadcrumb(
message:
"sendPangeaEvent with likely invalid parentEventId $parentEventId",
),
);
return null;
}
final Map<String, dynamic> repContent = {
// what is the functionality of m.reference?
"m.relates_to": {"rel_type": type, "event_id": parentEventId},
type: content,
};
final String? newEventId = await sendEvent(repContent, type: type);
if (newEventId == null) {
debugger(when: kDebugMode);
}
//PTODO - handle the frequent case of a null newEventId
final Event? newEvent = await getEventById(newEventId!);
if (newEvent == null) {
debugger(when: kDebugMode);
}
return newEvent;
// } catch (err, stack) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(
// e: err,
// s: stack,
// data: {
// "type": type,
// "parentEventId": parentEventId,
// "content": content,
// },
// );
// return null;
// }
}
ConstructEvent? _vocabEventLocal(String lemma) {

View file

@ -1,13 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/widget_measurement.dart';
class PangeaAnyState {
final Map<String, StreamController<WidgetMeasurements>?> _streams = {};
final Map<String, List<WidgetMeasurements>> _pastValues = {};
final Map<String, LayerLinkAndKey> _layerLinkAndKeys = {};
OverlayEntry? overlay;

View file

@ -1,94 +1,138 @@
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 MessageOverlay {
static void showOverlay(BuildContext context, GlobalKey targetKey) {
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;
class MessageOverlayController {
OverlayEntry? _overlayEntry;
final BuildContext _context;
final GlobalKey _targetKey;
MessageMode? _currentMode;
AnimationController? _animationController;
// Determines the vertical position of the overlay
final bool isBottomRoomAvailable =
MediaQuery.of(context).size.height - (offset.dy + size.height) >=
size.height;
OverlayEntry overlayEntry;
MessageMode currentMode = MessageMode.translation;
// Function to build the content based on the selected mode
Widget buildContent() {
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(); // Returns an empty container
}
}
// Function to show the overlay with an animation
overlayEntry = OverlayEntry(
builder: (context) => AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
left: offset.dx + size.width / 2 - screenWidth / 2,
right: screenWidth - (offset.dx + size.width / 2 + screenWidth / 2),
top: isBottomRoomAvailable ? offset.dy + size.height : null,
bottom: isBottomRoomAvailable
? null
: MediaQuery.of(context).size.height - offset.dy,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: screenWidth,
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Material(
elevation: 4.0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Wrap(
alignment: WrapAlignment.center,
children: MessageMode.values.map((mode) {
return IconButton(
icon: Icon(_getIconData(mode)),
onPressed: () {
currentMode = mode;
overlayEntry.markNeedsBuild();
},
);
}).toList(),
),
SizeTransition(
sizeFactor: currentMode != null
? CurvedAnimation(
parent: Overlay.of(context).animation!,
curve: Curves.fastOutSlowIn)
: const AlwaysStoppedAnimation(0),
axisAlignment: -1.0,
child: buildContent(),
),
],
),
),
),
),
MessageOverlayController(this._context, this._targetKey) {
_animationController = AnimationController(
vsync: Navigator.of(_context), // Using the Navigator's TickerProvider
duration: const Duration(milliseconds: 300),
);
Overlay.of(context).insert(overlayEntry);
}
static IconData _getIconData(MessageMode mode) {
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;
@ -101,7 +145,159 @@ class MessageOverlay {
case MessageMode.spellCheck:
return Icons.spellcheck;
default:
return Icons.error;
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();
},
),
],
);
}
}

View file

@ -105,6 +105,7 @@ class PangeaRichTextState extends State<PangeaRichText> {
cause: cause,
context: context,
),
onTap: () => messageToolbar?.onTextTap(context),
focusNode: widget.messageToolbar?.focusNode,
contextMenuBuilder: (context, state) =>
widget.messageToolbar?.contextMenuOverride(