590 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			590 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:math';
 | |
| 
 | |
| import 'package:desktop_multi_window/desktop_multi_window.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hbb/common.dart';
 | |
| import 'package:flutter_hbb/consts.dart';
 | |
| import 'package:flutter_hbb/main.dart';
 | |
| import 'package:get/get.dart';
 | |
| import 'package:window_manager/window_manager.dart';
 | |
| import 'package:scroll_pos/scroll_pos.dart';
 | |
| 
 | |
| import '../../utils/multi_window_manager.dart';
 | |
| 
 | |
| const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
 | |
| const double _kIconSize = 18;
 | |
| const double _kDividerIndent = 10;
 | |
| const double _kActionIconSize = 12;
 | |
| 
 | |
| class TabInfo {
 | |
|   final String key;
 | |
|   final String label;
 | |
|   final IconData? selectedIcon;
 | |
|   final IconData? unselectedIcon;
 | |
|   final bool closable;
 | |
|   final Widget page;
 | |
| 
 | |
|   TabInfo(
 | |
|       {required this.key,
 | |
|       required this.label,
 | |
|       this.selectedIcon,
 | |
|       this.unselectedIcon,
 | |
|       this.closable = true,
 | |
|       required this.page});
 | |
| }
 | |
| 
 | |
| class DesktopTabState {
 | |
|   final List<TabInfo> tabs = [];
 | |
|   final ScrollPosController scrollController =
 | |
|       ScrollPosController(itemCount: 0);
 | |
|   final PageController pageController = PageController();
 | |
|   int selected = 0;
 | |
| 
 | |
|   DesktopTabState() {
 | |
|     scrollController.itemCount = tabs.length;
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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.showTabBar = true,
 | |
|       this.showLogo = true,
 | |
|       this.showTitle = true,
 | |
|       this.showMinimize = true,
 | |
|       this.showMaximize = true,
 | |
|       this.showClose = true,
 | |
|       this.pageViewBuilder,
 | |
|       this.tail});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Column(children: [
 | |
|       Offstage(
 | |
|           offstage: !showTabBar,
 | |
|           child: Container(
 | |
|             height: _kTabBarHeight,
 | |
|             child: Column(
 | |
|               children: [
 | |
|                 Container(
 | |
|                   height: _kTabBarHeight - 1,
 | |
|                   child: _buildBar(),
 | |
|                 ),
 | |
|                 Divider(
 | |
|                   height: 1,
 | |
|                   thickness: 1,
 | |
|                 ),
 | |
|               ],
 | |
|             ),
 | |
|           )),
 | |
|       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,
 | |
|                     )),
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|         Offstage(offstage: tail == null, child: tail),
 | |
|         WindowActionPanel(
 | |
|           mainTab: isMainWindow,
 | |
|           theme: theme,
 | |
|           showMinimize: showMinimize,
 | |
|           showMaximize: showMaximize,
 | |
|           showClose: showClose,
 | |
|         )
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class WindowActionPanel extends StatelessWidget {
 | |
|   final bool mainTab;
 | |
|   final TarBarTheme theme;
 | |
| 
 | |
|   final bool showMinimize;
 | |
|   final bool showMaximize;
 | |
|   final bool showClose;
 | |
| 
 | |
|   const WindowActionPanel(
 | |
|       {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: [
 | |
|         Offstage(
 | |
|             offstage: !showMinimize,
 | |
|             child: ActionIcon(
 | |
|               message: 'Minimize',
 | |
|               icon: IconFont.min,
 | |
|               theme: theme,
 | |
|               onTap: () {
 | |
|                 if (mainTab) {
 | |
|                   windowManager.minimize();
 | |
|                 } else {
 | |
|                   WindowController.fromWindowId(windowId!).minimize();
 | |
|                 }
 | |
|               },
 | |
|               is_close: false,
 | |
|             )),
 | |
|         // TODO: drag makes window restore
 | |
|         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 {
 | |
|                         windowManager.maximize();
 | |
|                       }
 | |
|                     } else {
 | |
|                       // TODO: subwindow is maximized but first query result is not maximized.
 | |
|                       final wc = WindowController.fromWindowId(windowId!);
 | |
|                       if (is_maximized.value) {
 | |
|                         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,
 | |
|             )),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| // ignore: must_be_immutable
 | |
| class _ListView extends StatelessWidget {
 | |
|   final DesktopTabController controller;
 | |
|   late final Rx<DesktopTabState> state;
 | |
|   final Function(String key)? onTabClose;
 | |
|   final TarBarTheme theme;
 | |
| 
 | |
|   _ListView(
 | |
|       {required this.controller, required this.onTabClose, required this.theme})
 | |
|       : this.state = controller.state;
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Obx(() => ListView(
 | |
|         controller: state.value.scrollController,
 | |
|         scrollDirection: Axis.horizontal,
 | |
|         shrinkWrap: true,
 | |
|         physics: BouncingScrollPhysics(),
 | |
|         children: state.value.tabs.asMap().entries.map((e) {
 | |
|           final index = e.key;
 | |
|           final tab = e.value;
 | |
|           return _Tab(
 | |
|             index: index,
 | |
|             label: tab.label,
 | |
|             selectedIcon: tab.selectedIcon,
 | |
|             unselectedIcon: tab.unselectedIcon,
 | |
|             closable: tab.closable,
 | |
|             selected: state.value.selected,
 | |
|             onClose: () => controller.remove(index),
 | |
|             onSelected: () => controller.jumpTo(index),
 | |
|             theme: theme,
 | |
|           );
 | |
|         }).toList()));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _Tab extends StatelessWidget {
 | |
|   late final int index;
 | |
|   late final String label;
 | |
|   late final IconData? selectedIcon;
 | |
|   late final IconData? unselectedIcon;
 | |
|   late final bool closable;
 | |
|   late final int selected;
 | |
|   late final Function() onClose;
 | |
|   late final Function() onSelected;
 | |
|   final RxBool _hover = false.obs;
 | |
|   late final TarBarTheme theme;
 | |
| 
 | |
|   _Tab(
 | |
|       {Key? key,
 | |
|       required this.index,
 | |
|       required this.label,
 | |
|       this.selectedIcon,
 | |
|       this.unselectedIcon,
 | |
|       required this.closable,
 | |
|       required this.selected,
 | |
|       required this.onClose,
 | |
|       required this.onSelected,
 | |
|       required this.theme})
 | |
|       : super(key: key);
 | |
| 
 | |
|   @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(
 | |
|       child: InkWell(
 | |
|         onHover: (hover) => _hover.value = hover,
 | |
|         onTap: () => onSelected(),
 | |
|         child: Row(
 | |
|           children: [
 | |
|             Container(
 | |
|                 height: _kTabBarHeight,
 | |
|                 child: Row(
 | |
|                     crossAxisAlignment: CrossAxisAlignment.center,
 | |
|                     children: [
 | |
|                       Row(
 | |
|                         mainAxisAlignment: MainAxisAlignment.center,
 | |
|                         children: [
 | |
|                           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,
 | |
|                             style: TextStyle(
 | |
|                                 color: is_selected
 | |
|                                     ? theme.selectedTextColor
 | |
|                                     : theme.unSelectedTextColor),
 | |
|                           ),
 | |
|                         ],
 | |
|                       ),
 | |
|                       Offstage(
 | |
|                         offstage: !closable,
 | |
|                         child: Obx((() => _CloseButton(
 | |
|                               visiable: _hover.value,
 | |
|                               tabSelected: is_selected,
 | |
|                               onClose: () => onClose(),
 | |
|                               theme: theme,
 | |
|                             ))),
 | |
|                       )
 | |
|                     ])).paddingSymmetric(horizontal: 10),
 | |
|             Offstage(
 | |
|               offstage: !show_divider,
 | |
|               child: VerticalDivider(
 | |
|                 width: 1,
 | |
|                 indent: _kDividerIndent,
 | |
|                 endIndent: _kDividerIndent,
 | |
|                 color: theme.dividerColor,
 | |
|                 thickness: 1,
 | |
|               ),
 | |
|             )
 | |
|           ],
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _CloseButton extends StatelessWidget {
 | |
|   final bool visiable;
 | |
|   final bool tabSelected;
 | |
|   final Function onClose;
 | |
|   late final TarBarTheme theme;
 | |
| 
 | |
|   _CloseButton({
 | |
|     Key? key,
 | |
|     required this.visiable,
 | |
|     required this.tabSelected,
 | |
|     required this.onClose,
 | |
|     required this.theme,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return SizedBox(
 | |
|         width: _kIconSize,
 | |
|         child: Offstage(
 | |
|           offstage: !visiable,
 | |
|           child: InkWell(
 | |
|             customBorder: RoundedRectangleBorder(),
 | |
|             onTap: () => onClose(),
 | |
|             child: Icon(
 | |
|               Icons.close,
 | |
|               size: _kIconSize,
 | |
|               color: tabSelected
 | |
|                   ? theme.selectedIconColor
 | |
|                   : theme.unSelectedIconColor,
 | |
|             ),
 | |
|           ),
 | |
|         )).paddingOnly(left: 5);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ActionIcon extends StatelessWidget {
 | |
|   final String message;
 | |
|   final IconData icon;
 | |
|   final TarBarTheme theme;
 | |
|   final Function() onTap;
 | |
|   final bool is_close;
 | |
|   const ActionIcon({
 | |
|     Key? key,
 | |
|     required this.message,
 | |
|     required this.icon,
 | |
|     required this.theme,
 | |
|     required this.onTap,
 | |
|     required this.is_close,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     RxBool hover = false.obs;
 | |
|     return Obx(() => Tooltip(
 | |
|           message: translate(message),
 | |
|           waitDuration: Duration(seconds: 1),
 | |
|           child: InkWell(
 | |
|             hoverColor:
 | |
|                 is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor,
 | |
|             onHover: (value) => hover.value = value,
 | |
|             child: Container(
 | |
|               height: _kTabBarHeight - 1,
 | |
|               width: _kTabBarHeight - 1,
 | |
|               child: Icon(
 | |
|                 icon,
 | |
|                 color: hover.value && is_close
 | |
|                     ? Colors.white
 | |
|                     : theme.unSelectedIconColor,
 | |
|                 size: _kActionIconSize,
 | |
|               ),
 | |
|             ),
 | |
|             onTap: onTap,
 | |
|           ),
 | |
|         ));
 | |
|   }
 | |
| }
 | |
| 
 | |
| class AddButton extends StatelessWidget {
 | |
|   late final TarBarTheme 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 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;
 | |
| }
 |