Merge pull request #1346 from Heap-Hop/refactor_desktop_tab

refactor: flutter_desktop DesktopTab
This commit is contained in:
RustDesk 2022-08-24 22:07:21 +08:00 committed by GitHub
commit 06064ee9f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 442 additions and 495 deletions

View File

@ -206,7 +206,8 @@ closeConnection({String? id}) {
if (isAndroid || isIOS) { if (isAndroid || isIOS) {
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
} else { } else {
DesktopTabBar.close(id); final controller = Get.find<DesktopTabController>();
controller.closeBy(id);
} }
} }

View File

@ -21,28 +21,35 @@ class ConnectionTabPage extends StatefulWidget {
} }
class _ConnectionTabPageState extends State<ConnectionTabPage> { class _ConnectionTabPageState extends State<ConnectionTabPage> {
// refactor List<int> when using multi-tab final tabController = Get.put(DesktopTabController());
// this singleton is only for test
RxList<TabInfo> tabs = RxList<TabInfo>.empty(growable: true);
static final Rx<String> _fullscreenID = "".obs; static final Rx<String> _fullscreenID = "".obs;
final IconData selectedIcon = Icons.desktop_windows_sharp; static final IconData selectedIcon = Icons.desktop_windows_sharp;
final IconData unselectedIcon = Icons.desktop_windows_outlined; static final IconData unselectedIcon = Icons.desktop_windows_outlined;
var connectionMap = RxList<Widget>.empty(growable: true); var connectionMap = RxList<Widget>.empty(growable: true);
_ConnectionTabPageState(Map<String, dynamic> params) { _ConnectionTabPageState(Map<String, dynamic> params) {
if (params['id'] != null) { if (params['id'] != null) {
tabs.add(TabInfo( tabController.state.value.tabs.add(TabInfo(
key: params['id'], key: params['id'],
label: params['id'], label: params['id'],
selectedIcon: selectedIcon, selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon)); unselectedIcon: unselectedIcon,
page: RemotePage(
id: params['id'],
tabBarHeight:
_fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
} }
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemove = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); "call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
@ -51,18 +58,23 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
window_on_top(windowId()); window_on_top(windowId());
DesktopTabBar.onAdd( tabController.add(TabInfo(
tabs, key: id,
TabInfo( label: id,
key: id, selectedIcon: selectedIcon,
label: id, unselectedIcon: unselectedIcon,
selectedIcon: selectedIcon, closable: false,
unselectedIcon: unselectedIcon)); page: RemotePage(
id: id,
tabBarHeight: _fullscreenID.value.isNotEmpty
? 0
: kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)));
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
print( tabController.state.value.tabs.forEach((tab) {
"executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); print("executing onDestroy hook, closing ${tab.label}}");
tabs.forEach((tab) { final tag = tab.label;
final tag = '${tab.label}';
ffi(tag).close().then((_) { ffi(tag).close().then((_) {
Get.delete<FFI>(tag: tag); Get.delete<FFI>(tag: tag);
}); });
@ -74,49 +86,35 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea( return SubWindowDragToResizeArea(
windowId: windowId(), windowId: windowId(),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)), border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold( child: Scaffold(
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
body: Column( body: Obx(() => DesktopTab(
children: [ controller: tabController,
Obx(() => Visibility( theme: theme,
visible: _fullscreenID.value.isEmpty, isMainWindow: false,
child: DesktopTabBar( showTabBar: _fullscreenID.value.isEmpty,
tabs: tabs, tail: AddButton(
onTabClose: onRemoveId, theme: theme,
dark: isDarkTheme(), ).paddingOnly(left: 10),
mainTab: false, pageViewBuilder: (pageView) {
))), WindowController.fromWindowId(windowId())
Expanded(child: Obx(() { .setFullscreen(_fullscreenID.value.isNotEmpty);
WindowController.fromWindowId(windowId()) return pageView;
.setFullscreen(_fullscreenID.value.isNotEmpty); },
return PageView( ))),
controller: DesktopTabBar.controller.value,
children: tabs
.map((tab) => RemotePage(
key: ValueKey(tab.label),
id: tab.label,
tabBarHeight: _fullscreenID.value.isNotEmpty
? 0
: kDesktopRemoteTabBarHeight,
fullscreenID: _fullscreenID,
)) //RemotePage(key: ValueKey(e), id: e))
.toList());
})),
],
),
),
), ),
); );
} }
void onRemoveId(String id) { void onRemoveId(String id) {
ffi(id).close(); ffi(id).close();
if (tabs.length == 0) { if (tabController.state.value.tabs.length == 0) {
WindowController.fromWindowId(windowId()).close(); WindowController.fromWindowId(windowId()).close();
} }
} }

View File

@ -4,7 +4,6 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class DesktopTabPage extends StatefulWidget { class DesktopTabPage extends StatefulWidget {
@ -15,65 +14,51 @@ class DesktopTabPage extends StatefulWidget {
} }
class _DesktopTabPageState extends State<DesktopTabPage> { class _DesktopTabPageState extends State<DesktopTabPage> {
late RxList<TabInfo> tabs; final tabController = DesktopTabController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabs = RxList.from([ tabController.state.value.tabs.add(TabInfo(
TabInfo( key: kTabLabelHomePage,
key: kTabLabelHomePage, label: kTabLabelHomePage,
label: kTabLabelHomePage, selectedIcon: Icons.home_sharp,
selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined,
unselectedIcon: Icons.home_outlined, closable: false,
closable: false) page: DesktopHomePage()));
], growable: true);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dark = isDarkTheme();
return DragToResizeArea( return DragToResizeArea(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)), border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold( child: Scaffold(
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
body: Column( body: DesktopTab(
children: [ controller: tabController,
DesktopTabBar( theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
tabs: tabs, isMainWindow: true,
dark: isDarkTheme(), tail: ActionIcon(
mainTab: true, message: 'Settings',
onAddSetting: onAddSetting, icon: IconFont.menu,
theme: dark ? TarBarTheme.dark() : TarBarTheme.light(),
onTap: onAddSetting,
is_close: false,
), ),
Obx((() => Expanded( )),
child: PageView(
controller: DesktopTabBar.controller.value,
children: tabs.map((tab) {
switch (tab.label) {
case kTabLabelHomePage:
return DesktopHomePage(key: ValueKey(tab.label));
case kTabLabelSettingPage:
return DesktopSettingPage(key: ValueKey(tab.label));
default:
return Container();
}
}).toList()),
))),
],
),
),
), ),
); );
} }
void onAddSetting() { void onAddSetting() {
DesktopTabBar.onAdd( tabController.add(TabInfo(
tabs, key: kTabLabelSettingPage,
TabInfo( label: kTabLabelSettingPage,
key: kTabLabelSettingPage, selectedIcon: Icons.build_sharp,
label: kTabLabelSettingPage, unselectedIcon: Icons.build_outlined,
selectedIcon: Icons.build_sharp, page: DesktopSettingPage()));
unselectedIcon: Icons.build_outlined));
} }
} }

View File

@ -20,25 +20,26 @@ class FileManagerTabPage extends StatefulWidget {
} }
class _FileManagerTabPageState extends State<FileManagerTabPage> { class _FileManagerTabPageState extends State<FileManagerTabPage> {
// refactor List<int> when using multi-tab final tabController = Get.put(DesktopTabController());
// this singleton is only for test
RxList<TabInfo> tabs = List<TabInfo>.empty(growable: true).obs; static final IconData selectedIcon = Icons.file_copy_sharp;
final IconData selectedIcon = Icons.file_copy_sharp; static final IconData unselectedIcon = Icons.file_copy_outlined;
final IconData unselectedIcon = Icons.file_copy_outlined;
_FileManagerTabPageState(Map<String, dynamic> params) { _FileManagerTabPageState(Map<String, dynamic> params) {
if (params['id'] != null) { tabController.state.value.tabs.add(TabInfo(
tabs.add(TabInfo( key: params['id'],
key: params['id'], label: params['id'],
label: params['id'], selectedIcon: selectedIcon,
selectedIcon: selectedIcon, unselectedIcon: unselectedIcon,
unselectedIcon: unselectedIcon)); page: FileManagerPage(id: params['id'])));
}
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController.onRemove = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async { rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print( print(
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); "call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
@ -47,18 +48,16 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
final args = jsonDecode(call.arguments); final args = jsonDecode(call.arguments);
final id = args['id']; final id = args['id'];
window_on_top(windowId()); window_on_top(windowId());
DesktopTabBar.onAdd( tabController.add(TabInfo(
tabs, key: id,
TabInfo( label: id,
key: id, selectedIcon: selectedIcon,
label: id, unselectedIcon: unselectedIcon,
selectedIcon: selectedIcon, page: FileManagerPage(id: id)));
unselectedIcon: unselectedIcon));
} else if (call.method == "onDestroy") { } else if (call.method == "onDestroy") {
print( tabController.state.value.tabs.forEach((tab) {
"executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); print("executing onDestroy hook, closing ${tab.label}}");
tabs.forEach((tab) { final tag = tab.label;
final tag = 'ft_${tab.label}';
ffi(tag).close().then((_) { ffi(tag).close().then((_) {
Get.delete<FFI>(tag: tag); Get.delete<FFI>(tag: tag);
}); });
@ -70,43 +69,29 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light();
return SubWindowDragToResizeArea( return SubWindowDragToResizeArea(
windowId: windowId(), windowId: windowId(),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)), border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold( child: Scaffold(
backgroundColor: MyTheme.color(context).bg, backgroundColor: MyTheme.color(context).bg,
body: Column( body: DesktopTab(
children: [ controller: tabController,
DesktopTabBar( theme: theme,
tabs: tabs, isMainWindow: false,
onTabClose: onRemoveId, tail: AddButton(
dark: isDarkTheme(), theme: theme,
mainTab: false, ).paddingOnly(left: 10),
), )),
Expanded(
child: Obx(
() => PageView(
controller: DesktopTabBar.controller.value,
children: tabs
.map((tab) => FileManagerPage(
key: ValueKey(tab.label),
id: tab
.label)) //RemotePage(key: ValueKey(e), id: e))
.toList()),
),
)
],
),
),
), ),
); );
} }
void onRemoveId(String id) { void onRemoveId(String id) {
ffi("ft_$id").close(); ffi("ft_$id").close();
if (tabs.length == 0) { if (tabController.state.value.tabs.length == 0) {
WindowController.fromWindowId(windowId()).close(); WindowController.fromWindowId(windowId()).close();
} }
} }

View File

@ -79,6 +79,8 @@ class ConnectionManagerState extends State<ConnectionManager> {
@override @override
void initState() { void initState() {
gFFI.serverModel.updateClientState(); gFFI.serverModel.updateClientState();
gFFI.serverModel.tabController.onSelected = (index) =>
gFFI.chatModel.changeCurrentID(gFFI.serverModel.clients[index].id);
// test // test
// gFFI.serverModel.clients.forEach((client) { // gFFI.serverModel.clients.forEach((client) {
// DesktopTabBar.onAdd( // DesktopTabBar.onAdd(
@ -103,38 +105,20 @@ class ConnectionManagerState extends State<ConnectionManager> {
), ),
], ],
) )
: Column( : DesktopTab(
crossAxisAlignment: CrossAxisAlignment.start, theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(),
children: [ showTitle: false,
SizedBox( showMaximize: false,
height: kTextTabBarHeight, showMinimize: false,
child: Obx(() => DesktopTabBar( controller: serverModel.tabController,
dark: isDarkTheme(), isMainWindow: true,
mainTab: true, pageViewBuilder: (pageView) => Row(children: [
tabs: serverModel.tabs, Expanded(child: pageView),
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<ChatModel>( Consumer<ChatModel>(
builder: (_, model, child) => model.isShowChatPage builder: (_, model, child) => model.isShowChatPage
? Expanded(child: Scaffold(body: ChatPage())) ? Expanded(child: Scaffold(body: ChatPage()))
: Offstage()) : Offstage())
]), ]));
)
],
);
} }
Widget buildTitleBar(Widget middle) { Widget buildTitleBar(Widget middle) {
@ -156,23 +140,6 @@ class ConnectionManagerState extends State<ConnectionManager> {
); );
} }
Widget buildConnectionCard(Client client) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
key: ValueKey(client.id),
children: [
_CmHeader(client: client),
client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: _CmControlPanel(client: client),
))
],
).paddingSymmetric(vertical: 8.0, horizontal: 8.0);
}
Widget buildTab(Client client) { Widget buildTab(Client client) {
return Tab( return Tab(
child: Row( child: Row(
@ -191,6 +158,23 @@ class ConnectionManagerState extends State<ConnectionManager> {
} }
} }
Widget buildConnectionCard(Client client) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
key: ValueKey(client.id),
children: [
_CmHeader(client: client),
client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: _CmControlPanel(client: client),
))
],
).paddingSymmetric(vertical: 8.0, horizontal: 8.0);
}
class _AppIcon extends StatelessWidget { class _AppIcon extends StatelessWidget {
const _AppIcon({Key? key}) : super(key: key); const _AppIcon({Key? key}) : super(key: key);
@ -421,9 +405,11 @@ class _CmControlPanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return client.authorized return Consumer<ServerModel>(builder: (_, model, child) {
? buildAuthorized(context) return client.authorized
: buildUnAuthorized(context); ? buildAuthorized(context)
: buildUnAuthorized(context);
});
} }
buildAuthorized(BuildContext context) { buildAuthorized(BuildContext context) {

View File

@ -5,221 +5,239 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:scroll_pos/scroll_pos.dart'; import 'package:scroll_pos/scroll_pos.dart';
import '../../utils/multi_window_manager.dart';
const double _kTabBarHeight = kDesktopRemoteTabBarHeight; 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;
class TabInfo { class TabInfo {
late final String key; final String key;
late final String label; final String label;
late final IconData? selectedIcon; final IconData? selectedIcon;
late final IconData? unselectedIcon; final IconData? unselectedIcon;
late final bool closable; final bool closable;
final Widget page;
TabInfo( TabInfo(
{required this.key, {required this.key,
required this.label, required this.label,
this.selectedIcon, this.selectedIcon,
this.unselectedIcon, this.unselectedIcon,
this.closable = true}); this.closable = true,
required this.page});
} }
class DesktopTabBar extends StatelessWidget { class DesktopTabState {
late final RxList<TabInfo> tabs; final List<TabInfo> tabs = [];
late final Function(String)? onTabClose;
late final bool dark;
late final _Theme _theme;
late final bool mainTab;
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; final PageController pageController = PageController();
static final Rx<int> selected = 0.obs; int selected = 0;
static final _tabBarListViewKey = GlobalKey();
DesktopTabBar( DesktopTabState() {
{Key? key, scrollController.itemCount = tabs.length;
required this.tabs, }
}
class DesktopTabController {
final state = DesktopTabState().obs;
/// index, key
Function(int, String)? onRemove;
Function(int)? onSelected;
void add(TabInfo tab) {
if (!isDesktop) return;
final index = state.value.tabs.indexWhere((e) => e.key == tab.key);
int toIndex;
if (index >= 0) {
toIndex = index;
} else {
state.update((val) {
val!.tabs.add(tab);
});
toIndex = state.value.tabs.length - 1;
assert(toIndex >= 0);
}
try {
jumpTo(toIndex);
} catch (e) {
// call before binding controller will throw
debugPrint("Failed to jumpTo: $e");
}
}
void remove(int index) {
if (!isDesktop) return;
final len = state.value.tabs.length;
if (index < 0 || index > len - 1) return;
final key = state.value.tabs[index].key;
final currentSelected = state.value.selected;
int toIndex = 0;
if (index == len - 1) {
toIndex = max(0, currentSelected - 1);
} else if (index < len - 1 && index < currentSelected) {
toIndex = max(0, currentSelected - 1);
}
state.value.tabs.removeAt(index);
state.value.scrollController.itemCount = state.value.tabs.length;
jumpTo(toIndex);
onRemove?.call(index, key);
}
void jumpTo(int index) {
state.update((val) {
val!.selected = index;
val.pageController.jumpToPage(index);
val.scrollController.scrollToItem(index, center: true, animate: true);
});
onSelected?.call(index);
}
void closeBy(String? key) {
if (!isDesktop) return;
assert(onRemove != null);
if (key == null) {
if (state.value.selected < state.value.tabs.length) {
remove(state.value.selected);
}
} else {
state.value.tabs.indexWhere((tab) => tab.key == key);
remove(state.value.selected);
}
}
}
class DesktopTab extends StatelessWidget {
final Function(String)? onTabClose;
final TarBarTheme theme;
final bool isMainWindow;
final bool showTabBar;
final bool showLogo;
final bool showTitle;
final bool showMinimize;
final bool showMaximize;
final bool showClose;
final Widget Function(Widget pageView)? pageViewBuilder;
final Widget? tail;
final DesktopTabController controller;
late final state = controller.state;
DesktopTab(
{required this.controller,
required this.isMainWindow,
this.theme = const TarBarTheme.light(),
this.onTabClose, this.onTabClose,
required this.dark, this.showTabBar = true,
required this.mainTab,
this.onAddSetting,
this.onSelected,
this.showLogo = true, this.showLogo = true,
this.showTitle = true, this.showTitle = true,
this.showMinimize = true, this.showMinimize = true,
this.showMaximize = true, this.showMaximize = true,
this.showClose = true}) this.showClose = true,
: _theme = dark ? _Theme.dark() : _Theme.light(), this.pageViewBuilder,
super(key: key) { this.tail});
scrollController.itemCount = tabs.length;
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.scrollToItem(selected.value,
center: true, animate: true);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Column(children: [
height: _kTabBarHeight, Offstage(
child: Column( offstage: !showTabBar,
children: [ child: Container(
Container( height: _kTabBarHeight,
height: _kTabBarHeight - 1, child: Column(
child: Row(
children: [ children: [
Expanded( Container(
child: Row( height: _kTabBarHeight - 1,
children: [ child: _buildBar(),
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(
onPanStart: (_) {
if (mainTab) {
windowManager.startDragging();
} else {
WindowController.fromWindowId(windowId!)
.startDragging();
}
},
child: _ListView(
key: _tabBarListViewKey,
controller: controller,
scrollController: scrollController,
tabInfos: tabs,
selected: selected,
onTabClose: onTabClose,
theme: _theme,
onSelected: onSelected)),
),
Offstage(
offstage: mainTab,
child: _AddButton(
theme: _theme,
).paddingOnly(left: 10),
),
],
),
), ),
Offstage( Divider(
offstage: onAddSetting == null, height: 1,
child: _ActionIcon( thickness: 1,
message: 'Settings',
icon: IconFont.menu,
theme: _theme,
onTap: () => onAddSetting?.call(),
is_close: false,
),
), ),
WindowActionPanel(
mainTab: mainTab,
theme: _theme,
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
)
], ],
), ),
)),
Expanded(
child: pageViewBuilder != null
? pageViewBuilder!(_buildPageView())
: _buildPageView())
]);
}
Widget _buildPageView() {
return Obx(() => PageView(
controller: state.value.pageController,
children:
state.value.tabs.map((tab) => tab.page).toList(growable: false)));
}
Widget _buildBar() {
return Row(
children: [
Expanded(
child: Row(
children: [
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(
onPanStart: (_) {
if (isMainWindow) {
windowManager.startDragging();
} else {
WindowController.fromWindowId(windowId!)
.startDragging();
}
},
child: _ListView(
controller: controller,
onTabClose: onTabClose,
theme: theme,
)),
),
],
), ),
Divider( ),
height: 1, Offstage(offstage: tail == null, child: tail),
thickness: 1, WindowActionPanel(
), mainTab: isMainWindow,
], theme: theme,
), showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
)
],
); );
} }
static onAdd(RxList<TabInfo> tabs, TabInfo tab) {
if (!isDesktop) return;
int index = tabs.indexWhere((e) => e.key == tab.key);
if (index >= 0) {
selected.value = index;
} else {
tabs.add(tab);
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<TabInfo> tabs, int index) {
if (!isDesktop) return;
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<TabInfo> tabs, int index) {
if (!isDesktop) return;
if (index < 0 || index >= tabs.length) return;
selected.value = index;
controller.value.jumpToPage(selected.value);
}
static void close(String? key) {
if (!isDesktop) return;
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 TarBarTheme theme;
final bool showMinimize; final bool showMinimize;
final bool showMaximize; final bool showMaximize;
@ -240,7 +258,7 @@ class WindowActionPanel extends StatelessWidget {
children: [ children: [
Offstage( Offstage(
offstage: !showMinimize, offstage: !showMinimize,
child: _ActionIcon( child: ActionIcon(
message: 'Minimize', message: 'Minimize',
icon: IconFont.min, icon: IconFont.min,
theme: theme, theme: theme,
@ -269,7 +287,7 @@ class WindowActionPanel extends StatelessWidget {
}); });
} }
return Obx( return Obx(
() => _ActionIcon( () => ActionIcon(
message: is_maximized.value ? "Restore" : "Maximize", message: is_maximized.value ? "Restore" : "Maximize",
icon: is_maximized.value ? IconFont.restore : IconFont.max, icon: is_maximized.value ? IconFont.restore : IconFont.max,
theme: theme, theme: theme,
@ -297,7 +315,7 @@ class WindowActionPanel extends StatelessWidget {
})), })),
Offstage( Offstage(
offstage: !showClose, offstage: !showClose,
child: _ActionIcon( child: ActionIcon(
message: 'Close', message: 'Close',
icon: IconFont.close, icon: IconFont.close,
theme: theme, theme: theme,
@ -317,69 +335,37 @@ class WindowActionPanel extends StatelessWidget {
// ignore: must_be_immutable // ignore: must_be_immutable
class _ListView extends StatelessWidget { class _ListView extends StatelessWidget {
final Rx<PageController> controller; final DesktopTabController controller;
final ScrollPosController scrollController; late final Rx<DesktopTabState> state;
final RxList<TabInfo> tabInfos;
final Rx<int> selected;
final Function(String key)? onTabClose; final Function(String key)? onTabClose;
final _Theme _theme; final TarBarTheme theme;
late List<_Tab> tabs;
late final void Function(int)? onSelected;
_ListView( _ListView(
{Key? key, {required this.controller, required this.onTabClose, required this.theme})
required this.controller, : this.state = controller.state;
required this.scrollController,
required this.tabInfos,
required this.selected,
required this.onTabClose,
required _Theme theme,
this.onSelected})
: _theme = theme,
super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() { return Obx(() => ListView(
tabs = tabInfos.asMap().entries.map((e) { controller: state.value.scrollController,
int index = e.key; scrollDirection: Axis.horizontal,
return _Tab( shrinkWrap: true,
index: index, physics: BouncingScrollPhysics(),
label: e.value.label, children: state.value.tabs.asMap().entries.map((e) {
selectedIcon: e.value.selectedIcon, final index = e.key;
unselectedIcon: e.value.unselectedIcon, final tab = e.value;
closable: e.value.closable, return _Tab(
selected: selected.value, index: index,
onClose: () { label: tab.label,
tabInfos.removeWhere((tab) => tab.key == e.value.key); selectedIcon: tab.selectedIcon,
onTabClose?.call(e.value.key); unselectedIcon: tab.unselectedIcon,
if (index <= selected.value) { closable: tab.closable,
selected.value = max(0, selected.value - 1); selected: state.value.selected,
} onClose: () => controller.remove(index),
assert(tabInfos.length == 0 || selected.value < tabInfos.length); onSelected: () => controller.jumpTo(index),
scrollController.itemCount = tabInfos.length; theme: theme,
if (tabInfos.length > 0) { );
scrollController.scrollToItem(selected.value, }).toList()));
center: true, animate: true);
controller.value.jumpToPage(selected.value);
}
},
onSelected: () {
selected.value = index;
scrollController.scrollToItem(index, center: true, animate: true);
controller.value.jumpToPage(index);
onSelected?.call(selected.value);
},
theme: _theme,
);
}).toList();
return ListView(
controller: scrollController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: BouncingScrollPhysics(),
children: tabs);
});
} }
} }
@ -393,7 +379,7 @@ class _Tab extends StatelessWidget {
late final Function() onClose; late final Function() onClose;
late final Function() onSelected; late final Function() onSelected;
final RxBool _hover = false.obs; final RxBool _hover = false.obs;
late final _Theme theme; late final TarBarTheme theme;
_Tab( _Tab(
{Key? key, {Key? key,
@ -473,31 +459,11 @@ class _Tab extends StatelessWidget {
} }
} }
class _AddButton extends StatelessWidget {
late final _Theme theme;
_AddButton({
Key? key,
required this.theme,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return _ActionIcon(
message: 'New Connection',
icon: IconFont.add,
theme: theme,
onTap: () =>
rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
is_close: false);
}
}
class _CloseButton extends StatelessWidget { class _CloseButton extends StatelessWidget {
final bool visiable; final bool visiable;
final bool tabSelected; final bool tabSelected;
final Function onClose; final Function onClose;
late final _Theme theme; late final TarBarTheme theme;
_CloseButton({ _CloseButton({
Key? key, Key? key,
@ -528,13 +494,13 @@ class _CloseButton extends StatelessWidget {
} }
} }
class _ActionIcon extends StatelessWidget { class ActionIcon extends StatelessWidget {
final String message; final String message;
final IconData icon; final IconData icon;
final _Theme theme; final TarBarTheme theme;
final Function() onTap; final Function() onTap;
final bool is_close; final bool is_close;
const _ActionIcon({ const ActionIcon({
Key? key, Key? key,
required this.message, required this.message,
required this.icon, required this.icon,
@ -568,35 +534,54 @@ class _ActionIcon extends StatelessWidget {
} }
} }
class _Theme { class AddButton extends StatelessWidget {
late Color unSelectedtabIconColor; late final TarBarTheme theme;
late Color selectedtabIconColor;
late Color selectedTextColor;
late Color unSelectedTextColor;
late Color selectedIconColor;
late Color unSelectedIconColor;
late Color dividerColor;
late Color hoverColor;
_Theme.light() { AddButton({
unSelectedtabIconColor = Color.fromARGB(255, 162, 203, 241); Key? key,
selectedtabIconColor = MyTheme.accent; required this.theme,
selectedTextColor = Color.fromARGB(255, 26, 26, 26); }) : super(key: key);
unSelectedTextColor = Color.fromARGB(255, 96, 96, 96);
selectedIconColor = Color.fromARGB(255, 26, 26, 26);
unSelectedIconColor = Color.fromARGB(255, 96, 96, 96);
dividerColor = Color.fromARGB(255, 238, 238, 238);
hoverColor = Colors.grey.withOpacity(0.2);
}
_Theme.dark() { @override
unSelectedtabIconColor = Color.fromARGB(255, 30, 65, 98); Widget build(BuildContext context) {
selectedtabIconColor = MyTheme.accent; return ActionIcon(
selectedTextColor = Color.fromARGB(255, 255, 255, 255); message: 'New Connection',
unSelectedTextColor = Color.fromARGB(255, 207, 207, 207); icon: IconFont.add,
selectedIconColor = Color.fromARGB(255, 215, 215, 215); theme: theme,
unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); onTap: () =>
dividerColor = Color.fromARGB(255, 64, 64, 64); rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""),
hoverColor = Colors.black26; is_close: false);
} }
} }
class TarBarTheme {
final Color unSelectedtabIconColor;
final Color selectedtabIconColor;
final Color selectedTextColor;
final Color unSelectedTextColor;
final Color selectedIconColor;
final Color unSelectedIconColor;
final Color dividerColor;
final Color hoverColor;
const TarBarTheme.light()
: unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241),
selectedtabIconColor = MyTheme.accent,
selectedTextColor = const Color.fromARGB(255, 26, 26, 26),
unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96),
selectedIconColor = const Color.fromARGB(255, 26, 26, 26),
unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96),
dividerColor = const Color.fromARGB(255, 238, 238, 238),
hoverColor = const Color.fromARGB(
51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E
const TarBarTheme.dark()
: unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98),
selectedtabIconColor = MyTheme.accent,
selectedTextColor = const Color.fromARGB(255, 255, 255, 255),
unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207),
selectedIconColor = const Color.fromARGB(255, 215, 215, 215),
unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255),
dividerColor = const Color.fromARGB(255, 64, 64, 64),
hoverColor = Colors.black26;
}

View File

@ -200,6 +200,7 @@ class ChatModel with ChangeNotifier {
if (!_isShowChatPage) { if (!_isShowChatPage) {
toggleCMChatPage(id); toggleCMChatPage(id);
} }
_ffi.target?.serverModel.jumpTo(id);
late final chatUser; late final chatUser;
if (id == clientModeID) { if (id == clientModeID) {

View File

@ -4,10 +4,10 @@ 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/pages/server_page.dart' as Desktop;
import '../desktop/widgets/tabbar_widget.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';
@ -32,7 +32,7 @@ class ServerModel with ChangeNotifier {
late final TextEditingController _serverId; late final TextEditingController _serverId;
final _serverPasswd = TextEditingController(text: ""); final _serverPasswd = TextEditingController(text: "");
RxList<TabInfo> tabs = RxList<TabInfo>.empty(growable: true); final tabController = DesktopTabController();
List<Client> _clients = []; List<Client> _clients = [];
@ -352,16 +352,15 @@ class ServerModel with ChangeNotifier {
exit(0); exit(0);
} }
_clients.clear(); _clients.clear();
tabs.clear(); tabController.state.value.tabs.clear();
for (var clientJson in clientsJson) { for (var clientJson in clientsJson) {
final client = Client.fromJson(clientJson); final client = Client.fromJson(clientJson);
_clients.add(client); _clients.add(client);
DesktopTabBar.onAdd( tabController.add(TabInfo(
tabs, key: client.id.toString(),
TabInfo( label: client.name,
key: client.id.toString(), closable: false,
label: client.name, page: Desktop.buildConnectionCard(client)));
closable: false));
} }
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
@ -376,10 +375,11 @@ class ServerModel with ChangeNotifier {
return; return;
} }
_clients.add(client); _clients.add(client);
DesktopTabBar.onAdd( tabController.add(TabInfo(
tabs, key: client.id.toString(),
TabInfo( label: client.name,
key: client.id.toString(), label: client.name, closable: false)); closable: false,
page: Desktop.buildConnectionCard(client)));
scrollToBottom(); scrollToBottom();
notifyListeners(); notifyListeners();
if (isAndroid) showLoginDialog(client); if (isAndroid) showLoginDialog(client);
@ -456,7 +456,7 @@ class ServerModel with ChangeNotifier {
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);
final index = _clients.indexOf(client); final index = _clients.indexOf(client);
DesktopTabBar.remove(tabs, index); tabController.remove(index);
_clients.remove(client); _clients.remove(client);
} }
} }
@ -471,10 +471,11 @@ class ServerModel with ChangeNotifier {
} else { } else {
_clients[index].authorized = true; _clients[index].authorized = true;
} }
DesktopTabBar.onAdd( tabController.add(TabInfo(
tabs, key: client.id.toString(),
TabInfo( label: client.name,
key: client.id.toString(), label: client.name, closable: false)); closable: false,
page: Desktop.buildConnectionCard(client)));
scrollToBottom(); scrollToBottom();
notifyListeners(); notifyListeners();
} catch (e) {} } catch (e) {}
@ -486,7 +487,7 @@ class ServerModel with ChangeNotifier {
if (_clients.any((c) => c.id == id)) { if (_clients.any((c) => c.id == id)) {
final index = _clients.indexWhere((client) => client.id == id); final index = _clients.indexWhere((client) => client.id == id);
_clients.removeAt(index); _clients.removeAt(index);
DesktopTabBar.remove(tabs, index); tabController.remove(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);
} }
@ -501,7 +502,12 @@ class ServerModel with ChangeNotifier {
bind.cmCloseConnection(connId: client.id); bind.cmCloseConnection(connId: client.id);
}); });
_clients.clear(); _clients.clear();
tabs.clear(); tabController.state.value.tabs.clear();
}
void jumpTo(int id) {
final index = _clients.indexWhere((client) => client.id == id);
tabController.jumpTo(index);
} }
} }