5 changed files with 329 additions and 237 deletions
			
			
		@ -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<void> onSelected(final BuildContext context, final FilesFileAction action) async { | 
				
			||||||
 | 
					    final bloc = Provider.of<FilesBloc>(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<List<String>?>( | 
				
			||||||
 | 
					          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<List<String>?>( | 
				
			||||||
 | 
					          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<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/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, | 
				
			||||||
 | 
					} | 
				
			||||||
@ -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<FilesBloc>(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<FilesBloc>(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, | 
				
			||||||
 | 
					    ); | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue