overlay.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. import 'package:auto_size_text/auto_size_text.dart';
  2. import 'package:debounce_throttle/debounce_throttle.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_hbb/common.dart';
  5. import 'package:flutter_hbb/models/platform_model.dart';
  6. import 'package:get/get.dart';
  7. import 'package:provider/provider.dart';
  8. import '../../consts.dart';
  9. import '../../desktop/widgets/tabbar_widget.dart';
  10. import '../../models/chat_model.dart';
  11. import '../../models/model.dart';
  12. import 'chat_page.dart';
  13. class DraggableChatWindow extends StatelessWidget {
  14. const DraggableChatWindow(
  15. {Key? key,
  16. this.position = Offset.zero,
  17. required this.width,
  18. required this.height,
  19. required this.chatModel})
  20. : super(key: key);
  21. final Offset position;
  22. final double width;
  23. final double height;
  24. final ChatModel chatModel;
  25. @override
  26. Widget build(BuildContext context) {
  27. if (draggablePositions.chatWindow.isInvalid()) {
  28. draggablePositions.chatWindow.update(position);
  29. }
  30. return isIOS
  31. ? IOSDraggable(
  32. position: draggablePositions.chatWindow,
  33. chatModel: chatModel,
  34. width: width,
  35. height: height,
  36. builder: (context) {
  37. return Column(
  38. children: [
  39. _buildMobileAppBar(context),
  40. Expanded(
  41. child: ChatPage(chatModel: chatModel),
  42. ),
  43. ],
  44. );
  45. },
  46. )
  47. : Draggable(
  48. checkKeyboard: true,
  49. position: draggablePositions.chatWindow,
  50. width: width,
  51. height: height,
  52. chatModel: chatModel,
  53. builder: (context, onPanUpdate) {
  54. final child = Scaffold(
  55. resizeToAvoidBottomInset: false,
  56. appBar: CustomAppBar(
  57. onPanUpdate: onPanUpdate,
  58. appBar: (isDesktop || isWebDesktop)
  59. ? _buildDesktopAppBar(context)
  60. : _buildMobileAppBar(context),
  61. ),
  62. body: ChatPage(chatModel: chatModel),
  63. );
  64. return Container(
  65. decoration:
  66. BoxDecoration(border: Border.all(color: MyTheme.border)),
  67. child: child);
  68. });
  69. }
  70. Widget _buildMobileAppBar(BuildContext context) {
  71. return Container(
  72. color: Theme.of(context).colorScheme.primary,
  73. height: 50,
  74. child: Row(
  75. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  76. children: [
  77. Padding(
  78. padding: const EdgeInsets.symmetric(horizontal: 15),
  79. child: Text(
  80. translate("Chat"),
  81. style: const TextStyle(
  82. color: Colors.white,
  83. fontFamily: 'WorkSans',
  84. fontWeight: FontWeight.bold,
  85. fontSize: 20),
  86. )),
  87. Row(
  88. crossAxisAlignment: CrossAxisAlignment.center,
  89. children: [
  90. IconButton(
  91. onPressed: () {
  92. chatModel.hideChatWindowOverlay();
  93. },
  94. icon: const Icon(
  95. Icons.keyboard_arrow_down,
  96. color: Colors.white,
  97. )),
  98. IconButton(
  99. onPressed: () {
  100. chatModel.hideChatWindowOverlay();
  101. chatModel.hideChatIconOverlay();
  102. },
  103. icon: const Icon(
  104. Icons.close,
  105. color: Colors.white,
  106. ))
  107. ],
  108. )
  109. ],
  110. ),
  111. );
  112. }
  113. Widget _buildDesktopAppBar(BuildContext context) {
  114. return Container(
  115. decoration: BoxDecoration(
  116. border: Border(
  117. bottom: BorderSide(
  118. color: Theme.of(context).hintColor.withOpacity(0.4)))),
  119. height: 38,
  120. child: Row(
  121. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  122. children: [
  123. Padding(
  124. padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
  125. child: Obx(() => Opacity(
  126. opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4,
  127. child: Row(children: [
  128. Icon(Icons.chat_bubble_outline,
  129. size: 20, color: Theme.of(context).colorScheme.primary),
  130. SizedBox(width: 6),
  131. Text(translate("Chat"))
  132. ])))),
  133. Padding(
  134. padding: EdgeInsets.all(2),
  135. child: ActionIcon(
  136. message: 'Close',
  137. icon: IconFont.close,
  138. onTap: chatModel.hideChatWindowOverlay,
  139. isClose: true,
  140. boxSize: 32,
  141. ))
  142. ],
  143. ),
  144. );
  145. }
  146. }
  147. class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
  148. final GestureDragUpdateCallback onPanUpdate;
  149. final Widget appBar;
  150. const CustomAppBar(
  151. {Key? key, required this.onPanUpdate, required this.appBar})
  152. : super(key: key);
  153. @override
  154. Widget build(BuildContext context) {
  155. return GestureDetector(onPanUpdate: onPanUpdate, child: appBar);
  156. }
  157. @override
  158. Size get preferredSize => const Size.fromHeight(kToolbarHeight);
  159. }
  160. /// floating buttons of back/home/recent actions for android
  161. class DraggableMobileActions extends StatelessWidget {
  162. DraggableMobileActions(
  163. {this.onBackPressed,
  164. this.onRecentPressed,
  165. this.onHomePressed,
  166. this.onHidePressed,
  167. required this.position,
  168. required this.width,
  169. required this.height,
  170. required this.scale});
  171. final double scale;
  172. final DraggableKeyPosition position;
  173. final double width;
  174. final double height;
  175. final VoidCallback? onBackPressed;
  176. final VoidCallback? onHomePressed;
  177. final VoidCallback? onRecentPressed;
  178. final VoidCallback? onHidePressed;
  179. @override
  180. Widget build(BuildContext context) {
  181. return Draggable(
  182. position: position,
  183. width: scale * width,
  184. height: scale * height,
  185. builder: (_, onPanUpdate) {
  186. return GestureDetector(
  187. onPanUpdate: onPanUpdate,
  188. child: Card(
  189. color: Colors.transparent,
  190. shadowColor: Colors.transparent,
  191. child: Container(
  192. decoration: BoxDecoration(
  193. color: MyTheme.accent.withOpacity(0.4),
  194. borderRadius:
  195. BorderRadius.all(Radius.circular(15 * scale))),
  196. child: Row(
  197. mainAxisAlignment: MainAxisAlignment.spaceAround,
  198. children: [
  199. IconButton(
  200. color: Colors.white,
  201. onPressed: onBackPressed,
  202. splashRadius: kDesktopIconButtonSplashRadius,
  203. icon: const Icon(Icons.arrow_back),
  204. iconSize: 24 * scale),
  205. IconButton(
  206. color: Colors.white,
  207. onPressed: onHomePressed,
  208. splashRadius: kDesktopIconButtonSplashRadius,
  209. icon: const Icon(Icons.home),
  210. iconSize: 24 * scale),
  211. IconButton(
  212. color: Colors.white,
  213. onPressed: onRecentPressed,
  214. splashRadius: kDesktopIconButtonSplashRadius,
  215. icon: const Icon(Icons.more_horiz),
  216. iconSize: 24 * scale),
  217. const VerticalDivider(
  218. width: 0,
  219. thickness: 2,
  220. indent: 10,
  221. endIndent: 10,
  222. ),
  223. IconButton(
  224. color: Colors.white,
  225. onPressed: onHidePressed,
  226. splashRadius: kDesktopIconButtonSplashRadius,
  227. icon: const Icon(Icons.keyboard_arrow_down),
  228. iconSize: 24 * scale),
  229. ],
  230. ),
  231. )));
  232. });
  233. }
  234. }
  235. class DraggableKeyPosition {
  236. final String key;
  237. Offset _pos;
  238. late Debouncer<int> _debouncerStore;
  239. DraggableKeyPosition(this.key)
  240. : _pos = DraggablePositions.kInvalidDraggablePosition;
  241. get pos => _pos;
  242. _loadPosition(String k) {
  243. final value = bind.getLocalFlutterOption(k: k);
  244. if (value.isNotEmpty) {
  245. final parts = value.split(',');
  246. if (parts.length == 2) {
  247. return Offset(double.parse(parts[0]), double.parse(parts[1]));
  248. }
  249. }
  250. return DraggablePositions.kInvalidDraggablePosition;
  251. }
  252. load() {
  253. _pos = _loadPosition(key);
  254. _debouncerStore = Debouncer<int>(const Duration(milliseconds: 500),
  255. onChanged: (v) => _store(), initialValue: 0);
  256. }
  257. update(Offset pos) {
  258. _pos = pos;
  259. _triggerStore();
  260. }
  261. // Adjust position to keep it in the screen
  262. // Only used for desktop and web desktop
  263. tryAdjust(double w, double h, double scale) {
  264. final size = MediaQuery.of(Get.context!).size;
  265. w = w * scale;
  266. h = h * scale;
  267. double x = _pos.dx;
  268. double y = _pos.dy;
  269. if (x + w > size.width) {
  270. x = size.width - w;
  271. }
  272. final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0;
  273. if (y + h > (size.height - tabBarHeight)) {
  274. y = size.height - tabBarHeight - h;
  275. }
  276. if (x < 0) {
  277. x = 0;
  278. }
  279. if (y < 0) {
  280. y = 0;
  281. }
  282. if (x != _pos.dx || y != _pos.dy) {
  283. update(Offset(x, y));
  284. }
  285. }
  286. isInvalid() {
  287. return _pos == DraggablePositions.kInvalidDraggablePosition;
  288. }
  289. _triggerStore() => _debouncerStore.value = _debouncerStore.value + 1;
  290. _store() {
  291. bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}');
  292. }
  293. }
  294. class DraggablePositions {
  295. static const kChatWindow = 'draggablePositionChat';
  296. static const kMobileActions = 'draggablePositionMobile';
  297. static const kIOSDraggable = 'draggablePositionIOS';
  298. static const kInvalidDraggablePosition = Offset(-999999, -999999);
  299. final chatWindow = DraggableKeyPosition(kChatWindow);
  300. final mobileActions = DraggableKeyPosition(kMobileActions);
  301. final iOSDraggable = DraggableKeyPosition(kIOSDraggable);
  302. load() {
  303. chatWindow.load();
  304. mobileActions.load();
  305. iOSDraggable.load();
  306. }
  307. }
  308. DraggablePositions draggablePositions = DraggablePositions();
  309. class Draggable extends StatefulWidget {
  310. Draggable(
  311. {Key? key,
  312. this.checkKeyboard = false,
  313. this.checkScreenSize = false,
  314. required this.position,
  315. required this.width,
  316. required this.height,
  317. this.chatModel,
  318. required this.builder})
  319. : super(key: key);
  320. final bool checkKeyboard;
  321. final bool checkScreenSize;
  322. final DraggableKeyPosition position;
  323. final double width;
  324. final double height;
  325. final ChatModel? chatModel;
  326. final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
  327. @override
  328. State<StatefulWidget> createState() => _DraggableState(chatModel);
  329. }
  330. class _DraggableState extends State<Draggable> {
  331. late ChatModel? _chatModel;
  332. bool _keyboardVisible = false;
  333. double _saveHeight = 0;
  334. double _lastBottomHeight = 0;
  335. _DraggableState(ChatModel? chatModel) {
  336. _chatModel = chatModel;
  337. }
  338. get position => widget.position.pos;
  339. void onPanUpdate(DragUpdateDetails d) {
  340. final offset = d.delta;
  341. final size = MediaQuery.of(context).size;
  342. double x = 0;
  343. double y = 0;
  344. if (position.dx + offset.dx + widget.width > size.width) {
  345. x = size.width - widget.width;
  346. } else if (position.dx + offset.dx < 0) {
  347. x = 0;
  348. } else {
  349. x = position.dx + offset.dx;
  350. }
  351. if (position.dy + offset.dy + widget.height > size.height) {
  352. y = size.height - widget.height;
  353. } else if (position.dy + offset.dy < 0) {
  354. y = 0;
  355. } else {
  356. y = position.dy + offset.dy;
  357. }
  358. setState(() {
  359. widget.position.update(Offset(x, y));
  360. });
  361. _chatModel?.setChatWindowPosition(position);
  362. }
  363. checkScreenSize() {}
  364. checkKeyboard() {
  365. final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
  366. final currentVisible = bottomHeight != 0;
  367. // save
  368. if (!_keyboardVisible && currentVisible) {
  369. _saveHeight = position.dy;
  370. }
  371. // reset
  372. if (_lastBottomHeight > 0 && bottomHeight == 0) {
  373. setState(() {
  374. widget.position.update(Offset(position.dx, _saveHeight));
  375. });
  376. }
  377. // onKeyboardVisible
  378. if (_keyboardVisible && currentVisible) {
  379. final sumHeight = bottomHeight + widget.height;
  380. final contextHeight = MediaQuery.of(context).size.height;
  381. if (sumHeight + position.dy > contextHeight) {
  382. final y = contextHeight - sumHeight;
  383. setState(() {
  384. widget.position.update(Offset(position.dx, y));
  385. });
  386. }
  387. }
  388. _keyboardVisible = currentVisible;
  389. _lastBottomHeight = bottomHeight;
  390. }
  391. @override
  392. Widget build(BuildContext context) {
  393. if (widget.checkKeyboard) {
  394. checkKeyboard();
  395. }
  396. if (widget.checkScreenSize) {
  397. checkScreenSize();
  398. }
  399. return Stack(children: [
  400. Positioned(
  401. top: position.dy,
  402. left: position.dx,
  403. width: widget.width,
  404. height: widget.height,
  405. child: widget.builder(context, onPanUpdate))
  406. ]);
  407. }
  408. }
  409. class IOSDraggable extends StatefulWidget {
  410. const IOSDraggable(
  411. {Key? key,
  412. this.chatModel,
  413. required this.position,
  414. required this.width,
  415. required this.height,
  416. required this.builder})
  417. : super(key: key);
  418. final DraggableKeyPosition position;
  419. final ChatModel? chatModel;
  420. final double width;
  421. final double height;
  422. final Widget Function(BuildContext) builder;
  423. @override
  424. IOSDraggableState createState() =>
  425. IOSDraggableState(chatModel, width, height);
  426. }
  427. class IOSDraggableState extends State<IOSDraggable> {
  428. late ChatModel? _chatModel;
  429. late double _width;
  430. late double _height;
  431. bool _keyboardVisible = false;
  432. double _saveHeight = 0;
  433. double _lastBottomHeight = 0;
  434. IOSDraggableState(ChatModel? chatModel, double w, double h) {
  435. _chatModel = chatModel;
  436. _width = w;
  437. _height = h;
  438. }
  439. DraggableKeyPosition get position => widget.position;
  440. checkKeyboard() {
  441. final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
  442. final currentVisible = bottomHeight != 0;
  443. // save
  444. if (!_keyboardVisible && currentVisible) {
  445. _saveHeight = position.pos.dy;
  446. }
  447. // reset
  448. if (_lastBottomHeight > 0 && bottomHeight == 0) {
  449. setState(() {
  450. position.update(Offset(position.pos.dx, _saveHeight));
  451. });
  452. }
  453. // onKeyboardVisible
  454. if (_keyboardVisible && currentVisible) {
  455. final sumHeight = bottomHeight + _height;
  456. final contextHeight = MediaQuery.of(context).size.height;
  457. if (sumHeight + position.pos.dy > contextHeight) {
  458. final y = contextHeight - sumHeight;
  459. setState(() {
  460. position.update(Offset(position.pos.dx, y));
  461. });
  462. }
  463. }
  464. _keyboardVisible = currentVisible;
  465. _lastBottomHeight = bottomHeight;
  466. }
  467. @override
  468. Widget build(BuildContext context) {
  469. checkKeyboard();
  470. return Stack(
  471. children: [
  472. Positioned(
  473. left: position.pos.dx,
  474. top: position.pos.dy,
  475. child: GestureDetector(
  476. onPanUpdate: (details) {
  477. setState(() {
  478. position.update(position.pos + details.delta);
  479. });
  480. _chatModel?.setChatWindowPosition(position.pos);
  481. },
  482. child: Material(
  483. child: Container(
  484. width: _width,
  485. height: _height,
  486. decoration:
  487. BoxDecoration(border: Border.all(color: MyTheme.border)),
  488. child: widget.builder(context),
  489. ),
  490. ),
  491. ),
  492. ),
  493. ],
  494. );
  495. }
  496. }
  497. class QualityMonitor extends StatelessWidget {
  498. final QualityMonitorModel qualityMonitorModel;
  499. QualityMonitor(this.qualityMonitorModel);
  500. Widget _row(String info, String? value, {Color? rightColor}) {
  501. return Row(
  502. children: [
  503. Expanded(
  504. flex: 8,
  505. child: AutoSizeText(info,
  506. style: TextStyle(color: Color.fromARGB(255, 210, 210, 210)),
  507. textAlign: TextAlign.right,
  508. maxLines: 1)),
  509. Spacer(flex: 1),
  510. Expanded(
  511. flex: 8,
  512. child: AutoSizeText(value ?? '',
  513. style: TextStyle(color: rightColor ?? Colors.white),
  514. maxLines: 1)),
  515. ],
  516. );
  517. }
  518. @override
  519. Widget build(BuildContext context) => ChangeNotifierProvider.value(
  520. value: qualityMonitorModel,
  521. child: Consumer<QualityMonitorModel>(
  522. builder: (context, qualityMonitorModel, child) => qualityMonitorModel
  523. .show
  524. ? Container(
  525. constraints: BoxConstraints(maxWidth: 200),
  526. padding: const EdgeInsets.all(8),
  527. color: MyTheme.canvasColor.withAlpha(150),
  528. child: Column(
  529. crossAxisAlignment: CrossAxisAlignment.start,
  530. children: [
  531. _row("Speed", qualityMonitorModel.data.speed ?? '-'),
  532. _row("FPS", qualityMonitorModel.data.fps ?? '-'),
  533. // let delay be 0 if fps is 0
  534. _row(
  535. "Delay",
  536. "${qualityMonitorModel.data.delay == null ? '-' : (qualityMonitorModel.data.fps ?? "").replaceAll(' ', '').replaceAll('0', '').isEmpty ? 0 : qualityMonitorModel.data.delay}ms",
  537. rightColor: Colors.green),
  538. _row("Target Bitrate",
  539. "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
  540. _row(
  541. "Codec", qualityMonitorModel.data.codecFormat ?? '-'),
  542. _row("Chroma", qualityMonitorModel.data.chroma ?? '-'),
  543. ],
  544. ),
  545. )
  546. : const SizedBox.shrink()));
  547. }
  548. class BlockableOverlayState extends OverlayKeyState {
  549. final _middleBlocked = false.obs;
  550. VoidCallback? onMiddleBlockedClick; // to-do use listener
  551. RxBool get middleBlocked => _middleBlocked;
  552. void addMiddleBlockedListener(void Function(bool) cb) {
  553. _middleBlocked.listen(cb);
  554. }
  555. void setMiddleBlocked(bool blocked) {
  556. if (blocked != _middleBlocked.value) {
  557. _middleBlocked.value = blocked;
  558. }
  559. }
  560. void applyFfi(FFI ffi) {
  561. ffi.dialogManager.setOverlayState(this);
  562. ffi.chatModel.setOverlayState(this);
  563. // make remote page penetrable automatically, effective for chat over remote
  564. onMiddleBlockedClick = () {
  565. setMiddleBlocked(false);
  566. };
  567. }
  568. }
  569. class BlockableOverlay extends StatelessWidget {
  570. final Widget underlying;
  571. final List<OverlayEntry>? upperLayer;
  572. final BlockableOverlayState state;
  573. BlockableOverlay(
  574. {required this.underlying, required this.state, this.upperLayer});
  575. @override
  576. Widget build(BuildContext context) {
  577. final initialEntries = [
  578. OverlayEntry(builder: (_) => underlying),
  579. /// middle layer
  580. OverlayEntry(
  581. builder: (context) => Obx(() => Listener(
  582. onPointerDown: (_) {
  583. state.onMiddleBlockedClick?.call();
  584. },
  585. child: Container(
  586. color:
  587. state.middleBlocked.value ? Colors.transparent : null)))),
  588. ];
  589. if (upperLayer != null) {
  590. initialEntries.addAll(upperLayer!);
  591. }
  592. /// set key
  593. return Overlay(key: state.key, initialEntries: initialEntries);
  594. }
  595. }