568 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			568 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:auto_size_text/auto_size_text.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:provider/provider.dart';
 | |
| 
 | |
| import '../../consts.dart';
 | |
| import '../../desktop/widgets/tabbar_widget.dart';
 | |
| import '../../models/chat_model.dart';
 | |
| import '../../models/model.dart';
 | |
| import 'chat_page.dart';
 | |
| 
 | |
| class DraggableChatWindow extends StatelessWidget {
 | |
|   const DraggableChatWindow(
 | |
|       {Key? key,
 | |
|       this.position = Offset.zero,
 | |
|       required this.width,
 | |
|       required this.height,
 | |
|       required this.chatModel})
 | |
|       : super(key: key);
 | |
| 
 | |
|   final Offset position;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final ChatModel chatModel;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return isIOS
 | |
|     ? IOSDraggable (
 | |
|       position: position,
 | |
|       chatModel: chatModel,
 | |
|       width: width,
 | |
|       height: height,
 | |
|       builder: (context) {
 | |
|     return Column(
 | |
|       children: [
 | |
|         _buildMobileAppBar(context),
 | |
|         Expanded(
 | |
|           child: ChatPage(chatModel: chatModel),
 | |
|               ),
 | |
|             ],
 | |
|           );
 | |
|         },
 | |
|       )
 | |
|     : Draggable(
 | |
|         checkKeyboard: true,
 | |
|         position: position,
 | |
|         width: width,
 | |
|         height: height,
 | |
|         chatModel: chatModel,
 | |
|         builder: (context, onPanUpdate) {
 | |
|           final child = 
 | |
|               Scaffold(
 | |
|                   resizeToAvoidBottomInset: false,
 | |
|                   appBar: CustomAppBar(
 | |
|                     onPanUpdate: onPanUpdate,
 | |
|                     appBar: isDesktop
 | |
|                         ? _buildDesktopAppBar(context)
 | |
|                         : _buildMobileAppBar(context),
 | |
|                   ),
 | |
|                   body: ChatPage(chatModel: chatModel),
 | |
|                 );
 | |
|           return Container(
 | |
|               decoration:
 | |
|                   BoxDecoration(border: Border.all(color: MyTheme.border)),
 | |
|               child: child);
 | |
|         });
 | |
|   }
 | |
| 
 | |
|   Widget _buildMobileAppBar(BuildContext context) {
 | |
|     return Container(
 | |
|       color: Theme.of(context).colorScheme.primary,
 | |
|       height: 50,
 | |
|       child: Row(
 | |
|         mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|         children: [
 | |
|           Padding(
 | |
|               padding: const EdgeInsets.symmetric(horizontal: 15),
 | |
|               child: Text(
 | |
|                 translate("Chat"),
 | |
|                 style: const TextStyle(
 | |
|                     color: Colors.white,
 | |
|                     fontFamily: 'WorkSans',
 | |
|                     fontWeight: FontWeight.bold,
 | |
|                     fontSize: 20),
 | |
|               )),
 | |
|           Row(
 | |
|             crossAxisAlignment: CrossAxisAlignment.center,
 | |
|             children: [
 | |
|               IconButton(
 | |
|                   onPressed: () {
 | |
|                     chatModel.hideChatWindowOverlay();
 | |
|                   },
 | |
|                   icon: const Icon(
 | |
|                     Icons.keyboard_arrow_down,
 | |
|                     color: Colors.white,
 | |
|                   )),
 | |
|               IconButton(
 | |
|                   onPressed: () {
 | |
|                     chatModel.hideChatWindowOverlay();
 | |
|                     chatModel.hideChatIconOverlay();
 | |
|                   },
 | |
|                   icon: const Icon(
 | |
|                     Icons.close,
 | |
|                     color: Colors.white,
 | |
|                   ))
 | |
|             ],
 | |
|           )
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildDesktopAppBar(BuildContext context) {
 | |
|     return Container(
 | |
|       decoration: BoxDecoration(
 | |
|           border: Border(
 | |
|               bottom: BorderSide(
 | |
|                   color: Theme.of(context).hintColor.withOpacity(0.4)))),
 | |
|       height: 38,
 | |
|       child: Row(
 | |
|         mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|         children: [
 | |
|           Padding(
 | |
|               padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
 | |
|               child: Obx(() => Opacity(
 | |
|                   opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4,
 | |
|                   child: Row(children: [
 | |
|                     Icon(Icons.chat_bubble_outline,
 | |
|                         size: 20, color: Theme.of(context).colorScheme.primary),
 | |
|                     SizedBox(width: 6),
 | |
|                     Text(translate("Chat"))
 | |
|                   ])))),
 | |
|           Padding(
 | |
|               padding: EdgeInsets.all(2),
 | |
|               child: ActionIcon(
 | |
|                 message: 'Close',
 | |
|                 icon: IconFont.close,
 | |
|                 onTap: chatModel.hideChatWindowOverlay,
 | |
|                 isClose: true,
 | |
|                 boxSize: 32,
 | |
|               ))
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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 => const Size.fromHeight(kToolbarHeight);
 | |
| }
 | |
| 
 | |
| /// floating buttons of back/home/recent actions for android
 | |
| class DraggableMobileActions extends StatelessWidget {
 | |
|   DraggableMobileActions(
 | |
|       {this.position = Offset.zero,
 | |
|       this.onBackPressed,
 | |
|       this.onRecentPressed,
 | |
|       this.onHomePressed,
 | |
|       this.onHidePressed,
 | |
|       required this.width,
 | |
|       required this.height});
 | |
| 
 | |
|   final Offset position;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final VoidCallback? onBackPressed;
 | |
|   final VoidCallback? onHomePressed;
 | |
|   final VoidCallback? onRecentPressed;
 | |
|   final VoidCallback? onHidePressed;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Draggable(
 | |
|         position: position,
 | |
|         width: width,
 | |
|         height: height,
 | |
|         builder: (_, onPanUpdate) {
 | |
|           return GestureDetector(
 | |
|               onPanUpdate: onPanUpdate,
 | |
|               child: Card(
 | |
|                   color: Colors.transparent,
 | |
|                   shadowColor: Colors.transparent,
 | |
|                   child: Container(
 | |
|                     decoration: BoxDecoration(
 | |
|                         color: MyTheme.accent.withOpacity(0.4),
 | |
|                         borderRadius: BorderRadius.all(Radius.circular(15))),
 | |
|                     child: Row(
 | |
|                       mainAxisAlignment: MainAxisAlignment.spaceAround,
 | |
|                       children: [
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onBackPressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.arrow_back)),
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onHomePressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.home)),
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onRecentPressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.more_horiz)),
 | |
|                         const VerticalDivider(
 | |
|                           width: 0,
 | |
|                           thickness: 2,
 | |
|                           indent: 10,
 | |
|                           endIndent: 10,
 | |
|                         ),
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onHidePressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.keyboard_arrow_down)),
 | |
|                       ],
 | |
|                     ),
 | |
|                   )));
 | |
|         });
 | |
|   }
 | |
| }
 | |
| 
 | |
| class Draggable extends StatefulWidget {
 | |
|   const Draggable(
 | |
|       {Key? key,
 | |
|       this.checkKeyboard = false,
 | |
|       this.checkScreenSize = false,
 | |
|       this.position = Offset.zero,
 | |
|       required this.width,
 | |
|       required this.height,
 | |
|       this.chatModel,
 | |
|       required this.builder})
 | |
|       : super(key: key);
 | |
| 
 | |
|   final bool checkKeyboard;
 | |
|   final bool checkScreenSize;
 | |
|   final Offset position;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final ChatModel? chatModel;
 | |
|   final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
 | |
| 
 | |
|   @override
 | |
|   State<StatefulWidget> createState() => _DraggableState();
 | |
| }
 | |
| 
 | |
| class _DraggableState extends State<Draggable> {
 | |
|   late Offset _position;
 | |
|   late ChatModel? _chatModel;
 | |
|   bool _keyboardVisible = false;
 | |
|   double _saveHeight = 0;
 | |
|   double _lastBottomHeight = 0;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _position = widget.position;
 | |
|     _chatModel = widget.chatModel;
 | |
|   }
 | |
| 
 | |
|   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);
 | |
|     });
 | |
|     _chatModel?.setChatWindowPosition(_position);
 | |
|   }
 | |
| 
 | |
|   checkScreenSize() {}
 | |
| 
 | |
|   checkKeyboard() {
 | |
|     final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
 | |
|     final currentVisible = bottomHeight != 0;
 | |
| 
 | |
|     // 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.checkScreenSize) {
 | |
|       checkScreenSize();
 | |
|     }
 | |
|     return Stack(children: [
 | |
|       Positioned(
 | |
|           top: _position.dy,
 | |
|           left: _position.dx,
 | |
|           width: widget.width,
 | |
|           height: widget.height,
 | |
|           child: widget.builder(context, onPanUpdate))
 | |
|     ]);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class IOSDraggable extends StatefulWidget {
 | |
|   const IOSDraggable({
 | |
|     Key? key,
 | |
|     this.position = Offset.zero,
 | |
|     this.chatModel,
 | |
|     required this.width,
 | |
|     required this.height,
 | |
|     required this.builder})
 | |
|     : super(key: key);
 | |
| 
 | |
|   final Offset position;
 | |
|   final ChatModel? chatModel;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final Widget Function(BuildContext) builder;
 | |
| 
 | |
|   @override
 | |
|   _IOSDraggableState createState() => _IOSDraggableState();
 | |
| }
 | |
| 
 | |
| class _IOSDraggableState extends State<IOSDraggable> {
 | |
|   late Offset _position;
 | |
|   late ChatModel? _chatModel;
 | |
|   late double _width;
 | |
|   late double _height;
 | |
|   bool _keyboardVisible = false;
 | |
|   double _saveHeight = 0;
 | |
|   double _lastBottomHeight = 0;
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     _position = widget.position;
 | |
|     _chatModel = widget.chatModel;
 | |
|     _width = widget.width;
 | |
|     _height = widget.height;
 | |
|   }
 | |
| 
 | |
|   checkKeyboard() {
 | |
|     final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
 | |
|     final currentVisible = bottomHeight != 0;
 | |
| 
 | |
|     // 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 + _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) {
 | |
|     checkKeyboard();
 | |
|     return Stack(
 | |
|       children: [
 | |
|         Positioned(
 | |
|           left: _position.dx,
 | |
|           top: _position.dy,
 | |
|           child: GestureDetector(
 | |
|             onPanUpdate: (details) {
 | |
|               setState(() {
 | |
|                 _position += details.delta;
 | |
|               });
 | |
|               _chatModel?.setChatWindowPosition(_position);
 | |
|             },
 | |
|             child: Material(
 | |
|               child:
 | |
|             Container(
 | |
|               width: _width,
 | |
|               height: _height,
 | |
|               decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
 | |
|               child: widget.builder(context),
 | |
|               ),
 | |
|             ),
 | |
|           ),
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class QualityMonitor extends StatelessWidget {
 | |
|   final QualityMonitorModel qualityMonitorModel;
 | |
|   QualityMonitor(this.qualityMonitorModel);
 | |
| 
 | |
|   Widget _row(String info, String? value, {Color? rightColor}) {
 | |
|     return Row(
 | |
|       children: [
 | |
|         Expanded(
 | |
|             flex: 8,
 | |
|             child: AutoSizeText(info,
 | |
|                 style: TextStyle(color: Color.fromARGB(255, 210, 210, 210)),
 | |
|                 textAlign: TextAlign.right,
 | |
|                 maxLines: 1)),
 | |
|         Spacer(flex: 1),
 | |
|         Expanded(
 | |
|             flex: 8,
 | |
|             child: AutoSizeText(value ?? '',
 | |
|                 style: TextStyle(color: rightColor ?? Colors.white),
 | |
|                 maxLines: 1)),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) => ChangeNotifierProvider.value(
 | |
|       value: qualityMonitorModel,
 | |
|       child: Consumer<QualityMonitorModel>(
 | |
|           builder: (context, qualityMonitorModel, child) => qualityMonitorModel
 | |
|                   .show
 | |
|               ? Container(
 | |
|                   constraints: BoxConstraints(maxWidth: 200),
 | |
|                   padding: const EdgeInsets.all(8),
 | |
|                   color: MyTheme.canvasColor.withAlpha(150),
 | |
|                   child: Column(
 | |
|                     crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                     children: [
 | |
|                       _row("Speed", qualityMonitorModel.data.speed ?? '-'),
 | |
|                       _row("FPS", qualityMonitorModel.data.fps ?? '-'),
 | |
|                       _row(
 | |
|                           "Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
 | |
|                           rightColor: Colors.green),
 | |
|                       _row("Target Bitrate",
 | |
|                           "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
 | |
|                       _row(
 | |
|                           "Codec", qualityMonitorModel.data.codecFormat ?? '-'),
 | |
|                     ],
 | |
|                   ),
 | |
|                 )
 | |
|               : const SizedBox.shrink()));
 | |
| }
 | |
| 
 | |
| class BlockableOverlayState extends OverlayKeyState {
 | |
|   final _middleBlocked = false.obs;
 | |
| 
 | |
|   VoidCallback? onMiddleBlockedClick; // to-do use listener
 | |
| 
 | |
|   RxBool get middleBlocked => _middleBlocked;
 | |
| 
 | |
|   void addMiddleBlockedListener(void Function(bool) cb) {
 | |
|     _middleBlocked.listen(cb);
 | |
|   }
 | |
| 
 | |
|   void setMiddleBlocked(bool blocked) {
 | |
|     if (blocked != _middleBlocked.value) {
 | |
|       _middleBlocked.value = blocked;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void applyFfi(FFI ffi) {
 | |
|     ffi.dialogManager.setOverlayState(this);
 | |
|     ffi.chatModel.setOverlayState(this);
 | |
|     // make remote page penetrable automatically, effective for chat over remote
 | |
|     onMiddleBlockedClick = () {
 | |
|       setMiddleBlocked(false);
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| class BlockableOverlay extends StatelessWidget {
 | |
|   final Widget underlying;
 | |
|   final List<OverlayEntry>? upperLayer;
 | |
| 
 | |
|   final BlockableOverlayState state;
 | |
| 
 | |
|   BlockableOverlay(
 | |
|       {required this.underlying, required this.state, this.upperLayer});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final initialEntries = [
 | |
|       OverlayEntry(builder: (_) => underlying),
 | |
| 
 | |
|       /// middle layer
 | |
|       OverlayEntry(
 | |
|           builder: (context) => Obx(() => Listener(
 | |
|               onPointerDown: (_) {
 | |
|                 state.onMiddleBlockedClick?.call();
 | |
|               },
 | |
|               child: Container(
 | |
|                   color:
 | |
|                       state.middleBlocked.value ? Colors.transparent : null)))),
 | |
|     ];
 | |
| 
 | |
|     if (upperLayer != null) {
 | |
|       initialEntries.addAll(upperLayer!);
 | |
|     }
 | |
| 
 | |
|     /// set key
 | |
|     return Overlay(key: state.key, initialEntries: initialEntries);
 | |
|   }
 | |
| }
 |