import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc/ls_trashbin.dart'; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; import 'package:nc_photos/use_case/restore_trashbin.dart'; import 'package:nc_photos/widget/builder/photo_list_item_builder.dart'; import 'package:nc_photos/widget/empty_list_indicator.dart'; import 'package:nc_photos/widget/handler/remove_selection_handler.dart'; import 'package:nc_photos/widget/photo_list_item.dart'; import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util; import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart'; import 'package:nc_photos/widget/selection_app_bar.dart'; import 'package:nc_photos/widget/trashbin_viewer.dart'; import 'package:nc_photos/widget/zoom_menu_button.dart'; import 'package:np_async/np_async.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/object_util.dart'; part 'trashbin_browser.g.dart'; class TrashbinBrowserArguments { TrashbinBrowserArguments(this.account); final Account account; } class TrashbinBrowser extends StatefulWidget { static const routeName = "/trashbin-browser"; static Route buildRoute(TrashbinBrowserArguments args) => MaterialPageRoute( builder: (context) => TrashbinBrowser.fromArgs(args), ); const TrashbinBrowser({ super.key, required this.account, }); TrashbinBrowser.fromArgs(TrashbinBrowserArguments args, {Key? key}) : this( key: key, account: args.account, ); @override createState() => _TrashbinBrowserState(); final Account account; } @npLog class _TrashbinBrowserState extends State with SelectableItemStreamListMixin { @override initState() { super.initState(); _initBloc(); _thumbZoomLevel = Pref().getAlbumBrowserZoomLevelOr(0); } @override build(BuildContext context) { return Scaffold( body: BlocListener( bloc: _bloc, listener: (context, state) => _onStateChange(context, state), child: BlocBuilder( bloc: _bloc, builder: (context, state) => _buildContent(context, state), ), ), ); } @override onItemTap(SelectableItem item, int index) { item.as()?.run((fileItem) { Navigator.pushNamed( context, TrashbinViewer.routeName, arguments: TrashbinViewerArguments( widget.account, _backingFiles, fileItem.fileIndex), ); }); } void _initBloc() { _bloc = LsTrashbinBloc.of(widget.account); if (_bloc.state is LsTrashbinBlocInit) { _log.info("[_initBloc] Initialize bloc"); _reqQuery(); } else { // process the current state WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _onStateChange(context, _bloc.state); }); } _reqQuery(); }); } } Widget _buildContent(BuildContext context, LsTrashbinBlocState state) { if (state is LsTrashbinBlocSuccess && !_buildItemQueue.isProcessing && itemStreamListItems.isEmpty) { return Column( children: [ AppBar( title: Text(L10n.global().albumTrashLabel), elevation: 0, ), Expanded( child: EmptyListIndicator( icon: Icons.delete_outlined, text: L10n.global().listEmptyText, ), ), ], ); } else { return Stack( children: [ buildItemStreamListOuter( context, child: CustomScrollView( slivers: [ _buildAppBar(context), buildItemStreamList( maxCrossAxisExtent: _thumbSize.toDouble(), ), ], ), ), if (state is LsTrashbinBlocLoading || _buildItemQueue.isProcessing) const Align( alignment: Alignment.bottomCenter, child: LinearProgressIndicator(), ), ], ); } } Widget _buildAppBar(BuildContext context) { if (isSelectionMode) { return _buildSelectionAppBar(context); } else { return _buildNormalAppBar(context); } } Widget _buildSelectionAppBar(BuildContext context) { return SelectionAppBar( count: selectedListItems.length, onClosePressed: () { setState(() { clearSelectedItems(); }); }, actions: [ IconButton( icon: const Icon(Icons.restore_outlined), tooltip: L10n.global().restoreTooltip, onPressed: () { _onSelectionAppBarRestorePressed(); }, ), PopupMenuButton<_SelectionAppBarMenuOption>( tooltip: MaterialLocalizations.of(context).moreButtonTooltip, itemBuilder: (context) => [ PopupMenuItem( value: _SelectionAppBarMenuOption.delete, child: Text(L10n.global().deletePermanentlyTooltip), ), ], onSelected: (option) { switch (option) { case _SelectionAppBarMenuOption.delete: _onSelectionAppBarDeletePressed(context); break; default: _log.shout("[_buildSelectionAppBar] Unknown option: $option"); break; } }, ) ], ); } Widget _buildNormalAppBar(BuildContext context) { return SliverAppBar( title: Text(L10n.global().albumTrashLabel), floating: true, actions: [ ZoomMenuButton( initialZoom: _thumbZoomLevel, minZoom: 0, maxZoom: 2, onZoomChanged: (value) { setState(() { _thumbZoomLevel = value.round(); }); Pref().setAlbumBrowserZoomLevel(_thumbZoomLevel); }, ), PopupMenuButton<_AppBarMenuOption>( tooltip: MaterialLocalizations.of(context).moreButtonTooltip, itemBuilder: (context) => [ PopupMenuItem( value: _AppBarMenuOption.empty, child: Text(L10n.global().emptyTrashbinTooltip), ), ], onSelected: (option) { switch (option) { case _AppBarMenuOption.empty: _onEmptyTrashPressed(context); break; default: _log.shout("[_buildNormalAppBar] Unknown option: $option"); break; } }, ), ], ); } void _onStateChange(BuildContext context, LsTrashbinBlocState state) { if (state is LsTrashbinBlocInit) { itemStreamListItems = []; } else if (state is LsTrashbinBlocSuccess || state is LsTrashbinBlocLoading) { _transformItems(state.items); } else if (state is LsTrashbinBlocFailure) { _transformItems(state.items); SnackBarManager().showSnackBarForException(state.exception); } else if (state is LsTrashbinBlocInconsistent) { _reqQuery(); } } Future _onEmptyTrashPressed(BuildContext context) async { unawaited( showDialog( context: context, builder: (_) => AlertDialog( title: Text(L10n.global().emptyTrashbinConfirmationDialogTitle), content: Text(L10n.global().emptyTrashbinConfirmationDialogContent), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); _deleteFiles(_backingFiles); }, child: Text(L10n.global().confirmButtonLabel), ), ], ), ), ); } Future _onSelectionAppBarRestorePressed() async { SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global() .restoreSelectedProcessingNotification(selectedListItems.length)), duration: k.snackBarDurationShort, )); final selection = selectedListItems .whereType() .map((e) => e.file) .toList(); setState(() { clearSelectedItems(); }); final c = KiwiContainer().resolve(); final selectedFiles = await InflateFileDescriptor(c)(widget.account, selection); final failures = []; for (final f in selectedFiles) { try { await RestoreTrashbin(c)(widget.account, f); } catch (e, stacktrace) { _log.shout( "[_onSelectionAppBarRestorePressed] Failed while restoring file: ${logFilename(f.path)}", e, stacktrace); failures.add(f); } } if (failures.isEmpty) { SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global().restoreSelectedSuccessNotification), duration: k.snackBarDurationNormal, )); } else { SnackBarManager().showSnackBar(SnackBar( content: Text( L10n.global().restoreSelectedFailureNotification(failures.length)), duration: k.snackBarDurationNormal, )); } } Future _onSelectionAppBarDeletePressed(BuildContext context) async { unawaited( showDialog( context: context, builder: (_) => AlertDialog( title: Text(L10n.global().deletePermanentlyConfirmationDialogTitle), content: Text(L10n.global().deletePermanentlyConfirmationDialogContent), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); _deleteSelected(); }, child: Text(L10n.global().confirmButtonLabel), ), ], ), ), ); } void _transformItems(List files) { _buildItemQueue.addJob( PhotoListItemBuilderArguments( widget.account, files, sorter: _fileSorter, locale: language_util.getSelectedLocale() ?? PlatformDispatcher.instance.locale, ), buildPhotoListItem, (result) { if (mounted) { setState(() { _backingFiles = result.backingFiles.cast(); itemStreamListItems = result.listItems; }); } }, ); } Future _deleteSelected() async { final selectedFiles = selectedListItems .whereType() .map((e) => e.file) .toList(); setState(() { clearSelectedItems(); }); return _deleteFiles(selectedFiles); } Future _deleteFiles(List files) async { await RemoveSelectionHandler( filesController: context.read().filesController, )( account: widget.account, selection: files, shouldCleanupAlbum: false, ); } void _reqQuery() { _bloc.add(LsTrashbinBlocQuery(widget.account)); } late LsTrashbinBloc _bloc; var _backingFiles = []; final _buildItemQueue = ComputeQueue(); var _thumbZoomLevel = 0; int get _thumbSize => photo_list_util.getThumbSize(_thumbZoomLevel); } enum _AppBarMenuOption { empty, } enum _SelectionAppBarMenuOption { delete, } int _fileSorter(FileDescriptor fdA, FileDescriptor fdB) { final a = fdA as File; final b = fdB as File; if (a.trashbinDeletionTime == null && b.trashbinDeletionTime == null) { // ? return 0; } else if (a.trashbinDeletionTime == null) { return -1; } else if (b.trashbinDeletionTime == null) { return 1; } else { return b.trashbinDeletionTime!.compareTo(a.trashbinDeletionTime!); } }