From e4be6c931f4049cd734ff4f234bd7f6e3b46e7c2 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 7 Jul 2023 10:21:26 +0200 Subject: [PATCH 1/5] refactor(neon_files): externalise files widgets Signed-off-by: Nikolas Rimikis --- .../app/integration_test/screenshot_test.dart | 2 +- packages/neon/neon_files/lib/neon_files.dart | 2 +- .../neon/neon_files/lib/widgets/actions.dart | 163 ++++++++++++ .../neon_files/lib/widgets/browser_view.dart | 244 +----------------- .../lib/widgets/file_list_tile.dart | 155 +++++++++++ 5 files changed, 329 insertions(+), 237 deletions(-) create mode 100644 packages/neon/neon_files/lib/widgets/actions.dart create mode 100644 packages/neon/neon_files/lib/widgets/file_list_tile.dart diff --git a/packages/app/integration_test/screenshot_test.dart b/packages/app/integration_test/screenshot_test.dart index f108b1c8..14bec1f1 100644 --- a/packages/app/integration_test/screenshot_test.dart +++ b/packages/app/integration_test/screenshot_test.dart @@ -9,7 +9,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:neon/models.dart'; import 'package:neon/neon.dart'; -import 'package:neon_files/neon_files.dart'; +import 'package:neon_files/widgets/actions.dart'; import 'package:shared_preferences/shared_preferences.dart'; class MemorySharedPreferences implements SharedPreferences { diff --git a/packages/neon/neon_files/lib/neon_files.dart b/packages/neon/neon_files/lib/neon_files.dart index dbd95530..af0f57bd 100644 --- a/packages/neon/neon_files/lib/neon_files.dart +++ b/packages/neon/neon_files/lib/neon_files.dart @@ -19,11 +19,11 @@ import 'package:neon/nextcloud.dart'; import 'package:neon/platform.dart'; import 'package:neon/settings.dart'; import 'package:neon/sort_box.dart'; -import 'package:neon/theme.dart'; import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/routes.dart'; +import 'package:neon_files/widgets/file_list_tile.dart'; import 'package:open_file/open_file.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; diff --git a/packages/neon/neon_files/lib/widgets/actions.dart b/packages/neon/neon_files/lib/widgets/actions.dart new file mode 100644 index 00000000..59896af1 --- /dev/null +++ b/packages/neon/neon_files/lib/widgets/actions.dart @@ -0,0 +1,163 @@ +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:neon/utils.dart'; +import 'package:neon_files/l10n/localizations.dart'; +import 'package:neon_files/neon_files.dart'; +import 'package:provider/provider.dart'; + +class FileActions extends StatelessWidget { + const FileActions({ + required this.details, + super.key, + }); + + final FileDetails details; + + Future onSelected(final BuildContext context, final FilesFileAction action) async { + final bloc = Provider.of(context, listen: false); + final browserBloc = bloc.browser; + switch (action) { + case FilesFileAction.toggleFavorite: + if (details.isFavorite ?? false) { + bloc.removeFavorite(details.path); + } else { + bloc.addFavorite(details.path); + } + break; + case FilesFileAction.details: + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => FilesDetailsPage( + bloc: bloc, + details: details, + ), + ), + ); + break; + case FilesFileAction.rename: + final result = await showRenameDialog( + context: context, + title: + details.isDirectory ? AppLocalizations.of(context).folderRename : AppLocalizations.of(context).fileRename, + value: details.name, + ); + if (result != null) { + bloc.rename(details.path, result); + } + break; + case FilesFileAction.move: + final b = bloc.getNewFilesBrowserBloc(); + final originalPath = details.path.sublist(0, details.path.length - 1); + b.setPath(originalPath); + final result = await showDialog?>( + context: context, + builder: (final context) => FilesChooseFolderDialog( + bloc: b, + filesBloc: bloc, + originalPath: originalPath, + ), + ); + b.dispose(); + if (result != null) { + bloc.move(details.path, result..add(details.name)); + } + break; + case FilesFileAction.copy: + final b = bloc.getNewFilesBrowserBloc(); + final originalPath = details.path.sublist(0, details.path.length - 1); + b.setPath(originalPath); + final result = await showDialog?>( + context: context, + builder: (final context) => FilesChooseFolderDialog( + bloc: b, + filesBloc: bloc, + originalPath: originalPath, + ), + ); + b.dispose(); + if (result != null) { + bloc.copy(details.path, result..add(details.name)); + } + break; + case FilesFileAction.sync: + final sizeWarning = browserBloc.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.syncFile(details.path); + break; + case FilesFileAction.delete: + if (await showConfirmationDialog( + context, + details.isDirectory + ? AppLocalizations.of(context).folderDeleteConfirm(details.name) + : AppLocalizations.of(context).fileDeleteConfirm(details.name), + )) { + bloc.delete(details.path); + } + break; + } + } + + @override + Widget build(final BuildContext context) => PopupMenuButton( + itemBuilder: (final context) => [ + if (details.isFavorite != null) ...[ + PopupMenuItem( + value: FilesFileAction.toggleFavorite, + child: Text( + details.isFavorite! + ? AppLocalizations.of(context).removeFromFavorites + : AppLocalizations.of(context).addToFavorites, + ), + ), + ], + PopupMenuItem( + value: FilesFileAction.details, + child: Text(AppLocalizations.of(context).details), + ), + PopupMenuItem( + value: FilesFileAction.rename, + child: Text(AppLocalizations.of(context).actionRename), + ), + PopupMenuItem( + value: FilesFileAction.move, + child: Text(AppLocalizations.of(context).actionMove), + ), + PopupMenuItem( + value: FilesFileAction.copy, + child: Text(AppLocalizations.of(context).actionCopy), + ), + // TODO: https://github.com/provokateurin/nextcloud-neon/issues/4 + if (!details.isDirectory) ...[ + PopupMenuItem( + value: FilesFileAction.sync, + child: Text(AppLocalizations.of(context).actionSync), + ), + ], + PopupMenuItem( + value: FilesFileAction.delete, + child: Text(AppLocalizations.of(context).actionDelete), + ), + ], + onSelected: (final action) async => onSelected(context, action), + ); +} + +enum FilesFileAction { + toggleFavorite, + details, + rename, + move, + copy, + sync, + delete, +} diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 543529f9..4540d8de 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -73,7 +73,7 @@ class _FilesBrowserViewState extends State { builder: (final context, final uploadTaskProgressSnapshot) => !uploadTaskProgressSnapshot.hasData ? const SizedBox() - : _buildFile( + : FileListTile( context: context, details: FileDetails( path: uploadTask.path, @@ -87,6 +87,8 @@ class _FilesBrowserViewState extends State { ), uploadProgress: uploadTaskProgressSnapshot.data, downloadProgress: null, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, ), ), ], @@ -101,15 +103,14 @@ class _FilesBrowserViewState extends State { .where((final task) => _pathMatchesFile(task.path, file.name)); return StreamBuilder( - stream: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.progress - : Stream.value(null), + stream: + matchingUploadTasks.isNotEmpty ? matchingUploadTasks.first.progress : null, builder: (final context, final uploadTaskProgressSnapshot) => StreamBuilder( stream: matchingDownloadTasks.isNotEmpty ? matchingDownloadTasks.first.progress - : Stream.value(null), - builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( + : null, + builder: (final context, final downloadTaskProgressSnapshot) => FileListTile( context: context, details: FileDetails( path: [...widget.bloc.path.value, file.name], @@ -127,6 +128,8 @@ class _FilesBrowserViewState extends State { ), uploadProgress: uploadTaskProgressSnapshot.data, downloadProgress: downloadTaskProgressSnapshot.data, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, ), ), ); @@ -209,233 +212,4 @@ class _FilesBrowserViewState extends State { [...widget.bloc.path.value, name], path, ); - - Widget _buildFile({ - required final BuildContext context, - required final FileDetails details, - required final int? uploadProgress, - required final int? downloadProgress, - }) => - ListTile( - // When the ETag is null it means we are uploading this file right now - onTap: details.isDirectory || details.etag != null - ? () async { - if (details.isDirectory) { - widget.bloc.setPath(details.path); - } else { - if (widget.onPickFile != null) { - widget.onPickFile!.call(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: SizedBox.square( - dimension: 40, - child: Stack( - children: [ - Center( - child: uploadProgress != null || downloadProgress != null - ? Column( - children: [ - Icon( - uploadProgress != null ? MdiIcons.upload : MdiIcons.download, - color: Theme.of(context).colorScheme.primary, - ), - LinearProgressIndicator( - value: (uploadProgress ?? downloadProgress)! / 100, - ), - ], - ) - : FilePreview( - bloc: widget.filesBloc, - details: details, - withBackground: true, - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - ), - if (details.isFavorite ?? false) ...[ - const Align( - alignment: Alignment.bottomRight, - child: Icon( - Icons.star, - size: 14, - color: NcColors.starredColor, - ), - ), - ], - ], - ), - ), - trailing: uploadProgress == null && downloadProgress == null && widget.enableFileActions - ? PopupMenuButton( - itemBuilder: (final context) => [ - if (details.isFavorite != null) ...[ - PopupMenuItem( - value: FilesFileAction.toggleFavorite, - child: Text( - details.isFavorite! - ? AppLocalizations.of(context).removeFromFavorites - : AppLocalizations.of(context).addToFavorites, - ), - ), - ], - PopupMenuItem( - value: FilesFileAction.details, - child: Text(AppLocalizations.of(context).details), - ), - PopupMenuItem( - value: FilesFileAction.rename, - child: Text(AppLocalizations.of(context).actionRename), - ), - PopupMenuItem( - value: FilesFileAction.move, - child: Text(AppLocalizations.of(context).actionMove), - ), - PopupMenuItem( - value: FilesFileAction.copy, - child: Text(AppLocalizations.of(context).actionCopy), - ), - // TODO: https://github.com/nextcloud/neon/issues/4 - if (!details.isDirectory) ...[ - PopupMenuItem( - value: FilesFileAction.sync, - child: Text(AppLocalizations.of(context).actionSync), - ), - ], - PopupMenuItem( - value: FilesFileAction.delete, - child: Text(AppLocalizations.of(context).actionDelete), - ), - ], - onSelected: (final action) async { - switch (action) { - case FilesFileAction.toggleFavorite: - if (details.isFavorite ?? false) { - widget.filesBloc.removeFavorite(details.path); - } else { - widget.filesBloc.addFavorite(details.path); - } - break; - case FilesFileAction.details: - await Navigator.of(context).push( - MaterialPageRoute( - builder: (final context) => FilesDetailsPage( - bloc: widget.filesBloc, - details: details, - ), - ), - ); - break; - case FilesFileAction.rename: - final result = await showRenameDialog( - context: context, - title: details.isDirectory - ? AppLocalizations.of(context).folderRename - : AppLocalizations.of(context).fileRename, - value: details.name, - ); - if (result != null) { - widget.filesBloc.rename(details.path, result); - } - break; - case FilesFileAction.move: - final b = widget.filesBloc.getNewFilesBrowserBloc(); - final originalPath = details.path.sublist(0, details.path.length - 1); - b.setPath(originalPath); - final result = await showDialog?>( - context: context, - builder: (final context) => FilesChooseFolderDialog( - bloc: b, - filesBloc: widget.filesBloc, - originalPath: originalPath, - ), - ); - b.dispose(); - if (result != null) { - widget.filesBloc.move(details.path, result..add(details.name)); - } - break; - case FilesFileAction.copy: - final b = widget.filesBloc.getNewFilesBrowserBloc(); - final originalPath = details.path.sublist(0, details.path.length - 1); - b.setPath(originalPath); - final result = await showDialog?>( - context: context, - builder: (final context) => FilesChooseFolderDialog( - bloc: b, - filesBloc: widget.filesBloc, - originalPath: originalPath, - ), - ); - b.dispose(); - if (result != null) { - widget.filesBloc.copy(details.path, result..add(details.name)); - } - break; - case FilesFileAction.sync: - final sizeWarning = widget.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; - } - } - widget.filesBloc.syncFile(details.path); - break; - case FilesFileAction.delete: - if (await showConfirmationDialog( - context, - details.isDirectory - ? AppLocalizations.of(context).folderDeleteConfirm(details.name) - : AppLocalizations.of(context).fileDeleteConfirm(details.name), - )) { - widget.filesBloc.delete(details.path); - } - break; - } - }, - ) - : const SizedBox.square( - dimension: 48, - ), - ); -} - -enum FilesFileAction { - toggleFavorite, - details, - rename, - move, - copy, - sync, - delete, } diff --git a/packages/neon/neon_files/lib/widgets/file_list_tile.dart b/packages/neon/neon_files/lib/widgets/file_list_tile.dart new file mode 100644 index 00000000..400dec28 --- /dev/null +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -0,0 +1,155 @@ +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_files/neon_files.dart'; +import 'package:neon_files/widgets/actions.dart'; +import 'package:provider/provider.dart'; + +class FileListTile extends StatelessWidget { + const FileListTile({ + required this.context, + required this.details, + required this.uploadProgress, + required this.downloadProgress, + this.enableFileActions = true, + this.onPickFile, + super.key, + }); + + final BuildContext context; + final FileDetails details; + final int? uploadProgress; + final int? downloadProgress; + final bool enableFileActions; + final Function(FileDetails)? onPickFile; + + bool get _isUploading => uploadProgress != null; + + bool get _hasProgress => uploadProgress != null || downloadProgress != null; + + double? get _progress { + if (!_hasProgress) { + return null; + } + + return (uploadProgress ?? downloadProgress)! / 100; + } + + @override + Widget build(final BuildContext context) { + final bloc = Provider.of(context); + final browserBloc = bloc.browser; + + // 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; + + 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, + ), + ), + ], + ], + ), + leading: _FileIcon( + hasProgress: _hasProgress, + isUploading: _isUploading, + progress: _progress, + details: details, + ), + trailing: _hasProgress && enableFileActions + ? FileActions(details: details) + : const SizedBox.square( + dimension: 48, + ), + ); + } +} + +class _FileIcon extends StatelessWidget { + const _FileIcon({ + required this.details, + required this.hasProgress, + required this.isUploading, + this.progress, + }); + + final bool hasProgress; + final bool isUploading; + final double? progress; + final FileDetails details; + + @override + Widget build(final BuildContext context) { + final bloc = Provider.of(context); + + Widget icon = Center( + child: hasProgress + ? Column( + children: [ + Icon( + isUploading ? MdiIcons.upload : MdiIcons.download, + color: Theme.of(context).colorScheme.primary, + ), + LinearProgressIndicator( + value: progress, + ), + ], + ) + : FilePreview( + bloc: bloc, + details: details, + withBackground: true, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ); + + if (details.isFavorite ?? false) { + icon = Stack( + children: [ + icon, + const Align( + alignment: Alignment.bottomRight, + child: Icon( + Icons.star, + size: 14, + color: Colors.yellow, + ), + ) + ], + ); + } + + return SizedBox.square( + dimension: 40, + child: icon, + ); + } +} From 4bff5547a15749b745f1c438b7ea0002d9faff5a Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 7 Jul 2023 10:33:31 +0200 Subject: [PATCH 2/5] feat(neon_files): add FileDetails constructors Signed-off-by: Nikolas Rimikis --- .../neon_files/lib/models/file_details.dart | 23 ++++++++ .../neon_files/lib/widgets/browser_view.dart | 52 ++++++++----------- .../lib/widgets/file_list_tile.dart | 2 +- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/neon/neon_files/lib/models/file_details.dart b/packages/neon/neon_files/lib/models/file_details.dart index dec33ff5..19e8620b 100644 --- a/packages/neon/neon_files/lib/models/file_details.dart +++ b/packages/neon/neon_files/lib/models/file_details.dart @@ -13,6 +13,29 @@ class FileDetails { required this.isFavorite, }); + FileDetails.fromWebDav({ + required final WebDavFile file, + required final List path, + }) : path = List.from(path)..add(file.name), + isDirectory = file.isDirectory, + size = file.size, + etag = file.etag, + mimeType = file.mimeType, + lastModified = file.lastModified, + hasPreview = file.hasPreview, + isFavorite = file.favorite; + + FileDetails.fromUploadTask({ + required final UploadTask task, + }) : path = task.path, + size = task.size, + lastModified = task.lastModified, + isDirectory = false, + etag = null, + mimeType = null, + hasPreview = null, + isFavorite = null; + String get name => path.last; final List path; diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 4540d8de..58ccf39d 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -75,15 +75,8 @@ class _FilesBrowserViewState extends State { ? const SizedBox() : FileListTile( context: context, - details: FileDetails( - path: uploadTask.path, - isDirectory: false, - size: uploadTask.size, - etag: null, - mimeType: null, - lastModified: uploadTask.lastModified, - hasPreview: null, - isFavorite: null, + details: FileDetails.fromUploadTask( + task: uploadTask, ), uploadProgress: uploadTaskProgressSnapshot.data, downloadProgress: null, @@ -110,27 +103,26 @@ class _FilesBrowserViewState extends State { stream: matchingDownloadTasks.isNotEmpty ? matchingDownloadTasks.first.progress : null, - builder: (final context, final downloadTaskProgressSnapshot) => FileListTile( - context: context, - details: FileDetails( - path: [...widget.bloc.path.value, file.name], - isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, - size: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.size - : file.size, - etag: matchingUploadTasks.isNotEmpty ? null : file.etag, - mimeType: matchingUploadTasks.isNotEmpty ? null : file.mimeType, - lastModified: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.lastModified - : file.lastModified, - hasPreview: matchingUploadTasks.isNotEmpty ? null : file.hasPreview, - isFavorite: matchingUploadTasks.isNotEmpty ? null : file.favorite, - ), - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: downloadTaskProgressSnapshot.data, - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, - ), + builder: (final context, final downloadTaskProgressSnapshot) { + final path = widget.bloc.path.value; + final details = matchingUploadTasks.isEmpty + ? FileDetails.fromWebDav( + file: file, + path: path, + ) + : FileDetails.fromUploadTask( + task: matchingUploadTasks.first, + ); + + return FileListTile( + context: context, + details: details, + uploadProgress: uploadTaskProgressSnapshot.data, + downloadProgress: downloadTaskProgressSnapshot.data, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, + ); + }, ), ); }, 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 400dec28..b46e8900 100644 --- a/packages/neon/neon_files/lib/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -84,7 +84,7 @@ class FileListTile extends StatelessWidget { progress: _progress, details: details, ), - trailing: _hasProgress && enableFileActions + trailing: !_hasProgress && enableFileActions ? FileActions(details: details) : const SizedBox.square( dimension: 48, From 3cb05179afaa91c2ecd89df3a32c39b2682da6e5 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 7 Jul 2023 10:48:15 +0200 Subject: [PATCH 3/5] feat(neon_files): increase precision of upload/download tasks Signed-off-by: Nikolas Rimikis --- packages/neon/neon_files/lib/utils/download_task.dart | 6 ++++-- packages/neon/neon_files/lib/utils/upload_task.dart | 6 ++++-- packages/neon/neon_files/lib/widgets/browser_view.dart | 6 +++--- packages/neon/neon_files/lib/widgets/file_list_tile.dart | 6 +++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/neon/neon_files/lib/utils/download_task.dart b/packages/neon/neon_files/lib/utils/download_task.dart index 424fc04a..dced89b5 100644 --- a/packages/neon/neon_files/lib/utils/download_task.dart +++ b/packages/neon/neon_files/lib/utils/download_task.dart @@ -7,7 +7,9 @@ class DownloadTask { final List path; - final _streamController = StreamController(); + final _streamController = StreamController(); + + /// Upload progress in percent [0, 1]. late final progress = _streamController.stream.asBroadcastStream(); Future execute(final NextcloudClient client, final IOSink sink) async { @@ -20,7 +22,7 @@ class DownloadTask { sink.add(chunk); downloaded += chunk.length; - _streamController.add((downloaded / response.contentLength * 100).toInt()); + _streamController.add(downloaded / response.contentLength); if (downloaded >= response.contentLength) { completer.complete(); diff --git a/packages/neon/neon_files/lib/utils/upload_task.dart b/packages/neon/neon_files/lib/utils/upload_task.dart index 905f9caf..5559abc9 100644 --- a/packages/neon/neon_files/lib/utils/upload_task.dart +++ b/packages/neon/neon_files/lib/utils/upload_task.dart @@ -11,7 +11,9 @@ class UploadTask { final int size; final DateTime lastModified; - final _streamController = StreamController(); + final _streamController = StreamController(); + + /// Upload progress in percent [0, 1]. late final progress = _streamController.stream.asBroadcastStream(); Future execute(final NextcloudClient client, final Stream> stream) async { @@ -19,7 +21,7 @@ class UploadTask { await client.webdav.putStream( stream.map((final chunk) { uploaded += chunk.length; - _streamController.add((uploaded / size * 100).toInt()); + _streamController.add(uploaded / size); return Uint8List.fromList(chunk); }), diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 58ccf39d..56973863 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -68,7 +68,7 @@ class _FilesBrowserViewState extends State { (final task) => sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, )) ...[ - StreamBuilder( + StreamBuilder( stream: uploadTask.progress, builder: (final context, final uploadTaskProgressSnapshot) => !uploadTaskProgressSnapshot.hasData @@ -95,11 +95,11 @@ class _FilesBrowserViewState extends State { final matchingDownloadTasks = downloadTasksSnapshot.requireData .where((final task) => _pathMatchesFile(task.path, file.name)); - return StreamBuilder( + return StreamBuilder( stream: matchingUploadTasks.isNotEmpty ? matchingUploadTasks.first.progress : null, builder: (final context, final uploadTaskProgressSnapshot) => - StreamBuilder( + StreamBuilder( stream: matchingDownloadTasks.isNotEmpty ? matchingDownloadTasks.first.progress : null, 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 b46e8900..c29dea8e 100644 --- a/packages/neon/neon_files/lib/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -19,8 +19,8 @@ class FileListTile extends StatelessWidget { final BuildContext context; final FileDetails details; - final int? uploadProgress; - final int? downloadProgress; + final double? uploadProgress; + final double? downloadProgress; final bool enableFileActions; final Function(FileDetails)? onPickFile; @@ -33,7 +33,7 @@ class FileListTile extends StatelessWidget { return null; } - return (uploadProgress ?? downloadProgress)! / 100; + return (uploadProgress ?? downloadProgress)!; } @override From 4bd5a029beed99231fe7fa355d54eb2df848b642 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 7 Jul 2023 11:06:01 +0200 Subject: [PATCH 4/5] refactor(neon_files): move task progress into FileDetails Signed-off-by: Nikolas Rimikis --- .../neon_files/lib/models/file_details.dart | 33 +++++++- .../neon_files/lib/widgets/browser_view.dart | 75 ++++++++----------- .../lib/widgets/file_list_tile.dart | 52 ++++--------- 3 files changed, 76 insertions(+), 84 deletions(-) diff --git a/packages/neon/neon_files/lib/models/file_details.dart b/packages/neon/neon_files/lib/models/file_details.dart index 19e8620b..221cfa29 100644 --- a/packages/neon/neon_files/lib/models/file_details.dart +++ b/packages/neon/neon_files/lib/models/file_details.dart @@ -11,7 +11,9 @@ class FileDetails { required this.lastModified, required this.hasPreview, required this.isFavorite, - }); + }) : progress = null, + isUploading = false, + isDownloading = false; FileDetails.fromWebDav({ required final WebDavFile file, @@ -23,19 +25,40 @@ class FileDetails { mimeType = file.mimeType, lastModified = file.lastModified, hasPreview = file.hasPreview, - isFavorite = file.favorite; + isFavorite = file.favorite, + progress = null, + isUploading = false, + isDownloading = false; FileDetails.fromUploadTask({ required final UploadTask task, }) : path = task.path, size = task.size, lastModified = task.lastModified, + progress = task.progress, + isUploading = true, + isDownloading = false, isDirectory = false, etag = null, mimeType = null, hasPreview = null, isFavorite = null; + FileDetails.fromDownloadTask({ + required final DownloadTask task, + required final WebDavFile file, + }) : path = task.path, + isDirectory = file.isDirectory, + size = file.size, + etag = file.etag, + mimeType = file.mimeType, + lastModified = file.lastModified, + hasPreview = file.hasPreview, + isFavorite = file.favorite, + progress = task.progress, + isUploading = false, + isDownloading = true; + String get name => path.last; final List path; @@ -53,4 +76,10 @@ class FileDetails { final bool? hasPreview; final bool? isFavorite; + + final Stream? progress; + final bool isUploading; + final bool isDownloading; + + bool get isLoading => isUploading || isDownloading; } diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 56973863..34fe1f61 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -68,21 +68,13 @@ class _FilesBrowserViewState extends State { (final task) => sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, )) ...[ - StreamBuilder( - stream: uploadTask.progress, - builder: (final context, final uploadTaskProgressSnapshot) => - !uploadTaskProgressSnapshot.hasData - ? const SizedBox() - : FileListTile( - context: context, - details: FileDetails.fromUploadTask( - task: uploadTask, - ), - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: null, - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, - ), + FileListTile( + context: context, + details: FileDetails.fromUploadTask( + task: uploadTask, + ), + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, ), ], if (sorted != null) ...[ @@ -91,39 +83,32 @@ class _FilesBrowserViewState extends State { Builder( builder: (final context) { final matchingUploadTasks = uploadTasksSnapshot.requireData - .where((final task) => _pathMatchesFile(task.path, file.name)); + .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); final matchingDownloadTasks = downloadTasksSnapshot.requireData - .where((final task) => _pathMatchesFile(task.path, file.name)); + .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); - return StreamBuilder( - stream: - matchingUploadTasks.isNotEmpty ? matchingUploadTasks.first.progress : null, - builder: (final context, final uploadTaskProgressSnapshot) => - StreamBuilder( - stream: matchingDownloadTasks.isNotEmpty - ? matchingDownloadTasks.first.progress - : null, - builder: (final context, final downloadTaskProgressSnapshot) { - final path = widget.bloc.path.value; - final details = matchingUploadTasks.isEmpty - ? FileDetails.fromWebDav( - file: file, - path: path, - ) - : FileDetails.fromUploadTask( - task: matchingUploadTasks.first, - ); + final FileDetails details; + if (matchingDownloadTasks != null) { + details = FileDetails.fromDownloadTask( + task: matchingDownloadTasks, + file: file, + ); + } else if (matchingUploadTasks != null) { + details = FileDetails.fromUploadTask( + task: matchingUploadTasks, + ); + } else { + details = FileDetails.fromWebDav( + file: file, + path: widget.bloc.path.value, + ); + } - return FileListTile( - context: context, - details: details, - uploadProgress: uploadTaskProgressSnapshot.data, - downloadProgress: downloadTaskProgressSnapshot.data, - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, - ); - }, - ), + return FileListTile( + context: context, + details: details, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, ); }, ), 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 c29dea8e..33455c5e 100644 --- a/packages/neon/neon_files/lib/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -10,8 +10,6 @@ class FileListTile extends StatelessWidget { const FileListTile({ required this.context, required this.details, - required this.uploadProgress, - required this.downloadProgress, this.enableFileActions = true, this.onPickFile, super.key, @@ -19,23 +17,9 @@ class FileListTile extends StatelessWidget { final BuildContext context; final FileDetails details; - final double? uploadProgress; - final double? downloadProgress; final bool enableFileActions; final Function(FileDetails)? onPickFile; - bool get _isUploading => uploadProgress != null; - - bool get _hasProgress => uploadProgress != null || downloadProgress != null; - - double? get _progress { - if (!_hasProgress) { - return null; - } - - return (uploadProgress ?? downloadProgress)!; - } - @override Widget build(final BuildContext context) { final bloc = Provider.of(context); @@ -79,12 +63,9 @@ class FileListTile extends StatelessWidget { ], ), leading: _FileIcon( - hasProgress: _hasProgress, - isUploading: _isUploading, - progress: _progress, details: details, ), - trailing: !_hasProgress && enableFileActions + trailing: !details.isLoading && enableFileActions ? FileActions(details: details) : const SizedBox.square( dimension: 48, @@ -96,14 +77,8 @@ class FileListTile extends StatelessWidget { class _FileIcon extends StatelessWidget { const _FileIcon({ required this.details, - required this.hasProgress, - required this.isUploading, - this.progress, }); - final bool hasProgress; - final bool isUploading; - final double? progress; final FileDetails details; @override @@ -111,17 +86,20 @@ class _FileIcon extends StatelessWidget { final bloc = Provider.of(context); Widget icon = Center( - child: hasProgress - ? Column( - children: [ - Icon( - isUploading ? MdiIcons.upload : MdiIcons.download, - color: Theme.of(context).colorScheme.primary, - ), - LinearProgressIndicator( - value: progress, - ), - ], + child: details.isLoading + ? StreamBuilder( + stream: details.progress, + builder: (final context, final progress) => Column( + children: [ + Icon( + details.isUploading ? MdiIcons.upload : MdiIcons.download, + color: Theme.of(context).colorScheme.primary, + ), + LinearProgressIndicator( + value: progress.data, + ), + ], + ), ) : FilePreview( bloc: bloc, From a7b0ddaf7510b771ec00d57e6824d8e895ca0964 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 15 Aug 2023 01:29:33 +0200 Subject: [PATCH 5/5] refactor(neon_files): make UpladTask and DownloadTask inherrit from a sealed Task class Signed-off-by: Nikolas Rimikis --- packages/neon/neon_files/lib/blocs/files.dart | 24 +- .../neon_files/lib/models/file_details.dart | 41 +-- packages/neon/neon_files/lib/neon_files.dart | 3 +- .../neon_files/lib/utils/download_task.dart | 34 --- packages/neon/neon_files/lib/utils/task.dart | 66 +++++ .../neon_files/lib/utils/upload_task.dart | 32 --- .../neon_files/lib/widgets/browser_view.dart | 249 ++++++++---------- .../lib/widgets/file_list_tile.dart | 14 +- 8 files changed, 223 insertions(+), 240 deletions(-) delete mode 100644 packages/neon/neon_files/lib/utils/download_task.dart create mode 100644 packages/neon/neon_files/lib/utils/task.dart delete mode 100644 packages/neon/neon_files/lib/utils/upload_task.dart diff --git a/packages/neon/neon_files/lib/blocs/files.dart b/packages/neon/neon_files/lib/blocs/files.dart index 414ad37e..d9bee11f 100644 --- a/packages/neon/neon_files/lib/blocs/files.dart +++ b/packages/neon/neon_files/lib/blocs/files.dart @@ -21,9 +21,7 @@ abstract class FilesBlocEvents { } abstract class FilesBlocStates { - BehaviorSubject> get uploadTasks; - - BehaviorSubject> get downloadTasks; + BehaviorSubject> get tasks; } class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocStates { @@ -50,18 +48,14 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta void dispose() { _uploadQueue.dispose(); _downloadQueue.dispose(); - unawaited(uploadTasks.close()); - unawaited(downloadTasks.close()); + unawaited(tasks.close()); options.uploadQueueParallelism.removeListener(_uploadParalelismListener); options.downloadQueueParallelism.removeListener(_downloadParalelismListener); } @override - BehaviorSubject> uploadTasks = BehaviorSubject>.seeded([]); - - @override - BehaviorSubject> downloadTasks = BehaviorSubject>.seeded([]); + BehaviorSubject> tasks = BehaviorSubject>.seeded([]); @override void addFavorite(final List path) { @@ -169,14 +163,14 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta final file = File(localPath); // ignore: avoid_slow_async_io final stat = await file.stat(); - final task = UploadTask( + final task = FilesUploadTask( path: path, size: stat.size, lastModified: stat.modified, ); - uploadTasks.add(uploadTasks.value..add(task)); + tasks.add(tasks.value..add(task)); await _uploadQueue.add(() => task.execute(account.client, file.openRead())); - uploadTasks.add(uploadTasks.value..removeWhere((final t) => t == task)); + tasks.add(tasks.value..remove(task)); }, disableTimeout: true, ); @@ -188,12 +182,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta ) async { final sink = file.openWrite(); try { - final task = DownloadTask( + final task = FilesDownloadTask( path: path, ); - downloadTasks.add(downloadTasks.value..add(task)); + tasks.add(tasks.value..add(task)); await _downloadQueue.add(() => task.execute(account.client, sink)); - downloadTasks.add(downloadTasks.value..removeWhere((final t) => t == task)); + tasks.add(tasks.value..remove(task)); } finally { await sink.close(); } diff --git a/packages/neon/neon_files/lib/models/file_details.dart b/packages/neon/neon_files/lib/models/file_details.dart index 221cfa29..c999be2c 100644 --- a/packages/neon/neon_files/lib/models/file_details.dart +++ b/packages/neon/neon_files/lib/models/file_details.dart @@ -11,9 +11,7 @@ class FileDetails { required this.lastModified, required this.hasPreview, required this.isFavorite, - }) : progress = null, - isUploading = false, - isDownloading = false; + }) : task = null; FileDetails.fromWebDav({ required final WebDavFile file, @@ -26,18 +24,13 @@ class FileDetails { lastModified = file.lastModified, hasPreview = file.hasPreview, isFavorite = file.favorite, - progress = null, - isUploading = false, - isDownloading = false; + task = null; FileDetails.fromUploadTask({ - required final UploadTask task, + required FilesUploadTask this.task, }) : path = task.path, size = task.size, lastModified = task.lastModified, - progress = task.progress, - isUploading = true, - isDownloading = false, isDirectory = false, etag = null, mimeType = null, @@ -45,7 +38,7 @@ class FileDetails { isFavorite = null; FileDetails.fromDownloadTask({ - required final DownloadTask task, + required FilesDownloadTask this.task, required final WebDavFile file, }) : path = task.path, isDirectory = file.isDirectory, @@ -54,10 +47,22 @@ class FileDetails { mimeType = file.mimeType, lastModified = file.lastModified, hasPreview = file.hasPreview, - isFavorite = file.favorite, - progress = task.progress, - isUploading = false, - isDownloading = true; + isFavorite = file.favorite; + + factory FileDetails.fromTask({ + required final FilesTask task, + required final WebDavFile file, + }) { + switch (task) { + case FilesUploadTask(): + return FileDetails.fromUploadTask(task: task); + case FilesDownloadTask(): + return FileDetails.fromDownloadTask( + task: task, + file: file, + ); + } + } String get name => path.last; @@ -77,9 +82,7 @@ class FileDetails { final bool? isFavorite; - final Stream? progress; - final bool isUploading; - final bool isDownloading; + final FilesTask? task; - bool get isLoading => isUploading || isDownloading; + bool get hasTask => task != null; } diff --git a/packages/neon/neon_files/lib/neon_files.dart b/packages/neon/neon_files/lib/neon_files.dart index af0f57bd..2f01f134 100644 --- a/packages/neon/neon_files/lib/neon_files.dart +++ b/packages/neon/neon_files/lib/neon_files.dart @@ -40,8 +40,7 @@ part 'options.dart'; part 'pages/details.dart'; part 'pages/main.dart'; part 'sort/files.dart'; -part 'utils/download_task.dart'; -part 'utils/upload_task.dart'; +part 'utils/task.dart'; part 'widgets/browser_view.dart'; part 'widgets/file_preview.dart'; diff --git a/packages/neon/neon_files/lib/utils/download_task.dart b/packages/neon/neon_files/lib/utils/download_task.dart deleted file mode 100644 index dced89b5..00000000 --- a/packages/neon/neon_files/lib/utils/download_task.dart +++ /dev/null @@ -1,34 +0,0 @@ -part of '../neon_files.dart'; - -class DownloadTask { - DownloadTask({ - required this.path, - }); - - final List path; - - final _streamController = StreamController(); - - /// Upload progress in percent [0, 1]. - late final progress = _streamController.stream.asBroadcastStream(); - - Future execute(final NextcloudClient client, final IOSink sink) async { - final completer = Completer(); - - final response = await client.webdav.getStream(path.join('/')); - var downloaded = 0; - - response.listen((final chunk) async { - sink.add(chunk); - - downloaded += chunk.length; - _streamController.add(downloaded / response.contentLength); - - if (downloaded >= response.contentLength) { - completer.complete(); - } - }); - - return completer.future; - } -} diff --git a/packages/neon/neon_files/lib/utils/task.dart b/packages/neon/neon_files/lib/utils/task.dart new file mode 100644 index 00000000..09dcc1dc --- /dev/null +++ b/packages/neon/neon_files/lib/utils/task.dart @@ -0,0 +1,66 @@ +part of '../neon_files.dart'; + +sealed class FilesTask { + FilesTask({ + required this.path, + }); + + final List path; + + @protected + final streamController = StreamController(); + + /// Task progress in percent [0, 1]. + late final progress = streamController.stream.asBroadcastStream(); +} + +class FilesDownloadTask extends FilesTask { + FilesDownloadTask({ + required super.path, + }); + + Future execute(final NextcloudClient client, final IOSink sink) async { + final completer = Completer(); + + final response = await client.webdav.getStream(path.join('/')); + var downloaded = 0; + + response.listen((final chunk) async { + sink.add(chunk); + + downloaded += chunk.length; + streamController.add(downloaded / response.contentLength); + + if (downloaded >= response.contentLength) { + completer.complete(); + } + }); + + return completer.future; + } +} + +class FilesUploadTask extends FilesTask { + FilesUploadTask({ + required super.path, + required this.size, + required this.lastModified, + }); + + final int size; + final DateTime lastModified; + + Future execute(final NextcloudClient client, final Stream> stream) async { + var uploaded = 0; + await client.webdav.putStream( + stream.map((final chunk) { + uploaded += chunk.length; + streamController.add(uploaded / size); + + return Uint8List.fromList(chunk); + }), + path.join('/'), + lastModified: lastModified, + ); + } +} diff --git a/packages/neon/neon_files/lib/utils/upload_task.dart b/packages/neon/neon_files/lib/utils/upload_task.dart deleted file mode 100644 index 5559abc9..00000000 --- a/packages/neon/neon_files/lib/utils/upload_task.dart +++ /dev/null @@ -1,32 +0,0 @@ -part of '../neon_files.dart'; - -class UploadTask { - UploadTask({ - required this.path, - required this.size, - required this.lastModified, - }); - - final List path; - final int size; - final DateTime lastModified; - - final _streamController = StreamController(); - - /// Upload progress in percent [0, 1]. - late final progress = _streamController.stream.asBroadcastStream(); - - Future execute(final NextcloudClient client, final Stream> stream) async { - var uploaded = 0; - await client.webdav.putStream( - stream.map((final chunk) { - uploaded += chunk.length; - _streamController.add(uploaded / size); - - return Uint8List.fromList(chunk); - }), - path.join('/'), - lastModified: lastModified, - ); - } -} diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 34fe1f61..b65d5cfd 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -36,151 +36,136 @@ class _FilesBrowserViewState extends State { stream: widget.bloc.files, builder: (final context, final files) => StreamBuilder>( stream: widget.bloc.path, - builder: (final context, final pathSnapshot) => StreamBuilder>( - stream: widget.filesBloc.uploadTasks, - builder: (final context, final uploadTasksSnapshot) => StreamBuilder>( - stream: widget.filesBloc.downloadTasks, - builder: (final context, final downloadTasksSnapshot) => !pathSnapshot.hasData || - !uploadTasksSnapshot.hasData || - !downloadTasksSnapshot.hasData - ? const SizedBox() - : BackButtonListener( - onBackButtonPressed: () async { - final path = pathSnapshot.requireData; - if (path.isNotEmpty) { - widget.bloc.setPath(path.sublist(0, path.length - 1)); - return true; - } - return false; - }, - child: SortBoxBuilder( - sortBox: filesSortBox, - sortPropertyOption: widget.bloc.options.filesSortPropertyOption, - sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption, - input: files.data, - builder: (final context, final sorted) => NeonListView( - scrollKey: 'files-${pathSnapshot.requireData.join('/')}', - withFloatingActionButton: true, - items: [ - for (final uploadTask in sorted == null - ? [] - : uploadTasksSnapshot.requireData.where( - (final task) => - sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, - )) ...[ - FileListTile( - context: context, - details: FileDetails.fromUploadTask( - task: uploadTask, - ), - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, + builder: (final context, final pathSnapshot) => StreamBuilder>( + stream: widget.filesBloc.tasks, + builder: (final context, final tasksSnapshot) => !pathSnapshot.hasData || !tasksSnapshot.hasData + ? const SizedBox() + : BackButtonListener( + onBackButtonPressed: () async { + final path = pathSnapshot.requireData; + if (path.isNotEmpty) { + widget.bloc.setPath(path.sublist(0, path.length - 1)); + return true; + } + return false; + }, + child: SortBoxBuilder( + sortBox: filesSortBox, + sortPropertyOption: widget.bloc.options.filesSortPropertyOption, + sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption, + 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 ?? + false, + )) ...[ + FileListTile( + context: context, + details: FileDetails.fromUploadTask( + task: uploadTask, ), - ], - if (sorted != null) ...[ - for (final file in sorted) ...[ - if (!widget.onlyShowDirectories || file.isDirectory) ...[ - Builder( - builder: (final context) { - final matchingUploadTasks = uploadTasksSnapshot.requireData - .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); - final matchingDownloadTasks = downloadTasksSnapshot.requireData - .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, + ), + ], + if (sorted != null) ...[ + for (final file in sorted) ...[ + if (!widget.onlyShowDirectories || file.isDirectory) ...[ + Builder( + builder: (final context) { + final matchingTask = tasksSnapshot.requireData + .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); - final FileDetails details; - if (matchingDownloadTasks != null) { - details = FileDetails.fromDownloadTask( - task: matchingDownloadTasks, - file: file, - ); - } else if (matchingUploadTasks != null) { - details = FileDetails.fromUploadTask( - task: matchingUploadTasks, - ); - } else { - details = 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( - context: context, - details: details, - enableFileActions: widget.enableFileActions, - onPickFile: widget.onPickFile, - ); - }, - ), - ], + return FileListTile( + context: context, + details: details, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, + ); + }, + ), ], ], ], - 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([]); - }, + ], + 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, ), - 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('/')), - ), + 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('/')), ), - ); - }, + ), + ); + }, + ), + ], + ] + .intersperse( + const Icon( + Icons.keyboard_arrow_right, + size: 30, ), - ], - ] - .intersperse( - const Icon( - Icons.keyboard_arrow_right, - size: 30, - ), - ) - .toList(), - ), + ) + .toList(), ), ), - ], - ), + ), + ], ), ), - ), + ), ), ), ); 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 33455c5e..9cb70a40 100644 --- a/packages/neon/neon_files/lib/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -44,11 +44,10 @@ class FileListTile extends StatelessWidget { ), subtitle: Row( children: [ - if (details.lastModified != null) ...[ + if (details.lastModified != null) RelativeTime( date: details.lastModified!, ), - ], if (details.size != null && details.size! > 0) ...[ const SizedBox( width: 10, @@ -65,7 +64,7 @@ class FileListTile extends StatelessWidget { leading: _FileIcon( details: details, ), - trailing: !details.isLoading && enableFileActions + trailing: !details.hasTask && enableFileActions ? FileActions(details: details) : const SizedBox.square( dimension: 48, @@ -86,13 +85,16 @@ class _FileIcon extends StatelessWidget { final bloc = Provider.of(context); Widget icon = Center( - child: details.isLoading + child: details.hasTask ? StreamBuilder( - stream: details.progress, + stream: details.task!.progress, builder: (final context, final progress) => Column( children: [ Icon( - details.isUploading ? MdiIcons.upload : MdiIcons.download, + switch (details.task!) { + FilesUploadTask() => MdiIcons.upload, + FilesDownloadTask() => MdiIcons.download, + }, color: Theme.of(context).colorScheme.primary, ), LinearProgressIndicator(