You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
431 lines
19 KiB
431 lines
19 KiB
2 years ago
|
part of '../neon_files.dart';
|
||
2 years ago
|
|
||
|
class FilesBrowserView extends StatefulWidget {
|
||
|
const FilesBrowserView({
|
||
|
required this.bloc,
|
||
|
required this.filesBloc,
|
||
|
this.onPickFile,
|
||
|
this.enableFileActions = true,
|
||
|
this.enableCreateActions = true,
|
||
|
this.onlyShowDirectories = false,
|
||
|
super.key,
|
||
|
// ignore: prefer_asserts_with_message
|
||
|
}) : assert((onPickFile == null) == onlyShowDirectories);
|
||
|
|
||
|
final FilesBrowserBloc bloc;
|
||
|
final FilesBloc filesBloc;
|
||
|
final Function(FileDetails)? onPickFile;
|
||
|
final bool enableFileActions;
|
||
|
final bool enableCreateActions;
|
||
|
final bool onlyShowDirectories;
|
||
|
|
||
|
@override
|
||
|
State<FilesBrowserView> createState() => _FilesBrowserViewState();
|
||
|
}
|
||
|
|
||
|
class _FilesBrowserViewState extends State<FilesBrowserView> {
|
||
|
@override
|
||
|
void initState() {
|
||
|
super.initState();
|
||
|
|
||
|
widget.bloc.errors.listen((final error) {
|
||
|
ExceptionWidget.showSnackbar(context, error);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
@override
|
||
2 years ago
|
Widget build(final BuildContext context) => ResultBuilder<FilesBrowserBloc, List<WebDavFile>>(
|
||
|
stream: widget.bloc.files,
|
||
|
builder: (final context, final files) => StreamBuilder<List<String>>(
|
||
2 years ago
|
stream: widget.bloc.path,
|
||
2 years ago
|
builder: (final context, final pathSnapshot) => StreamBuilder<List<UploadTask>>(
|
||
2 years ago
|
stream: widget.filesBloc.uploadTasks,
|
||
2 years ago
|
builder: (final context, final uploadTasksSnapshot) => StreamBuilder<List<DownloadTask>>(
|
||
2 years ago
|
stream: widget.filesBloc.downloadTasks,
|
||
2 years ago
|
builder: (final context, final downloadTasksSnapshot) => !pathSnapshot.hasData ||
|
||
|
!uploadTasksSnapshot.hasData ||
|
||
|
!downloadTasksSnapshot.hasData
|
||
|
? Container()
|
||
|
: Scaffold(
|
||
|
resizeToAvoidBottomInset: false,
|
||
|
floatingActionButton: widget.enableCreateActions
|
||
|
? FloatingActionButton(
|
||
|
onPressed: () async {
|
||
|
await showDialog(
|
||
|
context: context,
|
||
|
builder: (final context) => FilesChooseCreateDialog(
|
||
|
bloc: widget.filesBloc,
|
||
|
basePath: widget.bloc.path.value,
|
||
|
),
|
||
|
);
|
||
|
},
|
||
|
child: const Icon(Icons.add),
|
||
|
)
|
||
|
: null,
|
||
|
body: CustomListView<Widget>(
|
||
|
scrollKey: 'files-${pathSnapshot.data!.join('/')}',
|
||
|
withFloatingActionButton: true,
|
||
|
items: [
|
||
|
for (final uploadTask in files.data == null
|
||
|
? <UploadTask>[]
|
||
|
: uploadTasksSnapshot.data!.where(
|
||
|
(final task) =>
|
||
|
files.data!.where((final file) => _pathMatchesFile(task.path, file.name)).isEmpty,
|
||
|
)) ...[
|
||
|
StreamBuilder<int>(
|
||
|
stream: uploadTask.progress,
|
||
|
builder: (final context, final uploadTaskProgressSnapshot) =>
|
||
|
!uploadTaskProgressSnapshot.hasData
|
||
|
? Container()
|
||
|
: _buildFile(
|
||
|
context: context,
|
||
|
details: FileDetails(
|
||
|
path: uploadTask.path,
|
||
|
isDirectory: false,
|
||
|
size: uploadTask.size,
|
||
|
etag: null,
|
||
|
mimeType: null,
|
||
|
lastModified: uploadTask.lastModified,
|
||
|
hasPreview: null,
|
||
|
isFavorite: null,
|
||
|
),
|
||
2 years ago
|
uploadProgress: uploadTaskProgressSnapshot.data,
|
||
2 years ago
|
downloadProgress: null,
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
if (files.data != null) ...[
|
||
|
for (final file in files.data!) ...[
|
||
|
if (!widget.onlyShowDirectories || file.isDirectory) ...[
|
||
|
Builder(
|
||
|
builder: (final context) {
|
||
|
final matchingUploadTasks = uploadTasksSnapshot.data!
|
||
|
.where((final task) => _pathMatchesFile(task.path, file.name));
|
||
|
final matchingDownloadTasks = downloadTasksSnapshot.data!
|
||
|
.where((final task) => _pathMatchesFile(task.path, file.name));
|
||
|
|
||
|
return StreamBuilder<int?>(
|
||
|
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,
|
||
|
details: FileDetails(
|
||
|
path: [...widget.bloc.path.value, file.name],
|
||
|
isDirectory: matchingUploadTasks.isEmpty && file.isDirectory,
|
||
|
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,
|
||
|
),
|
||
2 years ago
|
),
|
||
|
);
|
||
|
},
|
||
2 years ago
|
),
|
||
|
],
|
||
2 years ago
|
],
|
||
|
],
|
||
|
],
|
||
|
isLoading: files.loading,
|
||
|
error: files.error,
|
||
2 years ago
|
onRefresh: widget.bloc.refresh,
|
||
2 years ago
|
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>[
|
||
|
SizedBox(
|
||
|
height: 40,
|
||
|
child: InkWell(
|
||
|
onTap: () {
|
||
|
widget.bloc.setPath([]);
|
||
2 years ago
|
},
|
||
2 years ago
|
child: const Icon(Icons.house),
|
||
2 years ago
|
),
|
||
|
),
|
||
2 years ago
|
for (var i = 0; i < pathSnapshot.data!.length; i++) ...[
|
||
|
InkWell(
|
||
|
onTap: () {
|
||
|
widget.bloc.setPath(pathSnapshot.data!.sublist(0, i + 1));
|
||
|
},
|
||
|
child: Text(pathSnapshot.data![i]),
|
||
|
),
|
||
|
],
|
||
|
]
|
||
|
.intersperse(
|
||
|
const Icon(
|
||
|
Icons.keyboard_arrow_right,
|
||
|
size: 40,
|
||
2 years ago
|
),
|
||
2 years ago
|
)
|
||
|
.toList(),
|
||
2 years ago
|
),
|
||
2 years ago
|
),
|
||
2 years ago
|
),
|
||
2 years ago
|
],
|
||
|
),
|
||
|
),
|
||
2 years ago
|
),
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
|
||
|
bool _pathMatchesFile(final List<String> path, final String name) => const ListEquality().equals(
|
||
|
[...widget.bloc.path.value, name],
|
||
|
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: [
|
||
2 years ago
|
RelativeTime(
|
||
|
date: details.lastModified,
|
||
|
),
|
||
2 years ago
|
if (details.size > 0) ...[
|
||
|
const SizedBox(
|
||
|
width: 10,
|
||
|
),
|
||
|
Text(
|
||
|
filesize(details.size, 1),
|
||
|
style: DefaultTextStyle.of(context).style.copyWith(
|
||
|
color: Colors.grey,
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
],
|
||
|
),
|
||
|
leading: SizedBox(
|
||
|
height: 40,
|
||
|
width: 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,
|
||
2 years ago
|
withBackground: true,
|
||
2 years ago
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||
2 years ago
|
),
|
||
|
),
|
||
|
if (details.isFavorite ?? false) ...[
|
||
|
const Align(
|
||
|
alignment: Alignment.bottomRight,
|
||
|
child: Icon(
|
||
|
Icons.star,
|
||
|
size: 14,
|
||
|
color: Colors.yellow,
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
trailing: uploadProgress == null && downloadProgress == null && widget.enableFileActions
|
||
2 years ago
|
? PopupMenuButton<FilesFileAction>(
|
||
2 years ago
|
itemBuilder: (final context) => [
|
||
|
if (details.isFavorite != null) ...[
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.toggleFavorite,
|
||
2 years ago
|
child: Text(
|
||
|
details.isFavorite!
|
||
|
? AppLocalizations.of(context).filesRemoveFromFavorites
|
||
|
: AppLocalizations.of(context).filesAddToFavorites,
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.details,
|
||
2 years ago
|
child: Text(AppLocalizations.of(context).filesDetails),
|
||
|
),
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.rename,
|
||
2 years ago
|
child: Text(AppLocalizations.of(context).rename),
|
||
|
),
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.move,
|
||
2 years ago
|
child: Text(AppLocalizations.of(context).move),
|
||
|
),
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.copy,
|
||
2 years ago
|
child: Text(AppLocalizations.of(context).copy),
|
||
|
),
|
||
2 years ago
|
// TODO: https://github.com/provokateurin/nextcloud-neon/issues/4
|
||
2 years ago
|
if (!details.isDirectory) ...[
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.sync,
|
||
2 years ago
|
child: Text(AppLocalizations.of(context).filesSync),
|
||
|
),
|
||
|
],
|
||
|
PopupMenuItem(
|
||
2 years ago
|
value: FilesFileAction.delete,
|
||
2 years ago
|
child: Text(AppLocalizations.of(context).delete),
|
||
|
),
|
||
|
],
|
||
|
onSelected: (final action) async {
|
||
|
switch (action) {
|
||
2 years ago
|
case FilesFileAction.toggleFavorite:
|
||
2 years ago
|
if (details.isFavorite ?? false) {
|
||
|
widget.filesBloc.removeFavorite(details.path);
|
||
|
} else {
|
||
|
widget.filesBloc.addFavorite(details.path);
|
||
|
}
|
||
|
break;
|
||
2 years ago
|
case FilesFileAction.details:
|
||
2 years ago
|
await Navigator.of(context).push(
|
||
|
MaterialPageRoute(
|
||
|
builder: (final context) => FilesDetailsPage(
|
||
|
bloc: widget.filesBloc,
|
||
|
details: details,
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
break;
|
||
2 years ago
|
case FilesFileAction.rename:
|
||
2 years ago
|
final result = await showRenameDialog(
|
||
|
context: context,
|
||
|
title: details.isDirectory
|
||
|
? AppLocalizations.of(context).filesRenameFolder
|
||
|
: AppLocalizations.of(context).filesRenameFile,
|
||
|
value: details.name,
|
||
|
);
|
||
|
if (result != null) {
|
||
|
widget.filesBloc.rename(details.path, result);
|
||
|
}
|
||
|
break;
|
||
2 years ago
|
case FilesFileAction.move:
|
||
2 years ago
|
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;
|
||
2 years ago
|
case FilesFileAction.copy:
|
||
2 years ago
|
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;
|
||
2 years ago
|
case FilesFileAction.sync:
|
||
2 years ago
|
final sizeWarning = widget.bloc.options.downloadSizeWarning.value;
|
||
|
if (sizeWarning != null && details.size > sizeWarning) {
|
||
2 years ago
|
// ignore: use_build_context_synchronously
|
||
2 years ago
|
if (!(await showConfirmationDialog(
|
||
|
context,
|
||
2 years ago
|
// ignore: use_build_context_synchronously
|
||
2 years ago
|
AppLocalizations.of(context).filesConfirmDownloadSizeWarning(
|
||
|
filesize(sizeWarning),
|
||
|
filesize(details.size),
|
||
|
),
|
||
|
))) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
2 years ago
|
widget.filesBloc.syncFile(details.path);
|
||
|
break;
|
||
2 years ago
|
case FilesFileAction.delete:
|
||
2 years ago
|
// ignore: use_build_context_synchronously
|
||
2 years ago
|
if (await showConfirmationDialog(
|
||
|
context,
|
||
|
details.isDirectory
|
||
2 years ago
|
// ignore: use_build_context_synchronously
|
||
2 years ago
|
? AppLocalizations.of(context).filesDeleteFolderConfirm(details.name)
|
||
2 years ago
|
// ignore: use_build_context_synchronously
|
||
2 years ago
|
: AppLocalizations.of(context).filesDeleteFileConfirm(details.name),
|
||
|
)) {
|
||
|
widget.filesBloc.delete(details.path);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
},
|
||
|
)
|
||
|
: const SizedBox(
|
||
|
width: 48,
|
||
|
height: 48,
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
2 years ago
|
enum FilesFileAction {
|
||
2 years ago
|
toggleFavorite,
|
||
|
details,
|
||
|
rename,
|
||
|
move,
|
||
|
copy,
|
||
|
sync,
|
||
|
delete,
|
||
|
}
|