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 || 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.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 createState() => _DraggableState(); } class _DraggableState extends State { 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 { 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( 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 ?? '-'), _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? 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); } }