diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b991c7a96..6ba917bf3 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -664,8 +664,6 @@ Future initGlobalFFI() async { debugPrint("_globalFFI init end"); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); - // trigger connection status updater - await bind.mainCheckConnectStatus(); // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); } diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 29219df2a..d366d06ec 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -233,7 +233,6 @@ class _ConnectionPageState extends State { }, child: Container( height: 24, - width: 72, alignment: Alignment.center, decoration: BoxDecoration( color: ftPressed.value @@ -257,7 +256,7 @@ class _ConnectionPageState extends State { color: ftPressed.value ? MyTheme.color(context).bg : MyTheme.color(context).text), - ), + ).marginSymmetric(horizontal: 12), ), )), SizedBox( @@ -272,7 +271,6 @@ class _ConnectionPageState extends State { onTap: onConnect, child: Container( height: 24, - width: 65, decoration: BoxDecoration( color: connPressed.value ? MyTheme.accent @@ -289,12 +287,12 @@ class _ConnectionPageState extends State { child: Center( child: Text( translate( - "Connection", + "Connect", ), style: TextStyle( fontSize: 12, color: MyTheme.color(context).bg), ), - ), + ).marginSymmetric(horizontal: 12), ), ), ), diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 1b9e04ebf..4175bd11b 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -93,7 +93,7 @@ class _ConnectionTabPageState extends State { body: Obx(() => DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.remoteScreen, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index a7a93d7ad..57ee43e14 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -46,7 +46,7 @@ class _DesktopTabPageState extends State { body: DesktopTab( controller: tabController, theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - isMainWindow: true, + tabType: DesktopTabType.main, tail: ActionIcon( message: 'Settings', icon: IconFont.menu, diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index e7f08a516..6c8b58a30 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -37,7 +37,7 @@ class _FileManagerTabPageState extends State { @override void initState() { super.initState(); - + tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { @@ -74,9 +74,9 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.fileTransfer, onClose: () { - tabController.clear(); + tabController.clear(); }, tail: AddButton( theme: theme, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 8db4c7f98..1e2c8e2bc 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -19,11 +19,13 @@ class PortForwardTabPage extends StatefulWidget { class _PortForwardTabPageState extends State { final tabController = Get.put(DesktopTabController()); + late final bool isRDP; - static final IconData selectedIcon = Icons.forward_sharp; - static final IconData unselectedIcon = Icons.forward_outlined; + static const IconData selectedIcon = Icons.forward_sharp; + static const IconData unselectedIcon = Icons.forward_outlined; _PortForwardTabPageState(Map params) { + isRDP = params['isRDP']; tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -32,7 +34,7 @@ class _PortForwardTabPageState extends State { page: PortForwardPage( key: ValueKey(params['id']), id: params['id'], - isRDP: params['isRDP'], + isRDP: isRDP, ))); } @@ -76,7 +78,7 @@ class _PortForwardTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward, onClose: () { tabController.clear(); }, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d96efc710..e7922403b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -111,7 +111,7 @@ class ConnectionManagerState extends State { showMaximize: false, showMinimize: false, controller: serverModel.tabController, - isMainWindow: true, + tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: client.isFileTransfer, child: IconButton( - onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), + onPressed: () => checkClickTime( + client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), icon: Icon(Icons.message_outlined), ), ) @@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), padding: EdgeInsets.all(4.0), child: InkWell( - onTap: () => onTap?.call(!enabled), + onTap: () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)), child: Image( image: icon, width: 50, @@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => handleDisconnect(context), + onTap: () => + checkClickTime(client.id, () => handleDisconnect(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => handleAccept(context), + onTap: () => + checkClickTime(client.id, () => handleAccept(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget { borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey)), child: InkWell( - onTap: () => handleDisconnect(context), + onTap: () => + checkClickTime(client.id, () => handleDisconnect(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -572,3 +577,12 @@ Widget clientInfo(Client client) { ), ])); } + +void checkClickTime(int id, Function() callback) async { + var clickCallbackTime = DateTime.now().millisecondsSinceEpoch; + await bind.cmCheckClickTime(connId: id); + Timer(const Duration(milliseconds: 120), () async { + var d = clickCallbackTime - await bind.cmGetClickTime(); + if (d > 120) callback(); + }); +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 6e0ce747d..a89d0da38 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; @@ -34,6 +36,15 @@ class TabInfo { required this.page}); } +enum DesktopTabType { + main, + cm, + remoteScreen, + fileTransfer, + portForward, + rdp, +} + class DesktopTabState { final List tabs = []; final ScrollPosController scrollController = @@ -64,6 +75,7 @@ class DesktopTabController { state.update((val) { val!.tabs.add(tab); }); + state.value.scrollController.itemCount = state.value.tabs.length; toIndex = state.value.tabs.length - 1; assert(toIndex >= 0); } @@ -96,8 +108,16 @@ class DesktopTabController { void jumpTo(int index) { state.update((val) { val!.selected = index; - val.pageController.jumpToPage(index); - val.scrollController.scrollToItem(index, center: true, animate: true); + Future.delayed(Duration.zero, (() { + if (val.pageController.hasClients) { + val.pageController.jumpToPage(index); + } + if (val.scrollController.hasClients && + val.scrollController.canScroll && + val.scrollController.itemCount >= index) { + val.scrollController.scrollToItem(index, center: true, animate: true); + } + })); }); onSelected?.call(index); } @@ -134,6 +154,7 @@ typedef LabelGetter = Rx Function(String key); class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; + final DesktopTabType tabType; final bool isMainWindow; final bool showTabBar; final bool showLogo; @@ -152,7 +173,7 @@ class DesktopTab extends StatelessWidget { const DesktopTab({ required this.controller, - required this.isMainWindow, + required this.tabType, this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, @@ -166,7 +187,8 @@ class DesktopTab extends StatelessWidget { this.onClose, this.tabBuilder, this.labelGetter, - }); + }) : isMainWindow = + tabType == DesktopTabType.main || tabType == DesktopTabType.cm; @override Widget build(BuildContext context) { @@ -195,11 +217,48 @@ class DesktopTab extends StatelessWidget { ]); } + Widget _buildBlock({required Widget child}) { + if (tabType != DesktopTabType.main) { + return child; + } + var block = false.obs; + return Obx(() => MouseRegion( + onEnter: (_) async { + if (!option2bool( + 'allow-remote-config-modification', + await bind.mainGetOption( + key: 'allow-remote-config-modification'))) { + var time0 = DateTime.now().millisecondsSinceEpoch; + await bind.mainCheckMouseTime(); + Timer(const Duration(milliseconds: 120), () async { + var d = time0 - await bind.mainGetMouseTime(); + if (d < 120) { + block.value = true; + } + }); + } + }, + onExit: (_) => block.value = false, + child: Stack( + children: [ + child, + Offstage( + offstage: !block.value, + child: Container( + color: Colors.black.withOpacity(0.5), + )), + ], + ), + )); + } + Widget _buildPageView() { - return Obx(() => PageView( - controller: state.value.pageController, - children: - state.value.tabs.map((tab) => tab.page).toList(growable: false))); + return _buildBlock( + child: Obx(() => PageView( + controller: state.value.pageController, + children: state.value.tabs + .map((tab) => tab.page) + .toList(growable: false)))); } Widget _buildBar() { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index efca88180..e1c254942 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -81,6 +81,8 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { await initEnv(kAppTypeMain); + // trigger connection status updater + await bind.mainCheckConnectStatus(); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -89,10 +91,11 @@ void runMainApp(bool startService) async { } runApp(App()); // set window option - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(const Size(1280, 720)); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(const Size(1280, 720)); windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); + await windowManager.show(); + await windowManager.focus(); }); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index db8030782..a17b3d695 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,12 +22,13 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - check_super_user_permission, discover, forget_password, get_api_server, get_app_name, - get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, - get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, - get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, - set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, - store_fav, test_if_valid_server, update_temporary_password, using_public_server, + check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, + get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, + get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, + get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec, + has_rendezvous_service, post_request, set_local_option, set_option, set_options, + set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, + update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -472,7 +473,7 @@ pub fn main_get_connect_status() -> String { pub fn main_check_connect_status() { #[cfg(not(any(target_os = "android", target_os = "ios")))] - check_connect_status(true); + check_mouse_time(); // avoid multi calls } pub fn main_is_using_public_server() -> bool { @@ -764,6 +765,14 @@ pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } +pub fn main_check_mouse_time() { + check_mouse_time(); +} + +pub fn main_get_mouse_time() -> f64 { + get_mouse_time() +} + pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); }