diff --git a/lib/sftp_explorer.dart b/lib/sftp_explorer.dart index f2de2d6..7d8504d 100644 --- a/lib/sftp_explorer.dart +++ b/lib/sftp_explorer.dart @@ -7,10 +7,9 @@ import 'package:flutter/material.dart'; import 'package:fluxcloud/sftp_worker.dart'; class SftpExplorer extends StatefulWidget { - const SftpExplorer({super.key, required this.sftpWorker, this.path = '/'}); + const SftpExplorer({super.key, required this.sftpWorker}); final SftpWorker sftpWorker; - final String path; @override State createState() => _SftpExplorerState(); @@ -18,6 +17,8 @@ class SftpExplorer extends StatefulWidget { class _SftpExplorerState extends State { + String path = '/'; + bool _isLoading = true; late List _dirContents; @@ -32,7 +33,7 @@ class _SftpExplorerState extends State { Future _listDir() async { setState(() => _isLoading = true); try { - _dirContents = await widget.sftpWorker.listdir(widget.path); + _dirContents = await widget.sftpWorker.listdir(path); } catch (e) { if (mounted) { @@ -51,6 +52,20 @@ class _SftpExplorerState extends State { title: Text('Explorer'), elevation: 2, actionsPadding: EdgeInsets.only(right: 20), + 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) + ), actions: [ if (_progress != null) Stack( @@ -63,7 +78,7 @@ class _SftpExplorerState extends State { ), IconButton( onPressed: () { - + }, icon: Icon(Icons.upload) ), @@ -72,127 +87,143 @@ class _SftpExplorerState extends State { ], ), floatingActionButton: _buildFABs(context), - body: _isLoading ? Center(child: CircularProgressIndicator()) : ListView.builder( - 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: () { - - }, - icon: Icon(Icons.drive_file_move) - ), - IconButton( - onPressed: () { - - }, - 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' - ), - ), - actions: [ - TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), - TextButton( - onPressed: () async { - // try { - // await widget.sftpWorker.rename('${widget.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') - ), - - ], - ) - ); - }, - 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\nThis 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 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('${widget.path}${dirEntry.filename}/'); - // } - // else { - // await widget.sftpWorker.remove('${widget.path}${dirEntry.filename}'); - // } - // _listDir(); - // if (context.mounted) { - // Navigator.pop(context); - // } - }, - child: Text('Yes') - ), - ], - ) - ); - }, - icon: Icon(Icons.delete) - ), - ], - ), - onTap: () { - if (dirEntry.attr.isDirectory) { - Navigator.push(context, MaterialPageRoute( - builder: (context) => SftpExplorer( - sftpWorker: widget.sftpWorker, - path: '${widget.path}${dirEntry.filename}/', - ) - )); - } - }, + 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( + 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: () { + + }, + icon: Icon(Icons.drive_file_move) + ), + IconButton( + onPressed: () { + + }, + 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' + ), + ), + 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') + ), + + ], + ) + ); + }, + 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\nThis 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 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(); + } + }, + ); + }, + ) ) ); } @@ -220,23 +251,18 @@ class _SftpExplorerState extends State { TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), TextButton( onPressed: () async { - // try { - // await widget.sftpWorker.mkdir('${widget.path}${nameController.text}'); - // _listDir(); - // } - // on SftpStatusError catch (e) { - // if (context.mounted) { - // if (e.code == 4) { - // ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Folder Already Exists')); - // } - // else { - // ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Error: ${e.message}')); - // } - // } - // } - // if (context.mounted) { - // Navigator.pop(context); - // } + try { + await widget.sftpWorker.mkdir('${path}${nameController.text}'); + _listDir(); + } + catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, e.toString())); + } + } + if (context.mounted) { + Navigator.pop(context); + } }, child: Text('Ok') ), @@ -259,8 +285,7 @@ class _SftpExplorerState extends State { filePaths = files.map((file) => file.path).toList(); } try { - await for (final progress in widget.sftpWorker.uploadFiles(widget.path, filePaths)) { - print(progress); + await for (final progress in widget.sftpWorker.uploadFiles(path, filePaths)) { setState(() => _progress = progress); } } diff --git a/lib/sftp_worker.dart b/lib/sftp_worker.dart index 61966e0..878c50b 100644 --- a/lib/sftp_worker.dart +++ b/lib/sftp_worker.dart @@ -8,11 +8,13 @@ import 'package:path/path.dart'; import 'connection.dart'; sealed class SftpCommand {} + class ListDir extends SftpCommand { final String path; ListDir(this.path); } + class UploadFiles extends SftpCommand { final String path; final List fileNames; @@ -20,6 +22,13 @@ class UploadFiles extends SftpCommand { UploadFiles(this.path, this.fileNames); } +class MkDir extends SftpCommand { + final String path; + + MkDir(this.path); +} + + class SftpWorker { final ReceivePort _responses; @@ -79,7 +88,7 @@ class SftpWorker { static void _sftpCmdHandler(SendPort sendPort, ReceivePort receivePort, SftpClient sftpClient) { receivePort.listen((message) async { - final (int id, dynamic command) = message; + final (int id, SftpCommand command) = message; switch (command) { case ListDir(:final path): try { @@ -115,6 +124,14 @@ class SftpWorker { } sendPort.send((id, 1.0)); } + case MkDir(:final path): + try { + await sftpClient.mkdir(path); + sendPort.send((id, 0)); + } + on SftpStatusError catch (e) { + sendPort.send((id, RemoteError(e.message, ''))); + } } }); } @@ -149,7 +166,7 @@ class SftpWorker { Future> listdir(String path) async { - final completer = Completer.sync(); + final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; _commands.send((id, ListDir(path))); @@ -165,5 +182,13 @@ class SftpWorker { return controller.stream; } + Future mkdir(String path) async { + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, MkDir(path))); + await completer.future; + } + }