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. 249
      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 {
BehaviorSubject<List<UploadTask>> get uploadTasks;
BehaviorSubject<List<DownloadTask>> get downloadTasks;
BehaviorSubject<List<FilesTask>> 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<List<UploadTask>> uploadTasks = BehaviorSubject<List<UploadTask>>.seeded([]);
@override
BehaviorSubject<List<DownloadTask>> downloadTasks = BehaviorSubject<List<DownloadTask>>.seeded([]);
BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);
@override
void addFavorite(final List<String> 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();
}

41
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<double>? progress;
final bool isUploading;
final bool isDownloading;
final FilesTask? task;
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/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';

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,
);
}
}

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

@ -36,151 +36,136 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
stream: widget.bloc.files,
builder: (final context, final files) => StreamBuilder<List<String>>(
stream: widget.bloc.path,
builder: (final context, final pathSnapshot) => StreamBuilder<List<UploadTask>>(
stream: widget.filesBloc.uploadTasks,
builder: (final context, final uploadTasksSnapshot) => StreamBuilder<List<DownloadTask>>(
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<FilesSortProperty, WebDavFile>(
sortBox: filesSortBox,
sortPropertyOption: widget.bloc.options.filesSortPropertyOption,
sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption,
input: files.data,
builder: (final context, final sorted) => NeonListView<Widget>(
scrollKey: 'files-${pathSnapshot.requireData.join('/')}',
withFloatingActionButton: true,
items: [
for (final uploadTask in sorted == null
? <UploadTask>[]
: 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<List<FilesTask>>(
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<FilesSortProperty, WebDavFile>(
sortBox: filesSortBox,
sortPropertyOption: widget.bloc.options.filesSortPropertyOption,
sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption,
input: files.data,
builder: (final context, final sorted) => NeonListView<Widget>(
scrollKey: 'files-${pathSnapshot.requireData.join('/')}',
withFloatingActionButton: true,
items: [
for (final uploadTask in tasksSnapshot.requireData.whereType<FilesUploadTask>().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: <Widget>[
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: <Widget>[
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(),
),
),
],
),
),
],
),
),
),
),
),
),
);

14
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<FilesBloc>(context);
Widget icon = Center(
child: details.isLoading
child: details.hasTask
? StreamBuilder<double>(
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(

Loading…
Cancel
Save