387 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			387 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:draggable_float_widget/draggable_float_widget.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| 
 | |
| import '../models/model.dart';
 | |
| import '../pages/chat_page.dart';
 | |
| 
 | |
| OverlayEntry? chatIconOverlayEntry;
 | |
| OverlayEntry? chatWindowOverlayEntry;
 | |
| 
 | |
| OverlayEntry? mobileActionsOverlayEntry;
 | |
| 
 | |
| class DraggableChatWindow extends StatelessWidget {
 | |
|   DraggableChatWindow(
 | |
|       {this.position = Offset.zero, required this.width, required this.height});
 | |
| 
 | |
|   final Offset position;
 | |
|   final double width;
 | |
|   final double height;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Draggable(
 | |
|         checkKeyboard: true,
 | |
|         position: position,
 | |
|         width: width,
 | |
|         height: height,
 | |
|         builder: (_, onPanUpdate) {
 | |
|           return isIOS
 | |
|               ? chatPage
 | |
|               : Scaffold(
 | |
|                   resizeToAvoidBottomInset: false,
 | |
|                   appBar: CustomAppBar(
 | |
|                     onPanUpdate: onPanUpdate,
 | |
|                     appBar: Container(
 | |
|                       color: MyTheme.accent50,
 | |
|                       height: 50,
 | |
|                       child: Row(
 | |
|                         mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|                         children: [
 | |
|                           Padding(
 | |
|                               padding: EdgeInsets.symmetric(horizontal: 15),
 | |
|                               child: Text(
 | |
|                                 translate("Chat"),
 | |
|                                 style: TextStyle(
 | |
|                                     color: Colors.white,
 | |
|                                     fontFamily: 'WorkSans',
 | |
|                                     fontWeight: FontWeight.bold,
 | |
|                                     fontSize: 20),
 | |
|                               )),
 | |
|                           Row(
 | |
|                             crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                             children: [
 | |
|                               IconButton(
 | |
|                                   onPressed: () {
 | |
|                                     hideChatWindowOverlay();
 | |
|                                   },
 | |
|                                   icon: Icon(Icons.keyboard_arrow_down)),
 | |
|                               IconButton(
 | |
|                                   onPressed: () {
 | |
|                                     hideChatWindowOverlay();
 | |
|                                     hideChatIconOverlay();
 | |
|                                   },
 | |
|                                   icon: Icon(Icons.close))
 | |
|                             ],
 | |
|                           )
 | |
|                         ],
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                   body: chatPage,
 | |
|                 );
 | |
|         });
 | |
|   }
 | |
| }
 | |
| 
 | |
| class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
 | |
|   final GestureDragUpdateCallback onPanUpdate;
 | |
|   final Widget appBar;
 | |
| 
 | |
|   const CustomAppBar(
 | |
|       {Key? key, required this.onPanUpdate, required this.appBar})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return GestureDetector(onPanUpdate: onPanUpdate, child: appBar);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Size get preferredSize => new Size.fromHeight(kToolbarHeight);
 | |
| }
 | |
| 
 | |
| showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
 | |
|   if (chatIconOverlayEntry != null) {
 | |
|     chatIconOverlayEntry!.remove();
 | |
|   }
 | |
|   if (globalKey.currentState == null || globalKey.currentState!.overlay == null)
 | |
|     return;
 | |
|   final bar = navigationBarKey.currentWidget;
 | |
|   if (bar != null) {
 | |
|     if ((bar as BottomNavigationBar).currentIndex == 1) {
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
|   final globalOverlayState = globalKey.currentState!.overlay!;
 | |
| 
 | |
|   final overlay = OverlayEntry(builder: (context) {
 | |
|     return DraggableFloatWidget(
 | |
|         config: DraggableFloatWidgetBaseConfig(
 | |
|           initPositionYInTop: false,
 | |
|           initPositionYMarginBorder: 100,
 | |
|           borderTopContainTopBar: true,
 | |
|         ),
 | |
|         child: FloatingActionButton(
 | |
|             onPressed: () {
 | |
|               if (chatWindowOverlayEntry == null) {
 | |
|                 showChatWindowOverlay();
 | |
|               } else {
 | |
|                 hideChatWindowOverlay();
 | |
|               }
 | |
|             },
 | |
|             child: Icon(Icons.message)));
 | |
|   });
 | |
|   globalOverlayState.insert(overlay);
 | |
|   chatIconOverlayEntry = overlay;
 | |
| }
 | |
| 
 | |
| hideChatIconOverlay() {
 | |
|   if (chatIconOverlayEntry != null) {
 | |
|     chatIconOverlayEntry!.remove();
 | |
|     chatIconOverlayEntry = null;
 | |
|   }
 | |
| }
 | |
| 
 | |
| showChatWindowOverlay() {
 | |
|   if (chatWindowOverlayEntry != null) return;
 | |
|   if (globalKey.currentState == null || globalKey.currentState!.overlay == null)
 | |
|     return;
 | |
|   final globalOverlayState = globalKey.currentState!.overlay!;
 | |
| 
 | |
|   final overlay = OverlayEntry(builder: (context) {
 | |
|     return DraggableChatWindow(
 | |
|         position: Offset(20, 80), width: 250, height: 350);
 | |
|   });
 | |
|   globalOverlayState.insert(overlay);
 | |
|   chatWindowOverlayEntry = overlay;
 | |
| }
 | |
| 
 | |
| hideChatWindowOverlay() {
 | |
|   if (chatWindowOverlayEntry != null) {
 | |
|     chatWindowOverlayEntry!.remove();
 | |
|     chatWindowOverlayEntry = null;
 | |
|     return;
 | |
|   }
 | |
| }
 | |
| 
 | |
| toggleChatOverlay() {
 | |
|   if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) {
 | |
|     FFI.invokeMethod("enable_soft_keyboard", true);
 | |
|     showChatIconOverlay();
 | |
|     showChatWindowOverlay();
 | |
|   } else {
 | |
|     hideChatIconOverlay();
 | |
|     hideChatWindowOverlay();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /// floating buttons of back/home/recent actions for android
 | |
| class DraggableMobileActions extends StatelessWidget {
 | |
|   DraggableMobileActions(
 | |
|       {this.position = Offset.zero,
 | |
|       this.onBackPressed,
 | |
|       this.onRecentPressed,
 | |
|       this.onHomePressed,
 | |
|       required this.width,
 | |
|       required this.height});
 | |
| 
 | |
|   final Offset position;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final VoidCallback? onBackPressed;
 | |
|   final VoidCallback? onHomePressed;
 | |
|   final VoidCallback? onRecentPressed;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Draggable(
 | |
|         position: position,
 | |
|         width: width,
 | |
|         height: height,
 | |
|         builder: (_, onPanUpdate) {
 | |
|           return GestureDetector(
 | |
|               onPanUpdate: onPanUpdate,
 | |
|               child: Container(
 | |
|                 decoration: BoxDecoration(
 | |
|                     color: MyTheme.accent.withOpacity(0.4),
 | |
|                     borderRadius: BorderRadius.all(Radius.circular(15))),
 | |
|                 child: Row(
 | |
|                   mainAxisAlignment: MainAxisAlignment.spaceAround,
 | |
|                   children: [
 | |
|                     IconButton(
 | |
|                         color: MyTheme.white,
 | |
|                         onPressed: onBackPressed,
 | |
|                         icon: Icon(Icons.arrow_back)),
 | |
|                     IconButton(
 | |
|                         color: MyTheme.white,
 | |
|                         onPressed: onHomePressed,
 | |
|                         icon: Icon(Icons.home)),
 | |
|                     IconButton(
 | |
|                         color: MyTheme.white,
 | |
|                         onPressed: onRecentPressed,
 | |
|                         icon: Icon(Icons.more_horiz)),
 | |
|                     VerticalDivider(
 | |
|                       width: 0,
 | |
|                       thickness: 2,
 | |
|                       indent: 10,
 | |
|                       endIndent: 10,
 | |
|                     ),
 | |
|                     IconButton(
 | |
|                         color: MyTheme.white,
 | |
|                         onPressed: hideMobileActionsOverlay,
 | |
|                         icon: Icon(Icons.keyboard_arrow_down)),
 | |
|                   ],
 | |
|                 ),
 | |
|               ));
 | |
|         });
 | |
|   }
 | |
| }
 | |
| 
 | |
| resetMobileActionsOverlay() {
 | |
|   if (mobileActionsOverlayEntry == null) return;
 | |
|   hideMobileActionsOverlay();
 | |
|   showMobileActionsOverlay();
 | |
| }
 | |
| 
 | |
| showMobileActionsOverlay() {
 | |
|   if (mobileActionsOverlayEntry != null) return;
 | |
|   if (globalKey.currentContext == null ||
 | |
|       globalKey.currentState == null ||
 | |
|       globalKey.currentState!.overlay == null) return;
 | |
|   final globalOverlayState = globalKey.currentState!.overlay!;
 | |
| 
 | |
|   // compute overlay position
 | |
|   final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
 | |
|   final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
 | |
|   final double overlayW = 200;
 | |
|   final double overlayH = 45;
 | |
|   final left = (screenW - overlayW) / 2;
 | |
|   final top = screenH - overlayH - 80;
 | |
| 
 | |
|   final overlay = OverlayEntry(builder: (context) {
 | |
|     return DraggableMobileActions(
 | |
|       position: Offset(left, top),
 | |
|       width: overlayW,
 | |
|       height: overlayH,
 | |
|       onBackPressed: () => FFI.tap(MouseButtons.right),
 | |
|       onHomePressed: () => FFI.tap(MouseButtons.wheel),
 | |
|       onRecentPressed: () async {
 | |
|         FFI.sendMouse('down', MouseButtons.wheel);
 | |
|         await Future.delayed(Duration(milliseconds: 500));
 | |
|         FFI.sendMouse('up', MouseButtons.wheel);
 | |
|       },
 | |
|     );
 | |
|   });
 | |
|   globalOverlayState.insert(overlay);
 | |
|   mobileActionsOverlayEntry = overlay;
 | |
| }
 | |
| 
 | |
| hideMobileActionsOverlay() {
 | |
|   if (mobileActionsOverlayEntry != null) {
 | |
|     mobileActionsOverlayEntry!.remove();
 | |
|     mobileActionsOverlayEntry = null;
 | |
|     return;
 | |
|   }
 | |
| }
 | |
| 
 | |
| class Draggable extends StatefulWidget {
 | |
|   Draggable(
 | |
|       {this.checkKeyboard = false,
 | |
|       this.checkScreenSize = false,
 | |
|       this.position = Offset.zero,
 | |
|       required this.width,
 | |
|       required this.height,
 | |
|       required this.builder});
 | |
| 
 | |
|   final bool checkKeyboard;
 | |
|   final bool checkScreenSize;
 | |
|   final Offset position;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
 | |
| 
 | |
|   @override
 | |
|   State<StatefulWidget> createState() => _DraggableState();
 | |
| }
 | |
| 
 | |
| class _DraggableState extends State<Draggable> {
 | |
|   late Offset _position;
 | |
|   bool _keyboardVisible = false;
 | |
|   double _saveHeight = 0;
 | |
|   double _lastBottomHeight = 0;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _position = widget.position;
 | |
|   }
 | |
| 
 | |
|   void onPanUpdate(DragUpdateDetails d) {
 | |
|     final offset = d.delta;
 | |
|     final size = MediaQuery.of(context).size;
 | |
|     double x = 0;
 | |
|     double y = 0;
 | |
| 
 | |
|     if (_position.dx + offset.dx + widget.width > size.width) {
 | |
|       x = size.width - widget.width;
 | |
|     } else if (_position.dx + offset.dx < 0) {
 | |
|       x = 0;
 | |
|     } else {
 | |
|       x = _position.dx + offset.dx;
 | |
|     }
 | |
| 
 | |
|     if (_position.dy + offset.dy + widget.height > size.height) {
 | |
|       y = size.height - widget.height;
 | |
|     } else if (_position.dy + offset.dy < 0) {
 | |
|       y = 0;
 | |
|     } else {
 | |
|       y = _position.dy + offset.dy;
 | |
|     }
 | |
|     setState(() {
 | |
|       _position = Offset(x, y);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   checkScreenSize() {}
 | |
| 
 | |
|   checkKeyboard() {
 | |
|     final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
 | |
|     final currentVisible = bottomHeight != 0;
 | |
| 
 | |
|     debugPrint(bottomHeight.toString() + currentVisible.toString());
 | |
|     // save
 | |
|     if (!_keyboardVisible && currentVisible) {
 | |
|       _saveHeight = _position.dy;
 | |
|     }
 | |
| 
 | |
|     // reset
 | |
|     if (_lastBottomHeight > 0 && bottomHeight == 0) {
 | |
|       setState(() {
 | |
|         _position = Offset(_position.dx, _saveHeight);
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // onKeyboardVisible
 | |
|     if (_keyboardVisible && currentVisible) {
 | |
|       final sumHeight = bottomHeight + widget.height;
 | |
|       final contextHeight = MediaQuery.of(context).size.height;
 | |
|       if (sumHeight + _position.dy > contextHeight) {
 | |
|         final y = contextHeight - sumHeight;
 | |
|         setState(() {
 | |
|           _position = Offset(_position.dx, y);
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _keyboardVisible = currentVisible;
 | |
|     _lastBottomHeight = bottomHeight;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     if (widget.checkKeyboard) {
 | |
|       checkKeyboard();
 | |
|     }
 | |
|     if (widget.checkKeyboard) {
 | |
|       checkScreenSize();
 | |
|     }
 | |
|     return Positioned(
 | |
|         top: _position.dy,
 | |
|         left: _position.dx,
 | |
|         width: widget.width,
 | |
|         height: widget.height,
 | |
|         child: widget.builder(context, onPanUpdate));
 | |
|   }
 | |
| }
 |