import 'package:aves/app_mode.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/vaults/details.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/edit_vault_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/album_set.dart'; import 'package:aves/widgets/filter_grids/common/app_bar.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; Future pickAlbum({ required BuildContext context, required MoveType? moveType, }) async { final source = context.read(); if (source.initState != SourceInitializationState.full) { await reportService.log('Complete source initialization to pick album'); // source may not be fully initialized in view mode await source.init(); } final filter = await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: _AlbumPickPage.routeName), builder: (context) => _AlbumPickPage(source: source, moveType: moveType), ), ); return filter?.album; } class _AlbumPickPage extends StatefulWidget { static const routeName = '/album_pick'; final CollectionSource source; final MoveType? moveType; const _AlbumPickPage({ required this.source, required this.moveType, }); @override State<_AlbumPickPage> createState() => _AlbumPickPageState(); } class _AlbumPickPageState extends State<_AlbumPickPage> { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.pickFilterInternal); CollectionSource get source => widget.source; String get title { final l10n = context.l10n; return switch (widget.moveType) { MoveType.copy => l10n.albumPickPageTitleCopy, MoveType.move => l10n.albumPickPageTitleMove, MoveType.export => l10n.albumPickPageTitleExport, MoveType.toBin || MoveType.fromBin || null => l10n.albumPickPageTitlePick, }; } static const _quickActions = [ ChipSetAction.createAlbum, ]; // `null` items are converted to dividers static const _menuActions = [ ...ChipSetActions.general, null, ChipSetAction.toggleTitleSearch, null, ChipSetAction.createVault, ]; @override void dispose() { _appBarHeightNotifier.dispose(); _appModeNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableProvider>.value( value: _appModeNotifier, child: Selector( selector: (context, s) => (s.albumGroupFactor, s.albumSortFactor), builder: (context, s, child) { return StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { final gridItems = AlbumListPage.getAlbumGridItems(context, source); return SelectionProvider>( child: QueryProvider( enabled: settings.showAlbumPickQuery, child: FilterGridPage( settingsRouteKey: AlbumListPage.routeName, appBar: FilterGridAppBar( source: source, title: title, actionDelegate: _AlbumChipSetPickActionDelegate(gridItems), actionsBuilder: _buildActions, isEmpty: false, appBarHeightNotifier: _appBarHeightNotifier, ), appBarHeightNotifier: _appBarHeightNotifier, sections: AlbumListPage.groupToSections(context, source, gridItems), newFilters: source.getNewAlbumFilters(context), sortFactor: settings.albumSortFactor, showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none, selectable: false, applyQuery: AlbumListPage.applyQuery, emptyBuilder: () => EmptyContent( icon: AIcons.album, text: context.l10n.albumEmpty, ), heroType: HeroType.never, ), ), ); }, ); }, ), ); } List _buildActions( BuildContext context, AppMode appMode, Selection> selection, AlbumChipSetActionDelegate actionDelegate, ) { final itemCount = actionDelegate.allItems.length; final isSelecting = selection.isSelecting; final selectedItems = selection.selectedItems; final selectedFilters = selectedItems.map((v) => v.filter).toSet(); bool isVisible(ChipSetAction action) => actionDelegate.isVisible( action, appMode: appMode, isSelecting: isSelecting, itemCount: itemCount, selectedFilters: selectedFilters, ); void onActionSelected(ChipSetAction action) { switch (action) { case ChipSetAction.createAlbum: _createAlbum(); case ChipSetAction.createVault: _createVault(); default: actionDelegate.onActionSelected(context, action); } } return settings.useTvLayout ? _buildTelevisionActions( context: context, isVisible: isVisible, onActionSelected: onActionSelected, ) : _buildMobileActions( context: context, isVisible: isVisible, onActionSelected: onActionSelected, ); } List _buildTelevisionActions({ required BuildContext context, required bool Function(ChipSetAction action) isVisible, required void Function(ChipSetAction action) onActionSelected, }) { return [ ...ChipSetActions.general, ].where(isVisible).map((action) { return CaptionedButton( icon: action.getIcon(), caption: action.getText(context), onPressed: () => onActionSelected(action), ); }).toList(); } List _buildMobileActions({ required BuildContext context, required bool Function(ChipSetAction action) isVisible, required void Function(ChipSetAction action) onActionSelected, }) { final animations = context.select((s) => s.accessibilityAnimations); return [ if (widget.moveType != null) ..._quickActions.where(isVisible).map( (action) => IconButton( icon: action.getIcon(), onPressed: () => onActionSelected(action), tooltip: action.getText(context), ), ), PopupMenuButton( itemBuilder: (context) { return _menuActions.where((v) => v == null || isVisible(v)).map((action) { if (action == null) return const PopupMenuDivider(); return FilterGridAppBar.toMenuItem(context, action, enabled: true); }).toList(); }, onSelected: (action) async { // remove focus, if any, to prevent the keyboard from showing up // after the user is done with the popup menu FocusManager.instance.primaryFocus?.unfocus(); // wait for the popup menu to hide before proceeding with the action await Future.delayed(animations.popUpAnimationDelay * timeDilation); onActionSelected(action); }, popUpAnimationStyle: animations.popUpAnimationStyle, ), ]; } Future _createAlbum() async { final directory = await showDialog( context: context, builder: (context) => const CreateAlbumDialog(), routeSettings: const RouteSettings(name: CreateAlbumDialog.routeName), ); if (directory == null) return; // wait for the dialog to hide as applying the change may block the UI await Future.delayed(ADurations.dialogTransitionLoose * timeDilation); _pickAlbum(directory); } Future _createVault() async { final l10n = context.l10n; if (!await showSkippableConfirmationDialog( context: context, type: ConfirmationDialog.createVault, message: l10n.newVaultWarningDialogMessage, confirmationButtonLabel: l10n.continueButtonLabel, )) return; final details = await showDialog( context: context, builder: (context) => const EditVaultDialog(), routeSettings: const RouteSettings(name: EditVaultDialog.routeName), ); if (details == null) return; // wait for the dialog to hide as applying the change may block the UI await Future.delayed(ADurations.dialogTransitionLoose * timeDilation); await vaults.create(details); _pickAlbum(details.path); } void _pickAlbum(String directory) { source.createAlbum(directory); final filter = AlbumFilter(directory, source.getAlbumDisplayName(context, directory)); Navigator.maybeOf(context)?.pop(filter); } } class _AlbumChipSetPickActionDelegate extends AlbumChipSetActionDelegate { _AlbumChipSetPickActionDelegate(super.items); @override void onActionSelected(BuildContext context, ChipSetAction action) { if (action == ChipSetAction.toggleTitleSearch) { settings.showAlbumPickQuery = !settings.showAlbumPickQuery; } super.onActionSelected(context, action); } }