diff --git a/analysis_options.yaml b/analysis_options.yaml index 15369a7..f9b3034 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1 @@ include: package:flutter_lints/flutter.yaml -analyzer: - errors: - todo: ignore diff --git a/lib/sftp_connection_list.dart b/lib/sftp_connection_list.dart index 2514458..ca4d4fb 100644 --- a/lib/sftp_connection_list.dart +++ b/lib/sftp_connection_list.dart @@ -4,10 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:fluxcloud/connection.dart'; import 'package:fluxcloud/sftp_explorer.dart'; -import 'package:fluxcloud/sftp_provider.dart'; import 'package:fluxcloud/sftp_worker.dart'; import 'package:fluxcloud/widgets/add_server_modal.dart'; -import 'package:provider/provider.dart'; class SftpConnectionList extends StatefulWidget { const SftpConnectionList({ @@ -82,10 +80,7 @@ class _SftpConnectionListState extends State { onTap: () async { final sftpWorker = await SftpWorker.spawn(_connections[index]); if (context.mounted) { - Navigator.push(context, MaterialPageRoute(builder: (context) => ChangeNotifierProvider( - create: (_) => SftpProvider(sftpWorker), - child: SftpExplorer() - ))); + Navigator.push(context, MaterialPageRoute(builder: (context) => SftpExplorer(sftpWorker: sftpWorker,))); } }, borderRadius: BorderRadius.circular(10), diff --git a/lib/sftp_explorer.dart b/lib/sftp_explorer.dart index 3fdc9b8..00d6948 100644 --- a/lib/sftp_explorer.dart +++ b/lib/sftp_explorer.dart @@ -5,14 +5,50 @@ import 'package:file_picker/file_picker.dart'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:fluxcloud/main.dart'; -import 'package:fluxcloud/sftp_provider.dart'; -import 'package:path/path.dart'; -import 'package:provider/provider.dart'; +import 'package:fluxcloud/sftp_worker.dart'; import 'widgets/operation_buttons.dart'; -class SftpExplorer extends StatelessWidget { - const SftpExplorer({super.key}); +class SftpExplorer extends StatefulWidget { + const SftpExplorer({super.key, required this.sftpWorker}); + + final SftpWorker sftpWorker; + + @override + State createState() => _SftpExplorerState(); +} + +class _SftpExplorerState extends State { + + String path = '/'; + + bool _isLoading = true; + late List _dirContents; + + double? _uploadProgress; + double? _downloadProgress; + + void _setDownloadProgress(double? progress) => setState(() => _downloadProgress = progress); + + @override + void initState() { + super.initState(); + _listDir(); + } + + Future _listDir() async { + setState(() => _isLoading = true); + try { + _dirContents = await widget.sftpWorker.listdir(path); + } + catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(buildErrorSnackBar(context, e.toString())); + } + } + setState(() => _isLoading = false); + } + @override Widget build(BuildContext context) { @@ -24,103 +60,101 @@ class SftpExplorer extends StatelessWidget { actionsPadding: EdgeInsets.only(right: 20), leading: IconButton( onPressed: () { - if (context.read().path == '/') { + if (path == '/') { // TODO: figure this out // Navigator.pop(context); } else { - context.read().goToPrevDir(); + path = path.substring(0, path.length - 1); + path = path.substring(0, path.lastIndexOf('/')+1); + _listDir(); } }, icon: Icon(Icons.arrow_back) ), actions: [ - Selector( - selector: (_, sftpProvider) => sftpProvider.uploadProgress, - builder: (_, uploadProgress, __) => uploadProgress != null ? Stack( - alignment: Alignment.center, - children: [ - TweenAnimationBuilder( - tween: Tween(begin: 0, end: uploadProgress), - duration: Duration(milliseconds: 300), - builder: (context, value, _) => CircularProgressIndicator(strokeWidth: 3, value: value,) - ), - IconButton( - onPressed: () { - // TODO: show upload details here - }, - icon: Icon(Icons.upload) - ), - ] - ) : const SizedBox.shrink(), + if (_uploadProgress != null) + Stack( + alignment: Alignment.center, + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0, end: _uploadProgress), + duration: Duration(milliseconds: 300), + builder: (context, value, _) => CircularProgressIndicator(strokeWidth: 3, value: value,) + ), + IconButton( + onPressed: () { + // TODO: show upload details here + }, + icon: Icon(Icons.upload) + ), + ] ), - Selector( - selector: (_, sftpProvider) => sftpProvider.downloadProgress, - builder: (_, downloadProgress, __) => downloadProgress != null ? Stack( - alignment: Alignment.center, - children: [ - TweenAnimationBuilder( - tween: Tween(begin: 0, end: downloadProgress), - duration: Duration(milliseconds: 300), - builder: (context, value, _) => CircularProgressIndicator(strokeWidth: 3, value: value,) - ), - IconButton( - onPressed: () { - // TODO: show donwload details here - }, - icon: Icon(Icons.download) - ), - ] - ) : const SizedBox.shrink(), + if (_downloadProgress != null) + Stack( + alignment: Alignment.center, + children: [ + TweenAnimationBuilder( + tween: Tween(begin: 0, end: _downloadProgress), + duration: Duration(milliseconds: 300), + builder: (context, value, _) => CircularProgressIndicator(strokeWidth: 3, value: value,) + ), + IconButton( + onPressed: () { + // TODO: show donwload details here + }, + icon: Icon(Icons.download) + ), + ] ), ], ), floatingActionButton: _buildFABs(context), - bottomNavigationBar: _buildCopyMoveButton(context), body: PopScope( canPop: false, onPopInvokedWithResult: (_, _) { - if (context.read().path != '/') { - context.read().goToPrevDir(); + if (path != '/') { + path = path.substring(0, path.length - 1); + path = path.substring(0, path.lastIndexOf('/')+1); + _listDir(); } }, - child: Consumer( - builder: (_, sftpProvider, __) => AnimatedSwitcher( - duration: Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - final curved = CurvedAnimation( - parent: animation, - curve: Curves.fastOutSlowIn + child: 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: OperationButtons(sftpWorker: widget.sftpWorker, path: path, dirEntries: [dirEntry], listDir: _listDir, setDownloadProgress: _setDownloadProgress,), + onTap: () { + if (dirEntry.attr.isDirectory) { + path = '$path${dirEntry.filename}/'; + _listDir(); + } + }, ); - return FadeTransition( - opacity: curved, - child: ScaleTransition( - scale: Tween( - begin: 0.92, - end: 1 - ).animate(curved), - child: child, - ), - ); - }, - child: sftpProvider.isLoading ? Center(child: CircularProgressIndicator()) : ListView.builder( - key: ValueKey(sftpProvider.path), - itemCount: sftpProvider.dirContents.length, - itemBuilder: (context, index) { - final dirEntry = sftpProvider.dirContents[index]; - return ListTile( - leading: Icon(dirEntry.attr.isDirectory ? Icons.folder : Icons.description), - title: Text(dirEntry.filename), - trailing: OperationButtons(dirEntries: [dirEntry],), - onTap: () { - if (dirEntry.attr.isDirectory) { - sftpProvider.goToDir('${sftpProvider.path}${dirEntry.filename}/'); - } - }, - ); - }, - ) - ), + }, + ) ), ) ); @@ -149,10 +183,9 @@ class SftpExplorer extends StatelessWidget { TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), TextButton( onPressed: () async { - final sftpProvider = context.read(); try { - await sftpProvider.sftpWorker.mkdir('${sftpProvider.path}${nameController.text}'); - sftpProvider.listDir(); + await widget.sftpWorker.mkdir('$path${nameController.text}'); + _listDir(); } catch (e) { if (context.mounted) { @@ -174,7 +207,6 @@ class SftpExplorer extends StatelessWidget { FloatingActionButton( heroTag: 'upload-file', onPressed: () async { - final sftpProvider = context.read(); final List filePaths; if (Platform.isAndroid | Platform.isIOS) { final res = await FilePicker.platform.pickFiles(allowMultiple: true); @@ -186,10 +218,10 @@ class SftpExplorer extends StatelessWidget { } for (final filePath in filePaths) { try { - await for (final progress in sftpProvider.sftpWorker.uploadFile(sftpProvider.path, filePath)) { - sftpProvider.setUploadProgress(progress); + await for (final progress in widget.sftpWorker.uploadFile(path, filePath)) { + setState(() => _uploadProgress = progress); } - await sftpProvider.listDir(); + await _listDir(); } catch (e) { if (context.mounted) { @@ -197,8 +229,8 @@ class SftpExplorer extends StatelessWidget { } } } - sftpProvider.setUploadProgress(null); - sftpProvider.listDir(); + setState(() => _uploadProgress = null); + _listDir(); }, child: Icon(Icons.upload), ), @@ -206,68 +238,4 @@ class SftpExplorer extends StatelessWidget { ); } - Widget _buildCopyMoveButton(BuildContext context) { - return Selector?, bool)>( - selector: (_, sftpProvider) => (sftpProvider.toBeMovedOrCopied, sftpProvider.isCopy), - builder: (_, data, __) { - final (toBeMovedOrCopied, isCopy) = data; - if (toBeMovedOrCopied == null) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.all(12), - child: Row( - spacing: 10, - children: [ - Expanded(child: ElevatedButton( - onPressed: () async { - final sftpProvider = context.read(); - for (final filePath in toBeMovedOrCopied) { - try { - if (isCopy) { - - } - else { - final fileName = basename(filePath); - await sftpProvider.sftpWorker.rename(filePath, '${sftpProvider.path}$fileName'); - } - } - catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar(buildErrorSnackBar(context, e.toString())); - } - } - } - sftpProvider.setCopyOrMoveFiles(null, isCopy); - sftpProvider.listDir(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Text(isCopy ? 'Copy Here' : 'Move Here'), - ), - )), - IconButton( - onPressed: () { - context.read().setCopyOrMoveFiles(null, isCopy); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.secondaryContainer, - foregroundColor: Theme.of(context).colorScheme.onSecondaryContainer - ), - icon: Padding( - padding: const EdgeInsets.all(4), - child: Icon(Icons.close), - ), - ) - ], - ), - ); - } - ); - } - } diff --git a/lib/sftp_provider.dart b/lib/sftp_provider.dart deleted file mode 100644 index e3e12fd..0000000 --- a/lib/sftp_provider.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:dartssh2/dartssh2.dart'; -import 'package:flutter/material.dart'; -import 'package:fluxcloud/sftp_worker.dart'; - -class SftpProvider extends ChangeNotifier { - final SftpWorker _sftpWorker; - - String _path = '/'; - bool _isLoading = false; - late List _dirContents; - - double? _uploadProgress; - double? _downloadProgress; - - List? _toBeMovedOrCopied; - bool _isCopy = false; - - SftpProvider(this._sftpWorker) { - listDir(); - } - - SftpWorker get sftpWorker => _sftpWorker; - - String get path => _path; - bool get isLoading => _isLoading; - List get dirContents => _dirContents; - - double? get uploadProgress => _uploadProgress; - double? get downloadProgress => _downloadProgress; - - List? get toBeMovedOrCopied => _toBeMovedOrCopied; - bool get isCopy => _isCopy; - - Future listDir() async { - _isLoading = true; - notifyListeners(); - _dirContents = await _sftpWorker.listdir(_path); - _isLoading = false; - notifyListeners(); - } - - void goToPrevDir() { - _path = _path.substring(0, _path.length - 1); - _path = _path.substring(0, _path.lastIndexOf('/')+1); - listDir(); - } - - void goToDir(String path) { - _path = path; - listDir(); - } - - void setUploadProgress(double? progress) { - _uploadProgress = progress; - notifyListeners(); - } - - void setDownloadProgress(double? progress) { - _downloadProgress = progress; - notifyListeners(); - } - - void setCopyOrMoveFiles(List? files, bool isCopy) { - _toBeMovedOrCopied = files; - _isCopy = isCopy; - notifyListeners(); - } - -} diff --git a/lib/sftp_worker.dart b/lib/sftp_worker.dart index 87697bf..0221c94 100644 --- a/lib/sftp_worker.dart +++ b/lib/sftp_worker.dart @@ -50,13 +50,6 @@ class DownloadFile extends SftpCommand { DownloadFile(this.file, this.path, this.downloadPath); } -class Copy extends SftpCommand { - final String filePath; - final String copyToPath; - - Copy(this.filePath, this.copyToPath); -} - class SftpWorker { @@ -116,66 +109,6 @@ class SftpWorker { static void _sftpCmdHandler(SendPort sendPort, ReceivePort receivePort, SftpClient sftpClient) { - - final StreamController<(int, DownloadFile)> downloadController = StreamController(); - downloadController.stream.asyncMap((cmd) async { - final (int id, DownloadFile downloadCmd) = cmd; - try { - final localFile = File('${downloadCmd.downloadPath}/${downloadCmd.file.filename}'); - if (await localFile.exists()) { - sendPort.send((id, RemoteError('File Already Exists', ''))); - return; - } - final localFileWriter = await localFile.open(mode: FileMode.write); - final remoteFile = await sftpClient.open('${downloadCmd.path}${downloadCmd.file.filename}'); - final fileSize = downloadCmd.file.attr.size!; - bool timeout = true; - await for (final bytes in remoteFile.read( - onProgress: (progress) { - if (timeout) { - timeout = false; - sendPort.send((id, progress/fileSize)); - Future.delayed(Duration(seconds: 2), () => timeout = true); - } - } - )) { - await localFileWriter.writeFrom(bytes); - } - } - on SftpStatusError catch (e) { - sendPort.send((id, RemoteError(e.message, ''))); - } - sendPort.send((id, 1.0)); - }).listen((_) {}); - - final StreamController<(int, UploadFile)> uploadController = StreamController(); - uploadController.stream.asyncMap((cmd) async { - final (int id, UploadFile uploadCmd) = cmd; - try { - final file = File(uploadCmd.filePath); - final fileSize = await file.length(); - final remoteFile = await sftpClient.open( - '${uploadCmd.path}${basename(uploadCmd.filePath)}', - mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive - ); - bool timeout = true; - await remoteFile.write( - file.openRead().cast(), - onProgress: (progress) { - if (timeout) { - timeout = false; - sendPort.send((id, progress/fileSize)); - Future.delayed(Duration(seconds: 2), () => timeout = true); - } - } - ); - } - on SftpStatusError catch (e) { - sendPort.send((id, RemoteError(e.message, ''))); - } - sendPort.send((id, 1.0)); - }).listen((_) {}); - receivePort.listen((message) async { final (int id, SftpCommand command) = message; switch (command) { @@ -187,8 +120,30 @@ class SftpWorker { on SftpStatusError catch (e) { sendPort.send((id, RemoteError(e.message, ''))); } - case UploadFile(): - uploadController.add((id, command)); + case UploadFile(:final path, :final filePath): + try { + final file = File(filePath); + final fileSize = await file.length(); + final remoteFile = await sftpClient.open( + '$path${basename(filePath)}', + mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive + ); + bool timeout = true; + await remoteFile.write( + file.openRead().cast(), + onProgress: (progress) { + if (timeout) { + timeout = false; + sendPort.send((id, progress/fileSize)); + Future.delayed(Duration(seconds: 2), () => timeout = true); + } + } + ); + } + on SftpStatusError catch (e) { + sendPort.send((id, RemoteError(e.message, ''))); + } + sendPort.send((id, 1.0)); case MkDir(:final path): try { await sftpClient.mkdir(path); @@ -232,17 +187,33 @@ class SftpWorker { on SftpStatusError catch (e) { sendPort.send((id, RemoteError(e.message, ''))); } - case DownloadFile(): - downloadController.add((id, command)); - case Copy(:final filePath, :final copyToPath): + case DownloadFile(:final file, :final path, :final downloadPath): try { - // TODO: complete this - sendPort.send((id, 0)); + final localFile = File('$downloadPath/${file.filename}'); + if (await localFile.exists()) { + sendPort.send((id, RemoteError('File Already Exists', ''))); + break; + } + final localFileWriter = await localFile.open(mode: FileMode.write); + final remoteFile = await sftpClient.open('$path${file.filename}'); + final fileSize = file.attr.size!; + bool timeout = true; + await for (final bytes in remoteFile.read( + onProgress: (progress) { + if (timeout) { + timeout = false; + sendPort.send((id, progress/fileSize)); + Future.delayed(Duration(seconds: 2), () => timeout = true); + } + } + )) { + await localFileWriter.writeFrom(bytes); + } } on SftpStatusError catch (e) { sendPort.send((id, RemoteError(e.message, ''))); } - + sendPort.send((id, 1.0)); } }); } @@ -325,12 +296,4 @@ class SftpWorker { return controller.stream; } - Future copy(String filePath, String copyToPath) async { - final completer = Completer.sync(); - final id = _idCounter++; - _activeRequests[id] = completer; - _commands.send((id, Copy(filePath, copyToPath))); - await completer.future; - } - } diff --git a/lib/widgets/operation_buttons.dart b/lib/widgets/operation_buttons.dart index 82bc312..c720a79 100644 --- a/lib/widgets/operation_buttons.dart +++ b/lib/widgets/operation_buttons.dart @@ -1,34 +1,35 @@ import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:fluxcloud/main.dart'; -import 'package:fluxcloud/sftp_provider.dart'; +import 'package:fluxcloud/sftp_worker.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:provider/provider.dart'; class OperationButtons extends StatelessWidget { const OperationButtons({ - super.key, required this.dirEntries, + super.key, + required this.sftpWorker, required this.path, required this.dirEntries, required this.listDir, required this.setDownloadProgress, }); + final SftpWorker sftpWorker; + final String path; final List dirEntries; + final Function listDir; + final Function(double? progress) setDownloadProgress; @override Widget build(BuildContext context) { - final sftpProvider = context.read(); return Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( onPressed: () { - final filePaths = dirEntries.map((dirEntry) => '${sftpProvider.path}${dirEntry.filename}').toList(); - sftpProvider.setCopyOrMoveFiles(filePaths, false); + }, icon: Icon(Icons.drive_file_move) ), IconButton( onPressed: () { - final filePaths = dirEntries.map((dirEntry) => '${sftpProvider.path}${dirEntry.filename}').toList(); - sftpProvider.setCopyOrMoveFiles(filePaths, true); + }, icon: Icon(Icons.copy) ), @@ -53,8 +54,8 @@ class OperationButtons extends StatelessWidget { TextButton( onPressed: () async { try { - await sftpProvider.sftpWorker.rename('${sftpProvider.path}${dirEntry.filename}', '${sftpProvider.path}${newNameController.text}'); - sftpProvider.listDir(); + await sftpWorker.rename('$path${dirEntry.filename}', '$path${newNameController.text}'); + listDir(); } on SftpStatusError catch (e) { if (context.mounted) { @@ -96,14 +97,14 @@ class OperationButtons extends StatelessWidget { onPressed: () async { for (final dirEntry in dirEntries) { try { - await sftpProvider.sftpWorker.remove(dirEntry, sftpProvider.path); + await sftpWorker.remove(dirEntry, path); } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(buildErrorSnackBar(context, e.toString())); } } - sftpProvider.listDir(); + listDir(); if (context.mounted) { Navigator.pop(context); } @@ -124,8 +125,8 @@ class OperationButtons extends StatelessWidget { if (downloadsDir == null) return; for (final dirEntry in dirEntries) { try { - await for (final progress in sftpProvider.sftpWorker.downloadFile(dirEntry, sftpProvider.path, downloadsDir.path)) { - sftpProvider.setDownloadProgress(progress); + await for (final progress in sftpWorker.downloadFile(dirEntry, path, downloadsDir.path)) { + setDownloadProgress(progress); } } catch (e) { @@ -134,7 +135,7 @@ class OperationButtons extends StatelessWidget { } } } - sftpProvider.setDownloadProgress(null); + setDownloadProgress(null); }, icon: Icon(Icons.download) ) diff --git a/pubspec.lock b/pubspec.lock index 7089f8a..2a4a94f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -320,14 +320,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" path: dependency: "direct main" description: @@ -416,14 +408,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.1" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" - url: "https://pub.dev" - source: hosted - version: "6.1.5" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 8b93912..6b69008 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,6 @@ dependencies: path: ^1.9.1 file_picker: ^10.2.0 path_provider: ^2.1.5 - provider: ^6.1.5 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..dca0eae --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:fluxcloud/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}