diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e963993f7..8c4216020 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -195,7 +195,7 @@ closeConnection({String? id}) { if (isAndroid || isIOS) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } else { - closeTab(id); + DesktopTabBar.close(id); } } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index be5ec82af..ece7df5ca 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -33,6 +33,7 @@ class _ConnectionTabPageState extends State { _ConnectionTabPageState(Map params) { if (params['id'] != null) { tabs.add(TabInfo( + key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); @@ -53,6 +54,7 @@ class _ConnectionTabPageState extends State { DesktopTabBar.onAdd( tabs, TabInfo( + key: id, label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 5c108f39f..141b7ca0e 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -22,6 +22,7 @@ class _DesktopTabPageState extends State { super.initState(); tabs = RxList.from([ TabInfo( + key: kTabLabelHomePage, label: kTabLabelHomePage, selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined, @@ -70,6 +71,7 @@ class _DesktopTabPageState extends State { DesktopTabBar.onAdd( tabs, TabInfo( + key: kTabLabelSettingPage, label: kTabLabelSettingPage, selectedIcon: Icons.build_sharp, unselectedIcon: Icons.build_outlined)); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 12b5b20ff..7e94724bb 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -29,6 +29,7 @@ class _FileManagerTabPageState extends State { _FileManagerTabPageState(Map params) { if (params['id'] != null) { tabs.add(TabInfo( + key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); @@ -49,6 +50,7 @@ class _FileManagerTabPageState extends State { DesktopTabBar.onAdd( tabs, TabInfo( + key: id, label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b08fcae6e..e8f9ec26b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:flutter/material.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'; import 'package:window_manager/window_manager.dart'; @@ -24,8 +27,11 @@ class _DesktopServerPageState extends State Widget build(BuildContext context) { super.build(context); - return ChangeNotifierProvider.value( - value: gFFI.serverModel, + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.serverModel), + ChangeNotifierProvider.value(value: gFFI.chatModel), + ], child: Consumer( builder: (context, serverModel, child) => Material( child: Center( @@ -44,14 +50,28 @@ class _DesktopServerPageState extends State bool get wantKeepAlive => true; } -class ConnectionManager extends StatelessWidget { +class ConnectionManager extends StatefulWidget { + @override + State createState() => ConnectionManagerState(); +} + +class ConnectionManagerState extends State { + @override + void initState() { + gFFI.serverModel.updateClientState(); + // test + // gFFI.serverModel.clients.forEach((client) { + // DesktopTabBar.onAdd( + // gFFI.serverModel.tabs, + // TabInfo( + // key: client.id.toString(), label: client.name, closable: false)); + // }); + super.initState(); + } + @override Widget build(BuildContext context) { final serverModel = Provider.of(context); - // test case: - // serverModel.clients.clear(); - // serverModel.clients[0] = Client( - // false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); return serverModel.clients.isEmpty ? Column( children: [ @@ -63,27 +83,37 @@ class ConnectionManager extends StatelessWidget { ), ], ) - : DefaultTabController( - length: serverModel.clients.length, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: kTextTabBarHeight, - child: buildTitleBar(TabBar( - isScrollable: true, - tabs: serverModel.clients.entries - .map((entry) => buildTab(entry)) - .toList(growable: false))), - ), - Expanded( - child: TabBarView( - children: serverModel.clients.entries - .map((entry) => buildConnectionCard(entry)) - .toList(growable: false)), - ) - ], - ), + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: kTextTabBarHeight, + child: Obx(() => DesktopTabBar( + dark: isDarkTheme(), + mainTab: true, + tabs: serverModel.tabs, + showTitle: false, + showMaximize: false, + showMinimize: false, + onSelected: (index) => gFFI.chatModel + .changeCurrentID(serverModel.clients[index].id), + )), + ), + Expanded( + child: Row(children: [ + Expanded( + child: PageView( + controller: DesktopTabBar.controller.value, + children: serverModel.clients + .map((client) => buildConnectionCard(client)) + .toList(growable: false))), + Consumer( + builder: (_, model, child) => model.isShowChatPage + ? Expanded(child: Scaffold(body: ChatPage())) + : Offstage()) + ]), + ) + ], ); } @@ -106,12 +136,11 @@ class ConnectionManager extends StatelessWidget { ); } - Widget buildConnectionCard(MapEntry entry) { - final client = entry.value; + Widget buildConnectionCard(Client client) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, - key: ValueKey(entry.key), + key: ValueKey(client.id), children: [ _CmHeader(client: client), client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), @@ -124,14 +153,14 @@ class ConnectionManager extends StatelessWidget { ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); } - Widget buildTab(MapEntry entry) { + Widget buildTab(Client client) { return Tab( child: Row( children: [ SizedBox( width: 80, child: Text( - "${entry.value.name}", + "${client.name}", maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -261,7 +290,7 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: client.isFileTransfer, child: IconButton( - onPressed: handleSendMsg, + onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), icon: Icon(Icons.message_outlined), ), ) @@ -269,8 +298,6 @@ class _CmHeaderState extends State<_CmHeader> ); } - void handleSendMsg() {} - @override bool get wantKeepAlive => true; } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index bf39f4dc6..74019c815 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -14,36 +14,19 @@ const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kActionIconSize = 12; -final _tabBarKey = GlobalKey(); - -void closeTab(String? id) { - final tabBar = _tabBarKey.currentWidget as _ListView?; - if (tabBar == null) return; - final tabs = tabBar.tabs; - if (id == null) { - if (tabBar.selected.value < tabs.length) { - tabs[tabBar.selected.value].onClose(); - } - } else { - for (final tab in tabs) { - if (tab.label == id) { - tab.onClose(); - break; - } - } - } -} class TabInfo { + late final String key; late final String label; - late final IconData selectedIcon; - late final IconData unselectedIcon; + late final IconData? selectedIcon; + late final IconData? unselectedIcon; late final bool closable; TabInfo( - {required this.label, - required this.selectedIcon, - required this.unselectedIcon, + {required this.key, + required this.label, + this.selectedIcon, + this.unselectedIcon, this.closable = true}); } @@ -53,20 +36,33 @@ class DesktopTabBar extends StatelessWidget { late final bool dark; late final _Theme _theme; late final bool mainTab; - late final Function()? onAddSetting; + late final bool showLogo; + late final bool showTitle; + late final bool showMinimize; + late final bool showMaximize; + late final bool showClose; + late final void Function()? onAddSetting; + late final void Function(int)? onSelected; final ScrollPosController scrollController = ScrollPosController(itemCount: 0); static final Rx controller = PageController().obs; static final Rx selected = 0.obs; + static final _tabBarListViewKey = GlobalKey(); - DesktopTabBar({ - Key? key, - required this.tabs, - this.onTabClose, - required this.dark, - required this.mainTab, - this.onAddSetting, - }) : _theme = dark ? _Theme.dark() : _Theme.light(), + DesktopTabBar( + {Key? key, + required this.tabs, + this.onTabClose, + required this.dark, + required this.mainTab, + this.onAddSetting, + this.onSelected, + this.showLogo = true, + this.showTitle = true, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true}) + : _theme = dark ? _Theme.dark() : _Theme.light(), super(key: key) { scrollController.itemCount = tabs.length; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -88,22 +84,23 @@ class DesktopTabBar extends StatelessWidget { Expanded( child: Row( children: [ - Offstage( - offstage: !mainTab, - child: Row(children: [ - Image.asset( - 'assets/logo.ico', - width: 20, - height: 20, - ), - Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2), - ]).marginOnly( - left: 5, - right: 10, - ), + Row(children: [ + Offstage( + offstage: !showLogo, + child: Image.asset( + 'assets/logo.ico', + width: 20, + height: 20, + )), + Offstage( + offstage: !showTitle, + child: Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, ), Expanded( child: GestureDetector( @@ -116,13 +113,14 @@ class DesktopTabBar extends StatelessWidget { } }, child: _ListView( - key: _tabBarKey, + key: _tabBarListViewKey, controller: controller, scrollController: scrollController, tabInfos: tabs, selected: selected, onTabClose: onTabClose, - theme: _theme)), + theme: _theme, + onSelected: onSelected)), ), Offstage( offstage: mainTab, @@ -146,6 +144,9 @@ class DesktopTabBar extends StatelessWidget { WindowActionPanel( mainTab: mainTab, theme: _theme, + showMinimize: showMinimize, + showMaximize: showMaximize, + showClose: showClose, ) ], ), @@ -160,7 +161,7 @@ class DesktopTabBar extends StatelessWidget { } static onAdd(RxList tabs, TabInfo tab) { - int index = tabs.indexWhere((e) => e.label == tab.label); + int index = tabs.indexWhere((e) => e.key == tab.key); if (index >= 0) { selected.value = index; } else { @@ -168,86 +169,148 @@ class DesktopTabBar extends StatelessWidget { selected.value = tabs.length - 1; assert(selected.value >= 0); } + try { + controller.value.jumpToPage(selected.value); + } catch (e) { + // call before binding controller will throw + debugPrint("Failed to jumpToPage: $e"); + } + } + + static remove(RxList tabs, int index) { + if (index < 0) return; + if (index == tabs.length - 1) { + selected.value = max(0, selected.value - 1); + } else if (index < tabs.length - 1 && index < selected.value) { + selected.value = max(0, selected.value - 1); + } + tabs.removeAt(index); controller.value.jumpToPage(selected.value); } + + static void jumpTo(RxList tabs, int index) { + if (index < 0 || index >= tabs.length) return; + selected.value = index; + controller.value.jumpToPage(selected.value); + } + + static void close(String? key) { + final tabBar = _tabBarListViewKey.currentWidget as _ListView?; + if (tabBar == null) return; + final tabs = tabBar.tabs; + if (key == null) { + if (tabBar.selected.value < tabs.length) { + tabs[tabBar.selected.value].onClose(); + } + } else { + for (final tab in tabs) { + if (tab.key == key) { + tab.onClose(); + break; + } + } + } + } } class WindowActionPanel extends StatelessWidget { final bool mainTab; final _Theme theme; + final bool showMinimize; + final bool showMaximize; + final bool showClose; + const WindowActionPanel( - {Key? key, required this.mainTab, required this.theme}) + {Key? key, + required this.mainTab, + required this.theme, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true}) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ - _ActionIcon( - message: 'Minimize', - icon: IconFont.min, - theme: theme, - onTap: () { - if (mainTab) { - windowManager.minimize(); - } else { - WindowController.fromWindowId(windowId!).minimize(); - } - }, - is_close: false, - ), - FutureBuilder(builder: (context, snapshot) { - RxBool is_maximized = false.obs; - if (mainTab) { - windowManager.isMaximized().then((maximized) { - is_maximized.value = maximized; - }); - } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - is_maximized.value = maximized; - }); - } - return Obx( - () => _ActionIcon( - message: is_maximized.value ? "Restore" : "Maximize", - icon: is_maximized.value ? IconFont.restore : IconFont.max, + Offstage( + offstage: !showMinimize, + child: _ActionIcon( + message: 'Minimize', + icon: IconFont.min, theme: theme, onTap: () { if (mainTab) { - if (is_maximized.value) { - windowManager.unmaximize(); - } else { - windowManager.maximize(); - } + windowManager.minimize(); } else { - final wc = WindowController.fromWindowId(windowId!); - if (is_maximized.value) { - wc.unmaximize(); - } else { - wc.maximize(); - } + WindowController.fromWindowId(windowId!).minimize(); } - is_maximized.value = !is_maximized.value; }, is_close: false, - ), - ); - }), - _ActionIcon( - message: 'Close', - icon: IconFont.close, - theme: theme, - onTap: () { - if (mainTab) { - windowManager.close(); - } else { - WindowController.fromWindowId(windowId!).close(); - } - }, - is_close: true, - ), + )), + Offstage( + offstage: !showMaximize, + child: FutureBuilder(builder: (context, snapshot) { + RxBool is_maximized = false.obs; + if (mainTab) { + windowManager.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } + return Obx( + () => _ActionIcon( + message: is_maximized.value ? "Restore" : "Maximize", + icon: is_maximized.value ? IconFont.restore : IconFont.max, + theme: theme, + onTap: () { + if (mainTab) { + if (is_maximized.value) { + windowManager.unmaximize(); + } else { + WindowController.fromWindowId(windowId!).minimize(); + } + } else { + final wc = WindowController.fromWindowId(windowId!); + if (is_maximized.value) { + wc.unmaximize(); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + if (maximized) { + wc.unmaximize(); + } else { + wc.maximize(); + } + }); + } + } + is_maximized.value = !is_maximized.value; + }, + is_close: false, + ), + ); + })), + Offstage( + offstage: !showClose, + child: _ActionIcon( + message: 'Close', + icon: IconFont.close, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.close(); + } else { + WindowController.fromWindowId(windowId!).close(); + } + }, + is_close: true, + )), ], ); } @@ -259,19 +322,21 @@ class _ListView extends StatelessWidget { final ScrollPosController scrollController; final RxList tabInfos; final Rx selected; - final Function(String label)? onTabClose; + final Function(String key)? onTabClose; final _Theme _theme; late List<_Tab> tabs; + late final void Function(int)? onSelected; - _ListView({ - Key? key, - required this.controller, - required this.scrollController, - required this.tabInfos, - required this.selected, - required this.onTabClose, - required _Theme theme, - }) : _theme = theme, + _ListView( + {Key? key, + required this.controller, + required this.scrollController, + required this.tabInfos, + required this.selected, + required this.onTabClose, + required _Theme theme, + this.onSelected}) + : _theme = theme, super(key: key); @override @@ -279,17 +344,16 @@ class _ListView extends StatelessWidget { return Obx(() { tabs = tabInfos.asMap().entries.map((e) { int index = e.key; - String label = e.value.label; return _Tab( index: index, - label: label, + label: e.value.label, selectedIcon: e.value.selectedIcon, unselectedIcon: e.value.unselectedIcon, closable: e.value.closable, selected: selected.value, onClose: () { - tabInfos.removeWhere((tab) => tab.label == label); - onTabClose?.call(label); + tabInfos.removeWhere((tab) => tab.key == e.value.key); + onTabClose?.call(e.value.key); if (index <= selected.value) { selected.value = max(0, selected.value - 1); } @@ -305,6 +369,7 @@ class _ListView extends StatelessWidget { selected.value = index; scrollController.scrollToItem(index, center: true, animate: true); controller.value.jumpToPage(index); + onSelected?.call(selected.value); }, theme: _theme, ); @@ -322,8 +387,8 @@ class _ListView extends StatelessWidget { class _Tab extends StatelessWidget { late final int index; late final String label; - late final IconData selectedIcon; - late final IconData unselectedIcon; + late final IconData? selectedIcon; + late final IconData? unselectedIcon; late final bool closable; late final int selected; late final Function() onClose; @@ -335,8 +400,8 @@ class _Tab extends StatelessWidget { {Key? key, required this.index, required this.label, - required this.selectedIcon, - required this.unselectedIcon, + this.selectedIcon, + this.unselectedIcon, required this.closable, required this.selected, required this.onClose, @@ -346,6 +411,7 @@ class _Tab extends StatelessWidget { @override Widget build(BuildContext context) { + bool show_icon = selectedIcon != null && unselectedIcon != null; bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; return Ink( @@ -362,13 +428,15 @@ class _Tab extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - is_selected ? selectedIcon : unselectedIcon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5), + Offstage( + offstage: !show_icon, + child: Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5)), Text( translate(label), textAlign: TextAlign.center, diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 74e436ebb..abbc5aadc 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -359,12 +359,12 @@ class ConnectionManager extends StatelessWidget { Widget build(BuildContext context) { final serverModel = Provider.of(context); return Column( - children: serverModel.clients.entries - .map((entry) => PaddingCard( - title: translate(entry.value.isFileTransfer + children: serverModel.clients + .map((client) => PaddingCard( + title: translate(client.isFileTransfer ? "File Connection" : "Screen Connection"), - titleIcon: entry.value.isFileTransfer + titleIcon: client.isFileTransfer ? Icons.folder_outlined : Icons.mobile_screen_share, child: Column( @@ -373,16 +373,14 @@ class ConnectionManager extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: clientInfo(entry.value)), + Expanded(child: clientInfo(client)), Expanded( flex: -1, - child: entry.value.isFileTransfer || - !entry.value.authorized + child: client.isFileTransfer || !client.authorized ? SizedBox.shrink() : IconButton( onPressed: () { - gFFI.chatModel - .changeCurrentID(entry.value.id); + gFFI.chatModel.changeCurrentID(client.id); final bar = navigationBarKey.currentWidget; if (bar != null) { @@ -396,37 +394,35 @@ class ConnectionManager extends StatelessWidget { ))) ], ), - entry.value.authorized + client.authorized ? SizedBox.shrink() : Text( translate("android_new_connection_tip"), style: TextStyle(color: Colors.black54), ), - entry.value.authorized + client.authorized ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - bind.cmCloseConnection(connId: entry.key); + bind.cmCloseConnection(connId: client.id); gFFI.invokeMethod( - "cancel_notification", entry.key); + "cancel_notification", client.id); }, label: Text(translate("Close"))) : Row(children: [ TextButton( child: Text(translate("Dismiss")), onPressed: () { - serverModel.sendLoginResponse( - entry.value, false); + serverModel.sendLoginResponse(client, false); }), SizedBox(width: 20), ElevatedButton( child: Text(translate("Accept")), onPressed: () { - serverModel.sendLoginResponse( - entry.value, true); + serverModel.sendLoginResponse(client, true); }), ]), ], diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9f69dd04a..527cea689 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -4,9 +4,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; import 'package:wakelock/wakelock.dart'; import '../common.dart'; +import '../desktop/widgets/tabbar_widget.dart'; import '../mobile/pages/server_page.dart'; import 'model.dart'; @@ -30,7 +32,9 @@ class ServerModel with ChangeNotifier { late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); - Map _clients = {}; + RxList tabs = RxList.empty(growable: true); + + List _clients = []; bool get isStart => _isStart; @@ -76,7 +80,7 @@ class ServerModel with ChangeNotifier { TextEditingController get serverPasswd => _serverPasswd; - Map get clients => _clients; + List get clients => _clients; final controller = ScrollController(); @@ -338,6 +342,7 @@ class ServerModel with ChangeNotifier { notifyListeners(); } + // force updateClientState([String? json]) async { var res = await bind.mainGetClientsState(); try { @@ -347,9 +352,16 @@ class ServerModel with ChangeNotifier { exit(0); } _clients.clear(); + tabs.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); - _clients[client.id] = client; + _clients.add(client); + DesktopTabBar.onAdd( + tabs, + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false)); } notifyListeners(); } catch (e) { @@ -360,10 +372,14 @@ class ServerModel with ChangeNotifier { void loginRequest(Map evt) { try { final client = Client.fromJson(jsonDecode(evt["client"])); - if (_clients.containsKey(client.id)) { + if (_clients.any((c) => c.id == client.id)) { return; } - _clients[client.id] = client; + _clients.add(client); + DesktopTabBar.onAdd( + tabs, + TabInfo( + key: client.id.toString(), label: client.name, closable: false)); scrollToBottom(); notifyListeners(); if (isAndroid) showLoginDialog(client); @@ -419,6 +435,7 @@ class ServerModel with ChangeNotifier { } scrollToBottom() { + if (isDesktop) return; Future.delayed(Duration(milliseconds: 200), () { controller.animateTo(controller.position.maxScrollExtent, duration: Duration(milliseconds: 200), @@ -433,12 +450,14 @@ class ServerModel with ChangeNotifier { parent.target?.invokeMethod("start_capture"); } parent.target?.invokeMethod("cancel_notification", client.id); - _clients[client.id]?.authorized = true; + client.authorized = true; notifyListeners(); } else { bind.cmLoginRes(connId: client.id, res: res); parent.target?.invokeMethod("cancel_notification", client.id); - _clients.remove(client.id); + final index = _clients.indexOf(client); + DesktopTabBar.remove(tabs, index); + _clients.remove(client); } } @@ -446,7 +465,11 @@ class ServerModel with ChangeNotifier { try { final client = Client.fromJson(jsonDecode(evt['client'])); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); - _clients[client.id] = client; + _clients.add(client); + DesktopTabBar.onAdd( + tabs, + TabInfo( + key: client.id.toString(), label: client.name, closable: false)); scrollToBottom(); notifyListeners(); } catch (e) {} @@ -455,8 +478,10 @@ class ServerModel with ChangeNotifier { void onClientRemove(Map evt) { try { final id = int.parse(evt['id'] as String); - if (_clients.containsKey(id)) { - _clients.remove(id); + if (_clients.any((c) => c.id == id)) { + final index = _clients.indexWhere((client) => client.id == id); + _clients.removeAt(index); + DesktopTabBar.remove(tabs, index); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.invokeMethod("cancel_notification", id); } @@ -467,10 +492,11 @@ class ServerModel with ChangeNotifier { } closeAll() { - _clients.forEach((id, client) { - bind.cmCloseConnection(connId: id); + _clients.forEach((client) { + bind.cmCloseConnection(connId: client.id); }); _clients.clear(); + tabs.clear(); } } @@ -486,7 +512,7 @@ class Client { bool file = false; bool restart = false; - Client(this.authorized, this.isFileTransfer, this.name, this.peerId, + Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); Client.fromJson(Map json) {