379 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
		
		
			
		
	
	
			379 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 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; | ||
|  |   debugPrint("created"); | ||
|  | } | ||
|  | 
 | ||
|  | 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; | ||
|  |   debugPrint("chatEntry created"); | ||
|  | } | ||
|  | 
 | ||
|  | 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)), | ||
|  |                   ], | ||
|  |                 ), | ||
|  |               )); | ||
|  |         }); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | 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 - 60; | ||
|  |   debugPrint("left top : $left,$top"); | ||
|  | 
 | ||
|  |   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; | ||
|  |   debugPrint("mobileActionsOverlay created"); | ||
|  | } | ||
|  | 
 | ||
|  | 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)); | ||
|  |   } | ||
|  | } |