diff --git a/lib/sftp_connection_list.dart b/lib/sftp_connection_list.dart index c8b068d..0116fa4 100644 --- a/lib/sftp_connection_list.dart +++ b/lib/sftp_connection_list.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:dartssh2/dartssh2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:fluxcloud/add_server_modal.dart'; import 'package:fluxcloud/connection.dart'; import 'package:fluxcloud/sftp_explorer.dart'; +import 'package:fluxcloud/widgets/add_server_modal.dart'; class SftpConnectionList extends StatefulWidget { const SftpConnectionList({ diff --git a/lib/sftp_explorer.dart b/lib/sftp_explorer.dart index 8560c6a..daaa1a3 100644 --- a/lib/sftp_explorer.dart +++ b/lib/sftp_explorer.dart @@ -1,4 +1,5 @@ import 'package:dartssh2/dartssh2.dart'; +import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; class SftpExplorer extends StatefulWidget { @@ -15,7 +16,10 @@ class _SftpExplorerState extends State { bool _isLoading = true; late List _dirContents; - + + SftpFileWriter? _loader; + String _loadingFileName = ''; + double _progress = 0; @override void initState() { @@ -23,11 +27,10 @@ class _SftpExplorerState extends State { _listDir(); } - void _listDir() async { + Future _listDir() async { + setState(() => _isLoading = true); _dirContents = await widget.sftpClient.listdir(widget.path); - setState(() { - _isLoading = false; - }); + setState(() => _isLoading = false); } @@ -37,6 +40,8 @@ class _SftpExplorerState extends State { appBar: AppBar( title: Text('Explorer'), ), + bottomNavigationBar: _buildLoadingWidget(context), + floatingActionButton: _buildFABs(context), body: _isLoading ? Center(child: CircularProgressIndicator()) : ListView.builder( itemCount: _dirContents.length, itemBuilder: (context, index) { @@ -44,6 +49,71 @@ class _SftpExplorerState extends State { 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: () {}, + 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.sftpClient.listdir(path); + for (SftpName entry in dirContents) { + final fullPath = '$path${entry.filename}'; + if (entry.attr.isDirectory) { + await removeRecursively('$fullPath/'); + await widget.sftpClient.rmdir('$fullPath/'); + } + else { + await widget.sftpClient.remove(fullPath); + } + } + await widget.sftpClient.rmdir(path); + } + await removeRecursively('${widget.path}${dirEntry.filename}/'); + } + else { + await widget.sftpClient.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( @@ -59,4 +129,135 @@ class _SftpExplorerState extends State { ) ); } + + 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 { + try { + await widget.sftpClient.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); + } + }, + child: Text('Ok') + ), + ], + ) + ); + }, + child: Icon(Icons.create_new_folder), + ), + FloatingActionButton( + heroTag: 'upload-file', + onPressed: () async { + final List files = await openFiles(); + for (XFile file in files) { + try { + final remoteFile = await widget.sftpClient.open('${widget.path}${file.name}', mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive); + final fileSize = await file.length(); + final uploader = remoteFile.write( + file.openRead().cast(), + onProgress: (progress) => setState(() => _progress = progress/fileSize) + ); + setState(() { + _loader = uploader; + _loadingFileName = file.name; + }); + await uploader.done; + setState(() => _loader = null); + _listDir(); + } + on SftpStatusError catch (e) { + if (context.mounted) { + if (e.code == 4) { + ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'File Already Exists')); + } + else { + ScaffoldMessenger.of(context).showSnackBar(_buildErrorSnackBar(context, 'Error: ${e.message}')); + } + } + } + } + }, + child: Icon(Icons.upload), + ), + ], + ); + } + + 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),), + ], + ) + ); + } + + Widget _buildLoadingWidget(BuildContext context) { + return _loader != null ? Container( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + spacing: 10, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + spacing: 10, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Uploading file: $_loadingFileName', style: TextStyle(fontSize: 16),), + TextButton( + onPressed: () { + _loader!.abort(); + widget.sftpClient.remove('${widget.path}$_loadingFileName'); + }, + child: Text('Cancel') + ), + ], + ), + LinearProgressIndicator(value: _progress,) + ], + ), + ), + ) : SizedBox(); + } } diff --git a/lib/add_server_modal.dart b/lib/widgets/add_server_modal.dart similarity index 100% rename from lib/add_server_modal.dart rename to lib/widgets/add_server_modal.dart diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc index 3741b8b..4903bee 100644 --- a/linux/runner/my_application.cc +++ b/linux/runner/my_application.cc @@ -27,7 +27,7 @@ static void my_application_activate(GApplication* application) { // in case the window manager does more exotic layout, e.g. tiling. // If running on Wayland assume the header bar will work (may need changing // if future cases occur). - gboolean use_header_bar = TRUE; + gboolean use_header_bar = FALSE; #ifdef GDK_WINDOWING_X11 GdkScreen* screen = gtk_window_get_screen(window); if (GDK_IS_X11_SCREEN(screen)) {