select files feature and operations on them

This commit is contained in:
RafayAhmad7548 2025-08-19 15:45:31 +05:00
parent 7f3146d712
commit 99da431190
4 changed files with 85 additions and 30 deletions

View file

@ -9,6 +9,8 @@ class SftpProvider extends ChangeNotifier {
bool _isLoading = false; bool _isLoading = false;
late List<SftpName> _dirContents; late List<SftpName> _dirContents;
final List<SftpName> _selectedFiles = [];
SftpProvider(this._sftpWorker) { SftpProvider(this._sftpWorker) {
listDir(); listDir();
} }
@ -18,6 +20,8 @@ class SftpProvider extends ChangeNotifier {
String get path => _path; String get path => _path;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
List<SftpName> get dirContents => _dirContents; List<SftpName> get dirContents => _dirContents;
List<SftpName> get selectedFiles => _selectedFiles;
bool get isSelectionMode => _selectedFiles.isNotEmpty;
Future<void> listDir() async { Future<void> listDir() async {
_isLoading = true; _isLoading = true;
@ -38,4 +42,22 @@ class SftpProvider extends ChangeNotifier {
listDir(); listDir();
} }
void selectFile(SftpName file) {
_selectedFiles.add(file);
notifyListeners();
}
void toggleSelection(SftpName file) {
if (!_selectedFiles.remove(file)) {
_selectedFiles.add(file);
}
notifyListeners();
}
void clearSelectedFiles() {
_selectedFiles.clear();
notifyListeners();
}
} }

View file

@ -1,7 +1,9 @@
import 'dart:io'; import 'dart:io';
import 'package:dartssh2/dartssh2.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:file_selector/file_selector.dart'; import 'package:file_selector/file_selector.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluxcloud/main.dart'; import 'package:fluxcloud/main.dart';
import 'package:fluxcloud/providers/sftp_loading_provider.dart'; import 'package:fluxcloud/providers/sftp_loading_provider.dart';
@ -34,7 +36,7 @@ class SftpExplorer extends StatelessWidget {
}, },
icon: Icon(Icons.arrow_back) icon: Icon(Icons.arrow_back)
), ),
actions: _buildLoadingButtons, actions: [_buildAppBarButtons()]
), ),
floatingActionButton: _buildFABs(context), floatingActionButton: _buildFABs(context),
bottomNavigationBar: _buildCopyMoveButton(context), bottomNavigationBar: _buildCopyMoveButton(context),
@ -70,14 +72,28 @@ class SftpExplorer extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final dirEntry = sftpProvider.dirContents[index]; final dirEntry = sftpProvider.dirContents[index];
return ListTile( return ListTile(
leading: Icon(dirEntry.attr.isDirectory ? Icons.folder : Icons.description), leading: Row(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
if (sftpProvider.isSelectionMode)
Icon(sftpProvider.selectedFiles.contains(dirEntry) ? Icons.check_box : Icons.check_box_outline_blank),
Icon(dirEntry.attr.isDirectory ? Icons.folder : Icons.description),
],
),
title: Text(dirEntry.filename), title: Text(dirEntry.filename),
trailing: OperationButtons(dirEntries: [dirEntry],), trailing: sftpProvider.isSelectionMode ? null : OperationButtons(dirEntries: [dirEntry],),
onTap: () { onTap: () {
if (dirEntry.attr.isDirectory) { if (sftpProvider.isSelectionMode) {
sftpProvider.toggleSelection(dirEntry);
}
else if (dirEntry.attr.isDirectory) {
sftpProvider.goToDir('${sftpProvider.path}${dirEntry.filename}/'); sftpProvider.goToDir('${sftpProvider.path}${dirEntry.filename}/');
} }
}, },
onLongPress: () {
sftpProvider.selectFile(dirEntry);
},
); );
}, },
) )
@ -168,11 +184,11 @@ class SftpExplorer extends StatelessWidget {
} }
Widget _buildCopyMoveButton(BuildContext context) { Widget _buildCopyMoveButton(BuildContext context) {
return Selector<SftpLoadingProvider, (List<String>?, bool)>( return Selector<SftpLoadingProvider, (double?, List<String>?, bool)>(
selector: (_, sftpLoadingProvider) => (sftpLoadingProvider.toBeMovedOrCopied, sftpLoadingProvider.isCopy), selector: (_, sftpLoadingProvider) => (sftpLoadingProvider.copyProgress, sftpLoadingProvider.toBeMovedOrCopied, sftpLoadingProvider.isCopy),
builder: (_, data, __) { builder: (_, data, __) {
final (toBeMovedOrCopied, isCopy) = data; final (copyProgress, toBeMovedOrCopied, isCopy) = data;
if (toBeMovedOrCopied == null) { if (toBeMovedOrCopied == null || copyProgress != null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Padding( return Padding(
@ -202,7 +218,6 @@ class SftpExplorer extends StatelessWidget {
} }
} }
} }
// TODO: figure out where to put this line
sftpLoadingProvider.setCopyOrMoveFiles(null, isCopy); sftpLoadingProvider.setCopyOrMoveFiles(null, isCopy);
sftpLoadingProvider.setCopyProgress(null); sftpLoadingProvider.setCopyProgress(null);
sftpProvider.listDir(); sftpProvider.listDir();
@ -236,20 +251,35 @@ class SftpExplorer extends StatelessWidget {
); );
} }
List<Widget> get _buildLoadingButtons => [ Widget _buildAppBarButtons() {
Selector<SftpLoadingProvider, double?>( return Selector<SftpProvider, bool>(
selector: (_, sftpLoadingProvider) => sftpLoadingProvider.uploadProgress, selector: (_, sftpProvider) => sftpProvider.isSelectionMode,
builder: (_, uploadProgress, __) => _buildLoader(uploadProgress, Icons.upload) builder: (context, isSelectionMode, __) =>
), isSelectionMode ?
Selector<SftpLoadingProvider, double?>( Selector<SftpProvider, List<SftpName>>(
selector: (_, sftpLoadingProvider) => sftpLoadingProvider.downloadProgress, selector: (_, sftpProvider) => sftpProvider.selectedFiles,
builder: (_, downloadProgress, __) => _buildLoader(downloadProgress, Icons.download) shouldRebuild: (prev, next) => listEquals(prev, next),
), builder: (_, selectedFiles, __) => OperationButtons(dirEntries: [...selectedFiles])
Selector<SftpLoadingProvider, double?>( )
selector: (_, sftpLoadingProvider) => sftpLoadingProvider.copyProgress, : Row(
builder: (_, copyProgress, __) => _buildLoader(copyProgress, Icons.copy) mainAxisSize: MainAxisSize.min,
), children: [
]; Selector<SftpLoadingProvider, double?>(
selector: (_, sftpLoadingProvider) => sftpLoadingProvider.uploadProgress,
builder: (_, uploadProgress, __) => _buildLoader(uploadProgress, Icons.upload)
),
Selector<SftpLoadingProvider, double?>(
selector: (_, sftpLoadingProvider) => sftpLoadingProvider.downloadProgress,
builder: (_, downloadProgress, __) => _buildLoader(downloadProgress, Icons.download)
),
Selector<SftpLoadingProvider, double?>(
selector: (_, sftpLoadingProvider) => sftpLoadingProvider.copyProgress,
builder: (_, copyProgress, __) => _buildLoader(copyProgress, Icons.copy)
),
],
),
);
}
Widget _buildLoader(double? progress, IconData icon) { Widget _buildLoader(double? progress, IconData icon) {
return progress != null ? Stack( return progress != null ? Stack(

View file

@ -185,8 +185,7 @@ class SftpWorker {
final dstFile = await sftpClient.open(copyCmd.copyToPath, mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive); final dstFile = await sftpClient.open(copyCmd.copyToPath, mode: SftpFileOpenMode.create | SftpFileOpenMode.write | SftpFileOpenMode.exclusive);
final fileSize = (await srcFile.stat()).size!; final fileSize = (await srcFile.stat()).size!;
bool timeout = true; bool timeout = true;
await dstFile.write( await dstFile.write(srcFile.read(
srcFile.read(),
onProgress: (progress) { onProgress: (progress) {
if (timeout) { if (timeout) {
timeout = false; timeout = false;
@ -194,7 +193,7 @@ class SftpWorker {
Future.delayed(Duration(seconds: 2), () => timeout = true); Future.delayed(Duration(seconds: 2), () => timeout = true);
} }
} }
); ));
} }
on SftpStatusError catch (e) { on SftpStatusError catch (e) {
sendPort.send((id, RemoteError(e.message, ''))); sendPort.send((id, RemoteError(e.message, '')));

View file

@ -22,6 +22,7 @@ class OperationButtons extends StatelessWidget {
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
sftpProvider.clearSelectedFiles();
final filePaths = dirEntries.map((dirEntry) => '${sftpProvider.path}${dirEntry.filename}').toList(); final filePaths = dirEntries.map((dirEntry) => '${sftpProvider.path}${dirEntry.filename}').toList();
sftpLoadingProvider.setCopyOrMoveFiles(filePaths, false); sftpLoadingProvider.setCopyOrMoveFiles(filePaths, false);
}, },
@ -29,6 +30,7 @@ class OperationButtons extends StatelessWidget {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
sftpProvider.clearSelectedFiles();
final filePaths = dirEntries.map((dirEntry) => '${sftpProvider.path}${dirEntry.filename}').toList(); final filePaths = dirEntries.map((dirEntry) => '${sftpProvider.path}${dirEntry.filename}').toList();
sftpLoadingProvider.setCopyOrMoveFiles(filePaths, true); sftpLoadingProvider.setCopyOrMoveFiles(filePaths, true);
}, },
@ -105,11 +107,12 @@ class OperationButtons extends StatelessWidget {
ScaffoldMessenger.of(context).showSnackBar(buildErrorSnackBar(context, e.toString())); ScaffoldMessenger.of(context).showSnackBar(buildErrorSnackBar(context, e.toString()));
} }
} }
sftpProvider.listDir();
if (context.mounted) {
Navigator.pop(context);
}
} }
sftpProvider.listDir();
if (context.mounted) {
Navigator.pop(context);
}
sftpProvider.clearSelectedFiles();
}, },
child: Text('Yes') child: Text('Yes')
), ),
@ -122,6 +125,7 @@ class OperationButtons extends StatelessWidget {
), ),
IconButton( IconButton(
onPressed: () async { onPressed: () async {
sftpProvider.clearSelectedFiles();
final downloadsDir = await getDownloadsDirectory(); final downloadsDir = await getDownloadsDirectory();
if (downloadsDir == null) return; if (downloadsDir == null) return;
for (final dirEntry in dirEntries) { for (final dirEntry in dirEntries) {