2025-07-31 11:22:12 +05:00
import ' dart:io ' ;
2025-07-27 09:56:46 +05:00
import ' package:dartssh2/dartssh2.dart ' ;
2025-07-31 11:22:12 +05:00
import ' package:file_picker/file_picker.dart ' ;
2025-07-28 08:04:50 +05:00
import ' package:file_selector/file_selector.dart ' ;
2025-07-27 09:56:46 +05:00
import ' package:flutter/material.dart ' ;
2025-07-31 11:22:12 +05:00
import ' package:fluxcloud/sftp_worker.dart ' ;
2025-07-27 09:56:46 +05:00
class SftpExplorer extends StatefulWidget {
2025-08-07 20:28:39 +05:00
const SftpExplorer ( { super . key , required this . sftpWorker } ) ;
2025-07-27 09:56:46 +05:00
2025-07-31 11:22:12 +05:00
final SftpWorker sftpWorker ;
2025-07-27 09:56:46 +05:00
@ override
State < SftpExplorer > createState ( ) = > _SftpExplorerState ( ) ;
}
class _SftpExplorerState extends State < SftpExplorer > {
2025-08-07 20:28:39 +05:00
String path = ' / ' ;
2025-07-27 09:56:46 +05:00
bool _isLoading = true ;
late List < SftpName > _dirContents ;
2025-08-02 08:39:31 +05:00
double ? _progress ;
2025-07-28 08:04:50 +05:00
2025-07-27 09:56:46 +05:00
@ override
void initState ( ) {
super . initState ( ) ;
_listDir ( ) ;
}
2025-07-28 08:04:50 +05:00
Future < void > _listDir ( ) async {
setState ( ( ) = > _isLoading = true ) ;
2025-07-31 11:22:12 +05:00
try {
2025-08-07 20:28:39 +05:00
_dirContents = await widget . sftpWorker . listdir ( path ) ;
2025-07-31 11:22:12 +05:00
}
catch ( e ) {
if ( mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( _buildErrorSnackBar ( context , e . toString ( ) ) ) ;
}
}
2025-07-28 08:04:50 +05:00
setState ( ( ) = > _isLoading = false ) ;
2025-07-27 09:56:46 +05:00
}
@ override
Widget build ( BuildContext context ) {
return Scaffold (
appBar: AppBar (
2025-08-02 08:39:31 +05:00
toolbarHeight: 75 ,
2025-07-27 09:56:46 +05:00
title: Text ( ' Explorer ' ) ,
2025-08-02 08:39:31 +05:00
elevation: 2 ,
actionsPadding: EdgeInsets . only ( right: 20 ) ,
2025-08-07 20:28:39 +05:00
leading: IconButton (
onPressed: ( ) {
if ( path = = ' / ' ) {
// TODO: figure this out
// Navigator.pop(context);
}
else {
path = path . substring ( 0 , path . length - 1 ) ;
path = path . substring ( 0 , path . lastIndexOf ( ' / ' ) + 1 ) ;
_listDir ( ) ;
}
} ,
icon: Icon ( Icons . arrow_back )
) ,
2025-08-02 08:39:31 +05:00
actions: [
if ( _progress ! = null )
Stack (
alignment: Alignment . center ,
children: [
TweenAnimationBuilder < double > (
tween: Tween ( begin: 0 , end: _progress ) ,
duration: Duration ( milliseconds: 300 ) ,
builder: ( context , value , _ ) = > CircularProgressIndicator ( strokeWidth: 3 , value: value , )
) ,
IconButton (
onPressed: ( ) {
2025-08-07 20:28:39 +05:00
2025-08-02 08:39:31 +05:00
} ,
icon: Icon ( Icons . upload )
) ,
]
) ,
] ,
2025-07-27 09:56:46 +05:00
) ,
2025-07-28 08:04:50 +05:00
floatingActionButton: _buildFABs ( context ) ,
2025-08-07 20:28:39 +05:00
body: AnimatedSwitcher (
duration: Duration ( milliseconds: 300 ) ,
transitionBuilder: ( child , animation ) {
final curved = CurvedAnimation (
parent: animation ,
curve: Curves . fastOutSlowIn
) ;
return FadeTransition (
opacity: curved ,
child: ScaleTransition (
scale: Tween < double > (
begin: 0.92 ,
end: 1
) . animate ( curved ) ,
child: child ,
) ,
) ;
} ,
child: _isLoading ? Center ( child: CircularProgressIndicator ( ) ) : ListView . builder (
key: ValueKey ( path ) ,
itemCount: _dirContents . length ,
itemBuilder: ( context , index ) {
final dirEntry = _dirContents [ index ] ;
return ListTile (
leading: Icon ( dirEntry . attr . isDirectory ? Icons . folder : Icons . description ) ,
title: Text ( dirEntry . filename ) ,
trailing: Row (
mainAxisSize: MainAxisSize . min ,
children: [
IconButton (
onPressed: ( ) {
2025-07-28 08:04:50 +05:00
2025-08-07 20:28:39 +05:00
} ,
icon: Icon ( Icons . drive_file_move )
) ,
IconButton (
onPressed: ( ) {
2025-07-28 08:04:50 +05:00
2025-08-07 20:28:39 +05:00
} ,
icon: Icon ( Icons . copy )
) ,
IconButton (
onPressed: ( ) {
final newNameController = TextEditingController ( text: dirEntry . filename ) ;
showDialog (
context: context ,
builder: ( context ) = > AlertDialog (
title: Text ( ' Rename ' ) ,
content: TextField (
controller: newNameController ,
autofocus: true ,
decoration: InputDecoration (
labelText: ' Enter new name '
) ,
2025-07-28 09:20:08 +05:00
) ,
2025-08-07 20:28:39 +05:00
actions: [
TextButton ( onPressed: ( ) = > Navigator . pop ( context ) , child: Text ( ' Cancel ' ) ) ,
TextButton (
onPressed: ( ) async {
// try {
// await widget.sftpWorker.rename('${path}${dirEntry.filename}', '${widget.path}${newNameController.text}');
// _listDir();
// }
// on SftpStatusError catch (e) {
// if (context.mounted) {
// ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, e.message));
// }
// }
// if (context.mounted) {
// Navigator.pop(context);
// }
//
} ,
child: Text ( ' Rename ' )
) ,
2025-07-28 09:20:08 +05:00
2025-08-07 20:28:39 +05:00
] ,
)
) ;
} ,
icon: Icon ( Icons . drive_file_rename_outline )
) ,
IconButton (
onPressed: ( ) {
showDialog (
context: context ,
builder: ( context ) = > AlertDialog (
title: Text ( ' Delete Permanently? ' ) ,
content: Text ( dirEntry . attr . isDirectory ? ' The contents of this folder will be deleted as well \n This action cannot be undone ' : ' This action cannot be undone ' ) ,
actions: [
TextButton ( onPressed: ( ) = > Navigator . pop ( context ) , child: Text ( ' Cancel ' ) ) ,
TextButton (
onPressed: ( ) async {
// if (dirEntry.attr.isDirectory) {
// Future<void> removeRecursively (String path) async {
// final dirContents = await widget.sftpWorker.listdir(path);
// for (SftpName entry in dirContents) {
// final fullPath = '$path${entry.filename}';
// if (entry.attr.isDirectory) {
// await removeRecursively('$fullPath/');
// await widget.sftpWorker.rmdir('$fullPath/');
// }
// else {
// await widget.sftpWorker.remove(fullPath);
// }
// }
// await widget.sftpWorker.rmdir(path);
// }
// await removeRecursively('${path}${dirEntry.filename}/');
// }
// else {
// await widget.sftpWorker.remove('${path}${dirEntry.filename}');
// }
// _listDir();
// if (context.mounted) {
// Navigator.pop(context);
// }
} ,
child: Text ( ' Yes ' )
) ,
] ,
)
) ;
} ,
icon: Icon ( Icons . delete )
) ,
] ,
) ,
onTap: ( ) {
if ( dirEntry . attr . isDirectory ) {
path = ' $ path ${ dirEntry . filename } / ' ;
_listDir ( ) ;
}
} ,
) ;
} ,
)
2025-07-27 09:56:46 +05:00
)
) ;
}
2025-07-28 08:04:50 +05:00
Widget _buildFABs ( BuildContext context ) {
return Row (
mainAxisSize: MainAxisSize . min ,
spacing: 10 ,
children: [
FloatingActionButton (
heroTag: ' create-new-folder ' ,
onPressed: ( ) {
final nameController = TextEditingController ( ) ;
showDialog (
context: context ,
builder: ( context ) = > AlertDialog (
title: Text ( ' Create new folder ' ) ,
content: TextField (
controller: nameController ,
decoration: InputDecoration (
labelText: ' Enter folder name '
) ,
) ,
actions: [
TextButton ( onPressed: ( ) = > Navigator . pop ( context ) , child: Text ( ' Cancel ' ) ) ,
TextButton (
onPressed: ( ) async {
2025-08-02 08:59:05 +05:00
try {
2025-08-07 20:28:39 +05:00
await widget . sftpWorker . mkdir ( ' ${ path } ${ nameController . text } ' ) ;
2025-08-02 08:59:05 +05:00
_listDir ( ) ;
}
catch ( e ) {
if ( context . mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( _buildErrorSnackBar ( context , e . toString ( ) ) ) ;
}
}
if ( context . mounted ) {
Navigator . pop ( context ) ;
}
2025-07-28 08:04:50 +05:00
} ,
child: Text ( ' Ok ' )
) ,
] ,
)
) ;
} ,
child: Icon ( Icons . create_new_folder ) ,
) ,
FloatingActionButton (
heroTag: ' upload-file ' ,
onPressed: ( ) async {
2025-07-31 11:22:12 +05:00
final List < String > filePaths ;
if ( Platform . isAndroid | Platform . isIOS ) {
final res = await FilePicker . platform . pickFiles ( allowMultiple: true ) ;
filePaths = ( res ? . paths ? ? [ ] ) . whereType < String > ( ) . toList ( ) ;
2025-07-28 09:20:08 +05:00
}
2025-07-31 11:22:12 +05:00
else {
final files = await openFiles ( ) ;
filePaths = files . map ( ( file ) = > file . path ) . toList ( ) ;
2025-07-28 08:04:50 +05:00
}
2025-08-02 08:39:31 +05:00
try {
2025-08-07 20:28:39 +05:00
await for ( final progress in widget . sftpWorker . uploadFiles ( path , filePaths ) ) {
2025-08-02 08:39:31 +05:00
setState ( ( ) = > _progress = progress ) ;
}
}
catch ( e ) {
if ( context . mounted ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( _buildErrorSnackBar ( context , e . toString ( ) ) ) ;
}
}
setState ( ( ) = > _progress = null ) ;
2025-07-31 11:22:12 +05:00
_listDir ( ) ;
2025-07-28 08:04:50 +05:00
} ,
child: Icon ( Icons . upload ) ,
) ,
] ,
) ;
}
2025-07-28 08:24:22 +05:00
SnackBar _buildErrorSnackBar ( BuildContext context , String error ) {
return SnackBar (
backgroundColor: Theme . of ( context ) . colorScheme . secondaryContainer ,
behavior: SnackBarBehavior . floating ,
content: Row (
spacing: 10 ,
children: [
Icon ( Icons . error , color: Colors . red , ) ,
Text ( error , style: TextStyle ( color: Theme . of ( context ) . colorScheme . onSecondaryContainer ) , ) ,
] ,
)
) ;
}
2025-07-27 09:56:46 +05:00
}