diff --git a/CHANGELOG.md b/CHANGELOG.md index 461d8123d..2a7a86970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added +- Collection: filtering by rating range - About: data usage ### Changed diff --git a/lib/model/filters/rating.dart b/lib/model/filters/rating.dart index 631d13765..c9bd56290 100644 --- a/lib/model/filters/rating.dart +++ b/lib/model/filters/rating.dart @@ -8,18 +8,34 @@ class RatingFilter extends CollectionFilter { static const type = 'rating'; final int rating; + final String op; late final EntryFilter _test; - @override - List get props => [rating, reversed]; + static const opEqual = '='; + static const opOrLower = '<='; + static const opOrGreater = '>='; - RatingFilter(this.rating, {super.reversed = false}) { - _test = (entry) => entry.rating == rating; + @override + List get props => [rating, op, reversed]; + + RatingFilter(this.rating, {this.op = opEqual, super.reversed = false}) { + _test = switch (op) { + opOrLower => (entry) => entry.rating <= rating && entry.rating > 0, + opOrGreater => (entry) => entry.rating >= rating, + opEqual || _ => (entry) => entry.rating == rating, + }; } + RatingFilter copyWith(String op) => RatingFilter( + rating, + op: op, + reversed: reversed, + ); + factory RatingFilter.fromMap(Map json) { return RatingFilter( json['rating'] ?? 0, + op: json['op'] ?? opEqual, reversed: json['reversed'] ?? false, ); } @@ -28,6 +44,7 @@ class RatingFilter extends CollectionFilter { Map toMap() => { 'type': type, 'rating': rating, + 'op': op, 'reversed': reversed, }; @@ -38,37 +55,42 @@ class RatingFilter extends CollectionFilter { bool get exclusiveProp => true; @override - String get universalLabel => '$rating'; + String get universalLabel => '$op $rating'; @override - String getLabel(BuildContext context) => formatRating(context, rating); + String getLabel(BuildContext context) => switch (op) { + opOrLower || opOrGreater => '${UniChars.whiteMediumStar} ${formatRatingRange(context, rating, op)}', + opEqual || _ => formatRating(context, rating), + }; @override Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { - switch (rating) { - case -1: - return Icon(AIcons.ratingRejected, size: size); - case 0: - return Icon(AIcons.ratingUnrated, size: size); - default: - return null; - } + return switch (rating) { + -1 => Icon(AIcons.ratingRejected, size: size), + 0 => Icon(AIcons.ratingUnrated, size: size), + _ => null, + }; } @override String get category => type; @override - String get key => '$type-$reversed-$rating'; + String get key => '$type-$reversed-$rating-$op'; static String formatRating(BuildContext context, int rating) { - switch (rating) { - case -1: - return context.l10n.filterRatingRejectedLabel; - case 0: - return context.l10n.filterNoRatingLabel; - default: - return UniChars.whiteMediumStar * rating; - } + return switch (rating) { + -1 => context.l10n.filterRatingRejectedLabel, + 0 => context.l10n.filterNoRatingLabel, + _ => UniChars.whiteMediumStar * rating, + }; + } + + static String formatRatingRange(BuildContext context, int rating, String op) { + return switch (op) { + opOrLower => '1~$rating', + opOrGreater => '$rating~5', + opEqual || _ => '$rating', + }; } } diff --git a/lib/view/src/actions/chip.dart b/lib/view/src/actions/chip.dart index cf4b7f0a1..0dc57af26 100644 --- a/lib/view/src/actions/chip.dart +++ b/lib/view/src/actions/chip.dart @@ -14,6 +14,10 @@ extension ExtraChipActionView on ChipAction { return context.l10n.chipActionGoToPlacePage; case ChipAction.goToTagPage: return context.l10n.chipActionGoToTagPage; + case ChipAction.ratingOrGreater: + case ChipAction.ratingOrLower: + // different data depending on state + return toString(); case ChipAction.reverse: // different data depending on state return context.l10n.chipActionFilterOut; @@ -26,22 +30,14 @@ extension ExtraChipActionView on ChipAction { Widget getIcon() => Icon(_getIconData()); - IconData _getIconData() { - switch (this) { - case ChipAction.goToAlbumPage: - return AIcons.album; - case ChipAction.goToCountryPage: - return AIcons.country; - case ChipAction.goToPlacePage: - return AIcons.place; - case ChipAction.goToTagPage: - return AIcons.tag; - case ChipAction.reverse: - return AIcons.reverse; - case ChipAction.hide: - return AIcons.hide; - case ChipAction.lockVault: - return AIcons.vaultLock; - } - } + IconData _getIconData() => switch (this) { + ChipAction.goToAlbumPage => AIcons.album, + ChipAction.goToCountryPage => AIcons.country, + ChipAction.goToPlacePage => AIcons.place, + ChipAction.goToTagPage => AIcons.tag, + ChipAction.ratingOrGreater || ChipAction.ratingOrLower => AIcons.rating, + 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 63a181814..95a628d36 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -194,9 +194,9 @@ class _CollectionAppBarState extends State with SingleTickerPr ), ), if (showFilterBar) - NotificationListener( + NotificationListener( onNotification: (notification) { - collection.addFilter(notification.reversedFilter); + collection.addFilter(notification.filter); return true; }, child: FilterBar( diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index b0e2cca66..4d90fcd55 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -6,6 +6,7 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; @@ -100,6 +101,10 @@ class AvesFilterChip extends StatefulWidget { 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, @@ -122,10 +127,19 @@ class AvesFilterChip extends StatefulWidget { const PopupMenuDivider(), ...actions.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) { late String text; - if (action == ChipAction.reverse) { - text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut; - } else { - text = action.getText(context); + switch (action) { + case ChipAction.reverse: + text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut; + break; + case ChipAction.ratingOrGreater: + text = RatingFilter.formatRatingRange(context, (filter as RatingFilter).rating, RatingFilter.opOrGreater); + break; + case ChipAction.ratingOrLower: + text = RatingFilter.formatRatingRange(context, (filter as RatingFilter).rating, RatingFilter.opOrLower); + break; + default: + text = action.getText(context); + break; } return PopupMenuItem( value: action, diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index 5d3760a43..e9bfcff9f 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -1,5 +1,6 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/vaults/vaults.dart'; @@ -26,6 +27,8 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin { case ChipAction.goToCountryPage: case ChipAction.goToPlacePage: case ChipAction.goToTagPage: + case ChipAction.ratingOrGreater: + case ChipAction.ratingOrLower: case ChipAction.reverse: return true; case ChipAction.hide: @@ -46,8 +49,12 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin { _goTo(context, filter, PlaceListPage.routeName, (context) => const PlaceListPage()); case ChipAction.goToTagPage: _goTo(context, filter, TagListPage.routeName, (context) => const TagListPage()); + case ChipAction.ratingOrGreater: + FilterNotification((filter as RatingFilter).copyWith(RatingFilter.opOrGreater)).dispatch(context); + case ChipAction.ratingOrLower: + FilterNotification((filter as RatingFilter).copyWith(RatingFilter.opOrLower)).dispatch(context); case ChipAction.reverse: - ReverseFilterNotification(filter).dispatch(context); + FilterNotification(filter.reverse()).dispatch(context); case ChipAction.hide: _hide(context, filter); case ChipAction.lockVault: @@ -95,8 +102,8 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin { } @immutable -class ReverseFilterNotification extends Notification { - final CollectionFilter reversedFilter; +class FilterNotification extends Notification { + final CollectionFilter filter; - ReverseFilterNotification(CollectionFilter filter) : reversedFilter = filter.reverse(); + const FilterNotification(this.filter); } diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 665ccc8ca..75a01c57c 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -174,8 +174,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin onNotification: (notification) { if (notification is FilterSelectedNotification) { _goToCollection(notification.filter); - } else if (notification is ReverseFilterNotification) { - _goToCollection(notification.reversedFilter); + } else if (notification is FilterNotification) { + _goToCollection(notification.filter); } else { return false; } diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index fb1fd59d4..290514d4d 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -89,9 +89,9 @@ 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.reversedFilter); + _select(context, notification.filter); return true; }, child: ValueListenableBuilder( diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 0ed592d56..6df276911 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -166,9 +166,9 @@ class _StatsPageState extends State with FeedbackMixin, VaultAwareMix ], ), ); - child = NotificationListener( + child = NotificationListener( onNotification: (notification) { - _onFilterSelection(context, notification.reversedFilter); + _onFilterSelection(context, notification.filter); return true; }, child: AnimationLimiter( @@ -378,9 +378,9 @@ class StatsTopPage extends StatelessWidget { child: SafeArea( bottom: false, child: Builder(builder: (context) { - return NotificationListener( + return NotificationListener( onNotification: (notification) { - onFilterSelection(notification.reversedFilter); + onFilterSelection(notification.filter); return true; }, child: SingleChildScrollView( diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 1529e11bb..d8f8f4952 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -238,9 +238,9 @@ class _InfoPageContentState extends State<_InfoPageContent> { metadataNotifier: _metadataNotifier, ); - return NotificationListener( + return NotificationListener( onNotification: (notification) { - _onFilter(notification.reversedFilter); + _onFilter(notification.filter); return true; }, child: CustomScrollView( diff --git a/plugins/aves_model/lib/src/actions/chip.dart b/plugins/aves_model/lib/src/actions/chip.dart index e4cd6789b..46fc4cf10 100644 --- a/plugins/aves_model/lib/src/actions/chip.dart +++ b/plugins/aves_model/lib/src/actions/chip.dart @@ -3,6 +3,8 @@ enum ChipAction { goToCountryPage, goToPlacePage, goToTagPage, + ratingOrGreater, + ratingOrLower, reverse, hide, lockVault,