666 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			666 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:auto_size_text/auto_size_text.dart';
 | |
| import 'package:debounce_throttle/debounce_throttle.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| import 'package:flutter_hbb/models/platform_model.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) {
 | |
|     if (draggablePositions.chatWindow.isInvalid()) {
 | |
|       draggablePositions.chatWindow.update(position);
 | |
|     }
 | |
|     return isIOS
 | |
|         ? IOSDraggable(
 | |
|             position: draggablePositions.chatWindow,
 | |
|             chatModel: chatModel,
 | |
|             width: width,
 | |
|             height: height,
 | |
|             builder: (context) {
 | |
|               return Column(
 | |
|                 children: [
 | |
|                   _buildMobileAppBar(context),
 | |
|                   Expanded(
 | |
|                     child: ChatPage(chatModel: chatModel),
 | |
|                   ),
 | |
|                 ],
 | |
|               );
 | |
|             },
 | |
|           )
 | |
|         : Draggable(
 | |
|             checkKeyboard: true,
 | |
|             position: draggablePositions.chatWindow,
 | |
|             width: width,
 | |
|             height: height,
 | |
|             chatModel: chatModel,
 | |
|             builder: (context, onPanUpdate) {
 | |
|               final child = Scaffold(
 | |
|                 resizeToAvoidBottomInset: false,
 | |
|                 appBar: CustomAppBar(
 | |
|                   onPanUpdate: onPanUpdate,
 | |
|                   appBar: (isDesktop || isWebDesktop)
 | |
|                       ? _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.onBackPressed,
 | |
|       this.onRecentPressed,
 | |
|       this.onHomePressed,
 | |
|       this.onHidePressed,
 | |
|       required this.position,
 | |
|       required this.width,
 | |
|       required this.height,
 | |
|       required this.scale});
 | |
| 
 | |
|   final double scale;
 | |
|   final DraggableKeyPosition 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: scale * width,
 | |
|         height: scale * 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 * scale))),
 | |
|                     child: Row(
 | |
|                       mainAxisAlignment: MainAxisAlignment.spaceAround,
 | |
|                       children: [
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onBackPressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.arrow_back),
 | |
|                             iconSize: 24 * scale),
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onHomePressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.home),
 | |
|                             iconSize: 24 * scale),
 | |
|                         IconButton(
 | |
|                             color: Colors.white,
 | |
|                             onPressed: onRecentPressed,
 | |
|                             splashRadius: kDesktopIconButtonSplashRadius,
 | |
|                             icon: const Icon(Icons.more_horiz),
 | |
|                             iconSize: 24 * scale),
 | |
|                         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),
 | |
|                             iconSize: 24 * scale),
 | |
|                       ],
 | |
|                     ),
 | |
|                   )));
 | |
|         });
 | |
|   }
 | |
| }
 | |
| 
 | |
| class DraggableKeyPosition {
 | |
|   final String key;
 | |
|   Offset _pos;
 | |
|   late Debouncer<int> _debouncerStore;
 | |
|   DraggableKeyPosition(this.key)
 | |
|       : _pos = DraggablePositions.kInvalidDraggablePosition;
 | |
| 
 | |
|   get pos => _pos;
 | |
| 
 | |
|   _loadPosition(String k) {
 | |
|     final value = bind.getLocalFlutterOption(k: k);
 | |
|     if (value.isNotEmpty) {
 | |
|       final parts = value.split(',');
 | |
|       if (parts.length == 2) {
 | |
|         return Offset(double.parse(parts[0]), double.parse(parts[1]));
 | |
|       }
 | |
|     }
 | |
|     return DraggablePositions.kInvalidDraggablePosition;
 | |
|   }
 | |
| 
 | |
|   load() {
 | |
|     _pos = _loadPosition(key);
 | |
|     _debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
 | |
|         onChanged: (v) => _store(), initialValue: 0);
 | |
|   }
 | |
| 
 | |
|   update(Offset pos) {
 | |
|     _pos = pos;
 | |
|     _triggerStore();
 | |
|   }
 | |
| 
 | |
|   // Adjust position to keep it in the screen
 | |
|   // Only used for desktop and web desktop
 | |
|   tryAdjust(double w, double h, double scale) {
 | |
|     final size = MediaQuery.of(Get.context!).size;
 | |
|     w = w * scale;
 | |
|     h = h * scale;
 | |
|     double x = _pos.dx;
 | |
|     double y = _pos.dy;
 | |
|     if (x + w > size.width) {
 | |
|       x = size.width - w;
 | |
|     }
 | |
|     final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
 | |
|     if (y + h > (size.height - tabBarHeight)) {
 | |
|       y = size.height - tabBarHeight - h;
 | |
|     }
 | |
|     if (x < 0) {
 | |
|       x = 0;
 | |
|     }
 | |
|     if (y < 0) {
 | |
|       y = 0;
 | |
|     }
 | |
|     if (x != _pos.dx || y != _pos.dy) {
 | |
|       update(Offset(x, y));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   isInvalid() {
 | |
|     return _pos == DraggablePositions.kInvalidDraggablePosition;
 | |
|   }
 | |
| 
 | |
|   _triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
 | |
|   _store() {
 | |
|     bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
 | |
|   }
 | |
| }
 | |
| 
 | |
| class DraggablePositions {
 | |
|   static const kChatWindow = 'draggablePositionChat';
 | |
|   static const kMobileActions = 'draggablePositionMobile';
 | |
|   static const kIOSDraggable = 'draggablePositionIOS';
 | |
| 
 | |
|   static const kInvalidDraggablePosition = Offset(-999999, -999999);
 | |
|   final chatWindow = DraggableKeyPosition(kChatWindow);
 | |
|   final mobileActions = DraggableKeyPosition(kMobileActions);
 | |
|   final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
 | |
| 
 | |
|   load() {
 | |
|     chatWindow.load();
 | |
|     mobileActions.load();
 | |
|     iOSDraggable.load();
 | |
|   }
 | |
| }
 | |
| 
 | |
| DraggablePositions draggablePositions = DraggablePositions();
 | |
| 
 | |
| class Draggable extends StatefulWidget {
 | |
|   Draggable(
 | |
|       {Key? key,
 | |
|       this.checkKeyboard = false,
 | |
|       this.checkScreenSize = false,
 | |
|       required this.position,
 | |
|       required this.width,
 | |
|       required this.height,
 | |
|       this.chatModel,
 | |
|       required this.builder})
 | |
|       : super(key: key);
 | |
| 
 | |
|   final bool checkKeyboard;
 | |
|   final bool checkScreenSize;
 | |
|   final DraggableKeyPosition position;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final ChatModel? chatModel;
 | |
|   final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
 | |
| 
 | |
|   @override
 | |
|   State<StatefulWidget> createState() => _DraggableState(chatModel);
 | |
| }
 | |
| 
 | |
| class _DraggableState extends State<Draggable> {
 | |
|   late ChatModel? _chatModel;
 | |
|   bool _keyboardVisible = false;
 | |
|   double _saveHeight = 0;
 | |
|   double _lastBottomHeight = 0;
 | |
| 
 | |
|   _DraggableState(ChatModel? chatModel) {
 | |
|     _chatModel = chatModel;
 | |
|   }
 | |
| 
 | |
|   get position => widget.position.pos;
 | |
| 
 | |
|   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(() {
 | |
|       widget.position.update(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(() {
 | |
|         widget.position.update(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(() {
 | |
|           widget.position.update(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.chatModel,
 | |
|       required this.position,
 | |
|       required this.width,
 | |
|       required this.height,
 | |
|       required this.builder})
 | |
|       : super(key: key);
 | |
| 
 | |
|   final DraggableKeyPosition position;
 | |
|   final ChatModel? chatModel;
 | |
|   final double width;
 | |
|   final double height;
 | |
|   final Widget Function(BuildContext) builder;
 | |
| 
 | |
|   @override
 | |
|   IOSDraggableState createState() =>
 | |
|       IOSDraggableState(chatModel, width, height);
 | |
| }
 | |
| 
 | |
| class IOSDraggableState extends State<IOSDraggable> {
 | |
|   late ChatModel? _chatModel;
 | |
|   late double _width;
 | |
|   late double _height;
 | |
|   bool _keyboardVisible = false;
 | |
|   double _saveHeight = 0;
 | |
|   double _lastBottomHeight = 0;
 | |
| 
 | |
|   IOSDraggableState(ChatModel? chatModel, double w, double h) {
 | |
|     _chatModel = chatModel;
 | |
|     _width = w;
 | |
|     _height = h;
 | |
|   }
 | |
| 
 | |
|   DraggableKeyPosition get position => widget.position;
 | |
| 
 | |
|   checkKeyboard() {
 | |
|     final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
 | |
|     final currentVisible = bottomHeight != 0;
 | |
| 
 | |
|     // save
 | |
|     if (!_keyboardVisible && currentVisible) {
 | |
|       _saveHeight = position.pos.dy;
 | |
|     }
 | |
| 
 | |
|     // reset
 | |
|     if (_lastBottomHeight > 0 && bottomHeight == 0) {
 | |
|       setState(() {
 | |
|         position.update(Offset(position.pos.dx, _saveHeight));
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     // onKeyboardVisible
 | |
|     if (_keyboardVisible && currentVisible) {
 | |
|       final sumHeight = bottomHeight + _height;
 | |
|       final contextHeight = MediaQuery.of(context).size.height;
 | |
|       if (sumHeight + position.pos.dy > contextHeight) {
 | |
|         final y = contextHeight - sumHeight;
 | |
|         setState(() {
 | |
|           position.update(Offset(position.pos.dx, y));
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _keyboardVisible = currentVisible;
 | |
|     _lastBottomHeight = bottomHeight;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     checkKeyboard();
 | |
|     return Stack(
 | |
|       children: [
 | |
|         Positioned(
 | |
|           left: position.pos.dx,
 | |
|           top: position.pos.dy,
 | |
|           child: GestureDetector(
 | |
|             onPanUpdate: (details) {
 | |
|               setState(() {
 | |
|                 position.update(position.pos + details.delta);
 | |
|               });
 | |
|               _chatModel?.setChatWindowPosition(position.pos);
 | |
|             },
 | |
|             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 ?? '-'),
 | |
|                       // let delay be 0 if fps is 0
 | |
|                       _row(
 | |
|                           "Delay",
 | |
|                           "${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
 | |
|                           rightColor: Colors.green),
 | |
|                       _row("Target Bitrate",
 | |
|                           "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
 | |
|                       _row(
 | |
|                           "Codec", qualityMonitorModel.data.codecFormat ?? '-'),
 | |
|                       if (!isWeb)
 | |
|                         _row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
 | |
|                     ],
 | |
|                   ),
 | |
|                 )
 | |
|               : 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);
 | |
|   }
 | |
| }
 |