diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 968ae34e8..8544fc240 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -43,23 +43,28 @@ var version = ""; int androidVersion = 0; DesktopType? desktopType; +/// * debug or test only, DO NOT enable in release build +bool isTest = false; + typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); typedef StreamEventHandler = Future Function(Map); -late final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( +final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); -late final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( +final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); -late final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( +final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); -late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( +final iconFile = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); -late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( +final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); -late final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( +final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'))); +final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg=='))); enum DesktopType { main, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index fc7b6676e..e31a0e1d9 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math'; import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; @@ -229,13 +230,13 @@ class _FileManagerPageState extends State final entries = fd.entries; final sortIndex = (SortBy style) { switch (style) { - case SortBy.Name: + case SortBy.name: return 0; - case SortBy.Type: + case SortBy.type: return 0; - case SortBy.Modified: + case SortBy.modified: return 1; - case SortBy.Size: + case SortBy.size: return 2; } }(model.getSortStyle(isLocal)); @@ -265,7 +266,7 @@ class _FileManagerPageState extends State translate("Name"), ).marginSymmetric(horizontal: 4), onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Name, + model.changeSortStyle(SortBy.name, isLocal: isLocal, ascending: ascending); }), DataColumn( @@ -273,13 +274,13 @@ class _FileManagerPageState extends State translate("Modified"), ), onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Modified, + model.changeSortStyle(SortBy.modified, isLocal: isLocal, ascending: ascending); }), DataColumn( label: Text(translate("Size")), onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Size, + model.changeSortStyle(SortBy.size, isLocal: isLocal, ascending: ascending); }), ], @@ -304,18 +305,25 @@ class _FileManagerPageState extends State waitDuration: Duration(milliseconds: 500), message: entry.name, child: Row(children: [ - Icon( - entry.isFile - ? Icons.feed_outlined - : entry.isDrive - ? Icons.computer - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 20, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7), + ).marginSymmetric(horizontal: 2), Expanded( child: Text(entry.name, overflow: TextOverflow.ellipsis)) @@ -546,13 +554,6 @@ class _FileManagerPageState extends State children: [ Row( children: [ - IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: const Icon(Icons.home_outlined), - splashRadius: 20, - ), IconButton( icon: const Icon(Icons.arrow_back), splashRadius: 20, @@ -649,6 +650,13 @@ class _FileManagerPageState extends State mainAxisAlignment: isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: const Icon(Icons.home_outlined), + splashRadius: 20, + ), IconButton( onPressed: () { final name = TextEditingController(); @@ -786,14 +794,23 @@ class _FileManagerPageState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: BreadCrumb( - items: items, - divider: Text("/", - style: TextStyle(color: Theme.of(context).hintColor)) - .paddingSymmetric(horizontal: 2.0), - overflow: ScrollableOverflow( - controller: getBreadCrumbScrollController(isLocal)), - )), + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = getBreadCrumbScrollController(isLocal); + sc.jumpTo(sc.offset + e.scrollDelta.dy / 4); + } + }, + child: BreadCrumb( + items: items, + divider: Text("/", + style: TextStyle( + color: Theme.of(context).hintColor)), + overflow: ScrollableOverflow( + controller: + getBreadCrumbScrollController(isLocal)), + ))), ActionIcon( message: "", icon: Icons.arrow_drop_down, @@ -833,15 +850,25 @@ class _FileManagerPageState extends State await model.fetchDirectory("/", isLocal, isLocal); for (var entry in fd.entries) { menuItems.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - entry.name, - style: style, - ), + childBuilder: (TextStyle? style) => + Row(children: [ + Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)), + SizedBox(width: 10), + Text( + entry.name, + style: style, + ) + ]), proc: () { openDirectory(entry.name, isLocal: isLocal); }, dismissOnClicked: true)); - menuItems.add(MenuEntryDivider()); } } finally { if (!isLocal) { @@ -849,7 +876,7 @@ class _FileManagerPageState extends State } } } - + menuItems.add(MenuEntryDivider()); mod_menu.showMenu( context: context, position: RelativeRect.fromLTRB(x, y, x, y), @@ -879,10 +906,11 @@ class _FileManagerPageState extends State final breadCrumbList = List.empty(growable: true); breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( content: TextButton( - child: Text(e.value), - style: - ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); + child: Text(e.value), + style: ButtonStyle( + minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(list.sublist(0, e.key + 1))) + .marginSymmetric(horizontal: 4)))); return breadCrumbList; } diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e69557981..d721cda4d 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -124,6 +124,22 @@ class ConnectionManagerState extends State { showMinimize: true, showClose: true, controller: serverModel.tabController, + maxLabelWidth: 100, + tail: buildScrollJumper(), + selectedTabBackgroundColor: + Theme.of(context).hintColor.withOpacity(0.2), + tabBuilder: (key, icon, label, themeConf) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: key, + waitDuration: Duration(seconds: 1), + child: label), + ], + ); + }, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -158,6 +174,21 @@ class ConnectionManagerState extends State { ), ); } + + Widget buildScrollJumper() { + final offstage = gFFI.serverModel.clients.length < 2; + final sc = gFFI.serverModel.tabController.state.value.scrollController; + return Offstage( + offstage: offstage, + child: Row( + children: [ + ActionIcon( + icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward), + ActionIcon( + icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward), + ], + )); + } } Widget buildConnectionCard(Client client) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 1d774143c..03a8b6f01 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -3,12 +3,14 @@ import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -132,7 +134,8 @@ class DesktopTabController { if (val.scrollController.hasClients && val.scrollController.canScroll && val.scrollController.itemCount > index) { - val.scrollController.scrollToItem(index, center: true, animate: true); + val.scrollController + .scrollToItem(index, center: false, animate: true); } })); }); @@ -175,6 +178,9 @@ typedef TabBuilder = Widget Function( String key, Widget icon, Widget label, TabThemeConf themeConf); typedef LabelGetter = Rx Function(String key); +/// [_lastClickTime], help to handle double click +int _lastClickTime = DateTime.now().millisecondsSinceEpoch; + class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final bool showTabBar; @@ -188,10 +194,14 @@ class DesktopTab extends StatelessWidget { final Future Function()? onWindowCloseButton; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; + final double? maxLabelWidth; + final Color? selectedTabBackgroundColor; + final Color? unSelectedTabBackgroundColor; final DesktopTabController controller; Rx get state => controller.state; final isMaximized = false.obs; + final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); late final DesktopTabType tabType; late final bool isMainWindow; @@ -211,6 +221,9 @@ class DesktopTab extends StatelessWidget { this.onWindowCloseButton, this.tabBuilder, this.labelGetter, + this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor, }) : super(key: key) { tabType = controller.tabType; isMainWindow = @@ -292,46 +305,76 @@ class DesktopTab extends StatelessWidget { Widget _buildBar() { return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Offstage( - offstage: !Platform.isMacOS, - child: const SizedBox( - width: 78, - )), - GestureDetector( - onDoubleTap: showMaximize - ? () => toggleMaximize(isMainWindow) - .then((value) => isMaximized.value = value) + Expanded( + child: GestureDetector( + // custom double tap handler + onTap: showMaximize + ? () { + final current = DateTime.now().millisecondsSinceEpoch; + final elapsed = current - _lastClickTime; + _lastClickTime = current; + if (elapsed < kDesktopDoubleClickTimeMilli) { + // onDoubleTap + toggleMaximize(isMainWindow) + .then((value) => isMaximized.value = value); + } + } : null, onPanStart: (_) => startDragging(isMainWindow), - child: Row(children: [ - Offstage( - offstage: !showLogo, - child: SvgPicture.asset( - 'assets/logo.svg', - width: 16, - height: 16, - )), - Offstage( - offstage: !showTitle, - child: const Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2)) - ]).marginOnly( - left: 5, - right: 10, - )), - _ListView( - controller: controller, - onTabClose: onTabClose, - tabBuilder: tabBuilder, - labelGetter: labelGetter, - ), - ], - ), + child: Row( + children: [ + Offstage( + offstage: !Platform.isMacOS, + child: const SizedBox( + width: 78, + )), + Row(children: [ + Offstage( + offstage: !showLogo, + child: SvgPicture.asset( + 'assets/logo.svg', + width: 16, + height: 16, + )), + Offstage( + offstage: !showTitle, + child: const Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, + ), + Expanded( + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = + controller.state.value.scrollController; + if (!sc.canScroll) return; + _scrollDebounce.call(() { + sc.animateTo(sc.offset + e.scrollDelta.dy, + duration: Duration(milliseconds: 200), + curve: Curves.ease); + }); + } + }, + child: _ListView( + controller: controller, + onTabClose: onTabClose, + tabBuilder: tabBuilder, + labelGetter: labelGetter, + maxLabelWidth: maxLabelWidth, + selectedTabBackgroundColor: + selectedTabBackgroundColor, + unSelectedTabBackgroundColor: + unSelectedTabBackgroundColor))), + ], + ))), WindowActionPanel( isMainWindow: isMainWindow, tabType: tabType, @@ -435,14 +478,9 @@ class WindowActionPanelState extends State @override Widget build(BuildContext context) { - return Expanded( - child: Row( + return Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Expanded( - child: GestureDetector( - onDoubleTap: widget.showMaximize ? _toggleMaximize : null, - onPanStart: (_) => startDragging(widget.isMainWindow), - )), Offstage(offstage: widget.tail == null, child: widget.tail), Offstage( offstage: !widget.showMinimize, @@ -489,7 +527,7 @@ class WindowActionPanelState extends State isClose: true, )), ], - )); + ); } void _toggleMaximize() { @@ -580,13 +618,15 @@ Future closeConfirmDialog() async { return res == true; } -// ignore: must_be_immutable class _ListView extends StatelessWidget { final DesktopTabController controller; final Function(String key)? onTabClose; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; + final double? maxLabelWidth; + final Color? selectedTabBackgroundColor; + final Color? unSelectedTabBackgroundColor; Rx get state => controller.state; @@ -594,7 +634,10 @@ class _ListView extends StatelessWidget { {required this.controller, required this.onTabClose, this.tabBuilder, - this.labelGetter}); + this.labelGetter, + this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor}); /// Check whether to show ListView /// @@ -636,7 +679,7 @@ class _ListView extends StatelessWidget { onSelected: () => controller.jumpTo(index), tabBuilder: tabBuilder == null ? null - : (Widget icon, Widget labelWidget, + : (String key, Widget icon, Widget labelWidget, TabThemeConf themeConf) { return tabBuilder!( tab.label, @@ -645,6 +688,9 @@ class _ListView extends StatelessWidget { themeConf, ); }, + maxLabelWidth: maxLabelWidth, + selectedTabBackgroundColor: selectedTabBackgroundColor, + unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); } @@ -659,8 +705,10 @@ class _Tab extends StatefulWidget { final int selected; final Function() onClose; final Function() onSelected; - final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? - tabBuilder; + final TabBuilder? tabBuilder; + final double? maxLabelWidth; + final Color? selectedTabBackgroundColor; + final Color? unSelectedTabBackgroundColor; const _Tab({ Key? key, @@ -673,6 +721,9 @@ class _Tab extends StatefulWidget { required this.selected, required this.onClose, required this.onSelected, + this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor, }) : super(key: key); @override @@ -697,14 +748,17 @@ class _TabState extends State<_Tab> with RestorationMixin { : MyTheme.tabbar(context).unSelectedTabIconColor, ).paddingOnly(right: 5)); final labelWidget = Obx(() { - return Text( - translate(widget.label.value), - textAlign: TextAlign.center, - style: TextStyle( - color: isSelected - ? MyTheme.tabbar(context).selectedTextColor - : MyTheme.tabbar(context).unSelectedTextColor), - ); + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), + child: Text( + translate(widget.label.value), + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor), + overflow: TextOverflow.ellipsis, + )); }); if (widget.tabBuilder == null) { @@ -716,8 +770,8 @@ class _TabState extends State<_Tab> with RestorationMixin { ], ); } else { - return widget.tabBuilder!( - icon, labelWidget, TabThemeConf(iconSize: _kIconSize)); + return widget.tabBuilder!(widget.label.value, icon, labelWidget, + TabThemeConf(iconSize: _kIconSize)); } } @@ -734,32 +788,36 @@ class _TabState extends State<_Tab> with RestorationMixin { restoreHover.value = value; }, onTap: () => widget.onSelected(), - child: Row( - children: [ - SizedBox( - height: _kTabBarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildTabContent(), - Obx((() => _CloseButton( - visiable: hover.value && widget.closable, - tabSelected: isSelected, - onClose: () => widget.onClose(), - ))) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !showDivider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: MyTheme.tabbar(context).dividerColor, - thickness: 1, - ), - ) - ], - ), + child: Container( + color: isSelected + ? widget.selectedTabBackgroundColor + : widget.unSelectedTabBackgroundColor, + child: Row( + children: [ + SizedBox( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildTabContent(), + Obx((() => _CloseButton( + visiable: hover.value && widget.closable, + tabSelected: isSelected, + onClose: () => widget.onClose(), + ))) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !showDivider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: MyTheme.tabbar(context).dividerColor, + thickness: 1, + ), + ) + ], + )), ), ); } @@ -807,7 +865,7 @@ class _CloseButton extends StatelessWidget { } class ActionIcon extends StatelessWidget { - final String message; + final String? message; final IconData icon; final Function() onTap; final bool isClose; @@ -815,7 +873,7 @@ class ActionIcon extends StatelessWidget { final double boxSize; const ActionIcon( {Key? key, - required this.message, + this.message, required this.icon, required this.onTap, this.isClose = false, @@ -827,7 +885,7 @@ class ActionIcon extends StatelessWidget { Widget build(BuildContext context) { RxBool hover = false.obs; return Obx(() => Tooltip( - message: translate(message), + message: message != null ? translate(message!) : "", waitDuration: const Duration(seconds: 1), child: InkWell( hoverColor: isClose diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 0ff6c83da..982b8ffe3 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -46,7 +46,7 @@ class _FileManagerPageState extends State { @override Widget build(BuildContext context) => ChangeNotifierProvider.value( - value: gFFI.fileModel, + value: model, child: Consumer(builder: (_context, _model, _child) { return WillPopScope( onWillPop: () async { @@ -107,6 +107,7 @@ class _FileManagerPageState extends State { value: "refresh", ), PopupMenuItem( + enabled: model.currentDir.path != "/", child: Row( children: [ Icon(Icons.check, @@ -118,6 +119,7 @@ class _FileManagerPageState extends State { value: "select", ), PopupMenuItem( + enabled: model.currentDir.path != "/", child: Row( children: [ Icon(Icons.folder_outlined, @@ -129,6 +131,7 @@ class _FileManagerPageState extends State { value: "folder", ), PopupMenuItem( + enabled: model.currentDir.path != "/", child: Row( children: [ Icon( @@ -227,21 +230,29 @@ class _FileManagerPageState extends State { : ""; return Card( child: ListTile( - leading: Icon( - entries[index].isFile - ? Icons.feed_outlined - : entries[index].isDrive - ? Icons.computer + leading: entries[index].isDrive + ? Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7))) + : Icon( + entries[index].isFile + ? Icons.feed_outlined : Icons.folder, - size: 40), + size: 40), title: Text(entries[index].name), selected: selected, - subtitle: Text( - entries[index].isDrive - ? "" - : "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", - style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - ), + subtitle: entries[index].isDrive + ? null + : Text( + "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", + style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + ), trailing: entries[index].isDrive ? null : showCheckBox() @@ -366,8 +377,7 @@ class _FileManagerPageState extends State { itemBuilder: (context) { return SortBy.values .map((e) => PopupMenuItem( - child: - Text(translate(e.toString().split(".").last)), + child: Text(translate(e.toString())), value: e, )) .toList(); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 1ea9f65a1..ed52d03ee 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,15 +4,29 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; -import 'package:path/path.dart' as Path; +import 'package:path/path.dart' as path; import 'model.dart'; import 'platform_model.dart'; -enum SortBy { Name, Type, Modified, Size } +enum SortBy { + name, + type, + modified, + size; + + @override + String toString() { + final str = this.name.toString(); + return "${str[0].toUpperCase()}${str.substring(1)}"; + } +} class FileModel extends ChangeNotifier { - var _isLocal = false; + /// mobile, current selected page show on mobile screen + var _isSelectedLocal = false; + + /// mobile, select mode state var _selectMode = false; final _localOption = DirectoryOption(); @@ -30,7 +44,7 @@ class FileModel extends ChangeNotifier { RxList get jobTable => _jobTable; - bool get isLocal => _isLocal; + bool get isLocal => _isSelectedLocal; bool get selectMode => _selectMode; @@ -38,17 +52,17 @@ class FileModel extends ChangeNotifier { JobState get jobState => _jobProgress.state; - SortBy _sortStyle = SortBy.Name; + SortBy _sortStyle = SortBy.name; SortBy get sortStyle => _sortStyle; - SortBy _localSortStyle = SortBy.Name; + SortBy _localSortStyle = SortBy.name; bool _localSortAscending = true; bool _remoteSortAscending = true; - SortBy _remoteSortStyle = SortBy.Name; + SortBy _remoteSortStyle = SortBy.name; bool get localSortAscending => _localSortAscending; @@ -64,7 +78,8 @@ class FileModel extends ChangeNotifier { FileDirectory get currentRemoteDir => _currentRemoteDir; - FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir; + FileDirectory get currentDir => + _isSelectedLocal ? currentLocalDir : currentRemoteDir; FileDirectory getCurrentDir(bool isLocal) { return isLocal ? currentLocalDir : currentRemoteDir; @@ -75,7 +90,7 @@ class FileModel extends ChangeNotifier { final currentHome = getCurrentHome(isLocal); if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); - if (path.length == 0) return ""; + if (path.isEmpty) return ""; if (path[0] == "/" || path[0] == "\\") { // remove more '/' or '\' path = path.replaceFirst(path[0], ""); @@ -86,7 +101,8 @@ class FileModel extends ChangeNotifier { } } - String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; + String get currentHome => + _isSelectedLocal ? _localOption.home : _remoteOption.home; String getCurrentHome(bool isLocal) { return isLocal ? _localOption.home : _remoteOption.home; @@ -99,7 +115,7 @@ class FileModel extends ChangeNotifier { String get currentShortPath { if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); - if (path.length == 0) return ""; + if (path.isEmpty) return ""; if (path[0] == "/" || path[0] == "\\") { // remove more '/' or '\' path = path.replaceFirst(path[0], ""); @@ -114,7 +130,7 @@ class FileModel extends ChangeNotifier { final dir = isLocal ? currentLocalDir : currentRemoteDir; if (dir.path.startsWith(currentHome)) { var path = dir.path.replaceFirst(currentHome, ""); - if (path.length == 0) return ""; + if (path.isEmpty) return ""; if (path[0] == "/" || path[0] == "\\") { // remove more '/' or '\' path = path.replaceFirst(path[0], ""); @@ -126,14 +142,14 @@ class FileModel extends ChangeNotifier { } bool get currentShowHidden => - _isLocal ? _localOption.showHidden : _remoteOption.showHidden; + _isSelectedLocal ? _localOption.showHidden : _remoteOption.showHidden; bool getCurrentShowHidden(bool isLocal) { return isLocal ? _localOption.showHidden : _remoteOption.showHidden; } bool get currentIsWindows => - _isLocal ? _localOption.isWindows : _remoteOption.isWindows; + _isSelectedLocal ? _localOption.isWindows : _remoteOption.isWindows; bool getCurrentIsWindows(bool isLocal) { return isLocal ? _localOption.isWindows : _remoteOption.isWindows; @@ -156,12 +172,12 @@ class FileModel extends ChangeNotifier { } togglePage() { - _isLocal = !_isLocal; + _isSelectedLocal = !_isSelectedLocal; notifyListeners(); } toggleShowHidden({bool? showHidden, bool? local}) { - final isLocal = local ?? _isLocal; + final isLocal = local ?? _isSelectedLocal; if (isLocal) { _localOption.showHidden = showHidden ?? !_localOption.showHidden; } else { @@ -187,7 +203,7 @@ class FileModel extends ChangeNotifier { job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); - debugPrint("update job ${id} with ${evt}"); + debugPrint("update job $id with $evt"); } } notifyListeners(); @@ -197,7 +213,7 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - debugPrint("recv file dir:${evt}"); + debugPrint("recv file dir:$evt"); if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { @@ -209,9 +225,9 @@ class FileModel extends ChangeNotifier { final job = jobTable[jobIndex]; var totalSize = 0; var fileCount = fd.entries.length; - fd.entries.forEach((element) { + for (var element in fd.entries) { totalSize += element.size; - }); + } job.totalSize = totalSize; job.fileCount = fileCount; debugPrint("update receive details:${fd.path}"); @@ -343,7 +359,7 @@ class FileModel extends ChangeNotifier { jobReset(); // save config - Map msgMap = Map(); + Map msgMap = {}; msgMap["local_dir"] = _currentLocalDir.path; msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : ""; @@ -361,7 +377,7 @@ class FileModel extends ChangeNotifier { Future refresh({bool? isLocal}) async { if (isDesktop) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; isLocal ? await openDirectory(currentLocalDir.path, isLocal: isLocal) : await openDirectory(currentRemoteDir.path, isLocal: isLocal); @@ -371,7 +387,15 @@ class FileModel extends ChangeNotifier { } openDirectory(String path, {bool? isLocal, bool isBack = false}) async { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; + if (path == ".") { + refresh(isLocal: isLocal); + return; + } + if (path == "..") { + goToParentDirectory(isLocal: isLocal); + return; + } if (!isBack) { pushHistory(isLocal); } @@ -385,7 +409,7 @@ class FileModel extends ChangeNotifier { : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { path = path.substring(1); if (path[path.length - 1] != '\\') { - path = path + "\\"; + path = "$path\\"; } } try { @@ -416,12 +440,12 @@ class FileModel extends ChangeNotifier { } goHome({bool? isLocal}) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; openDirectory(getCurrentHome(isLocal), isLocal: isLocal); } goBack({bool? isLocal}) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; final history = isLocal ? localHistory : remoteHistory; if (history.isEmpty) return; final path = history.removeAt(history.length - 1); @@ -435,7 +459,7 @@ class FileModel extends ChangeNotifier { } goToParentDirectory({bool? isLocal}) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; final currDir = isLocal ? currentLocalDir : currentRemoteDir; @@ -457,7 +481,7 @@ class FileModel extends ChangeNotifier { isRemote ? _localOption.isWindows : _remoteOption.isWindows; final showHidden = isRemote ? _localOption.showHidden : _remoteOption.showHidden; - items.items.forEach((from) async { + for (var from in items.items) { final jobId = ++_jobId; _jobTable.add(JobProgress() ..jobName = from.path @@ -473,9 +497,9 @@ class FileModel extends ChangeNotifier { fileNum: 0, includeHidden: showHidden, isRemote: isRemote); - print( - "path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}"); - }); + debugPrint( + "path:${from.path}, toPath:$toPath, to:${PathUtil.join(toPath, from.name, isWindows)}"); + } } else { if (items.isLocal == null) { debugPrint("Failed to sendFiles ,wrong path state"); @@ -505,7 +529,7 @@ class FileModel extends ChangeNotifier { bool removeCheckboxRemember = false; removeAction(SelectedItems items, {bool? isLocal}) async { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; removeCheckboxRemember = false; if (items.isLocal == null) { debugPrint("Failed to removeFile, wrong path state"); @@ -520,7 +544,7 @@ class FileModel extends ChangeNotifier { late final List entries; if (item.isFile) { title = translate("Are you sure you want to delete this file?"); - content = "${item.name}"; + content = item.name; entries = [item]; } else if (item.isDirectory) { title = translate("Not an empty directory"); @@ -553,7 +577,7 @@ class FileModel extends ChangeNotifier { ? "${translate("Are you sure you want to delete the file of this directory?")}\n" : ""; final count = entries.length > 1 ? "${i + 1}/${entries.length}" : ""; - content = dirShow + "$count \n${entries[i].path}"; + content = "$dirShow$count \n${entries[i].path}"; final confirm = await showRemoveDialog(title, content, item.isDirectory); try { @@ -580,7 +604,7 @@ class FileModel extends ChangeNotifier { break; } } catch (e) { - print("remove error: ${e}"); + print("remove error: $e"); } } }); @@ -779,14 +803,14 @@ class FileModel extends ChangeNotifier { job.fileCount = num_entries; job.totalSize = total_size.toInt(); } - debugPrint("update folder files: ${info}"); + debugPrint("update folder files: $info"); notifyListeners(); } bool get remoteSortAscending => _remoteSortAscending; void loadLastJob(Map evt) { - debugPrint("load last job: ${evt}"); + debugPrint("load last job: $evt"); Map jobDetail = json.decode(evt['value']); // int id = int.parse(jobDetail['id']); String remote = jobDetail['remote']; @@ -824,7 +848,7 @@ class FileModel extends ChangeNotifier { id: '${parent.target?.id}', actId: job.id, isRemote: job.isRemote); job.state = JobState.inProgress; } else { - debugPrint("jobId ${jobId} is not exists"); + debugPrint("jobId $jobId is not exists"); } notifyListeners(); } @@ -833,7 +857,7 @@ class FileModel extends ChangeNotifier { class JobResultListener { Completer? _completer; Timer? _timer; - int _timeoutSecond = 5; + final int _timeoutSecond = 5; bool get isListening => _completer != null; @@ -871,20 +895,14 @@ class JobResultListener { } class FileFetcher { - // Map> localTasks = Map(); // now we only use read local dir sync - Map> remoteTasks = Map(); - Map> readRecursiveTasks = Map(); + // Map> localTasks = {}; // now we only use read local dir sync + Map> remoteTasks = {}; + Map> readRecursiveTasks = {}; - String? _id; - - String? get id => _id; - - set id(String? id) { - _id = id; - } + String? id; // if id == null, means to fetch global FFI - FFI get _ffi => ffi(_id ?? ""); + FFI get _ffi => ffi(id ?? ""); Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later @@ -921,7 +939,7 @@ class FileFetcher { tryCompleteTask(String? msg, String? isLocalStr) { if (msg == null || isLocalStr == null) return; - late final tasks; + late final Map> tasks; try { final fd = FileDirectory.fromJson(jsonDecode(msg)); if (fd.id > 0) { @@ -988,15 +1006,15 @@ class FileDirectory { id = json['id']; path = json['path']; json['entries'].forEach((v) { - entries.add(new Entry.fromJson(v)); + entries.add(Entry.fromJson(v)); }); } // generate full path for every entry , init sort style if need. format(bool isWindows, {SortBy? sort}) { - entries.forEach((entry) { + for (var entry in entries) { entry.path = PathUtil.join(path, entry.name, isWindows); - }); + } if (sort != null) { changeSortStyle(sort); } @@ -1094,8 +1112,8 @@ class _PathStat { } class PathUtil { - static final windowsContext = Path.Context(style: Path.Style.windows); - static final posixContext = Path.Context(style: Path.Style.posix); + static final windowsContext = path.Context(style: path.Style.windows); + static final posixContext = path.Context(style: path.Style.posix); static String join(String path1, String path2, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; @@ -1155,7 +1173,7 @@ class SelectedItems { remove(Entry e) { _items.remove(e); - if (_items.length == 0) { + if (_items.isEmpty) { _isLocal = null; } } @@ -1181,7 +1199,7 @@ class SelectedItems { // edited from [https://github.com/DevsOnFlutter/file_manager/blob/c1bf7f0225b15bcb86eba602c60acd5c4da90dd8/lib/file_manager.dart#L22] List _sortList(List list, SortBy sortType, bool ascending) { - if (sortType == SortBy.Name) { + if (sortType == SortBy.name) { // making list of only folders. final dirs = list .where((element) => element.isDirectory || element.isDrive) @@ -1198,22 +1216,22 @@ List _sortList(List list, SortBy sortType, bool ascending) { return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; - } else if (sortType == SortBy.Modified) { + } else if (sortType == SortBy.modified) { // making the list of Path & DateTime - List<_PathStat> _pathStat = []; + List<_PathStat> pathStat = []; for (Entry e in list) { - _pathStat.add(_PathStat(e.name, e.lastModified())); + pathStat.add(_PathStat(e.name, e.lastModified())); } // sort _pathStat according to date - _pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime)); + pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime)); // sorting [list] according to [_pathStat] - list.sort((a, b) => _pathStat + list.sort((a, b) => pathStat .indexWhere((element) => element.path == a.name) - .compareTo(_pathStat.indexWhere((element) => element.path == b.name))); + .compareTo(pathStat.indexWhere((element) => element.path == b.name))); return ascending ? list : list.reversed.toList(); - } else if (sortType == SortBy.Type) { + } else if (sortType == SortBy.type) { // making list of only folders. final dirs = list.where((element) => element.isDirectory).toList(); @@ -1232,11 +1250,11 @@ List _sortList(List list, SortBy sortType, bool ascending) { return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; - } else if (sortType == SortBy.Size) { + } else if (sortType == SortBy.size) { // create list of path and size - Map _sizeMap = {}; + Map sizeMap = {}; for (Entry e in list) { - _sizeMap[e.name] = e.size; + sizeMap[e.name] = e.size; } // making list of only folders. @@ -1248,14 +1266,13 @@ List _sortList(List list, SortBy sortType, bool ascending) { final files = list.where((element) => element.isFile).toList(); // creating sorted list of [_sizeMapList] by size. - final List> _sizeMapList = _sizeMap.entries.toList(); - _sizeMapList.sort((b, a) => a.value.compareTo(b.value)); + final List> sizeMapList = sizeMap.entries.toList(); + sizeMapList.sort((b, a) => a.value.compareTo(b.value)); // sort [list] according to [_sizeMapList] - files.sort((a, b) => _sizeMapList + files.sort((a, b) => sizeMapList .indexWhere((element) => element.key == a.name) - .compareTo( - _sizeMapList.indexWhere((element) => element.key == b.name))); + .compareTo(sizeMapList.indexWhere((element) => element.key == b.name))); return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 87ac9c8ea..c37af81f5 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -92,6 +92,7 @@ class ServerModel with ChangeNotifier { _serverId = IDTextEditingController(text: _emptyIdShow); Timer.periodic(Duration(seconds: 1), (timer) async { + if (isTest) return timer.cancel(); var status = await bind.mainGetOnlineStatue(); if (status > 0) { status = 1; @@ -343,6 +344,7 @@ class ServerModel with ChangeNotifier { // force updateClientState([String? json]) async { + if (isTest) return; var res = await bind.cmGetClientsState(); try { final List clientsJson = jsonDecode(res); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 4b78bab93..4692b3019 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -243,8 +243,8 @@ packages: dependency: "direct main" description: path: "." - ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf - resolved-ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf + ref: "318ebd0a70cc5868911591c04f84bf1541f1bf4e" + resolved-ref: "318ebd0a70cc5868911591c04f84bf1541f1bf4e" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -688,7 +688,7 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" @@ -1046,6 +1046,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + uni_links: + dependency: "direct main" + description: + name: uni_links + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1" + uni_links_desktop: + dependency: "direct main" + description: + name: uni_links_desktop + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" universal_io: dependency: transitive description: @@ -1221,6 +1249,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" window_manager: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index da086aab1..6d2cb31b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.2.0 environment: - sdk: ">=2.16.1" + sdk: ">=2.17.0" dependencies: flutter: @@ -97,6 +97,7 @@ dependencies: # ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 uni_links_desktop: ^0.1.3 + path: ^1.8.2 dev_dependencies: icons_launcher: ^2.0.4 diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 704124781..c709d618a 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -2,26 +2,63 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; +final testClients = [ + Client(0, false, false, "UserAAAAAA", "123123123", true, false, false), + Client(1, false, false, "UserBBBBB", "221123123", true, false, false), + Client(2, false, false, "UserC", "331123123", true, false, false), + Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) +]; + /// -t lib/cm_main.dart to test cm void main(List args) async { + isTest = true; WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); await windowManager.setSize(const Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); await initEnv(kAppTypeMain); - gFFI.serverModel.clients - .add(Client(0, false, false, "UserA", "123123123", true, false, false)); - gFFI.serverModel.clients - .add(Client(1, false, false, "UserB", "221123123", true, false, false)); - gFFI.serverModel.clients - .add(Client(2, false, false, "UserC", "331123123", true, false, false)); - gFFI.serverModel.clients - .add(Client(3, false, false, "UserD", "441123123", true, false, false)); - runApp(const GetMaterialApp( - debugShowCheckedModeBanner: false, home: DesktopServerPage())); + for (var client in testClients) { + gFFI.serverModel.clients.add(client); + gFFI.serverModel.tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: buildConnectionCard(client)), + authorized: client.authorized); + } + + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.currentThemeMode(), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + home: const DesktopServerPage())); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + // ensure initial window size to be changed + await windowManager.setSize(kConnectionManagerWindowSize); + await Future.wait([ + windowManager.setAlignment(Alignment.topRight), + windowManager.focus(), + windowManager.setOpacity(1) + ]); + // ensure + windowManager.setAlignment(Alignment.topRight); + }); }