Compare commits

..

2 commits

Author SHA1 Message Date
RafayAhmad7548
3d86afa592 error handling with nice snackbar on fabs 2025-07-28 08:24:22 +05:00
RafayAhmad7548
eb723b74fd delete operation, new folder, upload files 2025-07-28 08:04:50 +05:00
4 changed files with 208 additions and 7 deletions

View file

@ -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({

View file

@ -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 {
@ -16,6 +17,9 @@ class _SftpExplorerState extends State<SftpExplorer> {
bool _isLoading = true;
late List<SftpName> _dirContents;
SftpFileWriter? _loader;
String _loadingFileName = '';
double _progress = 0;
@override
void initState() {
@ -23,11 +27,10 @@ class _SftpExplorerState extends State<SftpExplorer> {
_listDir();
}
void _listDir() async {
Future<void> _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<SftpExplorer> {
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<SftpExplorer> {
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<void> 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<SftpExplorer> {
)
);
}
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<XFile> 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();
}
}

View file

@ -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)) {