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
2023-02-24 14:15:54 +08:00
import ' package:flutter_hbb/desktop/widgets/dragable_divider.dart ' ;
2023-02-22 22:13:21 +01:00
import ' package:percent_indicator/percent_indicator.dart ' ;
2023-05-12 13:22:49 +08:00
import ' package:desktop_drop/desktop_drop.dart ' ;
2022-10-20 11:20:41 +09:00
import ' package:flutter/gestures.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-12-13 12:55:41 +08:00
import ' package:flutter_hbb/desktop/widgets/list_search_action_listener.dart ' ;
2023-02-22 22:13:21 +01:00
import ' package:flutter_hbb/desktop/widgets/menu_button.dart ' ;
2022-10-18 23:56:36 +09:00
import ' package:flutter_hbb/desktop/widgets/tabbar_widget.dart ' ;
2022-06-17 22:57:41 +08:00
import ' package:flutter_hbb/models/file_model.dart ' ;
2023-02-22 22:13:21 +01:00
import ' package:flutter_svg/flutter_svg.dart ' ;
2022-06-17 22:57:41 +08:00
import ' package:get/get.dart ' ;
import ' package:wakelock/wakelock.dart ' ;
2023-02-22 22:13:21 +01:00
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-12-13 12:55:41 +08:00
/// The status of currently focused scope of the mouse
enum MouseFocusScope {
/// Mouse is in local field.
local ,
/// Mouse is in remote field.
remote ,
/// Mouse is not in local field, remote neither.
none
}
2022-06-17 22:57:41 +08:00
class FileManagerPage extends StatefulWidget {
2023-02-13 16:40:24 +08:00
const FileManagerPage ( { Key ? key , required this . id , this . forceRelay } )
: super ( key: key ) ;
2022-06-17 22:57:41 +08:00
final String id ;
2023-02-13 16:40:24 +08:00
final bool ? forceRelay ;
2022-06-17 22:57:41 +08:00
@ override
State < StatefulWidget > createState ( ) = > _FileManagerPageState ( ) ;
}
class _FileManagerPageState extends State < FileManagerPage >
with AutomaticKeepAliveClientMixin {
2022-12-13 12:55:41 +08:00
final _mouseFocusScope = Rx < MouseFocusScope > ( MouseFocusScope . none ) ;
2022-10-17 22:26:18 +09:00
2023-05-12 13:22:49 +08:00
final _dropMaskVisible = false . obs ; // TODO impl drop mask
2023-02-08 22:01:15 +09:00
final _overlayKeyState = OverlayKeyState ( ) ;
2022-08-16 13:28:48 +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 ;
2023-03-08 22:32:55 +09:00
JobController get jobController = > model . jobController ;
2022-06-17 22:57:41 +08:00
@ override
void initState ( ) {
super . initState ( ) ;
2022-08-12 18:42:02 +08:00
_ffi = FFI ( ) ;
2023-02-13 16:40:24 +08:00
_ffi . start ( widget . id , isFileTransfer: true , forceRelay: widget . forceRelay ) ;
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 } " ) ;
2023-02-08 22:01:15 +09:00
_ffi . dialogManager . setOverlayState ( _overlayKeyState ) ;
2022-06-17 22:57:41 +08:00
}
@ override
void dispose ( ) {
2023-03-08 21:05:55 +09:00
model . close ( ) . whenComplete ( ( ) {
2022-12-04 22:41:44 +09:00
_ffi . close ( ) ;
_ffi . dialogManager . dismissAll ( ) ;
if ( ! Platform . isLinux ) {
Wakelock . disable ( ) ;
}
Get . delete < FFI > ( tag: ' ft_ ${ widget . id } ' ) ;
} ) ;
2022-06-17 22:57:41 +08:00
super . dispose ( ) ;
}
2023-03-08 00:49:14 +09:00
@ override
bool get wantKeepAlive = > true ;
2022-06-17 22:57:41 +08:00
@ override
Widget build ( BuildContext context ) {
super . build ( context ) ;
2023-02-08 22:01:15 +09:00
return Overlay ( key: _overlayKeyState . key , initialEntries: [
OverlayEntry ( builder: ( _ ) {
2023-03-08 22:32:55 +09:00
return Scaffold (
backgroundColor: Theme . of ( context ) . scaffoldBackgroundColor ,
body: Row (
children: [
Flexible (
flex: 3 ,
child: dropArea ( FileManagerView (
model . localController , _ffi , _mouseFocusScope ) ) ) ,
Flexible (
flex: 3 ,
child: dropArea ( FileManagerView (
model . remoteController , _ffi , _mouseFocusScope ) ) ) ,
Flexible ( flex: 2 , child: statusList ( ) )
] ,
) ,
) ;
2022-08-19 12:44:35 +08:00
} )
] ) ;
2022-06-17 22:57:41 +08:00
}
2023-03-08 00:49:14 +09:00
Widget dropArea ( FileManagerView fileView ) {
2023-05-12 13:22:49 +08:00
return DropTarget (
onDragDone: ( detail ) = >
handleDragDone ( detail , fileView . controller . isLocal ) ,
onDragEntered: ( enter ) {
_dropMaskVisible . value = true ;
} ,
onDragExited: ( exit ) {
_dropMaskVisible . value = false ;
} ,
child: fileView ) ;
2022-10-17 22:26:18 +09:00
}
2023-02-26 09:13:42 +01:00
Widget generateCard ( Widget child ) {
return Container (
decoration: BoxDecoration (
color: Theme . of ( context ) . cardColor ,
borderRadius: BorderRadius . all (
Radius . circular ( 15.0 ) ,
) ,
) ,
child: child ,
) ;
}
2022-07-01 12:08:52 +08:00
/// transfer status list
/// watch transfer status
Widget statusList ( ) {
2023-03-09 11:40:06 +09:00
statusListView ( List < JobProgress > jobs ) = > ListView . builder (
2023-03-09 00:06:24 +09:00
controller: ScrollController ( ) ,
itemBuilder: ( BuildContext context , int index ) {
2023-03-09 11:40:06 +09:00
final item = jobs [ index ] ;
2023-03-09 00:06:24 +09:00
return Padding (
padding: const EdgeInsets . only ( bottom: 5 ) ,
child: generateCard (
Column (
mainAxisSize: MainAxisSize . min ,
children: [
Row (
crossAxisAlignment: CrossAxisAlignment . center ,
children: [
Transform . rotate (
angle: item . isRemoteToLocal ? pi : 0 ,
child: SvgPicture . asset (
" assets/arrow.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
) . paddingOnly ( left: 15 ) ,
const SizedBox (
width: 16.0 ,
) ,
Expanded (
child: Column (
mainAxisSize: MainAxisSize . min ,
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Tooltip (
waitDuration: Duration ( milliseconds: 500 ) ,
message: item . jobName ,
child: Text (
item . fileName ,
maxLines: 1 ,
overflow: TextOverflow . ellipsis ,
) . paddingSymmetric ( vertical: 10 ) ,
) ,
Text (
' ${ translate ( " Total " ) } ${ readableFileSize ( item . totalSize . toDouble ( ) ) } ' ,
style: TextStyle (
fontSize: 12 ,
color: MyTheme . darkGray ,
) ,
) ,
Offstage (
offstage: item . state ! = JobState . inProgress ,
child: Text (
' ${ translate ( " Speed " ) } ${ readableFileSize ( item . speed ) } /s ' ,
style: TextStyle (
fontSize: 12 ,
color: MyTheme . darkGray ,
2023-02-25 09:44:23 +01:00
) ,
) ,
2023-03-09 00:06:24 +09:00
) ,
Offstage (
offstage: item . state = = JobState . inProgress ,
child: Text (
translate (
item . display ( ) ,
) ,
style: TextStyle (
fontSize: 12 ,
color: MyTheme . darkGray ,
2023-02-22 22:13:21 +01:00
) ,
2023-02-25 09:44:23 +01:00
) ,
2023-03-09 00:06:24 +09:00
) ,
Offstage (
offstage: item . state ! = JobState . inProgress ,
child: LinearPercentIndicator (
padding: EdgeInsets . only ( right: 15 ) ,
animateFromLastPercent: true ,
center: Text (
' ${ ( item . finishedSize / item . totalSize * 100 ) . toStringAsFixed ( 0 ) } % ' ,
) ,
barRadius: Radius . circular ( 15 ) ,
percent: item . finishedSize / item . totalSize ,
progressColor: MyTheme . accent ,
backgroundColor: Theme . of ( context ) . hoverColor ,
lineHeight: kDesktopFileTransferRowHeight ,
) . paddingSymmetric ( vertical: 15 ) ,
) ,
] ,
) ,
) ,
Row (
mainAxisAlignment: MainAxisAlignment . end ,
children: [
Offstage (
offstage: item . state ! = JobState . paused ,
child: MenuButton (
onPressed: ( ) {
jobController . resumeJob ( item . id ) ;
} ,
child: SvgPicture . asset (
" assets/refresh.svg " ,
color: Colors . white ,
2023-02-25 09:44:23 +01:00
) ,
2023-03-09 00:06:24 +09:00
color: MyTheme . accent ,
hoverColor: MyTheme . accent80 ,
) ,
) ,
MenuButton (
padding: EdgeInsets . only ( right: 15 ) ,
child: SvgPicture . asset (
" assets/close.svg " ,
color: Colors . white ,
) ,
onPressed: ( ) {
jobController . jobTable . removeAt ( index ) ;
jobController . cancelJob ( item . id ) ;
} ,
color: MyTheme . accent ,
hoverColor: MyTheme . accent80 ,
2023-02-25 09:44:23 +01:00
) ,
2023-02-26 09:13:42 +01:00
] ,
2023-03-09 00:06:24 +09:00
) ,
] ,
) ,
] ,
) . paddingSymmetric ( vertical: 10 ) ,
2023-02-26 09:13:42 +01:00
) ,
2023-03-09 00:06:24 +09:00
) ;
} ,
itemCount: jobController . jobTable . length ,
2023-03-09 11:40:06 +09:00
) ;
2023-03-09 00:06:24 +09:00
return PreferredSize (
preferredSize: const Size ( 200 , double . infinity ) ,
child: Container (
margin: const EdgeInsets . only ( top: 16.0 , bottom: 16.0 , right: 16.0 ) ,
padding: const EdgeInsets . all ( 8.0 ) ,
child: Obx (
( ) = > jobController . jobTable . isEmpty
? generateCard (
Center (
child: Column (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
SvgPicture . asset (
" assets/transfer.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
height: 40 ,
) . paddingOnly ( bottom: 10 ) ,
Text (
translate ( " No transfers in progress " ) ,
textAlign: TextAlign . center ,
textScaleFactor: 1.20 ,
style: TextStyle (
color:
Theme . of ( context ) . tabBarTheme . labelColor ) ,
) ,
] ,
) ,
) ,
)
2023-03-09 11:40:06 +09:00
: statusListView ( jobController . jobTable ) ,
2023-03-09 00:06:24 +09:00
) ) ,
2023-02-25 09:44:23 +01:00
) ;
2022-07-01 12:08:52 +08:00
}
2023-05-12 13:22:49 +08:00
void handleDragDone ( DropDoneDetails details , bool isLocal ) {
if ( isLocal ) {
// ignore local
return ;
}
final items = SelectedItems ( isLocal: false ) ;
for ( var file in details . files ) {
final f = File ( file . path ) ;
items . add ( Entry ( )
. . path = file . path
. . name = file . name
. . size = FileSystemEntity . isDirectorySync ( f . path ) ? 0 : f . lengthSync ( ) ) ;
}
final otherSideData = model . localController . directoryData ( ) ;
model . remoteController . sendFiles ( items , otherSideData ) ;
}
2023-03-08 00:49:14 +09:00
}
class FileManagerView extends StatefulWidget {
final FileController controller ;
final FFI _ffi ;
final Rx < MouseFocusScope > _mouseFocusScope ;
FileManagerView ( this . controller , this . _ffi , this . _mouseFocusScope ) ;
@ override
State < StatefulWidget > createState ( ) = > _FileManagerViewState ( ) ;
}
class _FileManagerViewState extends State < FileManagerView > {
final _locationStatus = LocationStatus . bread . obs ;
final _locationNode = FocusNode ( ) ;
final _locationBarKey = GlobalKey ( ) ;
final _searchText = " " . obs ;
final _breadCrumbScroller = ScrollController ( ) ;
final _keyboardNode = FocusNode ( ) ;
final _listSearchBuffer = TimeoutStringBuffer ( ) ;
final _nameColWidth = kDesktopFileTransferNameColWidth . obs ;
final _modifiedColWidth = kDesktopFileTransferModifiedColWidth . obs ;
final _fileListScrollController = ScrollController ( ) ;
/// [_lastClickTime], [_lastClickEntry] help to handle double click
var _lastClickTime =
DateTime . now ( ) . millisecondsSinceEpoch - bind . getDoubleClickTime ( ) - 1000 ;
Entry ? _lastClickEntry ;
FileController get controller = > widget . controller ;
bool get isLocal = > widget . controller . isLocal ;
FFI get _ffi = > widget . _ffi ;
2023-03-09 18:05:09 +09:00
SelectedItems get selectedItems = > controller . selectedItems ;
2023-03-08 00:49:14 +09:00
@ override
void initState ( ) {
super . initState ( ) ;
// register location listener
_locationNode . addListener ( onLocationFocusChanged ) ;
2023-03-08 22:32:55 +09:00
controller . directory . listen ( ( e ) = > breadCrumbScrollToEnd ( ) ) ;
2023-03-08 00:49:14 +09:00
}
@ override
void dispose ( ) {
_locationNode . removeListener ( onLocationFocusChanged ) ;
_locationNode . dispose ( ) ;
_keyboardNode . dispose ( ) ;
_breadCrumbScroller . dispose ( ) ;
_fileListScrollController . dispose ( ) ;
super . dispose ( ) ;
}
@ override
Widget build ( BuildContext context ) {
return Container (
margin: const EdgeInsets . all ( 16.0 ) ,
padding: const EdgeInsets . all ( 8.0 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
headTools ( ) ,
Expanded (
child: Row (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Expanded (
child: MouseRegion (
onEnter: ( evt ) {
widget . _mouseFocusScope . value = isLocal
? MouseFocusScope . local
: MouseFocusScope . remote ;
_keyboardNode . requestFocus ( ) ;
} ,
onExit: ( evt ) = >
widget . _mouseFocusScope . value = MouseFocusScope . none ,
2023-03-09 00:06:24 +09:00
child: _buildFileList ( context , _fileListScrollController ) ,
2023-03-08 00:49:14 +09:00
) )
] ,
) ,
) ,
] ,
) ,
) ;
}
void onLocationFocusChanged ( ) {
debugPrint ( " focus changed on local " ) ;
if ( _locationNode . hasFocus ) {
// ignore
} else {
// lost focus, change to bread
if ( _locationStatus . value ! = LocationStatus . fileSearchBar ) {
_locationStatus . value = LocationStatus . bread ;
}
}
}
Widget headTools ( ) {
return Container (
child: Column (
children: [
// symbols
PreferredSize (
child: Row (
crossAxisAlignment: CrossAxisAlignment . center ,
children: [
Container (
width: 50 ,
height: 50 ,
decoration: BoxDecoration (
borderRadius: BorderRadius . all ( Radius . circular ( 8 ) ) ,
color: MyTheme . accent ,
) ,
padding: EdgeInsets . all ( 8.0 ) ,
child: FutureBuilder < String > (
future: bind . sessionGetPlatform (
2023-06-06 07:39:44 +08:00
sessionId: _ffi . sessionId ,
isRemote: ! isLocal ) ,
2023-03-08 00:49:14 +09:00
builder: ( context , snapshot ) {
if ( snapshot . hasData & &
snapshot . data ! . isNotEmpty ) {
return getPlatformImage ( ' ${ snapshot . data } ' ) ;
} else {
return CircularProgressIndicator (
color: Theme . of ( context )
. tabBarTheme
. labelColor ,
) ;
}
} ) ) ,
Text ( isLocal
? translate ( " Local Computer " )
: translate ( " Remote Computer " ) )
. marginOnly ( left: 8.0 )
] ,
) ,
preferredSize: Size ( double . infinity , 70 ) )
2023-02-22 22:13:21 +01:00
. paddingOnly ( bottom: 15 ) ,
// buttons
Row (
children: [
Row (
children: [
MenuButton (
padding: EdgeInsets . only (
right: 3 ,
) ,
2023-02-23 14:53:24 +01:00
child: RotatedBox (
quarterTurns: 2 ,
child: SvgPicture . asset (
" assets/arrow.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
2023-02-22 22:13:21 +01:00
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
onPressed: ( ) {
2023-03-09 18:05:09 +09:00
selectedItems . clear ( ) ;
2023-03-08 22:32:55 +09:00
controller . goBack ( ) ;
2023-02-22 22:13:21 +01:00
} ,
) ,
MenuButton (
child: RotatedBox (
quarterTurns: 3 ,
child: SvgPicture . asset (
" assets/arrow.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
onPressed: ( ) {
2023-03-09 18:05:09 +09:00
selectedItems . clear ( ) ;
2023-03-08 22:32:55 +09:00
controller . goToParentDirectory ( ) ;
2023-02-22 22:13:21 +01:00
} ,
) ,
] ,
) ,
Expanded (
child: Padding (
padding: const EdgeInsets . symmetric ( horizontal: 3.0 ) ,
child: Container (
decoration: BoxDecoration (
color: Theme . of ( context ) . cardColor ,
borderRadius: BorderRadius . all (
Radius . circular ( 8.0 ) ,
) ,
) ,
child: Padding (
padding: EdgeInsets . symmetric ( vertical: 2.5 ) ,
child: GestureDetector (
onTap: ( ) {
2023-03-08 00:49:14 +09:00
_locationStatus . value =
_locationStatus . value = = LocationStatus . bread
2023-02-22 22:13:21 +01:00
? LocationStatus . pathLocation
: LocationStatus . bread ;
Future . delayed ( Duration . zero , ( ) {
2023-03-08 00:49:14 +09:00
if ( _locationStatus . value = =
2023-02-22 22:13:21 +01:00
LocationStatus . pathLocation ) {
2023-03-08 00:49:14 +09:00
_locationNode . requestFocus ( ) ;
2023-02-22 22:13:21 +01:00
}
} ) ;
} ,
child: Obx (
( ) = > Container (
child: Row (
children: [
Expanded (
2023-03-08 00:49:14 +09:00
child: _locationStatus . value = =
2023-02-22 22:13:21 +01:00
LocationStatus . bread
2023-03-08 22:32:55 +09:00
? buildBread ( )
: buildPathLocation ( ) ) ,
2023-02-22 22:13:21 +01:00
] ,
) ,
) ,
) ,
) ,
) ,
) ,
) ,
) ,
Obx ( ( ) {
2023-03-08 00:49:14 +09:00
switch ( _locationStatus . value ) {
2023-02-22 22:13:21 +01:00
case LocationStatus . bread:
return MenuButton (
2022-10-13 10:17:20 +09:00
onPressed: ( ) {
2023-03-08 00:49:14 +09:00
_locationStatus . value = LocationStatus . fileSearchBar ;
2022-10-13 10:17:20 +09:00
Future . delayed (
2023-03-08 00:49:14 +09:00
Duration . zero , ( ) = > _locationNode . requestFocus ( ) ) ;
2022-10-13 10:17:20 +09:00
} ,
2023-02-22 22:13:21 +01:00
child: SvgPicture . asset (
" assets/search.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ;
case LocationStatus . pathLocation:
return MenuButton (
2022-10-13 10:17:20 +09:00
onPressed: null ,
2023-02-22 22:13:21 +01:00
child: SvgPicture . asset (
" assets/close.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
2022-10-13 10:17:20 +09:00
color: Theme . of ( context ) . disabledColor ,
2023-02-22 22:13:21 +01:00
hoverColor: Theme . of ( context ) . hoverColor ,
) ;
case LocationStatus . fileSearchBar:
return MenuButton (
2022-10-13 10:17:20 +09:00
onPressed: ( ) {
onSearchText ( " " , isLocal ) ;
2023-03-08 00:49:14 +09:00
_locationStatus . value = LocationStatus . bread ;
2022-10-13 10:17:20 +09:00
} ,
2023-02-22 22:13:21 +01:00
child: SvgPicture . asset (
" assets/close.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ;
}
} ) ,
MenuButton (
padding: EdgeInsets . only (
left: 3 ,
) ,
2022-08-16 11:46:51 +08:00
onPressed: ( ) {
2023-03-08 22:32:55 +09:00
controller . refresh ( ) ;
2022-08-16 11:46:51 +08:00
} ,
2023-02-22 22:13:21 +01:00
child: SvgPicture . asset (
" assets/refresh.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ,
] ,
) ,
Row (
textDirection: isLocal ? TextDirection . ltr : TextDirection . rtl ,
children: [
Expanded (
child: Row (
mainAxisAlignment:
isLocal ? MainAxisAlignment . start : MainAxisAlignment . end ,
children: [
MenuButton (
padding: EdgeInsets . only (
right: 3 ,
) ,
onPressed: ( ) {
2023-03-08 22:32:55 +09:00
controller . goToHomeDirectory ( ) ;
2023-02-22 22:13:21 +01:00
} ,
child: SvgPicture . asset (
" assets/home.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ,
MenuButton (
2022-07-11 16:07:49 +08:00
onPressed: ( ) {
2022-08-16 11:46:51 +08:00
final name = TextEditingController ( ) ;
2023-05-08 12:34:19 +08:00
_ffi . dialogManager . show ( ( setState , close , context ) {
2022-09-03 18:19:50 +08:00
submit ( ) {
if ( name . value . text . isNotEmpty ) {
2023-03-08 22:32:55 +09:00
controller . createDir ( PathUtil . join (
controller . directory . value . path ,
name . value . text ,
controller . options . value . isWindows ,
) ) ;
2022-09-03 18:19:50 +08:00
close ( ) ;
}
}
cancel ( ) = > close ( false ) ;
return CustomAlertDialog (
2023-02-27 09:44:52 +01:00
title: Row (
mainAxisAlignment: MainAxisAlignment . center ,
children: [
SvgPicture . asset ( " assets/folder_new.svg " ,
color: MyTheme . accent ) ,
Text (
translate ( " Create Folder " ) ,
) . paddingOnly (
left: 10 ,
) ,
] ,
) ,
2022-09-03 18:19:50 +08:00
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
2023-03-01 14:50:50 +01:00
TextFormField (
decoration: InputDecoration (
labelText: translate (
" Please enter the folder name " ,
2023-02-27 09:44:52 +01:00
) ,
2023-03-01 14:50:50 +01:00
) ,
controller: name ,
autofocus: true ,
2022-09-03 18:19:50 +08:00
) ,
] ,
) ,
2023-03-01 14:50:50 +01:00
actions: [
dialogButton (
" Cancel " ,
icon: Icon ( Icons . close_rounded ) ,
onPressed: cancel ,
isOutline: true ,
) ,
dialogButton (
" Ok " ,
icon: Icon ( Icons . done_rounded ) ,
onPressed: submit ,
) ,
] ,
2022-09-03 18:19:50 +08:00
onSubmit: submit ,
onCancel: cancel ,
) ;
} ) ;
2022-07-11 16:07:49 +08:00
} ,
2023-02-22 22:13:21 +01:00
child: SvgPicture . asset (
" assets/folder_new.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ,
2023-03-09 19:55:38 +09:00
Obx ( ( ) = > MenuButton (
onPressed: SelectedItems . valid ( selectedItems . items )
? ( ) async {
await ( controller
. removeAction ( selectedItems ) ) ;
selectedItems . clear ( ) ;
}
: null ,
child: SvgPicture . asset (
" assets/trash.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ) ,
2023-02-22 22:13:21 +01:00
menu ( isLocal: isLocal ) ,
] ,
) ,
2022-06-17 22:57:41 +08:00
) ,
2023-03-09 19:55:38 +09:00
Obx ( ( ) = > ElevatedButton . icon (
style: ButtonStyle (
padding: MaterialStateProperty . all < EdgeInsetsGeometry > (
isLocal
? EdgeInsets . only ( left: 10 )
: EdgeInsets . only ( right: 10 ) ) ,
backgroundColor: MaterialStateProperty . all (
selectedItems . items . isEmpty
? MyTheme . accent80
: MyTheme . accent ,
2023-02-22 22:13:21 +01:00
) ,
2023-03-09 19:55:38 +09:00
) ,
onPressed: SelectedItems . valid ( selectedItems . items )
? ( ) {
final otherSideData =
controller . getOtherSideDirectoryData ( ) ;
controller . sendFiles ( selectedItems , otherSideData ) ;
selectedItems . clear ( ) ;
}
: null ,
icon: isLocal
? Text (
translate ( ' Send ' ) ,
textAlign: TextAlign . right ,
style: TextStyle (
color: selectedItems . items . isEmpty
? Theme . of ( context ) . brightness = =
Brightness . light
? MyTheme . grayBg
: MyTheme . darkGray
: Colors . white ,
) ,
)
: RotatedBox (
quarterTurns: 2 ,
child: SvgPicture . asset (
" assets/arrow.svg " ,
color: selectedItems . items . isEmpty
? Theme . of ( context ) . brightness = =
Brightness . light
? MyTheme . grayBg
: MyTheme . darkGray
: Colors . white ,
alignment: Alignment . bottomRight ,
) ,
) ,
label: isLocal
? SvgPicture . asset (
" assets/arrow.svg " ,
color: selectedItems . items . isEmpty
? Theme . of ( context ) . brightness = =
Brightness . light
? MyTheme . grayBg
: MyTheme . darkGray
: Colors . white ,
)
: Text (
translate ( ' Receive ' ) ,
style: TextStyle (
color: selectedItems . items . isEmpty
? Theme . of ( context ) . brightness = =
Brightness . light
? MyTheme . grayBg
: MyTheme . darkGray
: Colors . white ,
) ,
) ,
) ) ,
2023-02-22 22:13:21 +01:00
] ,
) . marginOnly ( top: 8.0 )
] ,
) ,
) ;
2022-08-16 11:46:51 +08:00
}
2022-06-17 22:57:41 +08:00
2023-03-08 00:49:14 +09:00
Widget menu ( { bool isLocal = false } ) {
var menuPos = RelativeRect . fill ;
final List < MenuEntryBase < String > > items = [
MenuEntrySwitch < String > (
switchType: SwitchType . scheckbox ,
text: translate ( " Show Hidden Files " ) ,
getter: ( ) async {
2023-03-29 23:22:31 +02:00
return controller . options . value . showHidden ;
2023-03-08 00:49:14 +09:00
} ,
setter: ( bool v ) async {
2023-03-08 22:32:55 +09:00
controller . toggleShowHidden ( ) ;
2023-03-08 00:49:14 +09:00
} ,
padding: kDesktopMenuPadding ,
dismissOnClicked: true ,
) ,
MenuEntryButton (
childBuilder: ( style ) = > Text ( translate ( " Select All " ) , style: style ) ,
proc: ( ) = > setState ( ( ) = >
2023-03-09 18:05:09 +09:00
selectedItems . selectAll ( controller . directory . value . entries ) ) ,
2023-03-08 00:49:14 +09:00
padding: kDesktopMenuPadding ,
dismissOnClicked: true ) ,
MenuEntryButton (
childBuilder: ( style ) = >
Text ( translate ( " Unselect All " ) , style: style ) ,
2023-03-09 19:55:38 +09:00
proc: ( ) = > selectedItems . clear ( ) ,
2023-03-08 00:49:14 +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: MenuButton (
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 ,
) ,
child: SvgPicture . asset (
" assets/dots.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
color: Theme . of ( context ) . cardColor ,
hoverColor: Theme . of ( context ) . hoverColor ,
) ,
) ;
2022-10-19 10:52:29 +09:00
}
2023-03-08 00:49:14 +09:00
Widget _buildFileList (
2023-03-09 00:06:24 +09:00
BuildContext context , ScrollController scrollController ) {
2023-03-08 22:32:55 +09:00
final fd = controller . directory . value ;
2023-03-08 00:49:14 +09:00
final entries = fd . entries ;
2022-07-11 16:07:49 +08:00
2023-03-08 00:49:14 +09:00
return ListSearchActionListener (
node: _keyboardNode ,
buffer: _listSearchBuffer ,
onNext: ( buffer ) {
debugPrint ( " searching next for $ buffer " ) ;
assert ( buffer . length = = 1 ) ;
2023-03-09 19:55:38 +09:00
assert ( selectedItems . items . length < = 1 ) ;
2023-03-08 00:49:14 +09:00
var skipCount = 0 ;
2023-03-09 18:05:09 +09:00
if ( selectedItems . items . isNotEmpty ) {
final index = entries . indexOf ( selectedItems . items . first ) ;
2023-03-08 00:49:14 +09:00
if ( index < 0 ) {
return ;
}
skipCount = index + 1 ;
}
var searchResult = entries
. skip ( skipCount )
. 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 . toLowerCase ( ) . startsWith ( buffer ) ) ;
}
if ( searchResult . isEmpty ) {
2023-03-09 19:55:38 +09:00
selectedItems . clear ( ) ;
2023-03-08 00:49:14 +09:00
return ;
}
_jumpToEntry ( isLocal , searchResult . first , scrollController ,
kDesktopFileTransferRowHeight ) ;
} ,
onSearch: ( buffer ) {
debugPrint ( " searching for $ buffer " ) ;
2023-03-09 18:05:09 +09:00
final selectedEntries = selectedItems ;
2023-03-08 00:49:14 +09:00
final searchResult = entries
. where ( ( element ) = > element . name . toLowerCase ( ) . startsWith ( buffer ) ) ;
selectedEntries . clear ( ) ;
if ( searchResult . isEmpty ) {
2023-03-09 19:55:38 +09:00
selectedItems . clear ( ) ;
2023-03-08 00:49:14 +09:00
return ;
}
_jumpToEntry ( isLocal , searchResult . first , scrollController ,
kDesktopFileTransferRowHeight ) ;
} ,
2023-03-08 22:32:55 +09:00
child: Obx ( ( ) {
final entries = controller . directory . value . entries ;
final filteredEntries = _searchText . isNotEmpty
? entries . where ( ( element ) {
return element . name . contains ( _searchText . value ) ;
} ) . toList ( growable: false )
: entries ;
final rows = filteredEntries . map ( ( entry ) {
final sizeStr =
entry . isFile ? readableFileSize ( entry . size . toDouble ( ) ) : " " ;
final lastModifiedStr = entry . isDrive
? " "
: " ${ entry . lastModified ( ) . toString ( ) . replaceAll ( " .000 " , " " ) } " ;
return Padding (
padding: EdgeInsets . symmetric ( vertical: 1 ) ,
2023-03-09 19:55:38 +09:00
child: Obx ( ( ) = > Container (
2023-03-08 22:32:55 +09:00
decoration: BoxDecoration (
2023-03-09 19:55:38 +09:00
color: selectedItems . items . contains ( entry )
2023-03-08 22:32:55 +09:00
? Theme . of ( context ) . hoverColor
: Theme . of ( context ) . cardColor ,
borderRadius: BorderRadius . all (
Radius . circular ( 5.0 ) ,
2023-03-08 00:49:14 +09:00
) ,
2023-03-08 22:32:55 +09:00
) ,
key: ValueKey ( entry . name ) ,
height: kDesktopFileTransferRowHeight ,
child: Column (
mainAxisAlignment: MainAxisAlignment . spaceAround ,
children: [
Expanded (
child: InkWell (
child: Row (
children: [
GestureDetector (
child: Obx (
( ) = > Container (
width: _nameColWidth . value ,
2023-03-08 00:49:14 +09:00
child: Tooltip (
2023-03-08 22:32:55 +09:00
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 ) )
] ) ,
) ) ,
2023-03-08 00:49:14 +09:00
) ,
2023-03-08 22:32:55 +09:00
onTap: ( ) {
2023-03-09 18:05:09 +09:00
final items = selectedItems ;
2023-03-08 22:32:55 +09:00
// handle double click
if ( _checkDoubleClick ( entry ) ) {
controller . openDirectory ( entry . path ) ;
items . clear ( ) ;
return ;
}
_onSelectedChanged (
items , filteredEntries , entry , isLocal ) ;
} ,
) ,
SizedBox (
width: 2.0 ,
) ,
GestureDetector (
child: Obx (
( ) = > SizedBox (
width: _modifiedColWidth . value ,
2023-03-08 00:49:14 +09:00
child: Tooltip (
2023-03-08 22:32:55 +09:00
waitDuration: Duration ( milliseconds: 500 ) ,
message: lastModifiedStr ,
child: Text (
lastModifiedStr ,
overflow: TextOverflow . ellipsis ,
style: TextStyle (
fontSize: 12 ,
color: MyTheme . darkGray ,
) ,
) ) ,
) ,
) ,
) ,
// Divider from header.
SizedBox (
width: 2.0 ,
) ,
Expanded (
// width: 100,
child: GestureDetector (
child: Tooltip (
waitDuration: Duration ( milliseconds: 500 ) ,
message: sizeStr ,
child: Text (
sizeStr ,
overflow: TextOverflow . ellipsis ,
style: TextStyle (
fontSize: 10 , color: MyTheme . darkGray ) ,
2023-03-08 00:49:14 +09:00
) ,
) ,
) ,
2023-03-08 22:32:55 +09:00
) ,
] ,
2023-03-08 00:49:14 +09:00
) ,
) ,
2023-03-08 22:32:55 +09:00
) ,
] ,
2023-03-09 19:55:38 +09:00
) ) ) ,
2023-03-08 22:32:55 +09:00
) ;
} ) . toList ( growable: false ) ;
2023-03-08 00:49:14 +09:00
2023-03-08 22:32:55 +09:00
return Column (
children: [
// Header
Row (
children: [
2023-03-09 00:06:24 +09:00
Expanded ( child: _buildFileBrowserHeader ( context ) ) ,
2023-03-08 22:32:55 +09:00
] ,
) ,
// Body
Expanded (
child: ListView . builder (
controller: scrollController ,
itemExtent: kDesktopFileTransferRowHeight ,
itemBuilder: ( context , index ) {
return rows [ index ] ;
} ,
itemCount: rows . length ,
2023-03-08 00:49:14 +09:00
) ,
2023-03-08 22:32:55 +09:00
) ,
] ,
) ;
} ) ,
2023-03-08 00:49:14 +09:00
) ;
}
onSearchText ( String searchText , bool isLocal ) {
2023-03-09 18:05:09 +09:00
selectedItems . clear ( ) ;
2023-03-08 00:49:14 +09:00
_searchText . value = searchText ;
}
void _jumpToEntry ( bool isLocal , Entry entry ,
ScrollController scrollController , double rowHeight ) {
2023-03-08 22:32:55 +09:00
final entries = controller . directory . value . entries ;
2023-03-08 00:49:14 +09:00
final index = entries . indexOf ( entry ) ;
if ( index = = - 1 ) {
debugPrint ( " entry is not valid: ${ entry . path } " ) ;
2022-08-16 11:46:51 +08:00
}
2023-03-09 18:05:09 +09:00
final selectedEntries = selectedItems ;
2023-03-08 00:49:14 +09:00
final searchResult = entries . where ( ( element ) = > element = = entry ) ;
selectedEntries . clear ( ) ;
if ( searchResult . isEmpty ) {
return ;
}
final offset = min (
max ( scrollController . position . minScrollExtent ,
entries . indexOf ( searchResult . first ) * rowHeight ) ,
scrollController . position . maxScrollExtent ) ;
scrollController . jumpTo ( offset ) ;
2023-03-09 19:55:38 +09:00
selectedEntries . add ( searchResult . first ) ;
debugPrint ( " focused on ${ searchResult . first . name } " ) ;
2022-08-16 11:46:51 +08:00
}
2023-03-08 00:49:14 +09:00
void _onSelectedChanged ( SelectedItems selectedItems , List < Entry > entries ,
Entry entry , bool isLocal ) {
final isCtrlDown = RawKeyboard . instance . keysPressed
. contains ( LogicalKeyboardKey . controlLeft ) ;
final isShiftDown =
RawKeyboard . instance . keysPressed . contains ( LogicalKeyboardKey . shiftLeft ) ;
if ( isCtrlDown ) {
2023-03-09 19:55:38 +09:00
if ( selectedItems . items . contains ( entry ) ) {
2023-03-08 00:49:14 +09:00
selectedItems . remove ( entry ) ;
} else {
2023-03-08 22:32:55 +09:00
selectedItems . add ( entry ) ;
2023-03-08 00:49:14 +09:00
}
} else if ( isShiftDown ) {
final List < int > indexGroup = [ ] ;
for ( var selected in selectedItems . items ) {
indexGroup . add ( entries . indexOf ( selected ) ) ;
}
indexGroup . add ( entries . indexOf ( entry ) ) ;
indexGroup . removeWhere ( ( e ) = > e = = - 1 ) ;
final maxIndex = indexGroup . reduce ( max ) ;
final minIndex = indexGroup . reduce ( min ) ;
selectedItems . clear ( ) ;
entries
. getRange ( minIndex , maxIndex + 1 )
2023-03-08 22:32:55 +09:00
. forEach ( ( e ) = > selectedItems . add ( e ) ) ;
2022-08-16 11:46:51 +08:00
} else {
2023-03-08 00:49:14 +09:00
selectedItems . clear ( ) ;
2023-03-08 22:32:55 +09:00
selectedItems . add ( entry ) ;
2023-03-08 00:49:14 +09:00
}
setState ( ( ) { } ) ;
}
bool _checkDoubleClick ( Entry entry ) {
final current = DateTime . now ( ) . millisecondsSinceEpoch ;
final elapsed = current - _lastClickTime ;
_lastClickTime = current ;
if ( _lastClickEntry = = entry ) {
if ( elapsed < bind . getDoubleClickTime ( ) ) {
return true ;
2022-10-13 10:17:20 +09:00
}
2023-03-08 00:49:14 +09:00
} else {
_lastClickEntry = entry ;
2022-08-16 11:46:51 +08:00
}
2023-03-08 00:49:14 +09:00
return false ;
}
2023-03-09 00:06:24 +09:00
Widget _buildFileBrowserHeader ( BuildContext context ) {
2023-03-08 00:49:14 +09:00
final padding = EdgeInsets . all ( 1.0 ) ;
return SizedBox (
height: kDesktopFileTransferHeaderHeight ,
child: Row (
children: [
Obx (
( ) = > headerItemFunc (
2023-03-09 00:06:24 +09:00
_nameColWidth . value , SortBy . name , translate ( " Name " ) ) ,
2023-03-08 00:49:14 +09:00
) ,
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 ,
2023-03-09 00:06:24 +09:00
translate ( " Modified " ) ) ,
2023-03-08 00:49:14 +09:00
) ,
DraggableDivider (
axis: Axis . vertical ,
onPointerMove: ( dx ) {
_modifiedColWidth . value + = dx ;
_modifiedColWidth . value = min (
kDesktopFileTransferMaximumWidth ,
max ( kDesktopFileTransferMinimumWidth ,
_modifiedColWidth . value ) ) ;
} ,
padding: padding ) ,
2023-03-09 00:06:24 +09:00
Expanded ( child: headerItemFunc ( null , SortBy . size , translate ( " Size " ) ) )
2023-03-08 00:49:14 +09:00
] ,
) ,
) ;
}
2023-03-09 00:06:24 +09:00
Widget headerItemFunc ( double ? width , SortBy sortBy , String name ) {
2023-03-08 00:49:14 +09:00
final headerTextStyle =
Theme . of ( context ) . dataTableTheme . headingTextStyle ? ? TextStyle ( ) ;
return ObxValue < Rx < bool ? > > (
( ascending ) = > InkWell (
onTap: ( ) {
if ( ascending . value = = null ) {
ascending . value = true ;
} else {
ascending . value = ! ascending . value ! ;
}
2023-03-08 22:32:55 +09:00
controller . changeSortStyle ( sortBy ,
2023-03-08 00:49:14 +09:00
isLocal: isLocal , ascending: ascending . value ! ) ;
} ,
child: SizedBox (
width: width ,
height: kDesktopFileTransferHeaderHeight ,
child: Row (
children: [
Flexible (
flex: 2 ,
child: Text (
name ,
style: headerTextStyle ,
overflow: TextOverflow . ellipsis ,
) . marginSymmetric ( horizontal: 4 ) ,
) ,
Flexible (
flex: 1 ,
child: ascending . value ! = null
? Icon (
ascending . value !
? Icons . keyboard_arrow_up_rounded
: Icons . keyboard_arrow_down_rounded ,
)
: const Offstage ( ) )
] ,
) ,
) ,
) , ( ) {
2023-03-08 22:32:55 +09:00
if ( controller . sortBy . value = = sortBy ) {
2023-03-09 00:06:24 +09:00
return controller . sortAscending . obs ;
2023-03-08 00:49:14 +09:00
} else {
return Rx < bool ? > ( null ) ;
}
} ( ) ) ;
2022-08-16 11:46:51 +08:00
}
2023-03-08 22:32:55 +09:00
Widget buildBread ( ) {
2022-08-16 12:50:08 +08:00
final items = getPathBreadCrumbItems ( isLocal , ( list ) {
var path = " " ;
for ( var item in list ) {
2023-03-08 22:32:55 +09:00
path = PathUtil . join ( path , item , controller . options . value . isWindows ) ;
2022-08-16 12:50:08 +08:00
}
2023-03-08 22:32:55 +09:00
controller . openDirectory ( path ) ;
2022-08-16 12:50:08 +08:00
} ) ;
2022-10-18 23:56:36 +09:00
2022-08-16 12:50:08 +08:00
return items . isEmpty
? Offstage ( )
2022-10-18 23:56:36 +09:00
: Row (
2023-03-08 00:49:14 +09:00
key: _locationBarKey ,
2022-10-18 23:56:36 +09:00
mainAxisAlignment: MainAxisAlignment . spaceBetween ,
children: [
Expanded (
2023-02-22 22:13:21 +01:00
child: Listener (
// handle mouse wheel
onPointerSignal: ( e ) {
if ( e is PointerScrollEvent ) {
2023-03-08 00:49:14 +09:00
final sc = _breadCrumbScroller ;
2023-02-22 22:13:21 +01:00
final scale = Platform . isWindows ? 2 : 4 ;
sc . jumpTo ( sc . offset + e . scrollDelta . dy / scale ) ;
}
} ,
child: BreadCrumb (
items: items ,
divider: const Icon ( Icons . keyboard_arrow_right_rounded ) ,
overflow: ScrollableOverflow (
2023-03-08 00:49:14 +09:00
controller: _breadCrumbScroller ,
2023-02-22 22:13:21 +01:00
) ,
) ,
) ,
) ,
2022-10-18 23:56:36 +09:00
ActionIcon (
message: " " ,
2023-02-22 22:13:21 +01:00
icon: Icons . keyboard_arrow_down_rounded ,
2022-10-18 23:56:36 +09:00
onTap: ( ) async {
2023-03-08 00:49:14 +09:00
final renderBox = _locationBarKey . currentContext
2022-10-18 23:56:36 +09:00
? . findRenderObject ( ) as RenderBox ;
2023-03-08 00:49:14 +09:00
_locationBarKey . currentContext ? . size ;
2022-10-18 23:56:36 +09:00
final size = renderBox . size ;
final offset = renderBox . localToGlobal ( Offset . zero ) ;
final x = offset . dx ;
final y = offset . dy + size . height + 1 ;
2023-03-08 22:32:55 +09:00
final isPeerWindows = controller . options . value . isWindows ;
2022-10-19 11:24:44 +09:00
final List < MenuEntryBase > menuItems = [
MenuEntryButton (
2022-11-02 23:48:02 +09:00
childBuilder: ( TextStyle ? style ) = > isPeerWindows
2023-03-08 00:49:14 +09:00
? buildWindowsThisPC ( context , style )
2022-11-02 23:48:02 +09:00
: Text (
' / ' ,
style: style ,
) ,
2022-10-19 11:24:44 +09:00
proc: ( ) {
2023-03-08 22:32:55 +09:00
controller . openDirectory ( ' / ' ) ;
2022-10-19 11:24:44 +09:00
} ,
dismissOnClicked: true ) ,
MenuEntryDivider ( )
] ;
2022-11-02 23:48:02 +09:00
if ( isPeerWindows ) {
2022-10-19 10:52:29 +09:00
var loadingTag = " " ;
if ( ! isLocal ) {
loadingTag = _ffi . dialogManager . showLoading ( " Waiting " ) ;
}
2022-10-18 23:56:36 +09:00
try {
2023-03-08 22:32:55 +09:00
final showHidden = controller . options . value . showHidden ;
final fd = await controller . fileFetcher
. fetchDirectory ( " / " , isLocal , showHidden ) ;
2022-10-18 23:56:36 +09:00
for ( var entry in fd . entries ) {
menuItems . add ( MenuEntryButton (
2022-10-19 23:59:02 +09:00
childBuilder: ( TextStyle ? style ) = >
Row ( children: [
2022-10-20 10:31:31 +09:00
Image (
image: iconHardDrive ,
fit: BoxFit . scaleDown ,
color: Theme . of ( context )
. iconTheme
. color
? . withOpacity ( 0.7 ) ) ,
2022-10-19 23:59:02 +09:00
SizedBox ( width: 10 ) ,
Text (
entry . name ,
style: style ,
)
] ) ,
2022-10-18 23:56:36 +09:00
proc: ( ) {
2023-03-08 22:32:55 +09:00
controller . openDirectory ( ' ${ entry . name } \\ ' ) ;
2022-10-19 10:52:29 +09:00
} ,
dismissOnClicked: true ) ) ;
2022-10-18 23:56:36 +09:00
}
2023-03-16 19:39:37 +09:00
menuItems . add ( MenuEntryDivider ( ) ) ;
2022-12-20 23:55:54 +09:00
} catch ( e ) {
debugPrint ( " buildBread fetchDirectory err= $ e " ) ;
2022-10-18 23:56:36 +09:00
} finally {
2022-10-19 10:52:29 +09:00
if ( ! isLocal ) {
_ffi . dialogManager . dismissByTag ( loadingTag ) ;
}
2022-10-18 23:56:36 +09:00
}
}
mod_menu . showMenu (
context: context ,
position: RelativeRect . fromLTRB ( x , y , x , y ) ,
elevation: 4 ,
items: menuItems
. map ( ( e ) = > e . build (
context ,
MenuConfig (
commonColor:
CustomPopupMenuTheme . commonColor ,
height: CustomPopupMenuTheme . height ,
dividerHeight:
CustomPopupMenuTheme . dividerHeight ,
boxWidth: size . width ) ) )
. expand ( ( i ) = > i )
. toList ( ) ) ;
} ,
iconSize: 20 ,
)
] ) ;
2022-08-16 11:46:51 +08:00
}
List < BreadCrumbItem > getPathBreadCrumbItems (
bool isLocal , void Function ( List < String > ) onPressed ) {
2023-03-08 22:32:55 +09:00
final path = controller . directory . value . path ;
2022-08-16 11:46:51 +08:00
final breadCrumbList = List < BreadCrumbItem > . empty ( growable: true ) ;
2023-03-08 22:32:55 +09:00
final isWindows = controller . options . value . isWindows ;
2022-12-04 23:44:03 +09:00
if ( isWindows & & path = = ' / ' ) {
2022-11-02 23:48:02 +09:00
breadCrumbList . add ( BreadCrumbItem (
content: TextButton (
2023-03-08 00:49:14 +09:00
child: buildWindowsThisPC ( context ) ,
2022-11-02 23:48:02 +09:00
style: ButtonStyle (
minimumSize: MaterialStateProperty . all ( Size ( 0 , 0 ) ) ) ,
onPressed: ( ) = > onPressed ( [ ' / ' ] ) )
. marginSymmetric ( horizontal: 4 ) ) ) ;
} else {
2022-12-04 23:44:03 +09:00
final list = PathUtil . split ( path , isWindows ) ;
2023-02-22 22:13:21 +01:00
breadCrumbList . addAll (
list . asMap ( ) . entries . map (
( e ) = > BreadCrumbItem (
content: TextButton (
2022-11-02 23:48:02 +09:00
child: Text ( e . value ) ,
style: ButtonStyle (
2023-02-22 22:13:21 +01:00
minimumSize: MaterialStateProperty . all (
Size ( 0 , 0 ) ,
) ,
) ,
onPressed: ( ) = > onPressed (
list . sublist ( 0 , e . key + 1 ) ,
) ,
) . marginSymmetric ( horizontal: 4 ) ,
) ,
) ,
) ;
2022-11-02 23:48:02 +09:00
}
2022-08-16 11:46:51 +08:00
return breadCrumbList ;
}
2023-03-08 22:32:55 +09:00
breadCrumbScrollToEnd ( ) {
2022-08-16 12:28:12 +08:00
Future . delayed ( Duration ( milliseconds: 200 ) , ( ) {
2023-03-08 00:49:14 +09:00
if ( _breadCrumbScroller . hasClients ) {
_breadCrumbScroller . animateTo (
_breadCrumbScroller . position . maxScrollExtent ,
2022-10-13 20:22:11 +09:00
duration: Duration ( milliseconds: 200 ) ,
curve: Curves . fastLinearToSlowEaseIn ) ;
}
2022-08-16 12:28:12 +08:00
} ) ;
}
2023-03-08 22:32:55 +09:00
Widget buildPathLocation ( ) {
2023-03-08 00:49:14 +09:00
final text = _locationStatus . value = = LocationStatus . pathLocation
2023-03-08 22:32:55 +09:00
? controller . directory . value . path
2023-03-08 00:49:14 +09:00
: _searchText . value ;
2022-10-13 19:57:59 +09:00
final textController = TextEditingController ( text: text )
. . selection = TextSelection . collapsed ( offset: text . length ) ;
2023-02-22 22:13:21 +01:00
return Row (
children: [
SvgPicture . asset (
2023-03-08 00:49:14 +09:00
_locationStatus . value = = LocationStatus . pathLocation
2023-02-22 22:13:21 +01:00
? " assets/folder.svg "
: " assets/search.svg " ,
color: Theme . of ( context ) . tabBarTheme . labelColor ,
) ,
Expanded (
2022-10-13 10:17:20 +09:00
child: TextField (
2023-03-08 00:49:14 +09:00
focusNode: _locationNode ,
2023-02-22 22:13:21 +01:00
decoration: InputDecoration (
border: InputBorder . none ,
isDense: true ,
prefix: Padding (
padding: EdgeInsets . only ( left: 4.0 ) ,
) ,
) ,
controller: textController ,
onSubmitted: ( path ) {
2023-03-08 22:32:55 +09:00
controller . openDirectory ( path ) ;
2023-02-22 22:13:21 +01:00
} ,
2023-03-08 00:49:14 +09:00
onChanged: _locationStatus . value = = LocationStatus . fileSearchBar
2023-02-22 22:13:21 +01:00
? ( searchText ) = > onSearchText ( searchText , isLocal )
: null ,
) ,
)
] ,
) ;
2022-08-16 11:46:51 +08:00
}
2022-08-16 12:06:54 +08:00
2023-03-08 00:49:14 +09:00
// openDirectory(String path, {bool isLocal = false}) {
// model.openDirectory(path, isLocal: isLocal);
// }
}
2022-12-13 12:55:41 +08:00
2023-03-08 00:49:14 +09:00
Widget buildWindowsThisPC ( BuildContext context , [ TextStyle ? textStyle ] ) {
final color = Theme . of ( context ) . iconTheme . color ? . withOpacity ( 0.7 ) ;
return Row ( children: [
Icon ( Icons . computer , size: 20 , color: color ) ,
SizedBox ( width: 10 ) ,
Text ( translate ( ' This PC ' ) , style: textStyle )
] ) ;
2022-06-17 22:57:41 +08:00
}