filtering by rating range

This commit is contained in:
Thibault Deckers 2023-08-02 00:47:28 +02:00
parent 55bf563eab
commit 6a7991dd62
11 changed files with 103 additions and 61 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Collection: filtering by rating range
- About: data usage - About: data usage
### Changed ### Changed

View file

@ -8,18 +8,34 @@ class RatingFilter extends CollectionFilter {
static const type = 'rating'; static const type = 'rating';
final int rating; final int rating;
final String op;
late final EntryFilter _test; late final EntryFilter _test;
@override static const opEqual = '=';
List<Object?> get props => [rating, reversed]; static const opOrLower = '<=';
static const opOrGreater = '>=';
RatingFilter(this.rating, {super.reversed = false}) { @override
_test = (entry) => entry.rating == rating; List<Object?> 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<String, dynamic> json) { factory RatingFilter.fromMap(Map<String, dynamic> json) {
return RatingFilter( return RatingFilter(
json['rating'] ?? 0, json['rating'] ?? 0,
op: json['op'] ?? opEqual,
reversed: json['reversed'] ?? false, reversed: json['reversed'] ?? false,
); );
} }
@ -28,6 +44,7 @@ class RatingFilter extends CollectionFilter {
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
'type': type, 'type': type,
'rating': rating, 'rating': rating,
'op': op,
'reversed': reversed, 'reversed': reversed,
}; };
@ -38,37 +55,42 @@ class RatingFilter extends CollectionFilter {
bool get exclusiveProp => true; bool get exclusiveProp => true;
@override @override
String get universalLabel => '$rating'; String get universalLabel => '$op $rating';
@override @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 @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
switch (rating) { return switch (rating) {
case -1: -1 => Icon(AIcons.ratingRejected, size: size),
return Icon(AIcons.ratingRejected, size: size); 0 => Icon(AIcons.ratingUnrated, size: size),
case 0: _ => null,
return Icon(AIcons.ratingUnrated, size: size); };
default:
return null;
}
} }
@override @override
String get category => type; String get category => type;
@override @override
String get key => '$type-$reversed-$rating'; String get key => '$type-$reversed-$rating-$op';
static String formatRating(BuildContext context, int rating) { static String formatRating(BuildContext context, int rating) {
switch (rating) { return switch (rating) {
case -1: -1 => context.l10n.filterRatingRejectedLabel,
return context.l10n.filterRatingRejectedLabel; 0 => context.l10n.filterNoRatingLabel,
case 0: _ => UniChars.whiteMediumStar * rating,
return context.l10n.filterNoRatingLabel; };
default: }
return UniChars.whiteMediumStar * rating;
} static String formatRatingRange(BuildContext context, int rating, String op) {
return switch (op) {
opOrLower => '1~$rating',
opOrGreater => '$rating~5',
opEqual || _ => '$rating',
};
} }
} }

View file

@ -14,6 +14,10 @@ extension ExtraChipActionView on ChipAction {
return context.l10n.chipActionGoToPlacePage; return context.l10n.chipActionGoToPlacePage;
case ChipAction.goToTagPage: case ChipAction.goToTagPage:
return context.l10n.chipActionGoToTagPage; return context.l10n.chipActionGoToTagPage;
case ChipAction.ratingOrGreater:
case ChipAction.ratingOrLower:
// different data depending on state
return toString();
case ChipAction.reverse: case ChipAction.reverse:
// different data depending on state // different data depending on state
return context.l10n.chipActionFilterOut; return context.l10n.chipActionFilterOut;
@ -26,22 +30,14 @@ extension ExtraChipActionView on ChipAction {
Widget getIcon() => Icon(_getIconData()); Widget getIcon() => Icon(_getIconData());
IconData _getIconData() { IconData _getIconData() => switch (this) {
switch (this) { ChipAction.goToAlbumPage => AIcons.album,
case ChipAction.goToAlbumPage: ChipAction.goToCountryPage => AIcons.country,
return AIcons.album; ChipAction.goToPlacePage => AIcons.place,
case ChipAction.goToCountryPage: ChipAction.goToTagPage => AIcons.tag,
return AIcons.country; ChipAction.ratingOrGreater || ChipAction.ratingOrLower => AIcons.rating,
case ChipAction.goToPlacePage: ChipAction.reverse => AIcons.reverse,
return AIcons.place; ChipAction.hide => AIcons.hide,
case ChipAction.goToTagPage: ChipAction.lockVault => AIcons.vaultLock,
return AIcons.tag; };
case ChipAction.reverse:
return AIcons.reverse;
case ChipAction.hide:
return AIcons.hide;
case ChipAction.lockVault:
return AIcons.vaultLock;
}
}
} }

View file

@ -194,9 +194,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
), ),
), ),
if (showFilterBar) if (showFilterBar)
NotificationListener<ReverseFilterNotification>( NotificationListener<FilterNotification>(
onNotification: (notification) { onNotification: (notification) {
collection.addFilter(notification.reversedFilter); collection.addFilter(notification.filter);
return true; return true;
}, },
child: FilterBar( child: FilterBar(

View file

@ -6,6 +6,7 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.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/filters/tag.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.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.country)) ChipAction.goToCountryPage,
if ((filter is LocationFilter && filter.level == LocationLevel.place)) ChipAction.goToPlacePage, if ((filter is LocationFilter && filter.level == LocationLevel.place)) ChipAction.goToPlacePage,
if (filter is TagFilter) ChipAction.goToTagPage, 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.reverse,
ChipAction.hide, ChipAction.hide,
ChipAction.lockVault, ChipAction.lockVault,
@ -122,10 +127,19 @@ class AvesFilterChip extends StatefulWidget {
const PopupMenuDivider(), const PopupMenuDivider(),
...actions.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) { ...actions.where((action) => actionDelegate.isVisible(action, filter: filter)).map((action) {
late String text; late String text;
if (action == ChipAction.reverse) { switch (action) {
text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut; case ChipAction.reverse:
} else { text = filter.reversed ? context.l10n.chipActionFilterIn : context.l10n.chipActionFilterOut;
text = action.getText(context); 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( return PopupMenuItem(
value: action, value: action,

View file

@ -1,5 +1,6 @@
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.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/highlight.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/model/vaults/vaults.dart';
@ -26,6 +27,8 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
case ChipAction.goToCountryPage: case ChipAction.goToCountryPage:
case ChipAction.goToPlacePage: case ChipAction.goToPlacePage:
case ChipAction.goToTagPage: case ChipAction.goToTagPage:
case ChipAction.ratingOrGreater:
case ChipAction.ratingOrLower:
case ChipAction.reverse: case ChipAction.reverse:
return true; return true;
case ChipAction.hide: case ChipAction.hide:
@ -46,8 +49,12 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
_goTo(context, filter, PlaceListPage.routeName, (context) => const PlaceListPage()); _goTo(context, filter, PlaceListPage.routeName, (context) => const PlaceListPage());
case ChipAction.goToTagPage: case ChipAction.goToTagPage:
_goTo(context, filter, TagListPage.routeName, (context) => const TagListPage()); _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: case ChipAction.reverse:
ReverseFilterNotification(filter).dispatch(context); FilterNotification(filter.reverse()).dispatch(context);
case ChipAction.hide: case ChipAction.hide:
_hide(context, filter); _hide(context, filter);
case ChipAction.lockVault: case ChipAction.lockVault:
@ -95,8 +102,8 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
} }
@immutable @immutable
class ReverseFilterNotification extends Notification { class FilterNotification extends Notification {
final CollectionFilter reversedFilter; final CollectionFilter filter;
ReverseFilterNotification(CollectionFilter filter) : reversedFilter = filter.reverse(); const FilterNotification(this.filter);
} }

View file

@ -174,8 +174,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
onNotification: (notification) { onNotification: (notification) {
if (notification is FilterSelectedNotification) { if (notification is FilterSelectedNotification) {
_goToCollection(notification.filter); _goToCollection(notification.filter);
} else if (notification is ReverseFilterNotification) { } else if (notification is FilterNotification) {
_goToCollection(notification.reversedFilter); _goToCollection(notification.filter);
} else { } else {
return false; return false;
} }

View file

@ -89,9 +89,9 @@ class CollectionSearchDelegate extends AvesSearchDelegate with FeedbackMixin, Va
final upQuery = query.trim().toUpperCase(); final upQuery = query.trim().toUpperCase();
bool containQuery(String s) => s.toUpperCase().contains(upQuery); bool containQuery(String s) => s.toUpperCase().contains(upQuery);
return SafeArea( return SafeArea(
child: NotificationListener<ReverseFilterNotification>( child: NotificationListener<FilterNotification>(
onNotification: (notification) { onNotification: (notification) {
_select(context, notification.reversedFilter); _select(context, notification.filter);
return true; return true;
}, },
child: ValueListenableBuilder<String?>( child: ValueListenableBuilder<String?>(

View file

@ -166,9 +166,9 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix
], ],
), ),
); );
child = NotificationListener<ReverseFilterNotification>( child = NotificationListener<FilterNotification>(
onNotification: (notification) { onNotification: (notification) {
_onFilterSelection(context, notification.reversedFilter); _onFilterSelection(context, notification.filter);
return true; return true;
}, },
child: AnimationLimiter( child: AnimationLimiter(
@ -378,9 +378,9 @@ class StatsTopPage extends StatelessWidget {
child: SafeArea( child: SafeArea(
bottom: false, bottom: false,
child: Builder(builder: (context) { child: Builder(builder: (context) {
return NotificationListener<ReverseFilterNotification>( return NotificationListener<FilterNotification>(
onNotification: (notification) { onNotification: (notification) {
onFilterSelection(notification.reversedFilter); onFilterSelection(notification.filter);
return true; return true;
}, },
child: SingleChildScrollView( child: SingleChildScrollView(

View file

@ -238,9 +238,9 @@ class _InfoPageContentState extends State<_InfoPageContent> {
metadataNotifier: _metadataNotifier, metadataNotifier: _metadataNotifier,
); );
return NotificationListener<ReverseFilterNotification>( return NotificationListener<FilterNotification>(
onNotification: (notification) { onNotification: (notification) {
_onFilter(notification.reversedFilter); _onFilter(notification.filter);
return true; return true;
}, },
child: CustomScrollView( child: CustomScrollView(

View file

@ -3,6 +3,8 @@ enum ChipAction {
goToCountryPage, goToCountryPage,
goToPlacePage, goToPlacePage,
goToTagPage, goToTagPage,
ratingOrGreater,
ratingOrLower,
reverse, reverse,
hide, hide,
lockVault, lockVault,