123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665 |
- 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 ?? '-'),
- _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);
- }
- }
|