|
|
@ -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,32 +58,18 @@ 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, |
|
|
|
)) ...[ |
|
|
|
)) ...[ |
|
|
|
StreamBuilder<int>( |
|
|
|
FileListTile( |
|
|
|
stream: uploadTask.progress, |
|
|
|
|
|
|
|
builder: (final context, final uploadTaskProgressSnapshot) => |
|
|
|
|
|
|
|
!uploadTaskProgressSnapshot.hasData |
|
|
|
|
|
|
|
? const SizedBox() |
|
|
|
|
|
|
|
: _buildFile( |
|
|
|
|
|
|
|
context: context, |
|
|
|
context: context, |
|
|
|
details: FileDetails( |
|
|
|
details: FileDetails.fromUploadTask( |
|
|
|
path: uploadTask.path, |
|
|
|
task: uploadTask, |
|
|
|
isDirectory: false, |
|
|
|
|
|
|
|
size: uploadTask.size, |
|
|
|
|
|
|
|
etag: null, |
|
|
|
|
|
|
|
mimeType: null, |
|
|
|
|
|
|
|
lastModified: uploadTask.lastModified, |
|
|
|
|
|
|
|
hasPreview: null, |
|
|
|
|
|
|
|
isFavorite: null, |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
uploadProgress: uploadTaskProgressSnapshot.data, |
|
|
|
|
|
|
|
downloadProgress: null, |
|
|
|
|
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
enableFileActions: widget.enableFileActions, |
|
|
|
|
|
|
|
onPickFile: widget.onPickFile, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
], |
|
|
|
if (sorted != null) ...[ |
|
|
|
if (sorted != null) ...[ |
|
|
@ -95,40 +77,24 @@ 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 |
|
|
|
.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)); |
|
|
|
final details = matchingTask != null |
|
|
|
|
|
|
|
? FileDetails.fromTask( |
|
|
|
|
|
|
|
task: matchingTask, |
|
|
|
|
|
|
|
file: file, |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
: FileDetails.fromWebDav( |
|
|
|
|
|
|
|
file: file, |
|
|
|
|
|
|
|
path: widget.bloc.path.value, |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return StreamBuilder<int?>( |
|
|
|
return FileListTile( |
|
|
|
stream: matchingUploadTasks.isNotEmpty |
|
|
|
|
|
|
|
? matchingUploadTasks.first.progress |
|
|
|
|
|
|
|
: Stream.value(null), |
|
|
|
|
|
|
|
builder: (final context, final uploadTaskProgressSnapshot) => |
|
|
|
|
|
|
|
StreamBuilder<int?>( |
|
|
|
|
|
|
|
stream: matchingDownloadTasks.isNotEmpty |
|
|
|
|
|
|
|
? matchingDownloadTasks.first.progress |
|
|
|
|
|
|
|
: Stream.value(null), |
|
|
|
|
|
|
|
builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( |
|
|
|
|
|
|
|
context: context, |
|
|
|
context: context, |
|
|
|
details: FileDetails( |
|
|
|
details: details, |
|
|
|
path: [...widget.bloc.path.value, file.name], |
|
|
|
enableFileActions: widget.enableFileActions, |
|
|
|
isDirectory: matchingUploadTasks.isEmpty && file.isDirectory, |
|
|
|
onPickFile: widget.onPickFile, |
|
|
|
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, |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
); |
|
|
|
); |
|
|
|
}, |
|
|
|
}, |
|
|
|
), |
|
|
|
), |
|
|
@ -202,240 +168,10 @@ 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( |
|
|
|
[...widget.bloc.path.value, name], |
|
|
|
[...widget.bloc.path.value, name], |
|
|
|
path, |
|
|
|
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<FilesFileAction>( |
|
|
|
|
|
|
|
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<List<String>?>( |
|
|
|
|
|
|
|
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<List<String>?>( |
|
|
|
|
|
|
|
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, |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|