part of '../neon_files.dart'; 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 createState() => _FilesBrowserViewState(); } class _FilesBrowserViewState extends State { @override void initState() { super.initState(); widget.bloc.errors.listen((final error) { NeonException.showSnackbar(context, error); }); } @override Widget build(final BuildContext context) => ResultBuilder>( stream: widget.bloc.files, builder: (final context, final files) => StreamBuilder>( stream: widget.bloc.path, builder: (final context, final pathSnapshot) => StreamBuilder>( stream: widget.filesBloc.uploadTasks, builder: (final context, final uploadTasksSnapshot) => StreamBuilder>( stream: widget.filesBloc.downloadTasks, builder: (final context, final downloadTasksSnapshot) => !pathSnapshot.hasData || !uploadTasksSnapshot.hasData || !downloadTasksSnapshot.hasData ? Container() : BackButtonListener( onBackButtonPressed: () async { final path = pathSnapshot.data!; if (path.isNotEmpty) { widget.bloc.setPath(path.sublist(0, path.length - 1)); return true; } return false; }, child: 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: NeonListView( scrollKey: 'files-${pathSnapshot.data!.join('/')}', withFloatingActionButton: true, items: [ for (final uploadTask in files.data == null ? [] : uploadTasksSnapshot.data!.where( (final task) => files.data! .where((final file) => _pathMatchesFile(task.path, file.name)) .isEmpty, )) ...[ StreamBuilder( 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, ), uploadProgress: uploadTaskProgressSnapshot.data, 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( stream: matchingUploadTasks.isNotEmpty ? matchingUploadTasks.first.progress : Stream.value(null), builder: (final context, final uploadTaskProgressSnapshot) => StreamBuilder( 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, ), ), ); }, ), ], ], ], ], isLoading: files.loading, 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: [ SizedBox( height: 40, child: InkWell( onTap: () { widget.bloc.setPath([]); }, child: const Icon(Icons.house), ), ), 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, ), ) .toList(), ), ), ), ], ), ), ), ), ), ), ); bool _pathMatchesFile(final List 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: [ RelativeTime( date: details.lastModified, ), 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, 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: Colors.yellow, ), ), ], ], ), ), trailing: uploadProgress == null && downloadProgress == null && widget.enableFileActions ? PopupMenuButton( itemBuilder: (final context) => [ if (details.isFavorite != null) ...[ PopupMenuItem( value: FilesFileAction.toggleFavorite, child: Text( details.isFavorite! ? AppLocalizations.of(context).filesRemoveFromFavorites : AppLocalizations.of(context).filesAddToFavorites, ), ), ], PopupMenuItem( value: FilesFileAction.details, child: Text(AppLocalizations.of(context).filesDetails), ), PopupMenuItem( value: FilesFileAction.rename, child: Text(AppLocalizations.of(context).rename), ), PopupMenuItem( value: FilesFileAction.move, child: Text(AppLocalizations.of(context).move), ), PopupMenuItem( value: FilesFileAction.copy, child: Text(AppLocalizations.of(context).copy), ), // TODO: https://github.com/provokateurin/nextcloud-neon/issues/4 if (!details.isDirectory) ...[ PopupMenuItem( value: FilesFileAction.sync, child: Text(AppLocalizations.of(context).filesSync), ), ], PopupMenuItem( value: FilesFileAction.delete, child: Text(AppLocalizations.of(context).delete), ), ], 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).filesRenameFolder : AppLocalizations.of(context).filesRenameFile, 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?>( 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?>( 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 > sizeWarning) { // ignore: use_build_context_synchronously if (!(await showConfirmationDialog( context, // ignore: use_build_context_synchronously AppLocalizations.of(context).filesConfirmDownloadSizeWarning( filesize(sizeWarning), filesize(details.size), ), ))) { return; } } widget.filesBloc.syncFile(details.path); break; case FilesFileAction.delete: // ignore: use_build_context_synchronously if (await showConfirmationDialog( context, details.isDirectory // ignore: use_build_context_synchronously ? AppLocalizations.of(context).filesDeleteFolderConfirm(details.name) // ignore: use_build_context_synchronously : AppLocalizations.of(context).filesDeleteFileConfirm(details.name), )) { widget.filesBloc.delete(details.path); } break; } }, ) : const SizedBox( width: 48, height: 48, ), ); } enum FilesFileAction { toggleFavorite, details, rename, move, copy, sync, delete, }