harmonized action menus

This commit is contained in:
Thibault Deckers 2021-11-02 10:06:21 +09:00
parent 2b90d7cca8
commit 4ee510d7a0
9 changed files with 278 additions and 205 deletions

View file

@ -1,6 +1,6 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
enum ChipSetAction {
// general
@ -9,20 +9,50 @@ enum ChipSetAction {
select,
selectAll,
selectNone,
// browsing
search,
createAlbum,
// all or filter selection
// browsing or selecting
map,
stats,
// single/multiple filter selection
// selecting (single/multiple filters)
delete,
hide,
pin,
unpin,
// single filter selection
// selecting (single filter)
rename,
setCover,
}
class ChipSetActions {
static const general = [
ChipSetAction.sort,
ChipSetAction.group,
ChipSetAction.select,
ChipSetAction.selectAll,
ChipSetAction.selectNone,
];
static const browsing = [
ChipSetAction.search,
ChipSetAction.createAlbum,
ChipSetAction.map,
ChipSetAction.stats,
];
static const selection = [
ChipSetAction.setCover,
ChipSetAction.pin,
ChipSetAction.unpin,
ChipSetAction.delete,
ChipSetAction.rename,
ChipSetAction.hide,
ChipSetAction.map,
ChipSetAction.stats,
];
}
extension ExtraChipSetAction on ChipSetAction {
String getText(BuildContext context) {
switch (this) {
@ -37,13 +67,17 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.menuActionSelectAll;
case ChipSetAction.selectNone:
return context.l10n.menuActionSelectNone;
// browsing
case ChipSetAction.search:
return MaterialLocalizations.of(context).searchFieldLabel;
case ChipSetAction.createAlbum:
return context.l10n.chipActionCreateAlbum;
// browsing or selecting
case ChipSetAction.map:
return context.l10n.menuActionMap;
case ChipSetAction.stats:
return context.l10n.menuActionStats;
case ChipSetAction.createAlbum:
return context.l10n.chipActionCreateAlbum;
// single/multiple filters
// selecting (single/multiple filters)
case ChipSetAction.delete:
return context.l10n.chipActionDelete;
case ChipSetAction.hide:
@ -52,7 +86,7 @@ extension ExtraChipSetAction on ChipSetAction {
return context.l10n.chipActionPin;
case ChipSetAction.unpin:
return context.l10n.chipActionUnpin;
// single filter
// selecting (single filter)
case ChipSetAction.rename:
return context.l10n.chipActionRename;
case ChipSetAction.setCover:
@ -77,13 +111,17 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.selected;
case ChipSetAction.selectNone:
return AIcons.unselected;
// browsing
case ChipSetAction.search:
return AIcons.search;
case ChipSetAction.createAlbum:
return AIcons.add;
// browsing or selecting
case ChipSetAction.map:
return AIcons.map;
case ChipSetAction.stats:
return AIcons.stats;
case ChipSetAction.createAlbum:
return AIcons.add;
// single/multiple filters
// selecting (single/multiple filters)
case ChipSetAction.delete:
return AIcons.delete;
case ChipSetAction.hide:
@ -92,7 +130,7 @@ extension ExtraChipSetAction on ChipSetAction {
return AIcons.pin;
case ChipSetAction.unpin:
return AIcons.unpin;
// single filter
// selecting (single filter)
case ChipSetAction.rename:
return AIcons.rename;
case ChipSetAction.setCover:

View file

@ -201,12 +201,12 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
final browsingQuickActions = settings.collectionBrowsingQuickActions;
final selectionQuickActions = settings.collectionSelectionQuickActions;
final quickActions = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
(action) => _toActionButton(action, enabled: canApply(action)),
);
return [
...quickActions,
...quickActionButtons,
MenuIconTheme(
child: PopupMenuButton<EntrySetAction>(
// key is expected by test driver
@ -252,7 +252,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
onSelected: (action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
await _onCollectionActionSelected(action);
await _onActionSelected(action);
},
),
),
@ -262,16 +262,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
// key is expected by test driver (e.g. 'menu-sort', 'menu-group', 'menu-map')
Key _getActionKey(EntrySetAction action) => Key('menu-${action.toString().substring('EntrySetAction.'.length)}');
Widget _toActionButton(EntrySetAction action, {bool enabled = true}) {
Widget _toActionButton(EntrySetAction action, {required bool enabled}) {
return IconButton(
key: _getActionKey(action),
icon: action.getIcon(),
onPressed: enabled ? () => _onCollectionActionSelected(action) : null,
onPressed: enabled ? () => _onActionSelected(action) : null,
tooltip: action.getText(context),
);
}
PopupMenuItem<EntrySetAction> _toMenuItem(EntrySetAction action, {bool enabled = true}) {
PopupMenuItem<EntrySetAction> _toMenuItem(EntrySetAction action, {required bool enabled}) {
return PopupMenuItem(
key: _getActionKey(action),
value: action,
@ -339,7 +339,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
}
Future<void> _onCollectionActionSelected(EntrySetAction action) async {
Future<void> _onActionSelected(EntrySetAction action) async {
switch (action) {
// general
case EntrySetAction.sort:

View file

@ -151,13 +151,13 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
_share(context);
break;
case EntrySetAction.delete:
_showDeleteDialog(context);
_delete(context);
break;
case EntrySetAction.copy:
_moveSelection(context, moveType: MoveType.copy);
_move(context, moveType: MoveType.copy);
break;
case EntrySetAction.move:
_moveSelection(context, moveType: MoveType.move);
_move(context, moveType: MoveType.move);
break;
case EntrySetAction.rescan:
_rescan(context);
@ -203,7 +203,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
selection.browse();
}
Future<void> _showDeleteDialog(BuildContext context) async {
Future<void> _delete(BuildContext context) async {
final source = context.read<CollectionSource>();
final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection);
@ -256,7 +256,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
);
}
Future<void> _moveSelection(BuildContext context, {required MoveType moveType}) async {
Future<void> _move(BuildContext context, {required MoveType moveType}) async {
final l10n = context.l10n;
final source = context.read<CollectionSource>();
final selection = context.read<Selection<AvesEntry>>();

View file

@ -42,7 +42,6 @@ class AlbumListPage extends StatelessWidget {
source: source,
title: context.l10n.albumPageTitle,
sortFactor: settings.albumSortFactor,
groupable: true,
showHeaders: settings.albumGroupFactor != AlbumChipGroupFactor.none,
actionDelegate: AlbumChipSetActionDelegate(gridItems),
filterSections: groupToSections(context, source, gridItems),

View file

@ -1,10 +1,12 @@
import 'dart:io';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart';
@ -39,29 +41,54 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
set sortFactor(ChipSortFactor factor) => settings.albumSortFactor = factor;
@override
bool isVisible(ChipSetAction action, Set<AlbumFilter> filters) {
bool isVisible(
ChipSetAction action, {
required AppMode appMode,
required bool isSelecting,
required int itemCount,
required Set<AlbumFilter> selectedFilters,
}) {
switch (action) {
case ChipSetAction.group:
return true;
case ChipSetAction.createAlbum:
return appMode == AppMode.main && !isSelecting;
case ChipSetAction.delete:
case ChipSetAction.rename:
return true;
return appMode == AppMode.main && isSelecting;
default:
return super.isVisible(action, filters);
return super.isVisible(
action,
appMode: appMode,
isSelecting: isSelecting,
itemCount: itemCount,
selectedFilters: selectedFilters,
);
}
}
@override
bool canApply(ChipSetAction action, Set<AlbumFilter> filters) {
bool canApply(
ChipSetAction action, {
required bool isSelecting,
required int itemCount,
required Set<AlbumFilter> selectedFilters,
}) {
switch (action) {
case ChipSetAction.rename:
{
if (filters.length != 1) return false;
if (selectedFilters.length != 1) return false;
// do not allow renaming volume root
final dir = VolumeRelativeDirectory.fromPath(filters.first.album);
final dir = VolumeRelativeDirectory.fromPath(selectedFilters.first.album);
return dir != null && dir.relativeDir.isNotEmpty;
}
default:
return super.canApply(action, filters);
return super.canApply(
action,
isSelecting: isSelecting,
itemCount: itemCount,
selectedFilters: selectedFilters,
);
}
}
@ -70,18 +97,18 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
switch (action) {
// general
case ChipSetAction.group:
_showGroupDialog(context);
_group(context);
break;
case ChipSetAction.createAlbum:
_createAlbum(context);
break;
// single/multiple filters
case ChipSetAction.delete:
_showDeleteDialog(context, filters);
_delete(context, filters);
break;
// single filter
case ChipSetAction.rename:
_showRenameDialog(context, filters.first);
_rename(context, filters.first);
break;
default:
break;
@ -89,7 +116,9 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
super.onActionSelected(context, filters, action);
}
Future<void> _showGroupDialog(BuildContext context) async {
void _browse(BuildContext context) => context.read<Selection<FilterGridItem<AlbumFilter>>>().browse();
Future<void> _group(BuildContext context) async {
final factor = await showDialog<AlbumChipGroupFactor>(
context: context,
builder: (context) => AvesSelectionDialog<AlbumChipGroupFactor>(
@ -129,7 +158,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
}
}
Future<void> _showDeleteDialog(BuildContext context, Set<AlbumFilter> filters) async {
Future<void> _delete(BuildContext context, Set<AlbumFilter> filters) async {
final l10n = context.l10n;
final messenger = ScaffoldMessenger.of(context);
final source = context.read<CollectionSource>();
@ -173,6 +202,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
onDone: (processed) async {
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet();
await source.removeEntries(deletedUris);
_browse(context);
source.resumeMonitoring();
final deletedCount = deletedUris.length;
@ -187,7 +217,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
);
}
Future<void> _showRenameDialog(BuildContext context, AlbumFilter filter) async {
Future<void> _rename(BuildContext context, AlbumFilter filter) async {
final l10n = context.l10n;
final messenger = ScaffoldMessenger.of(context);
final source = context.read<CollectionSource>();
@ -238,6 +268,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> {
onDone: (processed) async {
final movedOps = processed.where((e) => e.success).toSet();
await source.renameAlbum(album, destinationAlbum, todoEntries, movedOps);
_browse(context);
source.resumeMonitoring();
final movedCount = movedOps.length;

View file

@ -1,3 +1,4 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/covers.dart';
import 'package:aves/model/entry.dart';
@ -16,6 +17,7 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/dialogs/cover_selection_dialog.dart';
import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/stats/stats_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -30,23 +32,63 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
set sortFactor(ChipSortFactor factor);
bool isVisible(ChipSetAction action, Set<T> filters) {
final hasSelection = filters.isNotEmpty;
bool isVisible(
ChipSetAction action, {
required AppMode appMode,
required bool isSelecting,
required int itemCount,
required Set<T> selectedFilters,
}) {
final selectedItemCount = selectedFilters.length;
final hasSelection = selectedFilters.isNotEmpty;
switch (action) {
// general
case ChipSetAction.sort:
return true;
case ChipSetAction.group:
return false;
case ChipSetAction.select:
return appMode.canSelect && !isSelecting;
case ChipSetAction.selectAll:
return isSelecting && selectedItemCount < itemCount;
case ChipSetAction.selectNone:
return isSelecting && selectedItemCount == itemCount;
// browsing
case ChipSetAction.search:
return appMode.canSearch && !isSelecting;
case ChipSetAction.createAlbum:
return false;
// browsing or selecting
case ChipSetAction.map:
case ChipSetAction.stats:
return appMode == AppMode.main;
// selecting (single/multiple filters)
case ChipSetAction.delete:
return false;
case ChipSetAction.hide:
return appMode == AppMode.main;
case ChipSetAction.pin:
return !hasSelection || !settings.pinnedFilters.containsAll(selectedFilters);
case ChipSetAction.unpin:
return hasSelection && settings.pinnedFilters.containsAll(selectedFilters);
// selecting (single filter)
case ChipSetAction.rename:
return false;
case ChipSetAction.pin:
return !hasSelection || !settings.pinnedFilters.containsAll(filters);
case ChipSetAction.unpin:
return hasSelection && settings.pinnedFilters.containsAll(filters);
default:
return true;
case ChipSetAction.setCover:
return appMode == AppMode.main;
}
}
bool canApply(ChipSetAction action, Set<T> filters) {
bool canApply(
ChipSetAction action, {
required bool isSelecting,
required int itemCount,
required Set<T> selectedFilters,
}) {
final selectedItemCount = selectedFilters.length;
final hasItems = itemCount > 0;
final hasSelection = selectedItemCount > 0;
switch (action) {
// general
case ChipSetAction.sort:
@ -54,20 +96,24 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.select:
case ChipSetAction.selectAll:
case ChipSetAction.selectNone:
case ChipSetAction.map:
case ChipSetAction.stats:
// browsing
case ChipSetAction.search:
case ChipSetAction.createAlbum:
return true;
// single/multiple filters
// browsing or selecting
case ChipSetAction.map:
case ChipSetAction.stats:
return (!isSelecting && hasItems) || (isSelecting && hasSelection);
// selecting (single/multiple filters)
case ChipSetAction.delete:
case ChipSetAction.hide:
case ChipSetAction.pin:
case ChipSetAction.unpin:
return filters.isNotEmpty;
// single filter
return hasSelection;
// selecting (single filter)
case ChipSetAction.rename:
case ChipSetAction.setCover:
return filters.length == 1;
return selectedItemCount == 1;
}
}
@ -77,11 +123,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.sort:
_showSortDialog(context);
break;
case ChipSetAction.map:
_goToMap(context, filters);
break;
case ChipSetAction.stats:
_goToStats(context, filters);
case ChipSetAction.group:
break;
case ChipSetAction.select:
context.read<Selection<FilterGridItem<T>>>().select();
@ -92,25 +134,44 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.selectNone:
context.read<Selection<FilterGridItem<T>>>().clearSelection();
break;
// single/multiple filters
case ChipSetAction.pin:
settings.pinnedFilters = settings.pinnedFilters..addAll(filters);
// browsing
case ChipSetAction.search:
_goToSearch(context);
break;
case ChipSetAction.unpin:
settings.pinnedFilters = settings.pinnedFilters..removeAll(filters);
case ChipSetAction.createAlbum:
break;
// browsing or selecting
case ChipSetAction.map:
_goToMap(context, filters);
break;
case ChipSetAction.stats:
_goToStats(context, filters);
break;
// selecting (single/multiple filters)
case ChipSetAction.delete:
break;
case ChipSetAction.hide:
_hide(context, filters);
break;
// single filter
case ChipSetAction.setCover:
_showCoverSelectionDialog(context, filters.first);
case ChipSetAction.pin:
settings.pinnedFilters = settings.pinnedFilters..addAll(filters);
_browse(context);
break;
default:
case ChipSetAction.unpin:
settings.pinnedFilters = settings.pinnedFilters..removeAll(filters);
_browse(context);
break;
// selecting (single filter)
case ChipSetAction.rename:
break;
case ChipSetAction.setCover:
_setCover(context, filters.first);
break;
}
}
void _browse(BuildContext context) => context.read<Selection<FilterGridItem<T>>>().browse();
Iterable<AvesEntry> _selectedEntries(BuildContext context, Set<dynamic> filters) {
final source = context.read<CollectionSource>();
final visibleEntries = source.visibleEntries;
@ -167,6 +228,17 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
);
}
void _goToSearch(BuildContext context) {
Navigator.push(
context,
SearchPageRoute(
delegate: CollectionSearchDelegate(
source: context.read<CollectionSource>(),
),
),
);
}
Future<void> _hide(BuildContext context, Set<T> filters) async {
final confirmed = await showDialog<bool>(
context: context,
@ -191,9 +263,11 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
final source = context.read<CollectionSource>();
source.changeFilterVisibility(filters, false);
_browse(context);
}
void _showCoverSelectionDialog(BuildContext context, T filter) async {
void _setCover(BuildContext context, T filter) async {
final contentId = covers.coverContentId(filter);
final customEntry = context.read<CollectionSource>().visibleEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
final coverSelection = await showDialog<Tuple2<bool, AvesEntry?>>(
@ -207,5 +281,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
final isCustom = coverSelection.item1;
await covers.set(filter, isCustom ? coverSelection.item2?.contentId : null);
_browse(context);
}
}

View file

@ -11,7 +11,6 @@ import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
import 'package:aves/widgets/search/search_button.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -21,15 +20,14 @@ import 'package:provider/provider.dart';
class FilterGridAppBar<T extends CollectionFilter> extends StatefulWidget {
final CollectionSource source;
final String title;
final ChipSetActionDelegate actionDelegate;
final bool groupable, isEmpty;
final ChipSetActionDelegate<T> actionDelegate;
final bool isEmpty;
const FilterGridAppBar({
Key? key,
required this.source,
required this.title,
required this.actionDelegate,
required this.groupable,
required this.isEmpty,
}) : super(key: key);
@ -45,15 +43,14 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
ChipSetActionDelegate get actionDelegate => widget.actionDelegate;
static const filterSelectionActions = [
static const browsingQuickActions = [
ChipSetAction.search,
];
static const selectionQuickActions = [
ChipSetAction.setCover,
ChipSetAction.pin,
ChipSetAction.unpin,
ChipSetAction.delete,
ChipSetAction.rename,
ChipSetAction.hide,
];
static const buttonActionCount = 2;
@override
void initState() {
@ -128,113 +125,78 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
}
List<Widget> _buildActions(AppMode appMode, Selection<FilterGridItem<T>> selection) {
final selectedFilters = selection.selectedItems.map((v) => v.filter).toSet();
PopupMenuItem<ChipSetAction> toMenuItem(ChipSetAction action, {bool enabled = true}) {
return PopupMenuItem(
value: action,
enabled: enabled && actionDelegate.canApply(action, selectedFilters),
child: MenuRow(text: action.getText(context), icon: action.getIcon()),
);
}
void applyAction(ChipSetAction action) {
actionDelegate.onActionSelected(context, selectedFilters, action);
if (filterSelectionActions.contains(action)) {
selection.browse();
}
}
final itemCount = actionDelegate.allItems.length;
final isSelecting = selection.isSelecting;
final selectionRowActions = <ChipSetAction>[];
final selectedItems = selection.selectedItems;
final selectedFilters = selectedItems.map((v) => v.filter).toSet();
final buttonActions = <Widget>[];
if (isSelecting) {
final selectedFilters = selection.selectedItems.map((v) => v.filter).toSet();
final visibleActions = filterSelectionActions.where((action) => actionDelegate.isVisible(action, selectedFilters)).toList();
buttonActions.addAll(visibleActions.take(buttonActionCount).map(
(action) {
final enabled = actionDelegate.canApply(action, selectedFilters);
return IconButton(
icon: action.getIcon(),
onPressed: enabled ? () => applyAction(action) : null,
tooltip: action.getText(context),
);
},
));
selectionRowActions.addAll(visibleActions.skip(buttonActionCount));
} else if (appMode.canSearch) {
buttonActions.add(CollectionSearchButton(source: source));
}
bool isVisible(ChipSetAction action) => actionDelegate.isVisible(
action,
appMode: appMode,
isSelecting: isSelecting,
itemCount: itemCount,
selectedFilters: selectedFilters,
);
bool canApply(ChipSetAction action) => actionDelegate.canApply(
action,
isSelecting: isSelecting,
itemCount: itemCount,
selectedFilters: selectedFilters,
);
final quickActionButtons = (isSelecting ? selectionQuickActions : browsingQuickActions).where(isVisible).map(
(action) => _toActionButton(action, enabled: canApply(action)),
);
return [
...buttonActions,
...quickActionButtons,
MenuIconTheme(
child: PopupMenuButton<ChipSetAction>(
itemBuilder: (context) {
final selectedItems = selection.selectedItems;
final hasSelection = selectedItems.isNotEmpty;
final hasItems = !widget.isEmpty;
final otherViewEnabled = (!isSelecting && hasItems) || (isSelecting && hasSelection);
final generalMenuItems = ChipSetActions.general.where(isVisible).map(
(action) => _toMenuItem(action, enabled: canApply(action)),
);
final menuItems = <PopupMenuEntry<ChipSetAction>>[
toMenuItem(ChipSetAction.sort),
if (widget.groupable) toMenuItem(ChipSetAction.group),
if (appMode == AppMode.main && !isSelecting)
toMenuItem(
ChipSetAction.select,
enabled: hasItems,
),
];
final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v));
final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v));
final contextualMenuItems = (isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
(action) => _toMenuItem(action, enabled: canApply(action)),
);
if (appMode == AppMode.main) {
menuItems.add(const PopupMenuDivider());
if (isSelecting) {
menuItems.addAll(selectionRowActions.map(toMenuItem));
}
menuItems.addAll([
toMenuItem(
ChipSetAction.map,
enabled: otherViewEnabled,
),
toMenuItem(
ChipSetAction.stats,
enabled: otherViewEnabled,
),
]);
if (!isSelecting && actionDelegate.isVisible(ChipSetAction.createAlbum, selectedFilters)) {
menuItems.addAll([
const PopupMenuDivider(),
toMenuItem(ChipSetAction.createAlbum),
]);
}
}
if (isSelecting) {
menuItems.addAll([
return [
...generalMenuItems,
if (contextualMenuItems.isNotEmpty) ...[
const PopupMenuDivider(),
toMenuItem(
ChipSetAction.selectAll,
enabled: selectedItems.length < actionDelegate.allItems.length,
),
toMenuItem(
ChipSetAction.selectNone,
enabled: hasSelection,
),
]);
}
return menuItems;
...contextualMenuItems,
],
];
},
onSelected: (action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
applyAction(action);
_onActionSelected(action);
},
),
),
];
}
Widget _toActionButton(ChipSetAction action, {required bool enabled}) {
return IconButton(
icon: action.getIcon(),
onPressed: enabled ? () => _onActionSelected(action) : null,
tooltip: action.getText(context),
);
}
PopupMenuItem<ChipSetAction> _toMenuItem(ChipSetAction action, {required bool enabled}) {
return PopupMenuItem(
value: action,
enabled: enabled,
child: MenuRow(text: action.getText(context), icon: action.getIcon()),
);
}
void _onActivityChange() {
if (context.read<Selection<FilterGridItem<T>>>().isSelecting) {
_browseToSelectAnimation.forward();
@ -243,6 +205,12 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
}
}
void _onActionSelected(ChipSetAction action) {
final selection = context.read<Selection<FilterGridItem<T>>>();
final selectedFilters = selection.selectedItems.map((v) => v.filter).toSet();
actionDelegate.onActionSelected(context, selectedFilters, action);
}
void _goToSearch() {
Navigator.push(
context,

View file

@ -15,8 +15,8 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source;
final String title;
final ChipSortFactor sortFactor;
final bool groupable, showHeaders;
final ChipSetActionDelegate actionDelegate;
final bool showHeaders;
final ChipSetActionDelegate<T> actionDelegate;
final Map<ChipSectionKey, List<FilterGridItem<T>>> filterSections;
final Set<T>? newFilters;
final Widget Function() emptyBuilder;
@ -26,7 +26,6 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
required this.source,
required this.title,
required this.sortFactor,
this.groupable = false,
this.showHeaders = false,
required this.actionDelegate,
required this.filterSections,
@ -43,7 +42,6 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
source: source,
title: title,
actionDelegate: actionDelegate,
groupable: groupable,
isEmpty: filterSections.isEmpty,
),
sections: filterSections,

View file

@ -1,37 +0,0 @@
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/material.dart';
class CollectionSearchButton extends StatelessWidget {
final CollectionSource source;
final CollectionLens? parentCollection;
const CollectionSearchButton({
Key? key,
required this.source,
this.parentCollection,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(AIcons.search),
onPressed: () => _goToSearch(context),
tooltip: MaterialLocalizations.of(context).searchFieldLabel,
);
}
void _goToSearch(BuildContext context) {
Navigator.push(
context,
SearchPageRoute(
delegate: CollectionSearchDelegate(
source: source,
parentCollection: parentCollection,
),
),
);
}
}