From e4be6c931f4049cd734ff4f234bd7f6e3b46e7c2 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Fri, 7 Jul 2023 10:21:26 +0200 Subject: [PATCH] refactor(neon_files): externalise files widgets Signed-off-by: Nikolas Rimikis --- .../app/integration_test/screenshot_test.dart | 2 +- packages/neon/neon_files/lib/neon_files.dart | 2 +- .../neon/neon_files/lib/widgets/actions.dart | 163 ++++++++++++ .../neon_files/lib/widgets/browser_view.dart | 244 +----------------- .../lib/widgets/file_list_tile.dart | 155 +++++++++++ 5 files changed, 329 insertions(+), 237 deletions(-) create mode 100644 packages/neon/neon_files/lib/widgets/actions.dart create mode 100644 packages/neon/neon_files/lib/widgets/file_list_tile.dart diff --git a/packages/app/integration_test/screenshot_test.dart b/packages/app/integration_test/screenshot_test.dart index f108b1c8..14bec1f1 100644 --- a/packages/app/integration_test/screenshot_test.dart +++ b/packages/app/integration_test/screenshot_test.dart @@ -9,7 +9,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:neon/models.dart'; import 'package:neon/neon.dart'; -import 'package:neon_files/neon_files.dart'; +import 'package:neon_files/widgets/actions.dart'; import 'package:shared_preferences/shared_preferences.dart'; class MemorySharedPreferences implements SharedPreferences { diff --git a/packages/neon/neon_files/lib/neon_files.dart b/packages/neon/neon_files/lib/neon_files.dart index dbd95530..af0f57bd 100644 --- a/packages/neon/neon_files/lib/neon_files.dart +++ b/packages/neon/neon_files/lib/neon_files.dart @@ -19,11 +19,11 @@ import 'package:neon/nextcloud.dart'; import 'package:neon/platform.dart'; import 'package:neon/settings.dart'; import 'package:neon/sort_box.dart'; -import 'package:neon/theme.dart'; import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/routes.dart'; +import 'package:neon_files/widgets/file_list_tile.dart'; import 'package:open_file/open_file.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; diff --git a/packages/neon/neon_files/lib/widgets/actions.dart b/packages/neon/neon_files/lib/widgets/actions.dart new file mode 100644 index 00000000..59896af1 --- /dev/null +++ b/packages/neon/neon_files/lib/widgets/actions.dart @@ -0,0 +1,163 @@ +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:neon/utils.dart'; +import 'package:neon_files/l10n/localizations.dart'; +import 'package:neon_files/neon_files.dart'; +import 'package:provider/provider.dart'; + +class FileActions extends StatelessWidget { + const FileActions({ + required this.details, + super.key, + }); + + final FileDetails details; + + Future onSelected(final BuildContext context, final FilesFileAction action) async { + final bloc = Provider.of(context, listen: false); + final browserBloc = bloc.browser; + switch (action) { + case FilesFileAction.toggleFavorite: + if (details.isFavorite ?? false) { + bloc.removeFavorite(details.path); + } else { + bloc.addFavorite(details.path); + } + break; + case FilesFileAction.details: + await Navigator.of(context).push( + MaterialPageRoute( + builder: (final context) => FilesDetailsPage( + bloc: bloc, + 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) { + bloc.rename(details.path, result); + } + break; + case FilesFileAction.move: + final b = bloc.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: bloc, + originalPath: originalPath, + ), + ); + b.dispose(); + if (result != null) { + bloc.move(details.path, result..add(details.name)); + } + break; + case FilesFileAction.copy: + final b = bloc.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: bloc, + originalPath: originalPath, + ), + ); + b.dispose(); + if (result != null) { + bloc.copy(details.path, result..add(details.name)); + } + break; + case FilesFileAction.sync: + final sizeWarning = browserBloc.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; + } + } + bloc.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), + )) { + bloc.delete(details.path); + } + break; + } + } + + @override + Widget build(final BuildContext context) => PopupMenuButton( + 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/provokateurin/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 => onSelected(context, action), + ); +} + +enum FilesFileAction { + toggleFavorite, + details, + rename, + move, + copy, + sync, + delete, +} diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 543529f9..4540d8de 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -73,7 +73,7 @@ class _FilesBrowserViewState extends State { builder: (final context, final uploadTaskProgressSnapshot) => !uploadTaskProgressSnapshot.hasData ? const SizedBox() - : _buildFile( + : FileListTile( context: context, details: FileDetails( path: uploadTask.path, @@ -87,6 +87,8 @@ class _FilesBrowserViewState extends State { ), uploadProgress: uploadTaskProgressSnapshot.data, downloadProgress: null, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, ), ), ], @@ -101,15 +103,14 @@ class _FilesBrowserViewState extends State { .where((final task) => _pathMatchesFile(task.path, file.name)); return StreamBuilder( - stream: matchingUploadTasks.isNotEmpty - ? matchingUploadTasks.first.progress - : Stream.value(null), + stream: + matchingUploadTasks.isNotEmpty ? matchingUploadTasks.first.progress : null, builder: (final context, final uploadTaskProgressSnapshot) => StreamBuilder( stream: matchingDownloadTasks.isNotEmpty ? matchingDownloadTasks.first.progress - : Stream.value(null), - builder: (final context, final downloadTaskProgressSnapshot) => _buildFile( + : null, + builder: (final context, final downloadTaskProgressSnapshot) => FileListTile( context: context, details: FileDetails( path: [...widget.bloc.path.value, file.name], @@ -127,6 +128,8 @@ class _FilesBrowserViewState extends State { ), uploadProgress: uploadTaskProgressSnapshot.data, downloadProgress: downloadTaskProgressSnapshot.data, + enableFileActions: widget.enableFileActions, + onPickFile: widget.onPickFile, ), ), ); @@ -209,233 +212,4 @@ class _FilesBrowserViewState extends State { [...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: [ - 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( - 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?>( - 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 != 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, } diff --git a/packages/neon/neon_files/lib/widgets/file_list_tile.dart b/packages/neon/neon_files/lib/widgets/file_list_tile.dart new file mode 100644 index 00000000..400dec28 --- /dev/null +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -0,0 +1,155 @@ +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:neon/widgets.dart'; +import 'package:neon_files/neon_files.dart'; +import 'package:neon_files/widgets/actions.dart'; +import 'package:provider/provider.dart'; + +class FileListTile extends StatelessWidget { + const FileListTile({ + required this.context, + required this.details, + required this.uploadProgress, + required this.downloadProgress, + this.enableFileActions = true, + this.onPickFile, + super.key, + }); + + final BuildContext context; + final FileDetails details; + final int? uploadProgress; + final int? downloadProgress; + final bool enableFileActions; + final Function(FileDetails)? onPickFile; + + bool get _isUploading => uploadProgress != null; + + bool get _hasProgress => uploadProgress != null || downloadProgress != null; + + double? get _progress { + if (!_hasProgress) { + return null; + } + + return (uploadProgress ?? downloadProgress)! / 100; + } + + @override + Widget build(final BuildContext context) { + final bloc = Provider.of(context); + final browserBloc = bloc.browser; + + // When the ETag is null it means we are uploading this file right now + final onTap = details.isDirectory || details.etag != null + ? () { + if (details.isDirectory) { + browserBloc.setPath(details.path); + } else { + onPickFile?.call(details); + } + } + : null; + + return ListTile( + onTap: onTap, + 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: _FileIcon( + hasProgress: _hasProgress, + isUploading: _isUploading, + progress: _progress, + details: details, + ), + trailing: _hasProgress && enableFileActions + ? FileActions(details: details) + : const SizedBox.square( + dimension: 48, + ), + ); + } +} + +class _FileIcon extends StatelessWidget { + const _FileIcon({ + required this.details, + required this.hasProgress, + required this.isUploading, + this.progress, + }); + + final bool hasProgress; + final bool isUploading; + final double? progress; + final FileDetails details; + + @override + Widget build(final BuildContext context) { + final bloc = Provider.of(context); + + Widget icon = Center( + child: hasProgress + ? Column( + children: [ + Icon( + isUploading ? MdiIcons.upload : MdiIcons.download, + color: Theme.of(context).colorScheme.primary, + ), + LinearProgressIndicator( + value: progress, + ), + ], + ) + : FilePreview( + bloc: bloc, + details: details, + withBackground: true, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + ); + + if (details.isFavorite ?? false) { + icon = Stack( + children: [ + icon, + const Align( + alignment: Alignment.bottomRight, + child: Icon( + Icons.star, + size: 14, + color: Colors.yellow, + ), + ) + ], + ); + } + + return SizedBox.square( + dimension: 40, + child: icon, + ); + } +}