import 'package:aves/app_mode.dart'; import 'package:aves/model/filters/container/album_group.dart'; import 'package:aves/model/filters/container/dynamic_album.dart'; import 'package:aves/model/filters/covered/stored_album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/grouping/common.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/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/vault_aware.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/filter_group_provider.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_group_dialog.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_stored_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, required Iterable albumChipTypes, required Uri? initialGroup, }) async { final source = context.read(); if (source.targetScope != CollectionSource.fullScope) { await reportService.log('Complete source initialization to pick album'); // source may not be fully initialized in view mode source.canAnalyze = true; await source.init(scope: CollectionSource.fullScope); } return await Navigator.maybeOf(context)?.push( MaterialPageRoute( settings: const RouteSettings(name: _AlbumPickPage.routeName), builder: (context) => _AlbumPickPage( source: source, moveType: moveType, albumChipTypes: albumChipTypes, initialGroup: initialGroup, ), ), ); } class _AlbumPickPage extends StatefulWidget { static const routeName = '/album_pick'; final CollectionSource source; final MoveType? moveType; final Iterable albumChipTypes; final Uri? initialGroup; const _AlbumPickPage({ required this.source, required this.moveType, required this.albumChipTypes, required this.initialGroup, }); @override State<_AlbumPickPage> createState() => _AlbumPickPageState(); } class _AlbumPickPageState extends State<_AlbumPickPage> with FeedbackMixin, VaultAwareMixin { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.pickFilterInternal); CollectionSource get source => widget.source; Iterable get albumChipTypes => widget.albumChipTypes; bool get isPickingGroup => albumChipTypes.length == 1 && albumChipTypes.contains(AlbumChipType.group); String get title { final l10n = context.l10n; if (isPickingGroup) { return l10n.groupPickerTitle; } else { return switch (widget.moveType) { MoveType.copy => l10n.albumPickPageTitleCopy, MoveType.move => l10n.albumPickPageTitleMove, MoveType.export => l10n.albumPickPageTitleExport, MoveType.toBin || MoveType.fromBin || null => l10n.albumPickPageTitlePick, }; } } @override void dispose() { _appBarHeightNotifier.dispose(); _appModeNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableProvider>.value( value: _appModeNotifier, child: MultiProvider( providers: [ ChangeNotifierProvider.value(value: albumGrouping), FilterGroupProvider(initialValue: widget.initialGroup), ], child: Builder( // to access filter group provider from subtree context builder: (context) { return Selector( selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor), builder: (context, s, child) { return StreamBuilder( stream: source.eventBus.on(), builder: (context, snapshot) { final groupUri = context.watch().value; final gridItems = AlbumListPage.getAlbumGridItems(context, source, albumChipTypes, groupUri); return SelectionProvider>( child: QueryProvider( startEnabled: settings.getShowTitleQuery(context.currentRouteName!), child: FilterGridPage( settingsRouteKey: AlbumListPage.routeName, appBar: FilterGridAppBar( source: source, title: title, actionDelegate: AlbumChipSetActionDelegate(gridItems), actionsBuilder: _buildActions, isEmpty: false, appBarHeightNotifier: _appBarHeightNotifier, ), appBarHeightNotifier: _appBarHeightNotifier, sections: AlbumListPage.groupToSections(context, source, gridItems), newFilters: source.getNewAlbumFilters(context), sortFactor: settings.albumSortFactor, showHeaders: settings.albumSectionFactor != AlbumChipSectionFactor.none, selectable: false, emptyBuilder: () => isPickingGroup ? EmptyContent( icon: AIcons.group, text: context.l10n.groupEmpty, ) : EmptyContent( icon: AIcons.album, text: context.l10n.albumEmpty, ), heroType: HeroType.never, floatingActionButton: _buildFab(context), onTileTap: (gridItem, _) async { final filter = gridItem.filter; if (!await unlockFilter(context, filter)) return; switch (filter) { case AlbumGroupFilter _: context.read().value = filter.uri; case StoredAlbumFilter _: case DynamicAlbumFilter _: _pickFilter(context, filter); } }, ), ), ); }, ); }, ); }, ), ), ); } Widget? _buildFab(BuildContext context) { return isPickingGroup ? FloatingActionButton.extended( onPressed: () { final groupUri = context.read().value; final filter = groupUri != null ? albumGrouping.uriToFilter(groupUri) : AlbumGroupFilter.root; if (filter is AlbumBaseFilter) { _pickFilter(context, filter); } }, icon: const Icon(AIcons.apply), label: Text(context.l10n.groupPickerUseThisGroupButton), ) : null; } 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.createGroup: final parentGroupUri = context.read().value; _createGroup(parentGroupUri); 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((v) => v.accessibilityAnimations); final canCreateStoredAlbums = widget.moveType != null; final quickActions = [ if (isPickingGroup) ChipSetAction.createGroup, if (canCreateStoredAlbums) ChipSetAction.createAlbum, ]; // `null` items are converted to dividers final menuActions = [ ...ChipSetActions.general, null, ChipSetAction.toggleTitleSearch, if (canCreateStoredAlbums) ...[ null, ChipSetAction.createVault, ] ]; return [ ...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 _createGroup(Uri? parentGroupUri) async { final uri = await showDialog( context: context, builder: (context) => CreateGroupDialog(grouping: albumGrouping, parentGroupUri: parentGroupUri), routeSettings: const RouteSettings(name: CreateGroupDialog.routeName), ); if (uri == null) return; // wait for the dialog to hide await Future.delayed(ADurations.dialogTransitionLoose * timeDilation); _pickFilter(context, AlbumGroupFilter.empty(uri)); } Future _createAlbum() async { final directory = await showDialog( context: context, builder: (context) => const CreateStoredAlbumDialog(), routeSettings: const RouteSettings(name: CreateStoredAlbumDialog.routeName), ); if (directory == null) return; // wait for the dialog to hide await Future.delayed(ADurations.dialogTransitionLoose * timeDilation); _pickStoredAlbum(context, 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 await Future.delayed(ADurations.dialogTransitionLoose * timeDilation); await vaults.create(details); _pickStoredAlbum(context, details.path); } void _pickStoredAlbum(BuildContext context, String directory) { source.createStoredAlbum(directory); final displayName = source.getStoredAlbumDisplayName(context, directory); _pickFilter(context, StoredAlbumFilter(directory, displayName)); } void _pickFilter(BuildContext context, AlbumBaseFilter filter) async { Navigator.maybeOf(context)?.pop(filter); } }