From edc6f58df80b7e2e56c75e1e86d4d4ea0124d971 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 25 Aug 2023 17:36:03 +0200 Subject: [PATCH 1/3] refactor(neon_files): add FilesBrowserMode for cleaner apis Signed-off-by: Nikolas Rimikis --- .../neon_files/lib/dialogs/choose_folder.dart | 3 +- packages/neon/neon_files/lib/pages/main.dart | 15 --- .../neon_files/lib/widgets/browser_view.dart | 37 +++--- .../lib/widgets/file_list_tile.dart | 110 ++++++++++-------- 4 files changed, 82 insertions(+), 83 deletions(-) diff --git a/packages/neon/neon_files/lib/dialogs/choose_folder.dart b/packages/neon/neon_files/lib/dialogs/choose_folder.dart index 551f461b..16b37295 100644 --- a/packages/neon/neon_files/lib/dialogs/choose_folder.dart +++ b/packages/neon/neon_files/lib/dialogs/choose_folder.dart @@ -25,8 +25,7 @@ class FilesChooseFolderDialog extends StatelessWidget { child: FilesBrowserView( bloc: bloc, filesBloc: filesBloc, - enableFileActions: false, - onlyShowDirectories: true, + mode: FilesBrowserMode.selectDirectory, ), ), StreamBuilder>( diff --git a/packages/neon/neon_files/lib/pages/main.dart b/packages/neon/neon_files/lib/pages/main.dart index 5254b7ba..ecd5f2da 100644 --- a/packages/neon/neon_files/lib/pages/main.dart +++ b/packages/neon/neon_files/lib/pages/main.dart @@ -27,21 +27,6 @@ class _FilesMainPageState extends State { body: FilesBrowserView( bloc: bloc.browser, filesBloc: bloc, - onPickFile: (final details) async { - final sizeWarning = bloc.options.downloadSizeWarning.value; - if (sizeWarning != null && details.size != null && details.size! > sizeWarning) { - if (!(await showConfirmationDialog( - context, - AppLocalizations.of(context).downloadConfirmSizeWarning( - filesize(sizeWarning), - filesize(details.size), - ), - ))) { - return; - } - } - bloc.openFile(details.path, details.etag!, details.mimeType); - }, ), floatingActionButton: FloatingActionButton( onPressed: () async { diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 2d06b8aa..7f066028 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -1,21 +1,30 @@ part of '../neon_files.dart'; +/// Mode to operate the [FilesBrowserView] in. +enum FilesBrowserMode { + /// Default file browser mode. + /// + /// When a file is selecteed 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.onPickFile, - this.enableFileActions = true, - this.onlyShowDirectories = false, + this.mode = FilesBrowserMode.browser, super.key, - // ignore: prefer_asserts_with_message - }) : assert((onPickFile == null) == onlyShowDirectories); + }); final FilesBrowserBloc bloc; final FilesBloc filesBloc; - final Function(FileDetails)? onPickFile; - final bool enableFileActions; - final bool onlyShowDirectories; + final FilesBrowserMode mode; @override State createState() => _FilesBrowserViewState(); @@ -24,11 +33,11 @@ class FilesBrowserView extends StatefulWidget { class _FilesBrowserViewState extends State { @override void initState() { - super.initState(); - widget.bloc.errors.listen((final error) { NeonException.showSnackbar(context, error); }); + + super.initState(); } @override @@ -68,12 +77,11 @@ class _FilesBrowserViewState extends State { details: FileDetails.fromUploadTask( task: uploadTask, ), - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, + mode: widget.mode, ), ], for (final file in sorted) ...[ - if (!widget.onlyShowDirectories || file.isDirectory) ...[ + if (widget.mode != FilesBrowserMode.selectDirectory || file.isDirectory) ...[ Builder( builder: (final context) { final matchingTask = tasksSnapshot.requireData @@ -93,8 +101,7 @@ class _FilesBrowserViewState extends State { bloc: widget.filesBloc, browserBloc: widget.bloc, details: details, - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, + mode: widget.mode, ); }, ), diff --git a/packages/neon/neon_files/lib/widgets/file_list_tile.dart b/packages/neon/neon_files/lib/widgets/file_list_tile.dart index 3db2d1f8..fb5aaf85 100644 --- a/packages/neon/neon_files/lib/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -1,7 +1,9 @@ import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; +import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/neon_files.dart'; import 'package:neon_files/widgets/actions.dart'; @@ -10,66 +12,72 @@ class FileListTile extends StatelessWidget { required this.bloc, required this.browserBloc, required this.details, - this.enableFileActions = true, - this.onPickFile, + this.mode = FilesBrowserMode.browser, super.key, }); final FilesBloc bloc; final FilesBrowserBloc browserBloc; final FileDetails details; - final bool enableFileActions; - final Function(FileDetails)? onPickFile; + final FilesBrowserMode mode; - @override - Widget build(final BuildContext context) { - // When the ETag is null it means we are uploading this file right now - final onTap = details.isDirectory || details.etag != null - ? () { - if (details.isDirectory) { - browserBloc.setPath(details.path); - } else { - onPickFile?.call(details); - } - } - : null; + Future _onTap(final BuildContext context, final FileDetails details) async { + if (details.isDirectory) { + browserBloc.setPath(details.path); + } else if (mode == FilesBrowserMode.browser) { + final sizeWarning = bloc.options.downloadSizeWarning.value; + if (sizeWarning != null && details.size != null && details.size! > sizeWarning) { + if (!(await showConfirmationDialog( + context, + AppLocalizations.of(context).downloadConfirmSizeWarning( + filesize(sizeWarning), + filesize(details.size), + ), + ))) { + return; + } + } + bloc.openFile(details.path, details.etag!, details.mimeType); + } + } - return ListTile( - onTap: onTap, - title: Text( - details.name, - overflow: TextOverflow.ellipsis, - ), - subtitle: Row( - children: [ - if (details.lastModified != null) - RelativeTime( - date: details.lastModified!, - ), - if (details.size != null && details.size! > 0) ...[ - const SizedBox( - width: 10, - ), - Text( - filesize(details.size, 1), - style: DefaultTextStyle.of(context).style.copyWith( - color: Colors.grey, - ), - ), + @override + Widget build(final BuildContext context) => ListTile( + // When the ETag is null it means we are uploading this file right now + onTap: details.isDirectory || details.etag != null ? () async => _onTap(context, details) : null, + title: Text( + details.name, + overflow: TextOverflow.ellipsis, + ), + subtitle: Row( + children: [ + if (details.lastModified != null) + RelativeTime( + date: details.lastModified!, + ), + if (details.size != null && details.size! > 0) ...[ + const SizedBox( + width: 10, + ), + Text( + filesize(details.size, 1), + style: DefaultTextStyle.of(context).style.copyWith( + color: Colors.grey, + ), + ), + ], ], - ], - ), - leading: _FileIcon( - details: details, - bloc: bloc, - ), - trailing: !details.hasTask && enableFileActions - ? FileActions(details: details) - : const SizedBox.square( - dimension: 48, - ), - ); - } + ), + leading: _FileIcon( + details: details, + bloc: bloc, + ), + trailing: !details.hasTask && mode != FilesBrowserMode.noActions + ? FileActions(details: details) + : const SizedBox.square( + dimension: 48, + ), + ); } class _FileIcon extends StatelessWidget { From ce7955dccdad2490e768767e2ab06cb22be2e2c1 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 25 Aug 2023 17:39:16 +0200 Subject: [PATCH 2/3] feat(neon_files): sort folders first in FilesBrowserView Signed-off-by: Nikolas Rimikis --- packages/neon/neon_files/lib/sort/files.dart | 1 + packages/neon/neon_files/lib/widgets/browser_view.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/neon/neon_files/lib/sort/files.dart b/packages/neon/neon_files/lib/sort/files.dart index d0d7d96e..b46bc14a 100644 --- a/packages/neon/neon_files/lib/sort/files.dart +++ b/packages/neon/neon_files/lib/sort/files.dart @@ -5,6 +5,7 @@ final filesSortBox = SortBox( FilesSortProperty.name: (final file) => file.name.toLowerCase(), FilesSortProperty.modifiedDate: (final file) => file.lastModified?.millisecondsSinceEpoch ?? 0, FilesSortProperty.size: (final file) => file.size ?? 0, + FilesSortProperty.isFolder: (final file) => file.isDirectory ? 0 : 1, }, { FilesSortProperty.modifiedDate: { diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 7f066028..e05298a7 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -62,6 +62,9 @@ class _FilesBrowserViewState extends State { sortBox: filesSortBox, sortPropertyOption: widget.bloc.options.filesSortPropertyOption, sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption, + presort: const { + (FilesSortProperty.isFolder, SortBoxOrder.ascending), + }, input: files.data, builder: (final context, final sorted) => NeonListView( scrollKey: 'files-${pathSnapshot.requireData.join('/')}', From c4d38589a74322f8ae7ee171a6b7329742cf28c9 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 25 Aug 2023 17:46:51 +0200 Subject: [PATCH 3/3] feat(neon_files): enable hiding files starting with a '.' Signed-off-by: Nikolas Rimikis --- packages/neon/neon_files/lib/l10n/en.arb | 1 + .../neon_files/lib/l10n/localizations.dart | 6 + .../neon_files/lib/l10n/localizations_en.dart | 3 + packages/neon/neon_files/lib/options.dart | 10 + .../neon_files/lib/widgets/browser_view.dart | 196 +++++++++--------- packages/nextcloud/lib/src/webdav/file.dart | 11 +- packages/nextcloud/pubspec.yaml | 1 + 7 files changed, 127 insertions(+), 101 deletions(-) diff --git a/packages/neon/neon_files/lib/l10n/en.arb b/packages/neon/neon_files/lib/l10n/en.arb index e53da81a..07661702 100644 --- a/packages/neon/neon_files/lib/l10n/en.arb +++ b/packages/neon/neon_files/lib/l10n/en.arb @@ -77,6 +77,7 @@ "optionsFilesSortPropertyModifiedDate": "Last modified", "optionsFilesSortPropertySize": "Size", "optionsFilesSortOrder": "Sort order of files", + "optionsShowHiddenFiles": "Show hidden files", "optionsShowPreviews": "Show previews for files", "optionsUploadQueueParallelism": "Upload queue parallelism", "optionsDownloadQueueParallelism": "Download queue parallelism", diff --git a/packages/neon/neon_files/lib/l10n/localizations.dart b/packages/neon/neon_files/lib/l10n/localizations.dart index afd2ae2a..f0372a5e 100644 --- a/packages/neon/neon_files/lib/l10n/localizations.dart +++ b/packages/neon/neon_files/lib/l10n/localizations.dart @@ -305,6 +305,12 @@ abstract class AppLocalizations { /// **'Sort order of files'** String get optionsFilesSortOrder; + /// No description provided for @optionsShowHiddenFiles. + /// + /// In en, this message translates to: + /// **'Show hidden files'** + String get optionsShowHiddenFiles; + /// No description provided for @optionsShowPreviews. /// /// In en, this message translates to: diff --git a/packages/neon/neon_files/lib/l10n/localizations_en.dart b/packages/neon/neon_files/lib/l10n/localizations_en.dart index a0a8d7f3..622b0ae2 100644 --- a/packages/neon/neon_files/lib/l10n/localizations_en.dart +++ b/packages/neon/neon_files/lib/l10n/localizations_en.dart @@ -122,6 +122,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get optionsFilesSortOrder => 'Sort order of files'; + @override + String get optionsShowHiddenFiles => 'Show hidden files'; + @override String get optionsShowPreviews => 'Show previews for files'; diff --git a/packages/neon/neon_files/lib/options.dart b/packages/neon/neon_files/lib/options.dart index ebbe7803..29e264ea 100644 --- a/packages/neon/neon_files/lib/options.dart +++ b/packages/neon/neon_files/lib/options.dart @@ -8,6 +8,7 @@ class FilesAppSpecificOptions extends NextcloudAppOptions { super.options = [ filesSortPropertyOption, filesSortBoxOrderOption, + showHiddenFilesOption, showPreviewsOption, uploadQueueParallelism, downloadQueueParallelism, @@ -43,6 +44,14 @@ class FilesAppSpecificOptions extends NextcloudAppOptions { values: sortBoxOrderOptionValues, ); + late final showHiddenFilesOption = ToggleOption( + storage: super.storage, + category: generalCategory, + key: 'show-hidden-files', + label: (final context) => AppLocalizations.of(context).optionsShowHiddenFiles, + defaultValue: false, + ); + late final showPreviewsOption = ToggleOption( storage: super.storage, category: generalCategory, @@ -117,4 +126,5 @@ enum FilesSortProperty { name, modifiedDate, size, + isFolder, } diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index e05298a7..c0daa200 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -66,112 +66,116 @@ class _FilesBrowserViewState extends State { (FilesSortProperty.isFolder, SortBoxOrder.ascending), }, input: files.data, - builder: (final context, final sorted) => NeonListView( - scrollKey: 'files-${pathSnapshot.requireData.join('/')}', - withFloatingActionButton: true, - items: [ - for (final uploadTask in tasksSnapshot.requireData.whereType().where( - (final task) => - sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, - )) ...[ - FileListTile( - bloc: widget.filesBloc, - browserBloc: widget.bloc, - details: FileDetails.fromUploadTask( - task: uploadTask, + builder: (final context, final sorted) => ValueListenableBuilder( + valueListenable: widget.bloc.options.showHiddenFilesOption, + builder: (final context, final showHiddenFiles, final _) => NeonListView( + scrollKey: 'files-${pathSnapshot.requireData.join('/')}', + withFloatingActionButton: true, + items: [ + for (final uploadTask in tasksSnapshot.requireData.whereType().where( + (final task) => + sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, + )) ...[ + FileListTile( + bloc: widget.filesBloc, + browserBloc: widget.bloc, + details: FileDetails.fromUploadTask( + task: uploadTask, + ), + mode: widget.mode, ), - mode: widget.mode, - ), - ], - for (final file in sorted) ...[ - if (widget.mode != FilesBrowserMode.selectDirectory || file.isDirectory) ...[ - Builder( - builder: (final context) { - final matchingTask = tasksSnapshot.requireData - .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); + ], + for (final file in sorted) ...[ + if ((widget.mode != FilesBrowserMode.selectDirectory || file.isDirectory) && + (!file.isHidden || showHiddenFiles)) ...[ + Builder( + builder: (final context) { + final matchingTask = tasksSnapshot.requireData + .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); - final details = matchingTask != null - ? FileDetails.fromTask( - task: matchingTask, - file: file, - ) - : FileDetails.fromWebDav( - file: file, - path: widget.bloc.path.value, - ); + final details = matchingTask != null + ? FileDetails.fromTask( + task: matchingTask, + file: file, + ) + : FileDetails.fromWebDav( + file: file, + path: widget.bloc.path.value, + ); - return FileListTile( - bloc: widget.filesBloc, - browserBloc: widget.bloc, - details: details, - mode: widget.mode, - ); - }, - ), + return FileListTile( + bloc: widget.filesBloc, + browserBloc: widget.bloc, + details: details, + mode: widget.mode, + ); + }, + ), + ], ], ], - ], - isLoading: files.isLoading, - error: files.error, - onRefresh: widget.bloc.refresh, - builder: (final context, final widget) => widget, - topScrollingChildren: [ - Align( - alignment: Alignment.topLeft, - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 10, - ), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - IconButton( - padding: EdgeInsets.zero, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, - ), - tooltip: AppLocalizations.of(context).goToPath(''), - icon: const Icon( - Icons.house, - size: 30, - ), - onPressed: () { - widget.bloc.setPath([]); - }, - ), - for (var i = 0; i < pathSnapshot.requireData.length; i++) ...[ - Builder( - builder: (final context) { - final path = pathSnapshot.requireData.sublist(0, i + 1); - return Tooltip( - message: AppLocalizations.of(context).goToPath(path.join('/')), - excludeFromSemantics: true, - child: TextButton( - onPressed: () { - widget.bloc.setPath(path); - }, - child: Text( - pathSnapshot.requireData[i], - semanticsLabel: AppLocalizations.of(context).goToPath(path.join('/')), - ), - ), - ); + isLoading: files.isLoading, + error: files.error, + onRefresh: widget.bloc.refresh, + builder: (final context, final widget) => widget, + topScrollingChildren: [ + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + IconButton( + padding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + tooltip: AppLocalizations.of(context).goToPath(''), + icon: const Icon( + Icons.house, + size: 30, + ), + onPressed: () { + widget.bloc.setPath([]); }, ), - ], - ] - .intersperse( - const Icon( - Icons.keyboard_arrow_right, - size: 30, + for (var i = 0; i < pathSnapshot.requireData.length; i++) ...[ + Builder( + builder: (final context) { + final path = pathSnapshot.requireData.sublist(0, i + 1); + return Tooltip( + message: AppLocalizations.of(context).goToPath(path.join('/')), + excludeFromSemantics: true, + child: TextButton( + onPressed: () { + widget.bloc.setPath(path); + }, + child: Text( + pathSnapshot.requireData[i], + semanticsLabel: AppLocalizations.of(context).goToPath(path.join('/')), + ), + ), + ); + }, ), - ) - .toList(), + ], + ] + .intersperse( + const Icon( + Icons.keyboard_arrow_right, + size: 30, + ), + ) + .toList(), + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/packages/nextcloud/lib/src/webdav/file.dart b/packages/nextcloud/lib/src/webdav/file.dart index a9cff423..a796270c 100644 --- a/packages/nextcloud/lib/src/webdav/file.dart +++ b/packages/nextcloud/lib/src/webdav/file.dart @@ -83,12 +83,13 @@ class WebDavFile { // normalised path (remove trailing slash) final end = path.endsWith('/') ? path.length - 1 : path.length; final segments = Uri.parse(path, 0, end).pathSegments; - if (segments.isNotEmpty) { - return segments.last; - } - return ''; + + return segments.lastOrNull ?? ''; }(); - /// Returns if the file is a directory + /// Whether the file is hidden. + late final bool isHidden = name.startsWith('.'); + + /// Whether the file is a directory late final bool isDirectory = (isCollection ?? false) || path.endsWith('/'); } diff --git a/packages/nextcloud/pubspec.yaml b/packages/nextcloud/pubspec.yaml index e5267070..fb3d93e9 100644 --- a/packages/nextcloud/pubspec.yaml +++ b/packages/nextcloud/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: built_collection: ^5.1.1 built_value: ^8.6.2 + collection: ^1.17.2 crypto: ^3.0.3 crypton: ^2.2.0 dynamite_runtime: