From 962b5d114417b1fb0434313b8646248d6f342a2a Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 25 Oct 2022 21:36:01 +0900 Subject: [PATCH 1/5] mv chat_page to common/widgets & opt chat_page color style --- flutter/lib/{mobile/pages => common/widgets}/chat_page.dart | 4 +++- flutter/lib/common/widgets/overlay.dart | 2 +- flutter/lib/desktop/pages/server_page.dart | 2 +- flutter/lib/mobile/pages/home_page.dart | 2 +- flutter/lib/models/chat_model.dart | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) rename flutter/lib/{mobile/pages => common/widgets}/chat_page.dart (97%) diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart similarity index 97% rename from flutter/lib/mobile/pages/chat_page.dart rename to flutter/lib/common/widgets/chat_page.dart index 11794cb3d..affd674a2 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -4,7 +4,7 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:provider/provider.dart'; -import 'home_page.dart'; +import '../../mobile/pages/home_page.dart'; class ChatPage extends StatelessWidget implements PageShape { late final ChatModel chatModel; @@ -94,6 +94,8 @@ class ChatPage extends StatelessWidget implements PageShape { messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, + currentUserTextColor: Colors.white, + textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, messageDecorationBuilder: (_, __, ___) => defaultMessageDecoration( diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 97815a998..44b3f1308 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -3,9 +3,9 @@ import 'package:flutter_hbb/common.dart'; import 'package:provider/provider.dart'; import '../../desktop/widgets/tabbar_widget.dart'; -import '../../mobile/pages/chat_page.dart'; import '../../models/chat_model.dart'; import '../../models/model.dart'; +import 'chat_page.dart'; class DraggableChatWindow extends StatelessWidget { const DraggableChatWindow( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d721cda4d..94e0c2197 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:flutter_hbb/mobile/pages/chat_page.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -13,6 +12,7 @@ import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../common.dart'; +import '../../common/widgets/chat_page.dart'; import '../../models/platform_model.dart'; import '../../models/server_model.dart'; diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 31240b895..f806c2576 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/mobile/pages/chat_page.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import '../../common.dart'; +import '../../common/widgets/chat_page.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 3ebc97d61..9a0d89ef4 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -108,6 +108,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay(); } }, + backgroundColor: Theme.of(context).colorScheme.primary, child: Icon(Icons.message))); }); overlayState.insert(overlay); From 64d11a9dda9dd8a251f1130cfe2bfeb7321085e7 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 26 Oct 2022 21:02:20 +0900 Subject: [PATCH 2/5] fix cm tabs scroll action --- flutter/lib/desktop/widgets/tabbar_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9c88575ec..edc9fd352 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -127,12 +127,12 @@ class DesktopTabController { if (!isDesktop || index < 0) return; state.update((val) { val!.selected = index; - Future.delayed(Duration.zero, (() { + Future.delayed(Duration(milliseconds: 100), (() { if (val.pageController.hasClients) { val.pageController.jumpToPage(index); } + val.scrollController.itemCount = val.tabs.length; if (val.scrollController.hasClients && - val.scrollController.canScroll && val.scrollController.itemCount > index) { val.scrollController .scrollToItem(index, center: false, animate: true); From e759b62f5d9dd15520ddd7b2e52d55702b810f6a Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 26 Oct 2022 21:13:32 +0900 Subject: [PATCH 3/5] opt cm chat icon style --- flutter/lib/common/widgets/overlay.dart | 9 ++++---- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/file_manager_page.dart | 22 +++++++++---------- flutter/lib/desktop/pages/server_page.dart | 8 +++---- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 44b3f1308..81797962e 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:provider/provider.dart'; +import '../../consts.dart'; import '../../desktop/widgets/tabbar_widget.dart'; import '../../models/chat_model.dart'; import '../../models/model.dart'; @@ -173,17 +174,17 @@ class DraggableMobileActions extends StatelessWidget { IconButton( color: Colors.white, onPressed: onBackPressed, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.arrow_back)), IconButton( color: Colors.white, onPressed: onHomePressed, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.home)), IconButton( color: Colors.white, onPressed: onRecentPressed, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.more_horiz)), const VerticalDivider( width: 0, @@ -194,7 +195,7 @@ class DraggableMobileActions extends StatelessWidget { IconButton( color: Colors.white, onPressed: onHidePressed, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.keyboard_arrow_down)), ], ), diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 726ae24be..b040ed9b0 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -37,6 +37,7 @@ const Size kConnectionManagerWindowSize = Size(300, 400); // Tabbar transition duration, now we remove the duration const Duration kTabTransitionDuration = Duration.zero; const double kEmptyMarginTop = 50; +const double kDesktopIconButtonSplashRadius = 20; /// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse const kDefaultScrollAmountMultiplier = 5.0; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e31a0e1d9..042158fcb 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -175,7 +175,7 @@ class _FileManagerPageState extends State }, child: IconButton( icon: const Icon(Icons.more_vert), - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, onPressed: () => mod_menu.showMenu( context: context, position: menuPos, @@ -482,12 +482,12 @@ class _FileManagerPageState extends State onPressed: () { model.resumeJob(item.id); }, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.restart_alt_rounded)), ), IconButton( icon: const Icon(Icons.delete_forever_outlined), - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, onPressed: () { model.jobTable.removeAt(index); model.cancelJob(item.id); @@ -556,7 +556,7 @@ class _FileManagerPageState extends State children: [ IconButton( icon: const Icon(Icons.arrow_back), - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, onPressed: () { selectedItems.clear(); model.goBack(isLocal: isLocal); @@ -564,7 +564,7 @@ class _FileManagerPageState extends State ), IconButton( icon: const Icon(Icons.arrow_upward), - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, onPressed: () { selectedItems.clear(); model.goToParentDirectory(isLocal: isLocal); @@ -614,13 +614,13 @@ class _FileManagerPageState extends State Future.delayed( Duration.zero, () => focusNode.requestFocus()); }, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: Icon(Icons.search)); case LocationStatus.pathLocation: return IconButton( color: Theme.of(context).disabledColor, onPressed: null, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: Icon(Icons.close)); case LocationStatus.fileSearchBar: return IconButton( @@ -638,7 +638,7 @@ class _FileManagerPageState extends State breadCrumbScrollToEnd(isLocal); model.refresh(isLocal: isLocal); }, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.refresh)), ], ), @@ -655,7 +655,7 @@ class _FileManagerPageState extends State model.goHome(isLocal: isLocal); }, icon: const Icon(Icons.home_outlined), - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, ), IconButton( onPressed: () { @@ -704,7 +704,7 @@ class _FileManagerPageState extends State ); }); }, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.create_new_folder_outlined)), IconButton( onPressed: validItems(selectedItems) @@ -714,7 +714,7 @@ class _FileManagerPageState extends State selectedItems.clear(); } : null, - splashRadius: 20, + splashRadius: kDesktopIconButtonSplashRadius, icon: const Icon(Icons.delete_forever_outlined)), menu(isLocal: isLocal), ], diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 94e0c2197..1dd4a0563 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -334,10 +334,10 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: !client.authorized || client.isFileTransfer, child: IconButton( - onPressed: () => checkClickTime( - client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), - icon: Icon(Icons.message_outlined), - ), + onPressed: () => checkClickTime( + client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), + icon: Icon(Icons.message_outlined), + splashRadius: kDesktopIconButtonSplashRadius), ) ], ); From 5a905174e7325d886969138c35fb8f66493cdb29 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 26 Oct 2022 21:39:28 +0900 Subject: [PATCH 4/5] cancel cm hidden timer when active --- flutter/lib/desktop/pages/server_page.dart | 68 +++++++++++-------- .../lib/desktop/widgets/tabbar_widget.dart | 12 +--- flutter/lib/models/server_model.dart | 36 ++++++---- 3 files changed, 62 insertions(+), 54 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 1dd4a0563..206095345 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -107,6 +107,13 @@ class ConnectionManagerState extends State { @override Widget build(BuildContext context) { final serverModel = Provider.of(context); + final pointerHandler = serverModel.cmHiddenTimer != null + ? (PointerEvent e) { + serverModel.cmHiddenTimer!.cancel(); + serverModel.cmHiddenTimer = null; + debugPrint("CM hidden timer has been canceled"); + } + : null; return serverModel.clients.isEmpty ? Column( children: [ @@ -118,35 +125,38 @@ class ConnectionManagerState extends State { ), ], ) - : DesktopTab( - showTitle: false, - showMaximize: false, - showMinimize: true, - showClose: true, - controller: serverModel.tabController, - maxLabelWidth: 100, - tail: buildScrollJumper(), - selectedTabBackgroundColor: - Theme.of(context).hintColor.withOpacity(0.2), - tabBuilder: (key, icon, label, themeConf) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - Tooltip( - message: key, - waitDuration: Duration(seconds: 1), - child: label), - ], - ); - }, - pageViewBuilder: (pageView) => Row(children: [ - Expanded(child: pageView), - Consumer( - builder: (_, model, child) => model.isShowChatPage - ? Expanded(child: Scaffold(body: ChatPage())) - : Offstage()) - ])); + : Listener( + onPointerDown: pointerHandler, + onPointerMove: pointerHandler, + child: DesktopTab( + showTitle: false, + showMaximize: false, + showMinimize: true, + showClose: true, + controller: serverModel.tabController, + maxLabelWidth: 100, + tail: buildScrollJumper(), + selectedTabBackgroundColor: + Theme.of(context).hintColor.withOpacity(0.2), + tabBuilder: (key, icon, label, themeConf) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: key, + waitDuration: Duration(seconds: 1), + child: label), + ], + ); + }, + pageViewBuilder: (pageView) => Row(children: [ + Expanded(child: pageView), + Consumer( + builder: (_, model, child) => model.isShowChatPage + ? Expanded(child: Scaffold(body: ChatPage())) + : Offstage()) + ]))); } Widget buildTitleBar() { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index edc9fd352..f4a50c725 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -73,7 +73,7 @@ class DesktopTabController { int get length => state.value.tabs.length; - void add(TabInfo tab, {bool authorized = false}) { + void add(TabInfo tab) { if (!isDesktop) return; final index = state.value.tabs.indexWhere((e) => e.key == tab.key); int toIndex; @@ -87,16 +87,6 @@ class DesktopTabController { toIndex = state.value.tabs.length - 1; assert(toIndex >= 0); } - if (tabType == DesktopTabType.cm) { - Future.delayed(Duration.zero, () async { - window_on_top(null); - }); - if (authorized) { - Future.delayed(const Duration(seconds: 3), () { - windowManager.minimize(); - }); - } - } try { jumpTo(toIndex); } catch (e) { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index c37af81f5..b2fdc0f30 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:wakelock/wakelock.dart'; +import 'package:window_manager/window_manager.dart'; import '../common.dart'; import '../common/formatter/id_formatter.dart'; @@ -37,6 +38,8 @@ class ServerModel with ChangeNotifier { final List _clients = []; + Timer? cmHiddenTimer; + bool get isStart => _isStart; bool get mediaOk => _mediaOk; @@ -353,13 +356,7 @@ class ServerModel with ChangeNotifier { for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); _clients.add(client); - tabController.add( - TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client)), - authorized: client.authorized); + _addTab(client); } notifyListeners(); } catch (e) { @@ -384,13 +381,7 @@ class ServerModel with ChangeNotifier { } _clients.add(client); } - tabController.add( - TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client)), - authorized: client.authorized); + _addTab(client); // remove disconnected final index_disconnected = _clients .indexWhere((c) => c.disconnected && c.peerId == client.peerId); @@ -406,6 +397,23 @@ class ServerModel with ChangeNotifier { } } + void _addTab(Client client) { + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); + Future.delayed(Duration.zero, () async { + window_on_top(null); + }); + if (client.authorized) { + cmHiddenTimer = Timer(const Duration(seconds: 3), () { + windowManager.minimize(); + cmHiddenTimer = null; + }); + } + } + void showLoginDialog(Client client) { parent.target?.dialogManager.show((setState, close) { cancel() { From c100505fa1a9f6e0a5cdab0aa12379d253d02495 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 26 Oct 2022 23:50:36 +0900 Subject: [PATCH 5/5] desktop cm chat feat: disable auto jumpTo other page when current hasFocus & add unread message mark on tab --- flutter/lib/common/widgets/chat_page.dart | 1 + flutter/lib/desktop/pages/server_page.dart | 8 +++- .../lib/desktop/widgets/tabbar_widget.dart | 38 +++++++--------- flutter/lib/models/chat_model.dart | 44 ++++++++++++------- flutter/lib/models/server_model.dart | 14 +++++- flutter/test/cm_test.dart | 12 +++-- 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index affd674a2..3b4e06a6f 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -61,6 +61,7 @@ class ChatPage extends StatelessWidget implements PageShape { [], inputOptions: InputOptions( sendOnEnter: true, + focusNode: chatModel.inputNode, inputTextStyle: TextStyle( fontSize: 14, color: Theme.of(context) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 206095345..2211a0b0d 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -139,14 +139,20 @@ class ConnectionManagerState extends State { selectedTabBackgroundColor: Theme.of(context).hintColor.withOpacity(0.2), tabBuilder: (key, icon, label, themeConf) { + final client = serverModel.clients.firstWhereOrNull( + (client) => client.id.toString() == key); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - icon, Tooltip( message: key, waitDuration: Duration(seconds: 1), child: label), + Obx(() => Offstage( + offstage: + !(client?.hasUnreadChatMessage.value ?? false), + child: + Icon(Icons.circle, color: Colors.red, size: 10))) ], ); }, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index f4a50c725..92c868c34 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -29,6 +29,7 @@ class TabInfo { final IconData? unselectedIcon; final bool closable; final VoidCallback? onTabCloseButton; + final VoidCallback? onTap; final Widget page; TabInfo( @@ -38,6 +39,7 @@ class TabInfo { this.unselectedIcon, this.closable = true, this.onTabCloseButton, + this.onTap, required this.page}); } @@ -56,6 +58,8 @@ class DesktopTabState { final PageController pageController = PageController(); int selected = 0; + TabInfo get selectedTabInfo => tabs[selected]; + DesktopTabState() { scrollController.itemCount = tabs.length; } @@ -173,7 +177,6 @@ typedef LabelGetter = Rx Function(String key); int _lastClickTime = DateTime.now().millisecondsSinceEpoch; class DesktopTab extends StatelessWidget { - final Function(String)? onTabClose; final bool showTabBar; final bool showLogo; final bool showTitle; @@ -201,7 +204,6 @@ class DesktopTab extends StatelessWidget { DesktopTab({ Key? key, required this.controller, - this.onTabClose, this.showTabBar = true, this.showLogo = true, this.showTitle = true, @@ -357,7 +359,6 @@ class DesktopTab extends StatelessWidget { }, child: _ListView( controller: controller, - onTabClose: onTabClose, tabBuilder: tabBuilder, labelGetter: labelGetter, maxLabelWidth: maxLabelWidth, @@ -613,7 +614,6 @@ Future closeConfirmDialog() async { class _ListView extends StatelessWidget { final DesktopTabController controller; - final Function(String key)? onTabClose; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; @@ -625,7 +625,6 @@ class _ListView extends StatelessWidget { const _ListView( {required this.controller, - required this.onTabClose, this.tabBuilder, this.labelGetter, this.maxLabelWidth, @@ -654,7 +653,9 @@ class _ListView extends StatelessWidget { final index = e.key; final tab = e.value; return _Tab( + key: ValueKey(tab.key), index: index, + tabInfoKey: tab.key, label: labelGetter == null ? Rx(tab.label) : labelGetter!(tab.label), @@ -669,18 +670,11 @@ class _ListView extends StatelessWidget { controller.remove(index); } }, - onSelected: () => controller.jumpTo(index), - tabBuilder: tabBuilder == null - ? null - : (String key, Widget icon, Widget labelWidget, - TabThemeConf themeConf) { - return tabBuilder!( - tab.label, - icon, - labelWidget, - themeConf, - ); - }, + onTap: () { + controller.jumpTo(index); + tab.onTap?.call(); + }, + tabBuilder: tabBuilder, maxLabelWidth: maxLabelWidth, selectedTabBackgroundColor: selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, @@ -691,13 +685,14 @@ class _ListView extends StatelessWidget { class _Tab extends StatefulWidget { final int index; + final String tabInfoKey; final Rx label; final IconData? selectedIcon; final IconData? unselectedIcon; final bool closable; final int selected; final Function() onClose; - final Function() onSelected; + final Function() onTap; final TabBuilder? tabBuilder; final double? maxLabelWidth; final Color? selectedTabBackgroundColor; @@ -706,6 +701,7 @@ class _Tab extends StatefulWidget { const _Tab({ Key? key, required this.index, + required this.tabInfoKey, required this.label, this.selectedIcon, this.unselectedIcon, @@ -713,7 +709,7 @@ class _Tab extends StatefulWidget { required this.closable, required this.selected, required this.onClose, - required this.onSelected, + required this.onTap, this.maxLabelWidth, this.selectedTabBackgroundColor, this.unSelectedTabBackgroundColor, @@ -763,7 +759,7 @@ class _TabState extends State<_Tab> with RestorationMixin { ], ); } else { - return widget.tabBuilder!(widget.label.value, icon, labelWidget, + return widget.tabBuilder!(widget.tabInfoKey, icon, labelWidget, TabThemeConf(iconSize: _kIconSize)); } } @@ -780,7 +776,7 @@ class _TabState extends State<_Tab> with RestorationMixin { hover.value = value; restoreHover.value = value; }, - onTap: () => widget.onSelected(), + onTap: () => widget.onTap(), child: Container( color: isSelected ? widget.selectedTabBackgroundColor diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 9a0d89ef4..aaa5e1da5 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -14,11 +14,11 @@ class MessageBody { MessageBody(this.chatUser, this.chatMessages); void insert(ChatMessage cm) { - this.chatMessages.insert(0, cm); + chatMessages.insert(0, cm); } void clear() { - this.chatMessages.clear(); + chatMessages.clear(); } } @@ -54,6 +54,8 @@ class ChatModel with ChangeNotifier { ChatModel(this.parent); + FocusNode inputNode = FocusNode(); + ChatUser get currentUser { final user = messages[currentID]?.chatUser; if (user == null) { @@ -199,6 +201,11 @@ class ChatModel with ChangeNotifier { } receive(int id, String text) async { + final session = parent.target; + if (session == null) { + debugPrint("Failed to receive msg, session state is null"); + return; + } if (text.isEmpty) return; // mobile: first message show overlay icon if (chatIconOverlayEntry == null) { @@ -208,27 +215,32 @@ class ChatModel with ChangeNotifier { if (!_isShowChatPage) { toggleCMChatPage(id); } - parent.target?.serverModel.jumpTo(id); - late final chatUser; + int toId = currentID; + + late final ChatUser chatUser; if (id == clientModeID) { chatUser = ChatUser( - firstName: parent.target?.ffiModel.pi.username, - id: await bind.mainGetLastRemoteId(), + firstName: session.ffiModel.pi.username, + id: session.id, ); + toId = id; } else { - final client = parent.target?.serverModel.clients - .firstWhere((client) => client.id == id); - if (client == null) { - return debugPrint("Failed to receive msg,user doesn't exist"); - } + final client = + session.serverModel.clients.firstWhere((client) => client.id == id); if (isDesktop) { window_on_top(null); - var index = parent.target?.serverModel.clients - .indexWhere((client) => client.id == id); - if (index != null && index >= 0) { - gFFI.serverModel.tabController.jumpTo(index); + // disable auto jumpTo other tab when hasFocus, and mark unread message + final currentSelectedTab = + session.serverModel.tabController.state.value.selectedTabInfo; + if (currentSelectedTab.key != id.toString() && inputNode.hasFocus) { + client.hasUnreadChatMessage.value = true; + } else { + parent.target?.serverModel.jumpTo(id); + toId = id; } + } else { + toId = id; } chatUser = ChatUser(id: client.peerId, firstName: client.name); } @@ -238,7 +250,7 @@ class ChatModel with ChangeNotifier { } _messages[id]!.insert( ChatMessage(text: text, user: chatUser, createdAt: DateTime.now())); - _currentID = id; + _currentID = toId; notifyListeners(); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index b2fdc0f30..f74ca620d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; import 'package:wakelock/wakelock.dart'; import 'package:window_manager/window_manager.dart'; @@ -402,6 +403,15 @@ class ServerModel with ChangeNotifier { key: client.id.toString(), label: client.name, closable: false, + onTap: () { + if (client.hasUnreadChatMessage.value) { + client.hasUnreadChatMessage.value = false; + final chatModel = parent.target!.chatModel; + if (!chatModel.isShowChatPage) { + chatModel.toggleCMChatPage(client.id); + } + } + }, page: Desktop.buildConnectionCard(client))); Future.delayed(Duration.zero, () async { window_on_top(null); @@ -538,6 +548,8 @@ class Client { bool recording = false; bool disconnected = false; + RxBool hasUnreadChatMessage = false.obs; + Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); @@ -557,7 +569,7 @@ class Client { } Map toJson() { - final Map data = new Map(); + final Map data = {}; data['id'] = id; data['is_start'] = authorized; data['is_file_transfer'] = isFileTransfer; diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index c709d618a..592a28fcf 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -26,13 +26,11 @@ void main(List args) async { await initEnv(kAppTypeMain); for (var client in testClients) { gFFI.serverModel.clients.add(client); - gFFI.serverModel.tabController.add( - TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: buildConnectionCard(client)), - authorized: client.authorized); + gFFI.serverModel.tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: buildConnectionCard(client))); } runApp(GetMaterialApp(