diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b73182fd..537784918 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -53,6 +53,8 @@ const int kDesktopMaxDisplayHeight = 1080; const double kDesktopFileTransferNameColWidth = 200; const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferMinimumWidth = 100; +const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0d55552af..68023f929 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; @@ -78,6 +79,10 @@ class _FileManagerPageState extends State final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote"); final _listSearchBufferLocal = TimeoutStringBuffer(); final _listSearchBufferRemote = TimeoutStringBuffer(); + final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs; + final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs; /// [_lastClickTime], [_lastClickEntry] help to handle double click int _lastClickTime = @@ -297,11 +302,12 @@ class _FileManagerPageState extends State } var searchResult = entries .skip(skipCount) - .where((element) => element.name.startsWith(buffer)); + .where((element) => element.name.toLowerCase().startsWith(buffer)); if (searchResult.isEmpty) { // cannot find next, lets restart search from head + debugPrint("restart search from head"); searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); } if (searchResult.isEmpty) { setState(() { @@ -316,7 +322,7 @@ class _FileManagerPageState extends State debugPrint("searching for $buffer"); final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); selectedEntries.clear(); if (searchResult.isEmpty) { setState(() { @@ -362,37 +368,41 @@ class _FileManagerPageState extends State child: Row( children: [ GestureDetector( - child: Container( - width: kDesktopFileTransferNameColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : SvgPicture.asset( - entry.isFile - ? "assets/file.svg" - : "assets/folder.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, - ), - Expanded( - child: Text( - entry.name.nonBreaking, - overflow: - TextOverflow.ellipsis)) - ]), - )), + child: Obx( + () => Container( + width: isLocal + ? _nameColWidthLocal.value + : _nameColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + Expanded( + child: Text( + entry.name.nonBreaking, + overflow: + TextOverflow.ellipsis)) + ]), + )), + ), onTap: () { final items = getSelectedItems(isLocal); // handle double click @@ -406,24 +416,35 @@ class _FileManagerPageState extends State items, filteredEntries, entry, isLocal); }, ), + SizedBox( + width: 2.0, + ), GestureDetector( - child: SizedBox( - width: kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - )), + child: Obx( + () => SizedBox( + width: isLocal + ? _modifiedColWidthLocal.value + : _modifiedColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), + ), ), ), + // Divider from header. SizedBox( - width: 100, + width: 2.0, + ), + Expanded( + // width: 100, child: GestureDetector( child: Tooltip( waitDuration: Duration(milliseconds: 500), @@ -1362,6 +1383,7 @@ class _FileManagerPageState extends State Text( name, style: headerTextStyle, + overflow: TextOverflow.ellipsis, ).marginSymmetric(horizontal: 4), ascending.value != null ? Icon( @@ -1383,16 +1405,48 @@ class _FileManagerPageState extends State } Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) { - return Row( - children: [ - headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name, - translate("Name"), isLocal), - headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified, - translate("Modified"), isLocal), - Expanded( - child: - headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) - ], + final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote; + final modifiedColWidth = + isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote; + final padding = EdgeInsets.all(1.0); + return SizedBox( + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Obx( + () => headerItemFunc( + nameColWidth.value, SortBy.name, translate("Name"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + nameColWidth.value += dx; + nameColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + nameColWidth.value)); + }, + padding: padding, + ), + Obx( + () => headerItemFunc(modifiedColWidth.value, SortBy.modified, + translate("Modified"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + modifiedColWidth.value += dx; + modifiedColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + modifiedColWidth.value)); + }, + padding: padding), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ), ); } } diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart new file mode 100644 index 000000000..3821b7e0d --- /dev/null +++ b/flutter/lib/desktop/widgets/dragable_divider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class DraggableDivider extends StatefulWidget { + final Axis axis; + final double thickness; + final Color color; + final Function(double)? onPointerMove; + final VoidCallback? onHover; + final EdgeInsets padding; + const DraggableDivider({ + super.key, + this.axis = Axis.horizontal, + this.thickness = 1.0, + this.color = const Color.fromARGB(200, 177, 175, 175), + this.onPointerMove, + this.padding = const EdgeInsets.symmetric(horizontal: 1.0), + this.onHover, + }); + + @override + State createState() => _DraggableDividerState(); +} + +class _DraggableDividerState extends State { + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (event) { + final dl = + widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx; + widget.onPointerMove?.call(dl); + }, + onPointerHover: (event) => widget.onHover?.call(), + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: Padding( + padding: widget.padding, + child: Container( + decoration: BoxDecoration(color: widget.color), + width: widget.axis == Axis.horizontal + ? double.infinity + : widget.thickness, + height: widget.axis == Axis.horizontal + ? widget.thickness + : double.infinity, + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart index 9598c3400..36128bf26 100644 --- a/flutter/lib/desktop/widgets/list_search_action_listener.dart +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -55,6 +55,7 @@ class TimeoutStringBuffer { } ListSearchAction input(String ch) { + ch = ch.toLowerCase(); final curr = DateTime.now(); try { if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {