2022-10-17 22:26:18 +09:00
import ' dart:async ' ;
2022-06-17 22:57:41 +08:00
import ' dart:io ' ;
2022-07-09 19:14:40 +08:00
import ' dart:math ' ;
2022-06-17 22:57:41 +08:00
2022-08-16 13:28:48 +08:00
import ' package:desktop_drop/desktop_drop.dart ' ;
2022-06-17 22:57:41 +08:00
import ' package:flutter/material.dart ' ;
2022-10-17 22:26:18 +09:00
import ' package:flutter/services.dart ' ;
2022-08-16 11:46:51 +08:00
import ' package:flutter_breadcrumb/flutter_breadcrumb.dart ' ;
2022-06-17 22:57:41 +08:00
import ' package:flutter_hbb/mobile/pages/file_manager_page.dart ' ;
import ' package:flutter_hbb/models/file_model.dart ' ;
import ' package:get/get.dart ' ;
import ' package:provider/provider.dart ' ;
import ' package:wakelock/wakelock.dart ' ;
2022-10-13 20:22:11 +09:00
import ' ../../consts.dart ' ;
import ' ../../desktop/widgets/material_mod_popup_menu.dart ' as mod_menu ;
2022-06-17 22:57:41 +08:00
import ' ../../common.dart ' ;
import ' ../../models/model.dart ' ;
2022-08-03 22:03:31 +08:00
import ' ../../models/platform_model.dart ' ;
2022-10-13 20:22:11 +09:00
import ' ../widgets/popup_menu.dart ' ;
2022-06-17 22:57:41 +08:00
2022-10-13 10:17:20 +09:00
/// status of location bar
enum LocationStatus {
/// normal bread crumb bar
bread ,
/// show path text field
pathLocation ,
/// show file search bar text field
fileSearchBar
}
2022-08-16 11:46:51 +08:00
2022-06-17 22:57:41 +08:00
class FileManagerPage extends StatefulWidget {
2022-09-06 02:08:59 -07:00
const FileManagerPage ( { Key ? key , required this . id } ) : super ( key: key ) ;
2022-06-17 22:57:41 +08:00
final String id ;
@ override
State < StatefulWidget > createState ( ) = > _FileManagerPageState ( ) ;
}
class _FileManagerPageState extends State < FileManagerPage >
with AutomaticKeepAliveClientMixin {
2022-07-09 19:14:40 +08:00
final _localSelectedItems = SelectedItems ( ) ;
final _remoteSelectedItems = SelectedItems ( ) ;
2022-06-17 22:57:41 +08:00
2022-08-16 11:46:51 +08:00
final _locationStatusLocal = LocationStatus . bread . obs ;
final _locationStatusRemote = LocationStatus . bread . obs ;
2022-10-17 22:26:18 +09:00
final _locationNodeLocal = FocusNode ( debugLabel: " locationNodeLocal " ) ;
final _locationNodeRemote = FocusNode ( debugLabel: " locationNodeRemote " ) ;
2022-08-16 12:28:12 +08:00
final _searchTextLocal = " " . obs ;
final _searchTextRemote = " " . obs ;
final _breadCrumbScrollerLocal = ScrollController ( ) ;
final _breadCrumbScrollerRemote = ScrollController ( ) ;
2022-10-17 22:26:18 +09:00
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime = DateTime . now ( ) . millisecondsSinceEpoch ;
Entry ? _lastClickEntry ;
2022-10-13 10:17:20 +09:00
final _dropMaskVisible = false . obs ; // TODO impl drop mask
2022-08-16 13:28:48 +08:00
2022-08-16 12:28:12 +08:00
ScrollController getBreadCrumbScrollController ( bool isLocal ) {
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote ;
}
2022-08-16 11:46:51 +08:00
2022-08-12 18:42:02 +08:00
late FFI _ffi ;
2022-06-17 22:57:41 +08:00
FileModel get model = > _ffi . fileModel ;
2022-07-09 19:14:40 +08:00
SelectedItems getSelectedItem ( bool isLocal ) {
return isLocal ? _localSelectedItems : _remoteSelectedItems ;
}
2022-06-17 22:57:41 +08:00
@ override
void initState ( ) {
super . initState ( ) ;
2022-08-12 18:42:02 +08:00
_ffi = FFI ( ) ;
2022-09-27 20:35:02 +08:00
_ffi . start ( widget . id , isFileTransfer: true ) ;
2022-09-06 19:08:45 +08:00
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
_ffi . dialogManager
. showLoading ( translate ( ' Connecting... ' ) , onCancel: closeConnection ) ;
} ) ;
2022-08-12 18:42:02 +08:00
Get . put ( _ffi , tag: ' ft_ ${ widget . id } ' ) ;
2022-06-17 22:57:41 +08:00
if ( ! Platform . isLinux ) {
Wakelock . enable ( ) ;
}
2022-09-13 21:36:38 +08:00
debugPrint ( " File manager page init success with id ${ widget . id } " ) ;
2022-08-16 11:46:51 +08:00
// register location listener
_locationNodeLocal . addListener ( onLocalLocationFocusChanged ) ;
_locationNodeRemote . addListener ( onRemoteLocationFocusChanged ) ;
2022-06-17 22:57:41 +08:00
}
@ override
void dispose ( ) {
model . onClose ( ) ;
_ffi . close ( ) ;
2022-08-12 18:42:02 +08:00
_ffi . dialogManager . dismissAll ( ) ;
2022-06-17 22:57:41 +08:00
if ( ! Platform . isLinux ) {
Wakelock . disable ( ) ;
}
Get . delete < FFI > ( tag: ' ft_ ${ widget . id } ' ) ;
2022-08-16 11:46:51 +08:00
_locationNodeLocal . removeListener ( onLocalLocationFocusChanged ) ;
_locationNodeRemote . removeListener ( onRemoteLocationFocusChanged ) ;
2022-10-13 10:17:20 +09:00
_locationNodeLocal . dispose ( ) ;
_locationNodeRemote . dispose ( ) ;
2022-06-17 22:57:41 +08:00
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
super . build ( context ) ;
2022-08-19 12:44:35 +08:00
return Overlay ( initialEntries: [
OverlayEntry ( builder: ( context ) {
_ffi . dialogManager . setOverlayState ( Overlay . of ( context ) ) ;
return ChangeNotifierProvider . value (
value: _ffi . fileModel ,
2022-10-11 09:59:27 +09:00
child: Consumer < FileModel > ( builder: ( context , model , child ) {
2022-10-13 10:17:20 +09:00
return Scaffold (
backgroundColor: Theme . of ( context ) . backgroundColor ,
body: Row (
children: [
Flexible ( flex: 3 , child: body ( isLocal: true ) ) ,
Flexible ( flex: 3 , child: body ( isLocal: false ) ) ,
Flexible ( flex: 2 , child: statusList ( ) )
] ,
) ,
) ;
2022-08-19 12:44:35 +08:00
} ) ) ;
} )
] ) ;
2022-06-17 22:57:41 +08:00
}
2022-06-27 16:44:34 +08:00
Widget menu ( { bool isLocal = false } ) {
2022-10-13 20:22:11 +09:00
var menuPos = RelativeRect . fill ;
final items = [
MenuEntrySwitch < String > (
switchType: SwitchType . scheckbox ,
text: translate ( " Show Hidden Files " ) ,
getter: ( ) async {
return model . getCurrentShowHidden ( isLocal ) ;
} ,
setter: ( bool v ) async {
model . toggleShowHidden ( local: isLocal ) ;
2022-06-27 16:44:34 +08:00
} ,
2022-10-13 20:22:11 +09:00
padding: kDesktopMenuPadding ,
dismissOnClicked: true ,
) ,
] ;
return Listener (
onPointerDown: ( e ) {
final x = e . position . dx ;
final y = e . position . dy ;
menuPos = RelativeRect . fromLTRB ( x , y , x , y ) ;
} ,
child: IconButton (
icon: const Icon ( Icons . more_vert ) ,
splashRadius: 20 ,
onPressed: ( ) = > mod_menu . showMenu (
context: context ,
position: menuPos ,
items: items
. map ( ( e ) = > e . build (
context ,
MenuConfig (
commonColor: CustomPopupMenuTheme . commonColor ,
height: CustomPopupMenuTheme . height ,
dividerHeight: CustomPopupMenuTheme . dividerHeight ) ) )
. expand ( ( i ) = > i )
. toList ( ) ,
elevation: 8 ,
) ,
) ) ;
2022-06-27 16:44:34 +08:00
}
2022-06-21 18:14:44 +08:00
Widget body ( { bool isLocal = false } ) {
2022-07-01 17:17:25 +08:00
return Container (
2022-08-16 11:46:51 +08:00
decoration: BoxDecoration ( border: Border . all ( color: Colors . black26 ) ) ,
2022-07-01 17:17:25 +08:00
margin: const EdgeInsets . all ( 16.0 ) ,
padding: const EdgeInsets . all ( 8.0 ) ,
2022-08-16 13:28:48 +08:00
child: DropTarget (
onDragDone: ( detail ) = > handleDragDone ( detail , isLocal ) ,
onDragEntered: ( enter ) {
_dropMaskVisible . value = true ;
} ,
onDragExited: ( exit ) {
_dropMaskVisible . value = false ;
} ,
child: Column ( crossAxisAlignment: CrossAxisAlignment . start , children: [
headTools ( isLocal ) ,
Expanded (
child: Row (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Expanded (
child: SingleChildScrollView (
2022-09-12 11:23:45 +08:00
controller: ScrollController ( ) ,
2022-10-17 22:26:18 +09:00
child: _buildDataTable ( context , isLocal ) ,
2022-07-01 17:17:25 +08:00
) ,
2022-08-16 13:28:48 +08:00
)
] ,
) ) ,
] ) ,
) ,
2022-07-01 17:17:25 +08:00
) ;
2022-06-17 22:57:41 +08:00
}
2022-10-17 22:26:18 +09:00
Widget _buildDataTable ( BuildContext context , bool isLocal ) {
final fd = model . getCurrentDir ( isLocal ) ;
final entries = fd . entries ;
final sortIndex = ( SortBy style ) {
switch ( style ) {
case SortBy . Name:
return 0 ;
case SortBy . Type:
return 0 ;
case SortBy . Modified:
return 1 ;
case SortBy . Size:
return 2 ;
}
} ( model . getSortStyle ( isLocal ) ) ;
final sortAscending =
isLocal ? model . localSortAscending : model . remoteSortAscending ;
return ObxValue < RxString > (
( searchText ) {
final filteredEntries = searchText . isNotEmpty
? entries . where ( ( element ) {
return element . name . contains ( searchText . value ) ;
} ) . toList ( growable: false )
: entries ;
return DataTable (
key: ValueKey ( isLocal ? 0 : 1 ) ,
showCheckboxColumn: false ,
dataRowHeight: 25 ,
headingRowHeight: 30 ,
horizontalMargin: 8 ,
columnSpacing: 8 ,
showBottomBorder: true ,
sortColumnIndex: sortIndex ,
sortAscending: sortAscending ,
columns: [
DataColumn (
label: Text (
translate ( " Name " ) ,
) . marginSymmetric ( horizontal: 4 ) ,
onSort: ( columnIndex , ascending ) {
model . changeSortStyle ( SortBy . Name ,
isLocal: isLocal , ascending: ascending ) ;
} ) ,
DataColumn (
label: Text (
translate ( " Modified " ) ,
) ,
onSort: ( columnIndex , ascending ) {
model . changeSortStyle ( SortBy . Modified ,
isLocal: isLocal , ascending: ascending ) ;
} ) ,
DataColumn (
label: Text ( translate ( " Size " ) ) ,
onSort: ( columnIndex , ascending ) {
model . changeSortStyle ( SortBy . Size ,
isLocal: isLocal , ascending: ascending ) ;
} ) ,
] ,
rows: filteredEntries . map ( ( entry ) {
final sizeStr =
entry . isFile ? readableFileSize ( entry . size . toDouble ( ) ) : " " ;
final lastModifiedStr =
" ${ entry . lastModified ( ) . toString ( ) . replaceAll ( " .000 " , " " ) } " ;
return DataRow (
key: ValueKey ( entry . name ) ,
onSelectChanged: ( s ) {
final isCtrlDown = RawKeyboard . instance . keysPressed
. contains ( LogicalKeyboardKey . controlLeft ) ;
final items = getSelectedItem ( isLocal ) ;
if ( isCtrlDown ) {
if ( s ! = null ) {
if ( s ) {
items . add ( isLocal , entry ) ;
} else {
items . remove ( entry ) ;
}
}
} else {
items . clear ( ) ;
items . add ( isLocal , entry ) ;
}
setState ( ( ) { } ) ;
} ,
selected: getSelectedItem ( isLocal ) . contains ( entry ) ,
cells: [
DataCell (
Container (
width: 200 ,
child: Tooltip (
waitDuration: Duration ( milliseconds: 500 ) ,
message: entry . name ,
child: Row ( children: [
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 ) )
] ) ,
) ) ,
onTap: ( ) {
final items = getSelectedItem ( isLocal ) ;
// handle double click
if ( _checkDoubleClick ( entry ) ) {
openDirectory ( entry . path , isLocal: isLocal ) ;
items . clear ( ) ;
return ;
}
final isCtrlDown = RawKeyboard . instance . keysPressed
. contains ( LogicalKeyboardKey . controlLeft ) ;
if ( isCtrlDown ) {
if ( items . contains ( entry ) ) {
items . remove ( entry ) ;
} else {
items . add ( isLocal , entry ) ;
}
} else {
items . clear ( ) ;
items . add ( isLocal , entry ) ;
}
setState ( ( ) { } ) ;
} ,
) ,
DataCell ( FittedBox (
child: Tooltip (
waitDuration: Duration ( milliseconds: 500 ) ,
message: lastModifiedStr ,
child: Text (
lastModifiedStr ,
style: TextStyle (
fontSize: 12 , color: MyTheme . darkGray ) ,
) ) ) ) ,
DataCell ( Tooltip (
waitDuration: Duration ( milliseconds: 500 ) ,
message: sizeStr ,
child: Text (
sizeStr ,
overflow: TextOverflow . ellipsis ,
style: TextStyle ( fontSize: 10 , color: MyTheme . darkGray ) ,
) ) ) ,
] ) ;
} ) . toList ( growable: false ) ,
) ;
} ,
isLocal ? _searchTextLocal : _searchTextRemote ,
) ;
}
bool _checkDoubleClick ( Entry entry ) {
final current = DateTime . now ( ) . millisecondsSinceEpoch ;
final elapsed = current - _lastClickTime ;
_lastClickTime = current ;
if ( _lastClickEntry = = entry ) {
if ( elapsed < kDesktopDoubleClickTimeMilli ) {
return true ;
}
} else {
_lastClickEntry = entry ;
}
return false ;
}
2022-07-01 12:08:52 +08:00
/// transfer status list
/// watch transfer status
Widget statusList ( ) {
2022-07-01 17:17:25 +08:00
return PreferredSize (
2022-09-06 19:08:45 +08:00
preferredSize: const Size ( 200 , double . infinity ) ,
2022-07-01 17:17:25 +08:00
child: Container (
2022-07-11 16:07:49 +08:00
margin: const EdgeInsets . only ( top: 16.0 , bottom: 16.0 , right: 16.0 ) ,
2022-07-01 17:17:25 +08:00
padding: const EdgeInsets . all ( 8.0 ) ,
2022-08-16 11:46:51 +08:00
decoration: BoxDecoration ( border: Border . all ( color: Colors . grey ) ) ,
2022-07-09 19:14:40 +08:00
child: Obx (
( ) = > ListView . builder (
2022-09-12 11:23:45 +08:00
controller: ScrollController ( ) ,
2022-07-11 10:30:45 +08:00
itemBuilder: ( BuildContext context , int index ) {
final item = model . jobTable [ index ] ;
return Column (
mainAxisSize: MainAxisSize . min ,
2022-07-09 19:14:40 +08:00
children: [
2022-07-11 10:30:45 +08:00
Row (
crossAxisAlignment: CrossAxisAlignment . center ,
children: [
Transform . rotate (
angle: item . isRemote ? pi : 0 ,
2022-09-06 19:08:45 +08:00
child: const Icon ( Icons . send ) ) ,
const SizedBox (
2022-07-11 16:07:49 +08:00
width: 16.0 ,
) ,
2022-07-11 10:30:45 +08:00
Expanded (
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Tooltip (
2022-10-17 22:26:18 +09:00
waitDuration: Duration ( milliseconds: 500 ) ,
2022-07-11 10:30:45 +08:00
message: item . jobName ,
2022-07-11 16:07:49 +08:00
child: Text (
2022-09-06 19:08:45 +08:00
item . jobName ,
2022-07-11 10:30:45 +08:00
maxLines: 1 ,
2022-07-11 16:07:49 +08:00
overflow: TextOverflow . ellipsis ,
) ) ,
2022-07-11 10:30:45 +08:00
Wrap (
children: [
2022-07-11 16:07:49 +08:00
Text (
' ${ item . state . display ( ) } ${ max ( 0 , item . fileNum ) } / ${ item . fileCount } ' ) ,
Text (
' ${ translate ( " files " ) } ${ readableFileSize ( item . totalSize . toDouble ( ) ) } ' ) ,
Offstage (
offstage:
item . state ! = JobState . inProgress ,
child: Text (
2022-09-06 19:08:45 +08:00
' ${ " $ {readableFileSize(item.speed) } /s"} ' ) ) ,
2022-07-11 16:07:49 +08:00
Offstage (
offstage: item . totalSize < = 0 ,
child: Text (
' ${ ( item . finishedSize . toDouble ( ) * 100 / item . totalSize . toDouble ( ) ) . toStringAsFixed ( 2 ) } % ' ) ,
) ,
2022-07-11 10:30:45 +08:00
] ,
) ,
] ,
) ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . end ,
children: [
2022-07-11 18:23:58 +08:00
Offstage (
offstage: item . state ! = JobState . paused ,
child: IconButton (
onPressed: ( ) {
model . resumeJob ( item . id ) ;
} ,
2022-09-06 19:08:45 +08:00
splashRadius: 20 ,
icon: const Icon ( Icons . restart_alt_rounded ) ) ,
2022-07-11 18:23:58 +08:00
) ,
2022-07-11 16:07:49 +08:00
IconButton (
2022-09-06 19:08:45 +08:00
icon: const Icon ( Icons . delete ) ,
splashRadius: 20 ,
2022-07-11 16:07:49 +08:00
onPressed: ( ) {
model . jobTable . removeAt ( index ) ;
model . cancelJob ( item . id ) ;
} ,
) ,
2022-07-11 10:30:45 +08:00
] ,
)
] ,
) ,
2022-07-11 16:07:49 +08:00
SizedBox (
height: 8.0 ,
) ,
Divider (
height: 2.0 ,
)
2022-07-09 19:14:40 +08:00
] ,
) ;
2022-07-11 16:07:49 +08:00
} ,
2022-07-09 19:14:40 +08:00
itemCount: model . jobTable . length ,
) ,
) ,
2022-09-06 19:08:45 +08:00
) ) ;
2022-07-01 12:08:52 +08:00
}
2022-08-16 11:46:51 +08:00
Widget headTools ( bool isLocal ) {
2022-10-11 09:59:27 +09:00
final locationStatus =
2022-08-16 11:46:51 +08:00
isLocal ? _locationStatusLocal : _locationStatusRemote ;
2022-10-11 09:59:27 +09:00
final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote ;
2022-08-16 11:46:51 +08:00
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 < String > (
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 (
2022-09-06 19:08:45 +08:00
onPressed: ( ) {
model . goHome ( isLocal: isLocal ) ;
} ,
icon: const Icon ( Icons . home_outlined ) ,
splashRadius: 20 ,
) ,
2022-10-11 17:25:34 +09:00
IconButton (
icon: const Icon ( Icons . arrow_back ) ,
splashRadius: 20 ,
onPressed: ( ) {
model . goBack ( isLocal: isLocal ) ;
} ,
) ,
2022-08-16 11:46:51 +08:00
IconButton (
2022-09-06 19:08:45 +08:00
icon: const Icon ( Icons . arrow_upward ) ,
splashRadius: 20 ,
2022-08-16 11:46:51 +08:00
onPressed: ( ) {
2022-10-11 17:25:34 +09:00
model . goToParentDirectory ( isLocal: isLocal ) ;
2022-08-16 11:46:51 +08:00
} ,
) ,
] ,
) ,
Expanded (
child: GestureDetector (
onTap: ( ) {
2022-10-11 09:59:27 +09:00
locationStatus . value =
locationStatus . value = = LocationStatus . bread
2022-10-13 10:17:20 +09:00
? LocationStatus . pathLocation
2022-08-16 11:46:51 +08:00
: LocationStatus . bread ;
Future . delayed ( Duration . zero , ( ) {
2022-10-13 10:17:20 +09:00
if ( locationStatus . value = = LocationStatus . pathLocation ) {
2022-10-11 09:59:27 +09:00
locationFocus . requestFocus ( ) ;
2022-08-16 11:46:51 +08:00
}
} ) ;
} ,
2022-10-13 10:17:20 +09:00
child: Obx ( ( ) = > Container (
decoration: BoxDecoration (
border: Border . all (
color: locationStatus . value = = LocationStatus . bread
? Colors . black12
: Theme . of ( context )
. colorScheme
. primary
. withOpacity ( 0.5 ) ) ) ,
2022-08-16 11:46:51 +08:00
child: Row (
children: [
Expanded (
2022-10-13 10:17:20 +09:00
child: locationStatus . value = = LocationStatus . bread
? buildBread ( isLocal )
: buildPathLocation ( isLocal ) ) ,
2022-08-16 11:46:51 +08:00
] ,
2022-10-13 10:17:20 +09:00
) ) ) ,
2022-08-16 11:46:51 +08:00
) ) ,
2022-10-13 10:17:20 +09:00
Obx ( ( ) {
switch ( locationStatus . value ) {
case LocationStatus . bread:
return IconButton (
onPressed: ( ) {
locationStatus . value = LocationStatus . fileSearchBar ;
final focusNode =
isLocal ? _locationNodeLocal : _locationNodeRemote ;
Future . delayed (
Duration . zero , ( ) = > focusNode . requestFocus ( ) ) ;
} ,
splashRadius: 20 ,
icon: Icon ( Icons . search ) ) ;
case LocationStatus . pathLocation:
return IconButton (
color: Theme . of ( context ) . disabledColor ,
onPressed: null ,
splashRadius: 20 ,
icon: Icon ( Icons . close ) ) ;
case LocationStatus . fileSearchBar:
return IconButton (
color: Theme . of ( context ) . disabledColor ,
onPressed: ( ) {
onSearchText ( " " , isLocal ) ;
locationStatus . value = LocationStatus . bread ;
} ,
splashRadius: 1 ,
icon: Icon ( Icons . close ) ) ;
}
} ) ,
2022-08-16 11:46:51 +08:00
IconButton (
onPressed: ( ) {
model . refresh ( isLocal: isLocal ) ;
} ,
2022-09-06 19:08:45 +08:00
splashRadius: 20 ,
icon: const Icon ( Icons . refresh ) ) ,
2022-08-16 11:46:51 +08:00
] ,
) ,
Row (
textDirection: isLocal ? TextDirection . ltr : TextDirection . rtl ,
children: [
Expanded (
child: Row (
mainAxisAlignment:
isLocal ? MainAxisAlignment . start : MainAxisAlignment . end ,
2022-07-11 16:07:49 +08:00
children: [
IconButton (
onPressed: ( ) {
2022-08-16 11:46:51 +08:00
final name = TextEditingController ( ) ;
2022-09-03 18:19:50 +08:00
_ffi . dialogManager . show ( ( setState , close ) {
submit ( ) {
if ( name . value . text . isNotEmpty ) {
model . createDir (
PathUtil . join (
model . getCurrentDir ( isLocal ) . path ,
name . value . text ,
model . getCurrentIsWindows ( isLocal ) ) ,
isLocal: isLocal ) ;
close ( ) ;
}
}
cancel ( ) = > close ( false ) ;
return CustomAlertDialog (
title: Text ( translate ( " Create Folder " ) ) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
TextFormField (
decoration: InputDecoration (
labelText: translate (
" Please enter the folder name " ) ,
) ,
controller: name ,
focusNode: FocusNode ( ) . . requestFocus ( ) ,
) ,
] ,
) ,
actions: [
TextButton (
style: flatButtonStyle ,
onPressed: cancel ,
child: Text ( translate ( " Cancel " ) ) ) ,
ElevatedButton (
style: flatButtonStyle ,
onPressed: submit ,
child: Text ( translate ( " OK " ) ) )
] ,
onSubmit: submit ,
onCancel: cancel ,
) ;
} ) ;
2022-07-11 16:07:49 +08:00
} ,
2022-09-06 19:08:45 +08:00
splashRadius: 20 ,
2022-09-03 18:19:50 +08:00
icon: const Icon ( Icons . create_new_folder_outlined ) ) ,
2022-07-11 16:07:49 +08:00
IconButton (
2022-08-16 11:46:51 +08:00
onPressed: ( ) async {
final items = isLocal
? _localSelectedItems
: _remoteSelectedItems ;
await ( model . removeAction ( items , isLocal: isLocal ) ) ;
items . clear ( ) ;
} ,
2022-09-06 19:08:45 +08:00
splashRadius: 20 ,
icon: const Icon ( Icons . delete_forever_outlined ) ) ,
2022-10-11 17:25:34 +09:00
menu ( isLocal: isLocal ) ,
2022-07-11 16:07:49 +08:00
] ,
2022-06-17 22:57:41 +08:00
) ,
2022-08-16 11:46:51 +08:00
) ,
TextButton . icon (
onPressed: ( ) {
final items = getSelectedItem ( isLocal ) ;
model . sendFiles ( items , isRemote: ! isLocal ) ;
items . clear ( ) ;
} ,
icon: Transform . rotate (
angle: isLocal ? 0 : pi ,
2022-09-06 19:08:45 +08:00
child: const Icon (
2022-08-16 11:46:51 +08:00
Icons . send ,
2022-07-11 16:07:49 +08:00
) ,
2022-08-16 11:46:51 +08:00
) ,
label: Text (
isLocal ? translate ( ' Send ' ) : translate ( ' Receive ' ) ,
) ) ,
] ,
) . marginOnly ( top: 8.0 )
] ,
) ) ;
}
2022-06-17 22:57:41 +08:00
@ override
bool get wantKeepAlive = > true ;
2022-07-11 16:07:49 +08:00
2022-08-16 11:46:51 +08:00
void onLocalLocationFocusChanged ( ) {
debugPrint ( " focus changed on local " ) ;
if ( _locationNodeLocal . hasFocus ) {
// ignore
} else {
// lost focus, change to bread
2022-10-13 10:17:20 +09:00
if ( _locationStatusLocal . value ! = LocationStatus . fileSearchBar ) {
_locationStatusLocal . value = LocationStatus . bread ;
}
2022-08-16 11:46:51 +08:00
}
}
void onRemoteLocationFocusChanged ( ) {
debugPrint ( " focus changed on remote " ) ;
if ( _locationNodeRemote . hasFocus ) {
// ignore
} else {
// lost focus, change to bread
2022-10-13 10:17:20 +09:00
if ( _locationStatusRemote . value ! = LocationStatus . fileSearchBar ) {
_locationStatusRemote . value = LocationStatus . bread ;
}
2022-08-16 11:46:51 +08:00
}
}
Widget buildBread ( bool isLocal ) {
2022-08-16 12:50:08 +08:00
final items = getPathBreadCrumbItems ( isLocal , ( list ) {
var path = " " ;
for ( var item in list ) {
path = PathUtil . join ( path , item , model . getCurrentIsWindows ( isLocal ) ) ;
}
openDirectory ( path , isLocal: isLocal ) ;
} ) ;
2022-10-13 10:17:20 +09:00
breadCrumbScrollToEnd ( isLocal ) ;
2022-08-16 12:50:08 +08:00
return items . isEmpty
? Offstage ( )
2022-10-13 10:17:20 +09:00
: Row ( mainAxisAlignment: MainAxisAlignment . spaceBetween , children: [
Expanded (
child: BreadCrumb (
items: items ,
divider: Text ( " / " ) . paddingSymmetric ( horizontal: 4.0 ) ,
overflow: ScrollableOverflow (
controller: getBreadCrumbScrollController ( isLocal ) ) ,
) ) ,
DropdownButton < String > (
isDense: true ,
underline: Offstage ( ) ,
items: [
// TODO: favourite
DropdownMenuItem (
child: Text ( ' / ' ) ,
value: ' / ' ,
)
] ,
onChanged: ( path ) {
if ( path is String & & path . isNotEmpty ) {
openDirectory ( path , isLocal: isLocal ) ;
}
} )
] ) ;
2022-08-16 11:46:51 +08:00
}
List < BreadCrumbItem > getPathBreadCrumbItems (
bool isLocal , void Function ( List < String > ) onPressed ) {
final path = model . getCurrentDir ( isLocal ) . path ;
final list = PathUtil . split ( path , model . getCurrentIsWindows ( isLocal ) ) ;
final breadCrumbList = List < BreadCrumbItem > . 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 ;
}
2022-08-16 12:28:12 +08:00
breadCrumbScrollToEnd ( bool isLocal ) {
Future . delayed ( Duration ( milliseconds: 200 ) , ( ) {
2022-10-11 09:59:27 +09:00
final breadCrumbScroller = getBreadCrumbScrollController ( isLocal ) ;
2022-10-13 20:22:11 +09:00
if ( breadCrumbScroller . hasClients ) {
breadCrumbScroller . animateTo (
breadCrumbScroller . position . maxScrollExtent ,
duration: Duration ( milliseconds: 200 ) ,
curve: Curves . fastLinearToSlowEaseIn ) ;
}
2022-08-16 12:28:12 +08:00
} ) ;
}
2022-08-16 11:46:51 +08:00
Widget buildPathLocation ( bool isLocal ) {
2022-10-13 10:17:20 +09:00
final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote ;
final locationStatus =
isLocal ? _locationStatusLocal : _locationStatusRemote ;
final focusNode = isLocal ? _locationNodeLocal : _locationNodeRemote ;
2022-10-13 19:57:59 +09:00
final text = locationStatus . value = = LocationStatus . pathLocation
? model . getCurrentDir ( isLocal ) . path
: searchTextObs . value ;
final textController = TextEditingController ( text: text )
. . selection = TextSelection . collapsed ( offset: text . length ) ;
2022-10-13 10:17:20 +09:00
return Row ( children: [
Icon (
locationStatus . value = = LocationStatus . pathLocation
? Icons . folder
: Icons . search ,
color: Theme . of ( context ) . hintColor ,
) . paddingSymmetric ( horizontal: 2 ) ,
Expanded (
child: TextField (
focusNode: focusNode ,
decoration: InputDecoration (
border: InputBorder . none ,
isDense: true ,
prefix: Padding ( padding: EdgeInsets . only ( left: 4.0 ) ) ) ,
2022-10-13 19:57:59 +09:00
controller: textController ,
2022-10-13 10:17:20 +09:00
onSubmitted: ( path ) {
openDirectory ( path , isLocal: isLocal ) ;
} ,
onChanged: locationStatus . value = = LocationStatus . fileSearchBar
? ( searchText ) = > onSearchText ( searchText , isLocal )
: null ,
) )
] ) ;
2022-08-16 11:46:51 +08:00
}
2022-08-16 12:06:54 +08:00
onSearchText ( String searchText , bool isLocal ) {
if ( isLocal ) {
2022-08-16 12:28:12 +08:00
_searchTextLocal . value = searchText ;
2022-08-16 12:06:54 +08:00
} else {
2022-08-16 12:28:12 +08:00
_searchTextRemote . value = searchText ;
2022-08-16 12:06:54 +08:00
}
}
2022-08-16 12:28:12 +08:00
openDirectory ( String path , { bool isLocal = false } ) {
model . openDirectory ( path , isLocal: isLocal ) . then ( ( _ ) {
breadCrumbScrollToEnd ( isLocal ) ;
} ) ;
}
2022-08-16 13:28:48 +08:00
void handleDragDone ( DropDoneDetails details , bool isLocal ) {
if ( isLocal ) {
// ignore local
return ;
}
var items = SelectedItems ( ) ;
2022-10-11 09:59:27 +09:00
for ( var file in details . files ) {
2022-08-16 13:28:48 +08:00
final f = File ( file . path ) ;
items . add (
true ,
Entry ( )
. . path = file . path
. . name = file . name
. . size =
FileSystemEntity . isDirectorySync ( f . path ) ? 0 : f . lengthSync ( ) ) ;
2022-10-11 09:59:27 +09:00
}
2022-08-16 13:28:48 +08:00
model . sendFiles ( items , isRemote: false ) ;
}
2022-06-17 22:57:41 +08:00
}