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

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

@ -36,151 +36,136 @@ 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, ? const SizedBox()
builder: (final context, final downloadTasksSnapshot) => !pathSnapshot.hasData || : BackButtonListener(
!uploadTasksSnapshot.hasData || onBackButtonPressed: () async {
!downloadTasksSnapshot.hasData final path = pathSnapshot.requireData;
? const SizedBox() if (path.isNotEmpty) {
: BackButtonListener( widget.bloc.setPath(path.sublist(0, path.length - 1));
onBackButtonPressed: () async { return true;
final path = pathSnapshot.requireData; }
if (path.isNotEmpty) { return false;
widget.bloc.setPath(path.sublist(0, path.length - 1)); },
return true; child: SortBoxBuilder<FilesSortProperty, WebDavFile>(
} sortBox: filesSortBox,
return false; sortPropertyOption: widget.bloc.options.filesSortPropertyOption,
}, sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption,
child: SortBoxBuilder<FilesSortProperty, WebDavFile>( input: files.data,
sortBox: filesSortBox, builder: (final context, final sorted) => NeonListView<Widget>(
sortPropertyOption: widget.bloc.options.filesSortPropertyOption, scrollKey: 'files-${pathSnapshot.requireData.join('/')}',
sortBoxOrderOption: widget.bloc.options.filesSortBoxOrderOption, withFloatingActionButton: true,
input: files.data, items: [
builder: (final context, final sorted) => NeonListView<Widget>( for (final uploadTask in tasksSnapshot.requireData.whereType<FilesUploadTask>().where(
scrollKey: 'files-${pathSnapshot.requireData.join('/')}', (final task) =>
withFloatingActionButton: true, sorted?.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty ??
items: [ false,
for (final uploadTask in sorted == null )) ...[
? <UploadTask>[] FileListTile(
: uploadTasksSnapshot.requireData.where( context: context,
(final task) => details: FileDetails.fromUploadTask(
sorted.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty, task: uploadTask,
)) ...[
FileListTile(
context: context,
details: FileDetails.fromUploadTask(
task: uploadTask,
),
enableFileActions: widget.enableFileActions,
onPickFile: widget.onPickFile,
), ),
], enableFileActions: widget.enableFileActions,
if (sorted != null) ...[ onPickFile: widget.onPickFile,
for (final file in sorted) ...[ ),
if (!widget.onlyShowDirectories || file.isDirectory) ...[ ],
Builder( if (sorted != null) ...[
builder: (final context) { for (final file in sorted) ...[
final matchingUploadTasks = uploadTasksSnapshot.requireData if (!widget.onlyShowDirectories || file.isDirectory) ...[
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); Builder(
final matchingDownloadTasks = downloadTasksSnapshot.requireData builder: (final context) {
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); final matchingTask = tasksSnapshot.requireData
.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, )
); : FileDetails.fromWebDav(
} else if (matchingUploadTasks != null) { file: file,
details = FileDetails.fromUploadTask( path: widget.bloc.path.value,
task: matchingUploadTasks, );
);
} else {
details = FileDetails.fromWebDav(
file: file,
path: widget.bloc.path.value,
);
}
return FileListTile( return FileListTile(
context: context, context: context,
details: details, details: details,
enableFileActions: widget.enableFileActions, enableFileActions: widget.enableFileActions,
onPickFile: widget.onPickFile, onPickFile: widget.onPickFile,
); );
}, },
), ),
],
], ],
], ],
], ],
isLoading: files.isLoading, ],
error: files.error, isLoading: files.isLoading,
onRefresh: widget.bloc.refresh, error: files.error,
builder: (final context, final widget) => widget, onRefresh: widget.bloc.refresh,
topScrollingChildren: [ builder: (final context, final widget) => widget,
Align( topScrollingChildren: [
alignment: Alignment.topLeft, Align(
child: Container( alignment: Alignment.topLeft,
margin: const EdgeInsets.symmetric( child: Container(
horizontal: 10, margin: const EdgeInsets.symmetric(
), horizontal: 10,
child: Wrap( ),
crossAxisAlignment: WrapCrossAlignment.center, child: Wrap(
children: <Widget>[ crossAxisAlignment: WrapCrossAlignment.center,
IconButton( children: <Widget>[
padding: EdgeInsets.zero, IconButton(
visualDensity: const VisualDensity( padding: EdgeInsets.zero,
horizontal: VisualDensity.minimumDensity, visualDensity: const VisualDensity(
vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity,
), vertical: VisualDensity.minimumDensity,
tooltip: AppLocalizations.of(context).goToPath(''), ),
icon: const Icon( tooltip: AppLocalizations.of(context).goToPath(''),
Icons.house, icon: const Icon(
size: 30, Icons.house,
), size: 30,
onPressed: () {
widget.bloc.setPath([]);
},
), ),
for (var i = 0; i < pathSnapshot.requireData.length; i++) ...[ onPressed: () {
Builder( widget.bloc.setPath([]);
builder: (final context) { },
final path = pathSnapshot.requireData.sublist(0, i + 1); ),
return Tooltip( for (var i = 0; i < pathSnapshot.requireData.length; i++) ...[
message: AppLocalizations.of(context).goToPath(path.join('/')), Builder(
excludeFromSemantics: true, builder: (final context) {
child: TextButton( final path = pathSnapshot.requireData.sublist(0, i + 1);
onPressed: () { return Tooltip(
widget.bloc.setPath(path); message: AppLocalizations.of(context).goToPath(path.join('/')),
}, excludeFromSemantics: true,
child: Text( child: TextButton(
pathSnapshot.requireData[i], onPressed: () {
semanticsLabel: AppLocalizations.of(context).goToPath(path.join('/')), 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,
), ),
], )
] .toList(),
.intersperse(
const Icon(
Icons.keyboard_arrow_right,
size: 30,
),
)
.toList(),
),
), ),
), ),
], ),
), ],
), ),
), ),
), ),
), ),
), ),
); );

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