1. update DesktopTabBar for cm.

2. refactor server_model clients map -> list.
3. update tab changing events.
This commit is contained in:
csf 2022-08-22 20:18:31 +08:00
parent b33d1f216f
commit 14b8140e45
8 changed files with 325 additions and 202 deletions

@ -195,7 +195,7 @@ closeConnection({String? id}) {
if (isAndroid || isIOS) { if (isAndroid || isIOS) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
} else { } else {
closeTab(id); DesktopTabBar.close(id);
} }
} }

@ -33,6 +33,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_ConnectionTabPageState(Map<String, dynamic> params) { _ConnectionTabPageState(Map<String, dynamic> params) {
if (params['id'] != null) { if (params['id'] != null) {
tabs.add(TabInfo( tabs.add(TabInfo(
key: params['id'],
label: params['id'], label: params['id'],
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon)); unselectedIcon: unselectedIcon));
@ -53,6 +54,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
DesktopTabBar.onAdd( DesktopTabBar.onAdd(
tabs, tabs,
TabInfo( TabInfo(
key: id,
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon)); unselectedIcon: unselectedIcon));

@ -22,6 +22,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
super.initState(); super.initState();
tabs = RxList.from([ tabs = RxList.from([
TabInfo( TabInfo(
key: kTabLabelHomePage,
label: kTabLabelHomePage, label: kTabLabelHomePage,
selectedIcon: Icons.home_sharp, selectedIcon: Icons.home_sharp,
unselectedIcon: Icons.home_outlined, unselectedIcon: Icons.home_outlined,
@ -70,6 +71,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
DesktopTabBar.onAdd( DesktopTabBar.onAdd(
tabs, tabs,
TabInfo( TabInfo(
key: kTabLabelSettingPage,
label: kTabLabelSettingPage, label: kTabLabelSettingPage,
selectedIcon: Icons.build_sharp, selectedIcon: Icons.build_sharp,
unselectedIcon: Icons.build_outlined)); unselectedIcon: Icons.build_outlined));

@ -29,6 +29,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
_FileManagerTabPageState(Map<String, dynamic> params) { _FileManagerTabPageState(Map<String, dynamic> params) {
if (params['id'] != null) { if (params['id'] != null) {
tabs.add(TabInfo( tabs.add(TabInfo(
key: params['id'],
label: params['id'], label: params['id'],
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon)); unselectedIcon: unselectedIcon));
@ -49,6 +50,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
DesktopTabBar.onAdd( DesktopTabBar.onAdd(
tabs, tabs,
TabInfo( TabInfo(
key: id,
label: id, label: id,
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon)); unselectedIcon: unselectedIcon));

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; 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:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -24,8 +27,11 @@ class _DesktopServerPageState extends State<DesktopServerPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return ChangeNotifierProvider.value( return MultiProvider(
value: gFFI.serverModel, providers: [
ChangeNotifierProvider.value(value: gFFI.serverModel),
ChangeNotifierProvider.value(value: gFFI.chatModel),
],
child: Consumer<ServerModel>( child: Consumer<ServerModel>(
builder: (context, serverModel, child) => Material( builder: (context, serverModel, child) => Material(
child: Center( child: Center(
@ -44,14 +50,28 @@ class _DesktopServerPageState extends State<DesktopServerPage>
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }
class ConnectionManager extends StatelessWidget { class ConnectionManager extends StatefulWidget {
@override
State<StatefulWidget> createState() => ConnectionManagerState();
}
class ConnectionManagerState extends State<ConnectionManager> {
@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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context); final serverModel = Provider.of<ServerModel>(context);
// test case:
// serverModel.clients.clear();
// serverModel.clients[0] = Client(
// false, false, "Readmi-M21sdfsdf", "123123123", true, false, false);
return serverModel.clients.isEmpty return serverModel.clients.isEmpty
? Column( ? Column(
children: [ children: [
@ -63,27 +83,37 @@ class ConnectionManager extends StatelessWidget {
), ),
], ],
) )
: DefaultTabController( : Column(
length: serverModel.clients.length, crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, SizedBox(
children: [ height: kTextTabBarHeight,
SizedBox( child: Obx(() => DesktopTabBar(
height: kTextTabBarHeight, dark: isDarkTheme(),
child: buildTitleBar(TabBar( mainTab: true,
isScrollable: true, tabs: serverModel.tabs,
tabs: serverModel.clients.entries showTitle: false,
.map((entry) => buildTab(entry)) showMaximize: false,
.toList(growable: false))), showMinimize: false,
), onSelected: (index) => gFFI.chatModel
Expanded( .changeCurrentID(serverModel.clients[index].id),
child: TabBarView( )),
children: serverModel.clients.entries ),
.map((entry) => buildConnectionCard(entry)) Expanded(
.toList(growable: false)), child: Row(children: [
) Expanded(
], child: PageView(
), controller: DesktopTabBar.controller.value,
children: serverModel.clients
.map((client) => buildConnectionCard(client))
.toList(growable: false))),
Consumer<ChatModel>(
builder: (_, model, child) => model.isShowChatPage
? Expanded(child: Scaffold(body: ChatPage()))
: Offstage())
]),
)
],
); );
} }
@ -106,12 +136,11 @@ class ConnectionManager extends StatelessWidget {
); );
} }
Widget buildConnectionCard(MapEntry<int, Client> entry) { Widget buildConnectionCard(Client client) {
final client = entry.value;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
key: ValueKey(entry.key), key: ValueKey(client.id),
children: [ children: [
_CmHeader(client: client), _CmHeader(client: client),
client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client),
@ -124,14 +153,14 @@ class ConnectionManager extends StatelessWidget {
).paddingSymmetric(vertical: 8.0, horizontal: 8.0); ).paddingSymmetric(vertical: 8.0, horizontal: 8.0);
} }
Widget buildTab(MapEntry<int, Client> entry) { Widget buildTab(Client client) {
return Tab( return Tab(
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
width: 80, width: 80,
child: Text( child: Text(
"${entry.value.name}", "${client.name}",
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -261,7 +290,7 @@ class _CmHeaderState extends State<_CmHeader>
Offstage( Offstage(
offstage: client.isFileTransfer, offstage: client.isFileTransfer,
child: IconButton( child: IconButton(
onPressed: handleSendMsg, onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id),
icon: Icon(Icons.message_outlined), icon: Icon(Icons.message_outlined),
), ),
) )
@ -269,8 +298,6 @@ class _CmHeaderState extends State<_CmHeader>
); );
} }
void handleSendMsg() {}
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }

@ -14,36 +14,19 @@ const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
const double _kIconSize = 18; const double _kIconSize = 18;
const double _kDividerIndent = 10; const double _kDividerIndent = 10;
const double _kActionIconSize = 12; 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 { class TabInfo {
late final String key;
late final String label; late final String label;
late final IconData selectedIcon; late final IconData? selectedIcon;
late final IconData unselectedIcon; late final IconData? unselectedIcon;
late final bool closable; late final bool closable;
TabInfo( TabInfo(
{required this.label, {required this.key,
required this.selectedIcon, required this.label,
required this.unselectedIcon, this.selectedIcon,
this.unselectedIcon,
this.closable = true}); this.closable = true});
} }
@ -53,20 +36,33 @@ class DesktopTabBar extends StatelessWidget {
late final bool dark; late final bool dark;
late final _Theme _theme; late final _Theme _theme;
late final bool mainTab; 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 = final ScrollPosController scrollController =
ScrollPosController(itemCount: 0); ScrollPosController(itemCount: 0);
static final Rx<PageController> controller = PageController().obs; static final Rx<PageController> controller = PageController().obs;
static final Rx<int> selected = 0.obs; static final Rx<int> selected = 0.obs;
static final _tabBarListViewKey = GlobalKey();
DesktopTabBar({ DesktopTabBar(
Key? key, {Key? key,
required this.tabs, required this.tabs,
this.onTabClose, this.onTabClose,
required this.dark, required this.dark,
required this.mainTab, required this.mainTab,
this.onAddSetting, this.onAddSetting,
}) : _theme = dark ? _Theme.dark() : _Theme.light(), 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) { super(key: key) {
scrollController.itemCount = tabs.length; scrollController.itemCount = tabs.length;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -88,22 +84,23 @@ class DesktopTabBar extends StatelessWidget {
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Offstage( Row(children: [
offstage: !mainTab, Offstage(
child: Row(children: [ offstage: !showLogo,
Image.asset( child: Image.asset(
'assets/logo.ico', 'assets/logo.ico',
width: 20, width: 20,
height: 20, height: 20,
), )),
Text( Offstage(
"RustDesk", offstage: !showTitle,
style: TextStyle(fontSize: 13), child: Text(
).marginOnly(left: 2), "RustDesk",
]).marginOnly( style: TextStyle(fontSize: 13),
left: 5, ).marginOnly(left: 2))
right: 10, ]).marginOnly(
), left: 5,
right: 10,
), ),
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
@ -116,13 +113,14 @@ class DesktopTabBar extends StatelessWidget {
} }
}, },
child: _ListView( child: _ListView(
key: _tabBarKey, key: _tabBarListViewKey,
controller: controller, controller: controller,
scrollController: scrollController, scrollController: scrollController,
tabInfos: tabs, tabInfos: tabs,
selected: selected, selected: selected,
onTabClose: onTabClose, onTabClose: onTabClose,
theme: _theme)), theme: _theme,
onSelected: onSelected)),
), ),
Offstage( Offstage(
offstage: mainTab, offstage: mainTab,
@ -146,6 +144,9 @@ class DesktopTabBar extends StatelessWidget {
WindowActionPanel( WindowActionPanel(
mainTab: mainTab, mainTab: mainTab,
theme: _theme, theme: _theme,
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
) )
], ],
), ),
@ -160,7 +161,7 @@ class DesktopTabBar extends StatelessWidget {
} }
static onAdd(RxList<TabInfo> tabs, TabInfo tab) { static onAdd(RxList<TabInfo> tabs, TabInfo tab) {
int index = tabs.indexWhere((e) => e.label == tab.label); int index = tabs.indexWhere((e) => e.key == tab.key);
if (index >= 0) { if (index >= 0) {
selected.value = index; selected.value = index;
} else { } else {
@ -168,86 +169,148 @@ class DesktopTabBar extends StatelessWidget {
selected.value = tabs.length - 1; selected.value = tabs.length - 1;
assert(selected.value >= 0); 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<TabInfo> 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); controller.value.jumpToPage(selected.value);
} }
static void jumpTo(RxList<TabInfo> 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 { class WindowActionPanel extends StatelessWidget {
final bool mainTab; final bool mainTab;
final _Theme theme; final _Theme theme;
final bool showMinimize;
final bool showMaximize;
final bool showClose;
const WindowActionPanel( 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); : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
_ActionIcon( Offstage(
message: 'Minimize', offstage: !showMinimize,
icon: IconFont.min, child: _ActionIcon(
theme: theme, message: 'Minimize',
onTap: () { icon: IconFont.min,
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,
theme: theme, theme: theme,
onTap: () { onTap: () {
if (mainTab) { if (mainTab) {
if (is_maximized.value) { windowManager.minimize();
windowManager.unmaximize();
} else {
windowManager.maximize();
}
} else { } else {
final wc = WindowController.fromWindowId(windowId!); WindowController.fromWindowId(windowId!).minimize();
if (is_maximized.value) {
wc.unmaximize();
} else {
wc.maximize();
}
} }
is_maximized.value = !is_maximized.value;
}, },
is_close: false, is_close: false,
), )),
); Offstage(
}), offstage: !showMaximize,
_ActionIcon( child: FutureBuilder(builder: (context, snapshot) {
message: 'Close', RxBool is_maximized = false.obs;
icon: IconFont.close, if (mainTab) {
theme: theme, windowManager.isMaximized().then((maximized) {
onTap: () { is_maximized.value = maximized;
if (mainTab) { });
windowManager.close(); } else {
} else { final wc = WindowController.fromWindowId(windowId!);
WindowController.fromWindowId(windowId!).close(); wc.isMaximized().then((maximized) {
} is_maximized.value = maximized;
}, });
is_close: true, }
), 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 ScrollPosController scrollController;
final RxList<TabInfo> tabInfos; final RxList<TabInfo> tabInfos;
final Rx<int> selected; final Rx<int> selected;
final Function(String label)? onTabClose; final Function(String key)? onTabClose;
final _Theme _theme; final _Theme _theme;
late List<_Tab> tabs; late List<_Tab> tabs;
late final void Function(int)? onSelected;
_ListView({ _ListView(
Key? key, {Key? key,
required this.controller, required this.controller,
required this.scrollController, required this.scrollController,
required this.tabInfos, required this.tabInfos,
required this.selected, required this.selected,
required this.onTabClose, required this.onTabClose,
required _Theme theme, required _Theme theme,
}) : _theme = theme, this.onSelected})
: _theme = theme,
super(key: key); super(key: key);
@override @override
@ -279,17 +344,16 @@ class _ListView extends StatelessWidget {
return Obx(() { return Obx(() {
tabs = tabInfos.asMap().entries.map((e) { tabs = tabInfos.asMap().entries.map((e) {
int index = e.key; int index = e.key;
String label = e.value.label;
return _Tab( return _Tab(
index: index, index: index,
label: label, label: e.value.label,
selectedIcon: e.value.selectedIcon, selectedIcon: e.value.selectedIcon,
unselectedIcon: e.value.unselectedIcon, unselectedIcon: e.value.unselectedIcon,
closable: e.value.closable, closable: e.value.closable,
selected: selected.value, selected: selected.value,
onClose: () { onClose: () {
tabInfos.removeWhere((tab) => tab.label == label); tabInfos.removeWhere((tab) => tab.key == e.value.key);
onTabClose?.call(label); onTabClose?.call(e.value.key);
if (index <= selected.value) { if (index <= selected.value) {
selected.value = max(0, selected.value - 1); selected.value = max(0, selected.value - 1);
} }
@ -305,6 +369,7 @@ class _ListView extends StatelessWidget {
selected.value = index; selected.value = index;
scrollController.scrollToItem(index, center: true, animate: true); scrollController.scrollToItem(index, center: true, animate: true);
controller.value.jumpToPage(index); controller.value.jumpToPage(index);
onSelected?.call(selected.value);
}, },
theme: _theme, theme: _theme,
); );
@ -322,8 +387,8 @@ class _ListView extends StatelessWidget {
class _Tab extends StatelessWidget { class _Tab extends StatelessWidget {
late final int index; late final int index;
late final String label; late final String label;
late final IconData selectedIcon; late final IconData? selectedIcon;
late final IconData unselectedIcon; late final IconData? unselectedIcon;
late final bool closable; late final bool closable;
late final int selected; late final int selected;
late final Function() onClose; late final Function() onClose;
@ -335,8 +400,8 @@ class _Tab extends StatelessWidget {
{Key? key, {Key? key,
required this.index, required this.index,
required this.label, required this.label,
required this.selectedIcon, this.selectedIcon,
required this.unselectedIcon, this.unselectedIcon,
required this.closable, required this.closable,
required this.selected, required this.selected,
required this.onClose, required this.onClose,
@ -346,6 +411,7 @@ class _Tab extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool show_icon = selectedIcon != null && unselectedIcon != null;
bool is_selected = index == selected; bool is_selected = index == selected;
bool show_divider = index != selected - 1 && index != selected; bool show_divider = index != selected - 1 && index != selected;
return Ink( return Ink(
@ -362,13 +428,15 @@ class _Tab extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Offstage(
is_selected ? selectedIcon : unselectedIcon, offstage: !show_icon,
size: _kIconSize, child: Icon(
color: is_selected is_selected ? selectedIcon : unselectedIcon,
? theme.selectedtabIconColor size: _kIconSize,
: theme.unSelectedtabIconColor, color: is_selected
).paddingOnly(right: 5), ? theme.selectedtabIconColor
: theme.unSelectedtabIconColor,
).paddingOnly(right: 5)),
Text( Text(
translate(label), translate(label),
textAlign: TextAlign.center, textAlign: TextAlign.center,

@ -359,12 +359,12 @@ class ConnectionManager extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context); final serverModel = Provider.of<ServerModel>(context);
return Column( return Column(
children: serverModel.clients.entries children: serverModel.clients
.map((entry) => PaddingCard( .map((client) => PaddingCard(
title: translate(entry.value.isFileTransfer title: translate(client.isFileTransfer
? "File Connection" ? "File Connection"
: "Screen Connection"), : "Screen Connection"),
titleIcon: entry.value.isFileTransfer titleIcon: client.isFileTransfer
? Icons.folder_outlined ? Icons.folder_outlined
: Icons.mobile_screen_share, : Icons.mobile_screen_share,
child: Column( child: Column(
@ -373,16 +373,14 @@ class ConnectionManager extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(child: clientInfo(entry.value)), Expanded(child: clientInfo(client)),
Expanded( Expanded(
flex: -1, flex: -1,
child: entry.value.isFileTransfer || child: client.isFileTransfer || !client.authorized
!entry.value.authorized
? SizedBox.shrink() ? SizedBox.shrink()
: IconButton( : IconButton(
onPressed: () { onPressed: () {
gFFI.chatModel gFFI.chatModel.changeCurrentID(client.id);
.changeCurrentID(entry.value.id);
final bar = final bar =
navigationBarKey.currentWidget; navigationBarKey.currentWidget;
if (bar != null) { if (bar != null) {
@ -396,37 +394,35 @@ class ConnectionManager extends StatelessWidget {
))) )))
], ],
), ),
entry.value.authorized client.authorized
? SizedBox.shrink() ? SizedBox.shrink()
: Text( : Text(
translate("android_new_connection_tip"), translate("android_new_connection_tip"),
style: TextStyle(color: Colors.black54), style: TextStyle(color: Colors.black54),
), ),
entry.value.authorized client.authorized
? ElevatedButton.icon( ? ElevatedButton.icon(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor:
MaterialStateProperty.all(Colors.red)), MaterialStateProperty.all(Colors.red)),
icon: Icon(Icons.close), icon: Icon(Icons.close),
onPressed: () { onPressed: () {
bind.cmCloseConnection(connId: entry.key); bind.cmCloseConnection(connId: client.id);
gFFI.invokeMethod( gFFI.invokeMethod(
"cancel_notification", entry.key); "cancel_notification", client.id);
}, },
label: Text(translate("Close"))) label: Text(translate("Close")))
: Row(children: [ : Row(children: [
TextButton( TextButton(
child: Text(translate("Dismiss")), child: Text(translate("Dismiss")),
onPressed: () { onPressed: () {
serverModel.sendLoginResponse( serverModel.sendLoginResponse(client, false);
entry.value, false);
}), }),
SizedBox(width: 20), SizedBox(width: 20),
ElevatedButton( ElevatedButton(
child: Text(translate("Accept")), child: Text(translate("Accept")),
onPressed: () { onPressed: () {
serverModel.sendLoginResponse( serverModel.sendLoginResponse(client, true);
entry.value, true);
}), }),
]), ]),
], ],

@ -4,9 +4,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/platform_model.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 'package:wakelock/wakelock.dart';
import '../common.dart'; import '../common.dart';
import '../desktop/widgets/tabbar_widget.dart';
import '../mobile/pages/server_page.dart'; import '../mobile/pages/server_page.dart';
import 'model.dart'; import 'model.dart';
@ -30,7 +32,9 @@ class ServerModel with ChangeNotifier {
late final TextEditingController _serverId; late final TextEditingController _serverId;
final _serverPasswd = TextEditingController(text: ""); final _serverPasswd = TextEditingController(text: "");
Map<int, Client> _clients = {}; RxList<TabInfo> tabs = RxList<TabInfo>.empty(growable: true);
List<Client> _clients = [];
bool get isStart => _isStart; bool get isStart => _isStart;
@ -76,7 +80,7 @@ class ServerModel with ChangeNotifier {
TextEditingController get serverPasswd => _serverPasswd; TextEditingController get serverPasswd => _serverPasswd;
Map<int, Client> get clients => _clients; List<Client> get clients => _clients;
final controller = ScrollController(); final controller = ScrollController();
@ -338,6 +342,7 @@ class ServerModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// force
updateClientState([String? json]) async { updateClientState([String? json]) async {
var res = await bind.mainGetClientsState(); var res = await bind.mainGetClientsState();
try { try {
@ -347,9 +352,16 @@ class ServerModel with ChangeNotifier {
exit(0); exit(0);
} }
_clients.clear(); _clients.clear();
tabs.clear();
for (var clientJson in clientsJson) { for (var clientJson in clientsJson) {
final client = Client.fromJson(clientJson); 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(); notifyListeners();
} catch (e) { } catch (e) {
@ -360,10 +372,14 @@ class ServerModel with ChangeNotifier {
void loginRequest(Map<String, dynamic> evt) { void loginRequest(Map<String, dynamic> evt) {
try { try {
final client = Client.fromJson(jsonDecode(evt["client"])); final client = Client.fromJson(jsonDecode(evt["client"]));
if (_clients.containsKey(client.id)) { if (_clients.any((c) => c.id == client.id)) {
return; return;
} }
_clients[client.id] = client; _clients.add(client);
DesktopTabBar.onAdd(
tabs,
TabInfo(
key: client.id.toString(), label: client.name, closable: false));
scrollToBottom(); scrollToBottom();
notifyListeners(); notifyListeners();
if (isAndroid) showLoginDialog(client); if (isAndroid) showLoginDialog(client);
@ -419,6 +435,7 @@ class ServerModel with ChangeNotifier {
} }
scrollToBottom() { scrollToBottom() {
if (isDesktop) return;
Future.delayed(Duration(milliseconds: 200), () { Future.delayed(Duration(milliseconds: 200), () {
controller.animateTo(controller.position.maxScrollExtent, controller.animateTo(controller.position.maxScrollExtent,
duration: Duration(milliseconds: 200), duration: Duration(milliseconds: 200),
@ -433,12 +450,14 @@ class ServerModel with ChangeNotifier {
parent.target?.invokeMethod("start_capture"); parent.target?.invokeMethod("start_capture");
} }
parent.target?.invokeMethod("cancel_notification", client.id); parent.target?.invokeMethod("cancel_notification", client.id);
_clients[client.id]?.authorized = true; client.authorized = true;
notifyListeners(); notifyListeners();
} else { } else {
bind.cmLoginRes(connId: client.id, res: res); bind.cmLoginRes(connId: client.id, res: res);
parent.target?.invokeMethod("cancel_notification", client.id); 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 { try {
final client = Client.fromJson(jsonDecode(evt['client'])); final client = Client.fromJson(jsonDecode(evt['client']));
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); 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(); scrollToBottom();
notifyListeners(); notifyListeners();
} catch (e) {} } catch (e) {}
@ -455,8 +478,10 @@ class ServerModel with ChangeNotifier {
void onClientRemove(Map<String, dynamic> evt) { void onClientRemove(Map<String, dynamic> evt) {
try { try {
final id = int.parse(evt['id'] as String); final id = int.parse(evt['id'] as String);
if (_clients.containsKey(id)) { if (_clients.any((c) => c.id == id)) {
_clients.remove(id); final index = _clients.indexWhere((client) => client.id == id);
_clients.removeAt(index);
DesktopTabBar.remove(tabs, index);
parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
parent.target?.invokeMethod("cancel_notification", id); parent.target?.invokeMethod("cancel_notification", id);
} }
@ -467,10 +492,11 @@ class ServerModel with ChangeNotifier {
} }
closeAll() { closeAll() {
_clients.forEach((id, client) { _clients.forEach((client) {
bind.cmCloseConnection(connId: id); bind.cmCloseConnection(connId: client.id);
}); });
_clients.clear(); _clients.clear();
tabs.clear();
} }
} }
@ -486,7 +512,7 @@ class Client {
bool file = false; bool file = false;
bool restart = 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); this.keyboard, this.clipboard, this.audio);
Client.fromJson(Map<String, dynamic> json) { Client.fromJson(Map<String, dynamic> json) {