filtering by rating range
This commit is contained in:
parent
55bf563eab
commit
6a7991dd62
11 changed files with 103 additions and 61 deletions
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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?>(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -3,6 +3,8 @@ enum ChipAction {
|
||||||
goToCountryPage,
|
goToCountryPage,
|
||||||
goToPlacePage,
|
goToPlacePage,
|
||||||
goToTagPage,
|
goToTagPage,
|
||||||
|
ratingOrGreater,
|
||||||
|
ratingOrLower,
|
||||||
reverse,
|
reverse,
|
||||||
hide,
|
hide,
|
||||||
lockVault,
|
lockVault,
|
||||||
|
|
Loading…
Reference in a new issue