Browse Source

refactor(neon_files): make UpladTask and DownloadTask inherrit from a sealed Task class

Signed-off-by: Nikolas Rimikis <rimikis.nikolas@gmail.com>
pull/556/head
Nikolas Rimikis 1 year ago
parent
commit
a7b0ddaf75
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 24
      packages/neon/neon_files/lib/blocs/files.dart
  2. 41
      packages/neon/neon_files/lib/models/file_details.dart
  3. 3
      packages/neon/neon_files/lib/neon_files.dart
  4. 34
      packages/neon/neon_files/lib/utils/download_task.dart
  5. 66
      packages/neon/neon_files/lib/utils/task.dart
  6. 32
      packages/neon/neon_files/lib/utils/upload_task.dart
  7. 39
      packages/neon/neon_files/lib/widgets/browser_view.dart
  8. 14
      packages/neon/neon_files/lib/widgets/file_list_tile.dart

24
packages/neon/neon_files/lib/blocs/files.dart

@ -21,9 +21,7 @@ abstract class FilesBlocEvents {
} }
abstract class FilesBlocStates { abstract class FilesBlocStates {
BehaviorSubject<List<UploadTask>> get uploadTasks; BehaviorSubject<List<FilesTask>> get tasks;
BehaviorSubject<List<DownloadTask>> get downloadTasks;
} }
class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocStates { class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocStates {
@ -50,18 +48,14 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
void dispose() { void dispose() {
_uploadQueue.dispose(); _uploadQueue.dispose();
_downloadQueue.dispose(); _downloadQueue.dispose();
unawaited(uploadTasks.close()); unawaited(tasks.close());
unawaited(downloadTasks.close());
options.uploadQueueParallelism.removeListener(_uploadParalelismListener); options.uploadQueueParallelism.removeListener(_uploadParalelismListener);
options.downloadQueueParallelism.removeListener(_downloadParalelismListener); options.downloadQueueParallelism.removeListener(_downloadParalelismListener);
} }
@override @override
BehaviorSubject<List<UploadTask>> uploadTasks = BehaviorSubject<List<UploadTask>>.seeded([]); BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);
@override
BehaviorSubject<List<DownloadTask>> downloadTasks = BehaviorSubject<List<DownloadTask>>.seeded([]);
@override @override
void addFavorite(final List<String> path) { void addFavorite(final List<String> path) {
@ -169,14 +163,14 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
final file = File(localPath); final file = File(localPath);
// ignore: avoid_slow_async_io // ignore: avoid_slow_async_io
final stat = await file.stat(); final stat = await file.stat();
final task = UploadTask( final task = FilesUploadTask(
path: path, path: path,
size: stat.size, size: stat.size,
lastModified: stat.modified, lastModified: stat.modified,
); );
uploadTasks.add(uploadTasks.value..add(task)); tasks.add(tasks.value..add(task));
await _uploadQueue.add(() => task.execute(account.client, file.openRead())); 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, disableTimeout: true,
); );
@ -188,12 +182,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
) async { ) async {
final sink = file.openWrite(); final sink = file.openWrite();
try { try {
final task = DownloadTask( final task = FilesDownloadTask(
path: path, path: path,
); );
downloadTasks.add(downloadTasks.value..add(task)); tasks.add(tasks.value..add(task));
await _downloadQueue.add(() => task.execute(account.client, sink)); await _downloadQueue.add(() => task.execute(account.client, sink));
downloadTasks.add(downloadTasks.value..removeWhere((final t) => t == task)); tasks.add(tasks.value..remove(task));
} finally { } finally {
await sink.close(); await sink.close();
} }

41
packages/neon/neon_files/lib/models/file_details.dart

@ -11,9 +11,7 @@ class FileDetails {
required this.lastModified, required this.lastModified,
required this.hasPreview, required this.hasPreview,
required this.isFavorite, required this.isFavorite,
}) : progress = null, }) : task = null;
isUploading = false,
isDownloading = false;
FileDetails.fromWebDav({ FileDetails.fromWebDav({
required final WebDavFile file, required final WebDavFile file,
@ -26,18 +24,13 @@ class FileDetails {
lastModified = file.lastModified, lastModified = file.lastModified,
hasPreview = file.hasPreview, hasPreview = file.hasPreview,
isFavorite = file.favorite, isFavorite = file.favorite,
progress = null, task = null;
isUploading = false,
isDownloading = false;
FileDetails.fromUploadTask({ FileDetails.fromUploadTask({
required final UploadTask task, required FilesUploadTask this.task,
}) : path = task.path, }) : path = task.path,
size = task.size, size = task.size,
lastModified = task.lastModified, lastModified = task.lastModified,
progress = task.progress,
isUploading = true,
isDownloading = false,
isDirectory = false, isDirectory = false,
etag = null, etag = null,
mimeType = null, mimeType = null,
@ -45,7 +38,7 @@ class FileDetails {
isFavorite = null; isFavorite = null;
FileDetails.fromDownloadTask({ FileDetails.fromDownloadTask({
required final DownloadTask task, required FilesDownloadTask this.task,
required final WebDavFile file, required final WebDavFile file,
}) : path = task.path, }) : path = task.path,
isDirectory = file.isDirectory, isDirectory = file.isDirectory,
@ -54,10 +47,22 @@ class FileDetails {
mimeType = file.mimeType, mimeType = file.mimeType,
lastModified = file.lastModified, lastModified = file.lastModified,
hasPreview = file.hasPreview, hasPreview = file.hasPreview,
isFavorite = file.favorite, isFavorite = file.favorite;
progress = task.progress,
isUploading = false, factory FileDetails.fromTask({
isDownloading = true; 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; String get name => path.last;
@ -77,9 +82,7 @@ class FileDetails {
final bool? isFavorite; final bool? isFavorite;
final Stream<double>? progress; final FilesTask? task;
final bool isUploading;
final bool isDownloading;
bool get isLoading => isUploading || isDownloading; bool get hasTask => task != null;
} }

3
packages/neon/neon_files/lib/neon_files.dart

@ -40,8 +40,7 @@ part 'options.dart';
part 'pages/details.dart'; part 'pages/details.dart';
part 'pages/main.dart'; part 'pages/main.dart';
part 'sort/files.dart'; part 'sort/files.dart';
part 'utils/download_task.dart'; part 'utils/task.dart';
part 'utils/upload_task.dart';
part 'widgets/browser_view.dart'; part 'widgets/browser_view.dart';
part 'widgets/file_preview.dart'; part 'widgets/file_preview.dart';

34
packages/neon/neon_files/lib/utils/download_task.dart

@ -1,34 +0,0 @@
part of '../neon_files.dart';
class DownloadTask {
DownloadTask({
required this.path,
});
final List<String> path;
final _streamController = StreamController<double>();
/// 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;
}
}

66
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<String> path;
@protected
final streamController = StreamController<double>();
/// 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<List<int>> 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,
);
}
}

32
packages/neon/neon_files/lib/utils/upload_task.dart

@ -1,32 +0,0 @@
part of '../neon_files.dart';
class UploadTask {
UploadTask({
required this.path,
required this.size,
required this.lastModified,
});
final List<String> path;
final int size;
final DateTime lastModified;
final _streamController = StreamController<double>();
/// Upload progress in percent [0, 1].
late final progress = _streamController.stream.asBroadcastStream();
Future execute(final NextcloudClient client, final Stream<List<int>> 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,
);
}
}

39
packages/neon/neon_files/lib/widgets/browser_view.dart

@ -36,13 +36,9 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
stream: widget.bloc.files, stream: widget.bloc.files,
builder: (final context, final files) => StreamBuilder<List<String>>( builder: (final context, final files) => StreamBuilder<List<String>>(
stream: widget.bloc.path, stream: widget.bloc.path,
builder: (final context, final pathSnapshot) => StreamBuilder<List<UploadTask>>( builder: (final context, final pathSnapshot) => StreamBuilder<List<FilesTask>>(
stream: widget.filesBloc.uploadTasks, stream: widget.filesBloc.tasks,
builder: (final context, final uploadTasksSnapshot) => StreamBuilder<List<DownloadTask>>( builder: (final context, final tasksSnapshot) => !pathSnapshot.hasData || !tasksSnapshot.hasData
stream: widget.filesBloc.downloadTasks,
builder: (final context, final downloadTasksSnapshot) => !pathSnapshot.hasData ||
!uploadTasksSnapshot.hasData ||
!downloadTasksSnapshot.hasData
? const SizedBox() ? const SizedBox()
: BackButtonListener( : BackButtonListener(
onBackButtonPressed: () async { onBackButtonPressed: () async {
@ -62,11 +58,10 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
scrollKey: 'files-${pathSnapshot.requireData.join('/')}', scrollKey: 'files-${pathSnapshot.requireData.join('/')}',
withFloatingActionButton: true, withFloatingActionButton: true,
items: [ items: [
for (final uploadTask in sorted == null for (final uploadTask in tasksSnapshot.requireData.whereType<FilesUploadTask>().where(
? <UploadTask>[]
: uploadTasksSnapshot.requireData.where(
(final task) => (final task) =>
sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, sorted?.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty ??
false,
)) ...[ )) ...[
FileListTile( FileListTile(
context: context, context: context,
@ -82,27 +77,18 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
if (!widget.onlyShowDirectories || file.isDirectory) ...[ if (!widget.onlyShowDirectories || file.isDirectory) ...[
Builder( Builder(
builder: (final context) { builder: (final context) {
final matchingUploadTasks = uploadTasksSnapshot.requireData final matchingTask = tasksSnapshot.requireData
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name));
final matchingDownloadTasks = downloadTasksSnapshot.requireData
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name));
final FileDetails details; final details = matchingTask != null
if (matchingDownloadTasks != null) { ? FileDetails.fromTask(
details = FileDetails.fromDownloadTask( task: matchingTask,
task: matchingDownloadTasks,
file: file, file: file,
); )
} else if (matchingUploadTasks != null) { : FileDetails.fromWebDav(
details = FileDetails.fromUploadTask(
task: matchingUploadTasks,
);
} else {
details = FileDetails.fromWebDav(
file: file, file: file,
path: widget.bloc.path.value, path: widget.bloc.path.value,
); );
}
return FileListTile( return FileListTile(
context: context, context: context,
@ -182,7 +168,6 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
), ),
), ),
), ),
),
); );
bool _pathMatchesFile(final List<String> path, final String name) => const ListEquality().equals( bool _pathMatchesFile(final List<String> path, final String name) => const ListEquality().equals(

14
packages/neon/neon_files/lib/widgets/file_list_tile.dart

@ -44,11 +44,10 @@ class FileListTile extends StatelessWidget {
), ),
subtitle: Row( subtitle: Row(
children: [ children: [
if (details.lastModified != null) ...[ if (details.lastModified != null)
RelativeTime( RelativeTime(
date: details.lastModified!, date: details.lastModified!,
), ),
],
if (details.size != null && details.size! > 0) ...[ if (details.size != null && details.size! > 0) ...[
const SizedBox( const SizedBox(
width: 10, width: 10,
@ -65,7 +64,7 @@ class FileListTile extends StatelessWidget {
leading: _FileIcon( leading: _FileIcon(
details: details, details: details,
), ),
trailing: !details.isLoading && enableFileActions trailing: !details.hasTask && enableFileActions
? FileActions(details: details) ? FileActions(details: details)
: const SizedBox.square( : const SizedBox.square(
dimension: 48, dimension: 48,
@ -86,13 +85,16 @@ class _FileIcon extends StatelessWidget {
final bloc = Provider.of<FilesBloc>(context); final bloc = Provider.of<FilesBloc>(context);
Widget icon = Center( Widget icon = Center(
child: details.isLoading child: details.hasTask
? StreamBuilder<double>( ? StreamBuilder<double>(
stream: details.progress, stream: details.task!.progress,
builder: (final context, final progress) => Column( builder: (final context, final progress) => Column(
children: [ children: [
Icon( Icon(
details.isUploading ? MdiIcons.upload : MdiIcons.download, switch (details.task!) {
FilesUploadTask() => MdiIcons.upload,
FilesDownloadTask() => MdiIcons.download,
},
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
LinearProgressIndicator( LinearProgressIndicator(

Loading…
Cancel
Save