From a7b0ddaf7510b771ec00d57e6824d8e895ca0964 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Tue, 15 Aug 2023 01:29:33 +0200 Subject: [PATCH] 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(