diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6ef22be9..5a91a5711 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+### Added
+
+- dynamic album decompose action
+
## [v1.12.0] - 2024-12-19
### Added
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index a04876369..7ab3fc430 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -92,6 +92,7 @@
"chipActionGoToPlacePage": "Show in Places",
"chipActionGoToTagPage": "Show in Tags",
"chipActionGoToExplorerPage": "Show in Explorer",
+ "chipActionDecompose": "Split",
"chipActionFilterOut": "Filter out",
"chipActionFilterIn": "Filter in",
"chipActionHide": "Hide",
diff --git a/lib/model/filters/set_and.dart b/lib/model/filters/set_and.dart
index ca4aea296..225bb2689 100644
--- a/lib/model/filters/set_and.dart
+++ b/lib/model/filters/set_and.dart
@@ -18,6 +18,8 @@ class SetAndFilter extends CollectionFilter {
CollectionFilter get _first => _filters.first;
+ Set get innerFilters => _filters.toSet();
+
SetAndFilter(Set filters, {super.reversed = false}) {
_filters = filters.toList().sorted();
_test = (entry) => _filters.every((v) => v.test(entry));
diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart
index 81d3bed0d..c77ddfef4 100644
--- a/lib/model/source/collection_lens.dart
+++ b/lib/model/source/collection_lens.dart
@@ -167,10 +167,12 @@ class CollectionLens with ChangeNotifier {
}
}
- void addFilter(CollectionFilter filter) {
- if (filters.contains(filter)) return;
- filters.removeWhere((other) => !filter.isCompatible(other));
- filters.add(filter);
+ void addFilters(Set newFilters) {
+ if (filters.containsAll(newFilters)) return;
+ for (final filter in newFilters) {
+ filters.removeWhere((other) => !filter.isCompatible(other));
+ }
+ filters.addAll(newFilters);
_onFilterChanged();
}
diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart
index 031788e88..25aa0b918 100644
--- a/lib/theme/icons.dart
+++ b/lib/theme/icons.dart
@@ -145,6 +145,7 @@ class AIcons {
static final showFullscreenArrows = MdiIcons.arrowExpand;
static const showFullscreenCorners = Icons.fullscreen_outlined;
static const slideshow = Icons.slideshow_outlined;
+ static const split = Icons.call_split_outlined;
static const stats = Icons.donut_small_outlined;
static const vaultLock = Icons.lock_outlined;
static const vaultAdd = Icons.enhanced_encryption_outlined;
diff --git a/lib/view/src/actions/chip.dart b/lib/view/src/actions/chip.dart
index 75b0f5ed6..03afd4455 100644
--- a/lib/view/src/actions/chip.dart
+++ b/lib/view/src/actions/chip.dart
@@ -16,6 +16,7 @@ extension ExtraChipActionView on ChipAction {
ChipAction.ratingOrLower =>
// different data depending on state
toString(),
+ ChipAction.decompose => l10n.chipActionDecompose,
ChipAction.reverse =>
// different data depending on state
l10n.chipActionFilterOut,
@@ -33,6 +34,7 @@ extension ExtraChipActionView on ChipAction {
ChipAction.goToTagPage => AIcons.tag,
ChipAction.goToExplorerPage => AIcons.explorer,
ChipAction.ratingOrGreater || ChipAction.ratingOrLower => AIcons.rating,
+ ChipAction.decompose => AIcons.split,
ChipAction.reverse => AIcons.reverse,
ChipAction.hide => AIcons.hide,
ChipAction.lockVault => AIcons.vaultLock,
diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart
index 89f921856..8946e9504 100644
--- a/lib/widgets/collection/app_bar.dart
+++ b/lib/widgets/collection/app_bar.dart
@@ -3,8 +3,10 @@ import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
+import 'package:aves/model/filters/covered/dynamic_album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart';
+import 'package:aves/model/filters/set_and.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
@@ -33,8 +35,8 @@ import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
-import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/search/search_delegate.dart';
+import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -199,10 +201,22 @@ class _CollectionAppBarState extends State with SingleTickerPr
),
),
if (showFilterBar)
- NotificationListener(
+ NotificationListener(
onNotification: (notification) {
- collection.addFilter(notification.filter);
- return true;
+ if (notification is SelectFilterNotification) {
+ collection.addFilters({notification.filter});
+ return true;
+ } else if (notification is DecomposeFilterNotification) {
+ final filter = notification.filter;
+ if (filter is DynamicAlbumFilter) {
+ final innerFilter = filter.filter;
+ final newFilters = innerFilter is SetAndFilter ? innerFilter.innerFilters : {innerFilter};
+ collection.addFilters(newFilters);
+ collection.removeFilter(filter);
+ return true;
+ }
+ }
+ return false;
},
child: FilterBar(
filters: visibleFilters,
diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart
index 4e33ce13c..e9c9ddd35 100644
--- a/lib/widgets/common/identity/aves_filter_chip.dart
+++ b/lib/widgets/common/identity/aves_filter_chip.dart
@@ -3,12 +3,8 @@ import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/covers.dart';
-import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart';
-import 'package:aves/model/filters/covered/location.dart';
-import 'package:aves/model/filters/path.dart';
import 'package:aves/model/filters/rating.dart';
-import 'package:aves/model/filters/covered/tag.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/colors.dart';
@@ -101,21 +97,6 @@ class AvesFilterChip extends StatefulWidget {
static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
if (context.read>().value.canNavigate) {
- final actions = [
- if (filter is AlbumBaseFilter) ChipAction.goToAlbumPage,
- if (filter is StoredAlbumFilter || filter is PathFilter) ChipAction.goToExplorerPage,
- if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage,
- if ((filter is LocationFilter && filter.level == LocationLevel.place)) ChipAction.goToPlacePage,
- if (filter is TagFilter) ChipAction.goToTagPage,
- if (filter is RatingFilter && 1 < filter.rating && filter.rating < 5) ...[
- if (filter.op != RatingFilter.opOrGreater) ChipAction.ratingOrGreater,
- if (filter.op != RatingFilter.opOrLower) ChipAction.ratingOrLower,
- ],
- ChipAction.reverse,
- ChipAction.hide,
- ChipAction.lockVault,
- ];
-
// remove focus, if any, to prevent the keyboard from showing up
// after the user is done with the popup menu
FocusManager.instance.primaryFocus?.unfocus();
@@ -132,7 +113,7 @@ class AvesFilterChip extends StatefulWidget {
child: Text(filter.getLabel(context)),
),
const PopupMenuDivider(),
- ...actions.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) {
+ ...ChipAction.values.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) {
late String text;
switch (action) {
case ChipAction.reverse:
diff --git a/lib/widgets/common/map/buttons/coordinate_filter.dart b/lib/widgets/common/map/buttons/coordinate_filter.dart
index df2f3661b..80405f34c 100644
--- a/lib/widgets/common/map/buttons/coordinate_filter.dart
+++ b/lib/widgets/common/map/buttons/coordinate_filter.dart
@@ -92,7 +92,7 @@ class _OverlayCoordinateFilterChipState extends State FilterSelectedNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context),
+ onTap: (filter) => SelectFilterNotification(CoordinateFilter(bounds.sw, bounds.ne)).dispatch(context),
),
),
);
diff --git a/lib/widgets/explorer/explorer_action_delegate.dart b/lib/widgets/explorer/explorer_action_delegate.dart
index f97a8438f..3d8641c69 100644
--- a/lib/widgets/explorer/explorer_action_delegate.dart
+++ b/lib/widgets/explorer/explorer_action_delegate.dart
@@ -97,7 +97,12 @@ class ExplorerActionDelegate with FeedbackMixin {
}
void _hide(BuildContext context) {
- ChipActionDelegate().onActionSelected(context, _getPathFilter(), ChipAction.hide);
+ final chipActionDelegate = ChipActionDelegate();
+ const action = ChipAction.hide;
+ final pathFilter = _getPathFilter();
+ if (chipActionDelegate.isVisible(action, filter: pathFilter)) {
+ chipActionDelegate.onActionSelected(context, pathFilter, action);
+ }
}
void _goToStats(BuildContext context) {
diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart
index 7f05b7fb5..c250ee3ee 100644
--- a/lib/widgets/filter_grids/common/action_delegates/chip.dart
+++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart
@@ -1,4 +1,7 @@
+import 'package:aves/model/filters/covered/dynamic_album.dart';
+import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
+import 'package:aves/model/filters/covered/tag.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/path.dart';
import 'package:aves/model/filters/rating.dart';
@@ -15,6 +18,7 @@ import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/places_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
+import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -26,12 +30,21 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
}) {
switch (action) {
case ChipAction.goToAlbumPage:
+ return filter is AlbumBaseFilter;
case ChipAction.goToCountryPage:
+ return filter is LocationFilter && filter.level == LocationLevel.country;
case ChipAction.goToPlacePage:
+ return filter is LocationFilter && filter.level == LocationLevel.place;
case ChipAction.goToTagPage:
+ return filter is TagFilter;
case ChipAction.goToExplorerPage:
+ return filter is StoredAlbumFilter || filter is PathFilter;
case ChipAction.ratingOrGreater:
+ return filter is RatingFilter && 1 < filter.rating && filter.rating < 5 && filter.op != RatingFilter.opOrGreater;
case ChipAction.ratingOrLower:
+ return filter is RatingFilter && 1 < filter.rating && filter.rating < 5 && filter.op != RatingFilter.opOrLower;
+ case ChipAction.decompose:
+ return filter is DynamicAlbumFilter;
case ChipAction.reverse:
return true;
case ChipAction.hide:
@@ -69,11 +82,13 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
);
}
case ChipAction.ratingOrGreater:
- FilterNotification((filter as RatingFilter).copyWith(RatingFilter.opOrGreater)).dispatch(context);
+ SelectFilterNotification((filter as RatingFilter).copyWith(RatingFilter.opOrGreater)).dispatch(context);
case ChipAction.ratingOrLower:
- FilterNotification((filter as RatingFilter).copyWith(RatingFilter.opOrLower)).dispatch(context);
+ SelectFilterNotification((filter as RatingFilter).copyWith(RatingFilter.opOrLower)).dispatch(context);
+ case ChipAction.decompose:
+ DecomposeFilterNotification(filter).dispatch(context);
case ChipAction.reverse:
- FilterNotification(filter.reverse()).dispatch(context);
+ SelectFilterNotification(filter.reverse()).dispatch(context);
case ChipAction.hide:
_hide(context, filter);
case ChipAction.lockVault:
@@ -119,10 +134,3 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
settings.changeFilterVisibility({filter}, false);
}
}
-
-@immutable
-class FilterNotification extends Notification {
- final CollectionFilter filter;
-
- const FilterNotification(this.filter);
-}
diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart
index 4d4743f49..0dad07d20 100644
--- a/lib/widgets/map/map_page.dart
+++ b/lib/widgets/map/map_page.dart
@@ -4,8 +4,8 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/filters/coordinate.dart';
-import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/covered/location.dart';
+import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/media/geotiff.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
@@ -31,7 +31,6 @@ import 'package:aves/widgets/common/map/map_action_delegate.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/map_theme_provider.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
-import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/map/scroller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
@@ -203,9 +202,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (notification) {
- if (notification is FilterSelectedNotification) {
- _goToCollection(notification.filter);
- } else if (notification is FilterNotification) {
+ if (notification is SelectFilterNotification) {
_goToCollection(notification.filter);
} else if (notification is OpenMapAppNotification) {
_openMapApp();
diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart
index d648f4b57..e73acf4bc 100644
--- a/lib/widgets/search/search_delegate.dart
+++ b/lib/widgets/search/search_delegate.dart
@@ -1,16 +1,18 @@
import 'package:aves/model/dynamic_albums.dart';
-import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/aspect_ratio.dart';
+import 'package:aves/model/filters/covered/dynamic_album.dart';
+import 'package:aves/model/filters/covered/location.dart';
+import 'package:aves/model/filters/covered/stored_album.dart';
+import 'package:aves/model/filters/covered/tag.dart';
import 'package:aves/model/filters/date.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
-import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/missing.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/recent.dart';
-import 'package:aves/model/filters/covered/tag.dart';
+import 'package:aves/model/filters/set_and.dart';
import 'package:aves/model/filters/type.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.dart';
@@ -29,7 +31,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/search/delegate.dart';
import 'package:aves/widgets/common/search/page.dart';
-import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
+import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -91,10 +93,21 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
final upQuery = query.trim().toUpperCase();
bool containQuery(String s) => s.toUpperCase().contains(upQuery);
return SafeArea(
- child: NotificationListener(
+ child: NotificationListener(
onNotification: (notification) {
- _select(context, notification.filter);
- return true;
+ if (notification is SelectFilterNotification) {
+ _select(context, {notification.filter});
+ return true;
+ } else if (notification is DecomposeFilterNotification) {
+ final filter = notification.filter;
+ if (filter is DynamicAlbumFilter) {
+ final innerFilter = filter.filter;
+ final newFilters = innerFilter is SetAndFilter ? innerFilter.innerFilters : {innerFilter};
+ _select(context, newFilters);
+ return true;
+ }
+ }
+ return false;
},
child: ValueListenableBuilder(
valueListenable: _expandedSectionNotifier,
@@ -159,7 +172,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
required List filters,
HeroType Function(CollectionFilter filter)? heroTypeBuilder,
}) {
- void onTap(filter) => _select(context, filter is QueryFilter ? QueryFilter(filter.query) : filter);
+ void onTap(filter) => _select(context, {filter is QueryFilter ? QueryFilter(filter.query) : filter});
const onLongPress = AvesFilterChip.showDefaultLongPressMenu;
return title != null
? TitledExpandableFilterRow(
@@ -303,7 +316,7 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
// `buildResults` is called in the build phase,
// so we post the call that will filter the collection
// and possibly trigger a rebuild here
- _select(context, _buildQueryFilter(true));
+ _select(context, {_buildQueryFilter(true)});
});
}
return const SizedBox();
@@ -314,29 +327,33 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
return cleanQuery.isNotEmpty ? QueryFilter(cleanQuery, colorful: colorful) : null;
}
- Future _select(BuildContext context, CollectionFilter? filter) async {
- if (filter == null) {
+ Future _select(BuildContext context, Set filters) async {
+ final newFilters = filters.nonNulls.toSet();
+ if (newFilters.isEmpty) {
goBack(context);
return;
}
- if (!await unlockFilter(context, filter)) return;
+ for (final filter in newFilters) {
+ if (!await unlockFilter(context, filter)) return;
- if (settings.saveSearchHistory) {
- final history = settings.searchHistory
- ..remove(filter)
- ..insert(0, filter);
- settings.searchHistory = history.take(searchHistoryCount).toList();
+ if (settings.saveSearchHistory) {
+ final history = settings.searchHistory
+ ..remove(filter)
+ ..insert(0, filter);
+ settings.searchHistory = history.take(searchHistoryCount).toList();
+ }
}
+
if (parentCollection != null) {
- _applyToParentCollectionPage(context, filter);
+ _applyToParentCollectionPage(context, newFilters);
} else {
- _jumpToCollectionPage(context, {filter});
+ _jumpToCollectionPage(context, newFilters);
}
}
- void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) {
- parentCollection!.addFilter(filter);
+ void _applyToParentCollectionPage(BuildContext context, Set filters) {
+ parentCollection!.addFilters(filters);
if (Navigator.canPop(context)) {
// We delay closing the current page after applying the filter selection
// so that hero animation target is ready in the `FilterBar`,
diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart
index 8100e3ce1..64dc0dd92 100644
--- a/lib/widgets/stats/stats_page.dart
+++ b/lib/widgets/stats/stats_page.dart
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:aves/model/entry/entry.dart';
-import 'package:aves/model/filters/covered/stored_album.dart';
-import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/covered/location.dart';
-import 'package:aves/model/filters/rating.dart';
+import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/covered/tag.dart';
+import 'package:aves/model/filters/filters.dart';
+import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
@@ -24,11 +24,11 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/identity/empty.dart';
-import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/stats/date/histogram.dart';
import 'package:aves/widgets/stats/filter_table.dart';
import 'package:aves/widgets/stats/mime_donut.dart';
import 'package:aves/widgets/stats/percent_text.dart';
+import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -172,7 +172,7 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix
],
),
);
- child = NotificationListener(
+ child = NotificationListener(
onNotification: (notification) {
_onFilterSelection(context, notification.filter);
return true;
@@ -336,7 +336,7 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix
}
void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) {
- widget.parentCollection!.addFilter(filter);
+ widget.parentCollection!.addFilters({filter});
// We delay closing the current page after applying the filter selection
// so that hero animation target is ready in the `FilterBar`,
// even when the target is a child of an `AnimatedList`.
@@ -384,7 +384,7 @@ class StatsTopPage extends StatelessWidget {
child: SafeArea(
bottom: false,
child: Builder(builder: (context) {
- return NotificationListener(
+ return NotificationListener(
onNotification: (notification) {
onFilterSelection(notification.filter);
return true;
diff --git a/lib/widgets/viewer/controls/notifications.dart b/lib/widgets/viewer/controls/notifications.dart
index ab9c78fa3..29ac67f68 100644
--- a/lib/widgets/viewer/controls/notifications.dart
+++ b/lib/widgets/viewer/controls/notifications.dart
@@ -89,13 +89,23 @@ class CastNotification extends Notification with EquatableMixin {
}
@immutable
-class FilterSelectedNotification extends Notification with EquatableMixin {
+class SelectFilterNotification extends Notification with EquatableMixin {
final CollectionFilter filter;
@override
List