From d9c93655204665b6da79b182de6170aa0a88e4da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 11:46:51 +0800 Subject: [PATCH] feat: switch breadcrumb&path with focus node Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 456 +++++++++++------- flutter/lib/models/file_model.dart | 16 + 2 files changed, 297 insertions(+), 175 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e5279a7e2..9febc462b 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:io'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; @@ -12,6 +13,8 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +enum LocationStatus { bread, textField } + class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -25,6 +28,17 @@ class _FileManagerPageState extends State final _localSelectedItems = SelectedItems(); final _remoteSelectedItems = SelectedItems(); + final _locationStatusLocal = LocationStatus.bread.obs; + final _locationStatusRemote = LocationStatus.bread.obs; + final FocusNode _locationNodeLocal = + FocusNode(debugLabel: "locationNodeLocal"); + final FocusNode _locationNodeRemote = + FocusNode(debugLabel: "locationNodeRemote"); + final FocusNode _locationSearchLocal = + FocusNode(debugLabel: "locationSearchLocal"); + final FocusNode _locationSearchRemote = + FocusNode(debugLabel: "locationSearchRemote"); + late FFI _ffi; FileModel get model => _ffi.fileModel; @@ -44,6 +58,9 @@ class _FileManagerPageState extends State Wakelock.enable(); } print("init success with id ${widget.id}"); + // register location listener + _locationNodeLocal.addListener(onLocalLocationFocusChanged); + _locationNodeRemote.addListener(onRemoteLocationFocusChanged); } @override @@ -55,6 +72,8 @@ class _FileManagerPageState extends State Wakelock.disable(); } Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); super.dispose(); } @@ -129,8 +148,7 @@ class _FileManagerPageState extends State final sortAscending = isLocal ? model.localSortAscending : model.remoteSortAscending; return Container( - decoration: BoxDecoration( - color: Colors.white54, border: Border.all(color: Colors.black26)), + decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -142,6 +160,7 @@ class _FileManagerPageState extends State Expanded( child: SingleChildScrollView( child: DataTable( + key: ValueKey(isLocal ? 0 : 1), showCheckboxColumn: true, dataRowHeight: 25, headingRowHeight: 30, @@ -223,9 +242,9 @@ class _FileManagerPageState extends State }), DataCell(Text( entry - .lastModified() - .toString() - .replaceAll(".000", "") + + .lastModified() + .toString() + .replaceAll(".000", "") + " ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), @@ -355,8 +374,7 @@ class _FileManagerPageState extends State child: Container( margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Colors.white70, border: Border.all(color: Colors.grey)), + decoration: BoxDecoration(border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( itemBuilder: (BuildContext context, int index) { @@ -449,183 +467,206 @@ class _FileManagerPageState extends State model.goToParentDirectory(isLocal: isLocal); } - Widget headTools(bool isLocal) => Container( - child: Column( - children: [ - // symbols - PreferredSize( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration(color: Colors.blue), - padding: EdgeInsets.all(8.0), - child: FutureBuilder( - future: bind.sessionGetPlatform( - id: _ffi.id, isRemote: !isLocal), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return getPlatformImage('${snapshot.data}'); - } else { - return CircularProgressIndicator( - color: Colors.white, - ); + Widget headTools(bool isLocal) { + final _locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + return Container( + child: Column( + children: [ + // symbols + PreferredSize( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration(color: Colors.blue), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Colors.white, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) + ], + ), + preferredSize: Size(double.infinity, 70)), + // buttons + Row( + children: [ + Row( + children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: Icon(Icons.home_outlined)), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: () { + goBack(isLocal: isLocal); + }, + ), + menu(isLocal: isLocal), + ], + ), + Expanded( + child: GestureDetector( + onTap: () { + _locationStatus.value = + _locationStatus.value == LocationStatus.bread + ? LocationStatus.textField + : LocationStatus.bread; + Future.delayed(Duration.zero, () { + if (_locationStatus.value == LocationStatus.textField) { + _locationFocus.requestFocus(); + } + }); + }, + child: Container( + decoration: + BoxDecoration(border: Border.all(color: Colors.black12)), + child: Row( + children: [ + Expanded( + child: Obx(() => + _locationStatus.value == LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal))), + DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + model.openDirectory(path, isLocal: isLocal); } - })), - Text(isLocal - ? translate("Local Computer") - : translate("Remote Computer")) - .marginOnly(left: 8.0) - ], - ), - preferredSize: Size(double.infinity, 70)), - // buttons - Row( - children: [ - Row( + }) + ], + )), + )), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + enabled: false, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: 200), + child: TextField( + decoration: InputDecoration(), + ), + )) + ], + child: Icon(Icons.search), + ), + IconButton( + onPressed: () { + model.refresh(isLocal: isLocal); + }, + icon: Icon(Icons.refresh)), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ IconButton( onPressed: () { - model.goHome(isLocal: isLocal); + final name = TextEditingController(); + _ffi.dialogManager + .show((setState, close) => CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model + .getCurrentDir( + isLocal) + .path, + name.value.text, + model.getCurrentIsWindows( + isLocal)), + isLocal: isLocal); + close(); + } + }, + child: Text(translate("OK"))) + ])); }, - icon: Icon(Icons.home_outlined)), + icon: Icon(Icons.create_new_folder_outlined)), IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: () { - goBack(isLocal: isLocal); - }, - ), - menu(isLocal: isLocal), + onPressed: () async { + final items = isLocal + ? _localSelectedItems + : _remoteSelectedItems; + await (model.removeAction(items, isLocal: isLocal)); + items.clear(); + }, + icon: Icon(Icons.delete_forever_outlined)), ], ), - Expanded( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black12)), - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: - Padding(padding: EdgeInsets.only(left: 4.0)), - suffix: DropdownButton( - isDense: true, - underline: Offstage(), - items: [ - // TODO: favourite - DropdownMenuItem( - child: Text('/'), - value: '/', - ) - ], - onChanged: (path) { - if (path is String && path.isNotEmpty) { - model.openDirectory(path, isLocal: isLocal); - } - })), - controller: TextEditingController( - text: isLocal - ? model.currentLocalDir.path - : model.currentRemoteDir.path), - onSubmitted: (path) { - model.openDirectory(path, isLocal: isLocal); - }, - ))), - IconButton( - onPressed: () { - model.refresh(isLocal: isLocal); - }, - icon: Icon(Icons.refresh)) - ], - ), - Row( - textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, - children: [ - Expanded( - child: Row( - mainAxisAlignment: - isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - final name = TextEditingController(); - _ffi.dialogManager.show((setState, close) => - CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir( - PathUtil.join( - model - .getCurrentDir(isLocal) - .path, - name.value.text, - model.getCurrentIsWindows( - isLocal)), - isLocal: isLocal); - close(); - } - }, - child: Text(translate("OK"))) - ])); - }, - icon: Icon(Icons.create_new_folder_outlined)), - IconButton( - onPressed: () async { - final items = isLocal - ? _localSelectedItems - : _remoteSelectedItems; - await (model.removeAction(items, isLocal: isLocal)); - items.clear(); - }, - icon: Icon(Icons.delete_forever_outlined)), - ], - ), - ), - TextButton.icon( - onPressed: () { - final items = getSelectedItem(isLocal); - model.sendFiles(items, isRemote: !isLocal); - items.clear(); - }, - icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: Icon( - Icons.send, - color: Colors.black54, - ), + ), + TextButton.icon( + onPressed: () { + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + items.clear(); + }, + icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send, ), - label: Text( - isLocal ? translate('Send') : translate('Receive'), - style: TextStyle( - color: Colors.black54, - ), - )), - ], - ).marginOnly(top: 8.0) - ], - )); + ), + label: Text( + isLocal ? translate('Send') : translate('Receive'), + )), + ], + ).marginOnly(top: 8.0) + ], + )); + } Widget listTail({bool isLocal = false}) { final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; @@ -663,4 +704,69 @@ class _FileManagerPageState extends State else if (platform != 'linux' && platform != 'android') platform = 'win'; return Image.asset('assets/$platform.png', width: 25, height: 25); } + + void onLocalLocationFocusChanged() { + debugPrint("focus changed on local"); + if (_locationNodeLocal.hasFocus) { + // ignore + } else { + // lost focus, change to bread + _locationStatusLocal.value = LocationStatus.bread; + } + } + + void onRemoteLocationFocusChanged() { + debugPrint("focus changed on remote"); + if (_locationNodeRemote.hasFocus) { + // ignore + } else { + // lost focus, change to bread + _locationStatusRemote.value = LocationStatus.bread; + } + } + + Widget buildBread(bool isLocal) { + final directory = model.getCurrentDir(isLocal); + print(directory.path); + return BreadCrumb( + items: getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + } + model.openDirectory(path, isLocal: isLocal); + }), + divider: Text("/").paddingSymmetric(horizontal: 4.0), + ); + } + + List getPathBreadCrumbItems( + bool isLocal, void Function(List) onPressed) { + final path = model.getCurrentDir(isLocal).path; + final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal)); + 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)))))); + return breadCrumbList; + } + + Widget buildPathLocation(bool isLocal) { + return TextField( + focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding(padding: EdgeInsets.only(left: 4.0)), + ), + controller: + TextEditingController(text: model.getCurrentDir(isLocal).path), + onSubmitted: (path) { + model.openDirectory(path, isLocal: isLocal); + }, + ); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 74be258a0..1c3960b50 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -68,6 +68,22 @@ class FileModel extends ChangeNotifier { return isLocal ? currentLocalDir : currentRemoteDir; } + String getCurrentShortPath(bool isLocal) { + final currentDir = getCurrentDir(isLocal); + final currentHome = getCurrentHome(isLocal); + if (currentDir.path.startsWith(currentHome)) { + var path = currentDir.path.replaceFirst(currentHome, ""); + if (path.length == 0) return ""; + if (path[0] == "/" || path[0] == "\\") { + // remove more '/' or '\' + path = path.replaceFirst(path[0], ""); + } + return path; + } else { + return currentDir.path.replaceFirst(currentHome, ""); + } + } + String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; String getCurrentHome(bool isLocal) {