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
- Collection: filtering by rating range
- About: data usage
### Changed

View file

@ -8,18 +8,34 @@ class RatingFilter extends CollectionFilter {
static const type = 'rating';
final int rating;
final String op;
late final EntryFilter _test;
@override
List<Object?> 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<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) {
return RatingFilter(
json['rating'] ?? 0,
op: json['op'] ?? opEqual,
reversed: json['reversed'] ?? false,
);
}
@ -28,6 +44,7 @@ class RatingFilter extends CollectionFilter {
Map<String, dynamic> 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',
};
}
}

View file

@ -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,
};
}

View file

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

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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<ReverseFilterNotification>(
child: NotificationListener<FilterNotification>(
onNotification: (notification) {
_select(context, notification.reversedFilter);
_select(context, notification.filter);
return true;
},
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) {
_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<ReverseFilterNotification>(
return NotificationListener<FilterNotification>(
onNotification: (notification) {
onFilterSelection(notification.reversedFilter);
onFilterSelection(notification.filter);
return true;
},
child: SingleChildScrollView(

View file

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

View file

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