From 681529a71bdda959a050ee9cadde8f22d300391a Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 29 Dec 2023 21:26:51 +0100 Subject: [PATCH] fix(neon_files): do not allow to copy or move a folder into itself. Signed-off-by: Nikolas Rimikis --- .../neon_files/lib/src/blocs/browser.dart | 61 +++++++- .../neon/neon_files/lib/src/blocs/files.dart | 7 +- .../neon/neon_files/lib/src/utils/dialog.dart | 7 +- .../lib/src/widgets/browser_view.dart | 142 +++++++----------- .../neon_files/lib/src/widgets/dialog.dart | 7 +- .../lib/src/widgets/file_list_tile.dart | 7 +- 6 files changed, 124 insertions(+), 107 deletions(-) diff --git a/packages/neon/neon_files/lib/src/blocs/browser.dart b/packages/neon/neon_files/lib/src/blocs/browser.dart index b446e9cabcf..47344aa30b8 100644 --- a/packages/neon/neon_files/lib/src/blocs/browser.dart +++ b/packages/neon/neon_files/lib/src/blocs/browser.dart @@ -8,17 +8,33 @@ import 'package:neon_framework/utils.dart'; import 'package:nextcloud/webdav.dart'; import 'package:rxdart/rxdart.dart'; +/// Mode to operate the `FilesBrowserView` in. +enum FilesBrowserMode { + /// Default file browser mode. + /// + /// When a file is selected it will be opened or downloaded. + browser, + + /// Select directory. + selectDirectory, + + /// Do not show file actions. + noActions, +} + sealed class FilesBrowserBloc implements InteractiveBloc { @internal factory FilesBrowserBloc( final FilesOptions options, final Account account, { final PathUri? initialPath, + final FilesBrowserMode? mode, }) => _FilesBrowserBloc( options, account, initialPath: initialPath, + mode: mode, ); void setPath(final PathUri uri); @@ -30,18 +46,25 @@ sealed class FilesBrowserBloc implements InteractiveBloc { BehaviorSubject get uri; FilesOptions get options; + + /// Mode to operate the `FilesBrowserView` in. + FilesBrowserMode get mode; } class _FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBloc { _FilesBrowserBloc( this.options, this.account, { - final PathUri? initialPath, - }) { - if (initialPath != null) { - uri.add(initialPath); + this.initialPath, + final FilesBrowserMode? mode, + }) : mode = mode ?? FilesBrowserMode.browser { + final parent = initialPath?.parent; + if (parent != null) { + uri.add(parent); } + options.showHiddenFilesOption.addListener(refresh); + unawaited(refresh()); } @@ -49,8 +72,15 @@ class _FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBloc { final FilesOptions options; final Account account; + @override + final FilesBrowserMode mode; + + final PathUri? initialPath; + @override void dispose() { + options.showHiddenFilesOption.removeListener(refresh); + unawaited(files.close()); unawaited(uri.close()); super.dispose(); @@ -80,7 +110,28 @@ class _FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBloc { ), depth: WebDavDepth.one, ), - (final response) => response.toWebDavFiles().sublist(1), + (final response) { + final unwrapped = response.toWebDavFiles().sublist(1); + + return unwrapped.where((final file) { + // Do not show files when selecting a directory + if (mode == FilesBrowserMode.selectDirectory && !file.isDirectory) { + return false; + } + + // Do not show itself when selecting a directory + if (mode == FilesBrowserMode.selectDirectory && initialPath == file.path) { + return false; + } + + // Do not show hidden files unless the option is enabled + if (!options.showHiddenFilesOption.value && file.isHidden) { + return false; + } + + return true; + }).toList(); + }, emitEmptyCache: true, ); } diff --git a/packages/neon/neon_files/lib/src/blocs/files.dart b/packages/neon/neon_files/lib/src/blocs/files.dart index 990ad812ab1..eb3cea2218a 100644 --- a/packages/neon/neon_files/lib/src/blocs/files.dart +++ b/packages/neon/neon_files/lib/src/blocs/files.dart @@ -56,7 +56,7 @@ sealed class FilesBloc implements InteractiveBloc { FilesBrowserBloc get browser; - FilesBrowserBloc getNewFilesBrowserBloc({final PathUri? initialUri}); + FilesBrowserBloc getNewFilesBrowserBloc({final PathUri? initialUri, final FilesBrowserMode? mode}); } class _FilesBloc extends InteractiveBloc implements FilesBloc { @@ -235,13 +235,12 @@ class _FilesBloc extends InteractiveBloc implements FilesBloc { } @override - FilesBrowserBloc getNewFilesBrowserBloc({ - final PathUri? initialUri, - }) => + FilesBrowserBloc getNewFilesBrowserBloc({final PathUri? initialUri, final FilesBrowserMode? mode}) => FilesBrowserBloc( options, account, initialPath: initialUri, + mode: mode, ); void downloadParallelismListener() { diff --git a/packages/neon/neon_files/lib/src/utils/dialog.dart b/packages/neon/neon_files/lib/src/utils/dialog.dart index 747c1eb3bc6..16f1a6d92f7 100644 --- a/packages/neon/neon_files/lib/src/utils/dialog.dart +++ b/packages/neon/neon_files/lib/src/utils/dialog.dart @@ -2,6 +2,7 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:neon_files/l10n/localizations.dart'; +import 'package:neon_files/src/blocs/browser.dart'; import 'package:neon_files/src/blocs/files.dart'; import 'package:neon_files/src/models/file_details.dart'; import 'package:neon_files/src/widgets/dialog.dart'; @@ -73,7 +74,11 @@ Future showChooseFolderDialog(final BuildContext context, final FileDe final bloc = NeonProvider.of(context); final originalUri = details.uri; - final b = bloc.getNewFilesBrowserBloc(initialUri: originalUri); + final b = bloc.getNewFilesBrowserBloc( + initialUri: originalUri, + mode: FilesBrowserMode.selectDirectory, + ); + final result = await showDialog( context: context, builder: (final context) => FilesChooseFolderDialog( diff --git a/packages/neon/neon_files/lib/src/widgets/browser_view.dart b/packages/neon/neon_files/lib/src/widgets/browser_view.dart index 3f70414185b..a2e8da3cc35 100644 --- a/packages/neon/neon_files/lib/src/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/src/widgets/browser_view.dart @@ -13,31 +13,15 @@ import 'package:neon_framework/sort_box.dart'; import 'package:neon_framework/widgets.dart'; import 'package:nextcloud/webdav.dart'; -/// Mode to operate the [FilesBrowserView] in. -enum FilesBrowserMode { - /// Default file browser mode. - /// - /// When a file is selected it will be opened or downloaded. - browser, - - /// Select directory. - selectDirectory, - - /// Don't show file actions. - noActions, -} - class FilesBrowserView extends StatefulWidget { const FilesBrowserView({ required this.bloc, required this.filesBloc, - this.mode = FilesBrowserMode.browser, super.key, }); final FilesBrowserBloc bloc; final FilesBloc filesBloc; - final FilesBrowserMode mode; @override State createState() => _FilesBrowserViewState(); @@ -64,81 +48,63 @@ class _FilesBrowserViewState extends State { if (!uriSnapshot.hasData || !tasksSnapshot.hasData) { return const SizedBox(); } - return ValueListenableBuilder( - valueListenable: widget.bloc.options.showHiddenFilesOption, - builder: (final context, final showHiddenFiles, final _) { - final files = filesSnapshot.data?.where((final file) { - var hideFile = false; - if (widget.mode == FilesBrowserMode.selectDirectory && !file.isDirectory) { - hideFile = true; - } - if (!showHiddenFiles && file.isHidden) { - hideFile = true; - } - - return !hideFile; - }).toList(); - - return BackButtonListener( - onBackButtonPressed: () async { - final parent = uriSnapshot.requireData.parent; - if (parent != null) { - widget.bloc.setPath(parent); - return true; - } - return false; - }, - child: SortBoxBuilder( - sortBox: filesSortBox, - sortProperty: widget.bloc.options.filesSortPropertyOption, - sortBoxOrder: widget.bloc.options.filesSortBoxOrderOption, - presort: const { - (property: FilesSortProperty.isFolder, order: SortBoxOrder.ascending), - }, - input: files, - builder: (final context, final sorted) { - final uploadingTaskTiles = buildUploadTasks(tasksSnapshot.requireData, sorted); - - return NeonListView( - scrollKey: 'files-${uriSnapshot.requireData.path}', - itemCount: sorted.length, - itemBuilder: (final context, final index) { - final file = sorted[index]; - final matchingTask = tasksSnapshot.requireData.firstWhereOrNull( - (final task) => file.name == task.uri.name && widget.bloc.uri.value == task.uri.parent, - ); - - final details = matchingTask != null - ? FileDetails.fromTask( - task: matchingTask, - file: file, - ) - : FileDetails.fromWebDav( - file: file, - ); + return BackButtonListener( + onBackButtonPressed: () async { + final parent = uriSnapshot.requireData.parent; + if (parent != null) { + widget.bloc.setPath(parent); + return true; + } + return false; + }, + child: SortBoxBuilder( + sortBox: filesSortBox, + sortProperty: widget.bloc.options.filesSortPropertyOption, + sortBoxOrder: widget.bloc.options.filesSortBoxOrderOption, + presort: const { + (property: FilesSortProperty.isFolder, order: SortBoxOrder.ascending), + }, + input: filesSnapshot.data, + builder: (final context, final sorted) { + final uploadingTaskTiles = buildUploadTasks(tasksSnapshot.requireData, sorted); + + return NeonListView( + scrollKey: 'files-${uriSnapshot.requireData.path}', + itemCount: sorted.length, + itemBuilder: (final context, final index) { + final file = sorted[index]; + final matchingTask = tasksSnapshot.requireData.firstWhereOrNull( + (final task) => file.name == task.uri.name && widget.bloc.uri.value == task.uri.parent, + ); - return FileListTile( - bloc: widget.filesBloc, - browserBloc: widget.bloc, - details: details, - mode: widget.mode, - ); - }, - isLoading: filesSnapshot.isLoading, - error: filesSnapshot.error, - onRefresh: widget.bloc.refresh, - topScrollingChildren: [ - FilesBrowserNavigator( - uri: uriSnapshot.requireData, - bloc: widget.bloc, - ), - ...uploadingTaskTiles, - ], + final details = matchingTask != null + ? FileDetails.fromTask( + task: matchingTask, + file: file, + ) + : FileDetails.fromWebDav( + file: file, + ); + + return FileListTile( + bloc: widget.filesBloc, + browserBloc: widget.bloc, + details: details, ); }, - ), - ); - }, + isLoading: filesSnapshot.isLoading, + error: filesSnapshot.error, + onRefresh: widget.bloc.refresh, + topScrollingChildren: [ + FilesBrowserNavigator( + uri: uriSnapshot.requireData, + bloc: widget.bloc, + ), + ...uploadingTaskTiles, + ], + ); + }, + ), ); }, ), diff --git a/packages/neon/neon_files/lib/src/widgets/dialog.dart b/packages/neon/neon_files/lib/src/widgets/dialog.dart index 0565972b1d5..5abf11f7b15 100644 --- a/packages/neon/neon_files/lib/src/widgets/dialog.dart +++ b/packages/neon/neon_files/lib/src/widgets/dialog.dart @@ -212,7 +212,7 @@ class FilesChooseFolderDialog extends StatelessWidget { const FilesChooseFolderDialog({ required this.bloc, required this.filesBloc, - required this.originalPath, + this.originalPath, super.key, }); @@ -220,7 +220,7 @@ class FilesChooseFolderDialog extends StatelessWidget { final FilesBloc filesBloc; /// The initial path to start at. - final PathUri originalPath; + final PathUri? originalPath; @override Widget build(final BuildContext context) { @@ -244,7 +244,7 @@ class FilesChooseFolderDialog extends StatelessWidget { ), ), ElevatedButton( - onPressed: originalPath != uriSnapshot.data ? () => Navigator.of(context).pop(uriSnapshot.data) : null, + onPressed: () => Navigator.of(context).pop(uriSnapshot.data), child: Text( FilesLocalizations.of(context).folderChoose, textAlign: TextAlign.end, @@ -261,7 +261,6 @@ class FilesChooseFolderDialog extends StatelessWidget { child: FilesBrowserView( bloc: bloc, filesBloc: filesBloc, - mode: FilesBrowserMode.selectDirectory, ), ), ), diff --git a/packages/neon/neon_files/lib/src/widgets/file_list_tile.dart b/packages/neon/neon_files/lib/src/widgets/file_list_tile.dart index 67385db249f..f3ac47f06bb 100644 --- a/packages/neon/neon_files/lib/src/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/src/widgets/file_list_tile.dart @@ -7,7 +7,6 @@ import 'package:neon_files/src/models/file_details.dart'; import 'package:neon_files/src/utils/dialog.dart'; import 'package:neon_files/src/utils/task.dart'; import 'package:neon_files/src/widgets/actions.dart'; -import 'package:neon_files/src/widgets/browser_view.dart'; import 'package:neon_files/src/widgets/file_preview.dart'; import 'package:neon_framework/theme.dart'; import 'package:neon_framework/widgets.dart'; @@ -17,19 +16,17 @@ class FileListTile extends StatelessWidget { required this.bloc, required this.browserBloc, required this.details, - this.mode = FilesBrowserMode.browser, super.key, }); final FilesBloc bloc; final FilesBrowserBloc browserBloc; final FileDetails details; - final FilesBrowserMode mode; Future _onTap(final BuildContext context, final FileDetails details) async { if (details.isDirectory) { browserBloc.setPath(details.uri); - } else if (mode == FilesBrowserMode.browser) { + } else if (browserBloc.mode == FilesBrowserMode.browser) { final sizeWarning = bloc.options.downloadSizeWarning.value; if (sizeWarning != null && details.size != null && details.size! > sizeWarning) { final decision = await showDownloadConfirmationDialog(context, sizeWarning, details.size!); @@ -73,7 +70,7 @@ class FileListTile extends StatelessWidget { details: details, bloc: bloc, ), - trailing: !details.hasTask && mode == FilesBrowserMode.browser + trailing: !details.hasTask && browserBloc.mode == FilesBrowserMode.browser ? FileActions(details: details) : const SizedBox.square( dimension: largeIconSize,