#1012 show chip selection in collection via or filter

This commit is contained in:
Thibault Deckers 2024-05-30 23:32:13 +02:00
parent ece28db3f8
commit fe0c2e345b
26 changed files with 139 additions and 56 deletions

View file

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
### Added
- Albums / Countries / Tags: show selection in Collection
### Changed
- Screen saver: black background, consistent with slideshow

View file

@ -32,7 +32,10 @@ extension ExtraAppMode on AppMode {
AppMode.pickMultipleMediaExternal,
}.contains(this);
bool get canSelectFilter => this == AppMode.main;
bool get canSelectFilter => {
AppMode.main,
AppMode.pickCollectionFiltersExternal,
}.contains(this);
bool get canCreateFilter => {
AppMode.main,

View file

@ -85,6 +85,7 @@
"sourceStateLocatingPlaces": "Locating places",
"chipActionDelete": "Delete",
"chipActionShowCollection": "Show in Collection",
"chipActionGoToAlbumPage": "Show in Albums",
"chipActionGoToCountryPage": "Show in Countries",
"chipActionGoToPlacePage": "Show in Places",

View file

@ -68,9 +68,7 @@ class AspectRatioFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return Icon(AIcons.aspectRatio, size: size);
}
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.aspectRatio, size: size);
@override
String get category => type;

View file

@ -69,7 +69,7 @@ class CoordinateFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.geoBounds, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.geoBounds, size: size);
@override
String get category => type;

View file

@ -122,9 +122,7 @@ class DateFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return Icon(AIcons.date, size: size);
}
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.date, size: size);
@override
String get category => type;

View file

@ -45,7 +45,7 @@ class FavouriteFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size);
@override
Future<Color> color(BuildContext context) {

View file

@ -10,6 +10,7 @@ import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/missing.dart';
import 'package:aves/model/filters/or.dart';
import 'package:aves/model/filters/path.dart';
import 'package:aves/model/filters/placeholder.dart';
import 'package:aves/model/filters/query.dart';
@ -43,6 +44,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
AspectRatioFilter.type,
MissingFilter.type,
PathFilter.type,
OrFilter.type,
];
final bool reversed;
@ -68,6 +70,8 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
return MimeFilter.fromMap(jsonMap);
case MissingFilter.type:
return MissingFilter.fromMap(jsonMap);
case OrFilter.type:
return OrFilter.fromMap(jsonMap);
case PathFilter.type:
return PathFilter.fromMap(jsonMap);
case PlaceholderFilter.type:

View file

@ -89,7 +89,7 @@ class LocationFilter extends CoveredCollectionFilter {
String getLabel(BuildContext context) => _isUnlocated ? context.l10n.filterNoLocationLabel : _location;
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
if (_isUnlocated) {
return Icon(AIcons.locationUnlocated, size: size);
}

View file

@ -77,7 +77,7 @@ class MimeFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override
Future<Color> color(BuildContext context) {

View file

@ -70,7 +70,7 @@ class MissingFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override
String get category => type;

72
lib/model/filters/or.dart Normal file
View file

@ -0,0 +1,72 @@
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart';
import 'package:aves/theme/icons.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
class OrFilter extends CollectionFilter {
static const type = 'or';
late final List<CollectionFilter> _filters;
late final EntryFilter _test;
late final IconData? _genericIcon;
@override
List<Object?> get props => [_filters, reversed];
CollectionFilter get _first => _filters.first;
OrFilter(Set<CollectionFilter> filters, {super.reversed = false}) {
_filters = filters.toList().sorted();
_test = (entry) => _filters.any((v) => v.test(entry));
switch (_first) {
case AlbumFilter():
_genericIcon = AIcons.album;
case LocationFilter(level: LocationLevel.country):
_genericIcon = AIcons.country;
case LocationFilter(level: LocationLevel.state):
_genericIcon = AIcons.state;
default:
_genericIcon = null;
}
}
factory OrFilter.fromMap(Map<String, dynamic> json) {
return OrFilter(
(json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).whereNotNull().toSet(),
reversed: json['reversed'] ?? false,
);
}
@override
Map<String, dynamic> toMap() => {
'type': type,
'filters': _filters.map((v) => v.toJson()).toList(),
'reversed': reversed,
};
@override
EntryFilter get positiveTest => _test;
@override
bool get exclusiveProp => false;
@override
String get universalLabel => _filters.map((v) => v.universalLabel).join(', ');
@override
String getLabel(BuildContext context) => _filters.map((v) => v.getLabel(context)).join(', ');
@override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, showGenericIcon: showGenericIcon);
}
@override
String get category => _first.category;
@override
String get key => '$type-$reversed-${_filters.map((v) => v.key)}';
}

View file

@ -96,7 +96,7 @@ class PlaceholderFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override
String get category => type;

View file

@ -82,7 +82,7 @@ class QueryFilter extends CollectionFilter {
String get universalLabel => query;
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size);
@override
Future<Color> color(BuildContext context) {

View file

@ -51,7 +51,7 @@ class RecentlyAddedFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterRecentlyAddedLabel;
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.dateRecent, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.dateRecent, size: size);
@override
String get category => type;

View file

@ -47,7 +47,9 @@ class TagFilter extends CoveredCollectionFilter {
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterNoTagLabel : tag;
@override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagUntagged : AIcons.tag, size: size) : null;
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagUntagged : AIcons.tag, size: size) : null;
}
@override
String get category => type;

View file

@ -41,7 +41,7 @@ class TrashFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterBinLabel;
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.bin, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.bin, size: size);
@override
String get category => type;

View file

@ -99,7 +99,7 @@ class TypeFilter extends CollectionFilter {
}
@override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override
Future<Color> color(BuildContext context) {

View file

@ -30,6 +30,7 @@ extension ExtraChipSetActionView on ChipSetAction {
ChipSetAction.unpin => l10n.chipActionUnpin,
ChipSetAction.lockVault => l10n.chipActionLock,
ChipSetAction.showCountryStates => l10n.chipActionShowCountryStates,
ChipSetAction.showCollection => l10n.chipActionShowCollection,
// selecting (single filter)
ChipSetAction.rename => l10n.chipActionRename,
ChipSetAction.setCover => l10n.chipActionSetCover,
@ -64,6 +65,7 @@ extension ExtraChipSetActionView on ChipSetAction {
ChipSetAction.unpin => AIcons.unpin,
ChipSetAction.lockVault => AIcons.vaultLock,
ChipSetAction.showCountryStates => AIcons.state,
ChipSetAction.showCollection => AIcons.allCollection,
// selecting (single filter)
ChipSetAction.rename => AIcons.name,
ChipSetAction.setCover => AIcons.setCover,

View file

@ -385,7 +385,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
_saveTopEntries();
break;
default:
break;
}
@ -393,7 +392,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
availability.onResume();
RecentlyAddedFilter.updateNow();
_mediaStoreSource.checkForChanges();
break;
default:
break;
}
@ -639,10 +637,8 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.screenSaver:
// we cannot modify brightness without access to the activity
_screenBrightness = null;
break;
default:
_screenBrightness = ScreenBrightness();
break;
}
}
}

View file

@ -133,16 +133,12 @@ class AvesFilterChip extends StatefulWidget {
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

@ -414,10 +414,8 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
case TransformActivity.pan:
case TransformActivity.resize:
_gridDivisionNotifier.value = panResizeGridDivision;
break;
case TransformActivity.straighten:
_gridDivisionNotifier.value = straightenGridDivision;
break;
}
if (activity == TransformActivity.none) {
_gridAnimationController.reverse();
@ -452,12 +450,10 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
case ChangeSource.internal:
case ChangeSource.animation:
_setOutline(currentOutline);
break;
case ChangeSource.gesture:
// TODO TLAD [crop] use other strat
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
_updateCropRegion();
break;
}
}
@ -584,19 +580,15 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
case CropAspectRatio.original:
longCoef = contentSize.longestSide.round();
shortCoef = contentSize.shortestSide.round();
break;
case CropAspectRatio.square:
longCoef = 1;
shortCoef = 1;
break;
case CropAspectRatio.ar_16_9:
longCoef = 16;
shortCoef = 9;
break;
case CropAspectRatio.ar_4_3:
longCoef = 4;
shortCoef = 3;
break;
}
final contentRect = Offset.zero & contentSize;

View file

@ -49,27 +49,20 @@ class Transformation extends Equatable {
break;
case TransformOrientation.rotate90:
matrix.rotateZ(math.pi / 2);
break;
case TransformOrientation.rotate180:
matrix.rotateZ(math.pi);
break;
case TransformOrientation.rotate270:
matrix.rotateZ(3 * math.pi / 2);
break;
case TransformOrientation.transverse:
matrix.scale(-1.0, 1.0, 1.0);
matrix.rotateZ(-3 * math.pi / 2);
break;
case TransformOrientation.flipVertical:
matrix.scale(1.0, -1.0, 1.0);
break;
case TransformOrientation.transpose:
matrix.scale(-1.0, 1.0, 1.0);
matrix.rotateZ(-1 * math.pi / 2);
break;
case TransformOrientation.flipHorizontal:
matrix.scale(-1.0, 1.0, 1.0);
break;
}
return matrix;
}

View file

@ -3,6 +3,7 @@ import 'package:aves/model/covers.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/or.dart';
import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
@ -13,6 +14,7 @@ import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
@ -99,9 +101,11 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.hide:
return isMain;
case ChipSetAction.pin:
return !hasSelection || !settings.pinnedFilters.containsAll(selectedFilters);
return isMain && (!hasSelection || !settings.pinnedFilters.containsAll(selectedFilters));
case ChipSetAction.unpin:
return hasSelection && settings.pinnedFilters.containsAll(selectedFilters);
return isMain && (hasSelection && settings.pinnedFilters.containsAll(selectedFilters));
case ChipSetAction.showCollection:
return appMode.canNavigate;
case ChipSetAction.delete:
case ChipSetAction.lockVault:
case ChipSetAction.showCountryStates:
@ -149,6 +153,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.unpin:
case ChipSetAction.lockVault:
case ChipSetAction.showCountryStates:
case ChipSetAction.showCollection:
return hasSelection;
// selecting (single filter)
case ChipSetAction.rename:
@ -194,6 +199,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.unpin:
settings.pinnedFilters = settings.pinnedFilters..removeAll(getSelectedFilters(context));
browse(context);
case ChipSetAction.showCollection:
_goToCollection(context);
case ChipSetAction.delete:
case ChipSetAction.lockVault:
case ChipSetAction.showCountryStates:
@ -251,6 +258,22 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
}
}
Future<void> _goToCollection(context) async {
final filters = getSelectedFilters(context);
if (filters.isEmpty) return;
final filter = filters.length > 1 ? OrFilter(filters) : filters.first;
await Navigator.maybeOf(context)?.push(
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
source: context.read<CollectionSource>(),
filters: {filter},
),
),
);
}
Future<void> _goToMap(BuildContext context) async {
final mapCollection = CollectionLens(
source: context.read<CollectionSource>(),
@ -264,9 +287,9 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
);
}
void _goToSlideshow(BuildContext context) {
Future<void> _goToSlideshow(BuildContext context) async {
final entries = _selectedEntries(context).toList();
Navigator.maybeOf(context)?.push(
await Navigator.maybeOf(context)?.push(
MaterialPageRoute(
settings: const RouteSettings(name: SlideshowPage.routeName),
builder: (context) {
@ -281,9 +304,9 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
);
}
void _goToStats(BuildContext context) {
Future<void> _goToStats(BuildContext context) async {
final entries = _selectedEntries(context).toSet();
Navigator.maybeOf(context)?.push(
await Navigator.maybeOf(context)?.push(
MaterialPageRoute(
settings: const RouteSettings(name: StatsPage.routeName),
builder: (context) {
@ -296,8 +319,8 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
);
}
void _goToSearch(BuildContext context) {
Navigator.maybeOf(context)?.push(
Future<void> _goToSearch(BuildContext context) async {
await Navigator.maybeOf(context)?.push(
SearchPageRoute(
delegate: CollectionSearchDelegate(
searchFieldLabel: context.l10n.searchCollectionFieldHint,

View file

@ -3,6 +3,7 @@ import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_caption.dart';
import 'package:flutter/material.dart';
class SettingsCollectionTile extends StatelessWidget {
@ -19,7 +20,6 @@ class SettingsCollectionTile extends StatelessWidget {
Widget build(BuildContext context) {
final l10n = context.l10n;
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final hasSubtitle = filters.isEmpty;
// size and padding to match `ListTile`
@ -39,13 +39,10 @@ class SettingsCollectionTile extends StatelessWidget {
children: [
Text(
l10n.settingsCollectionTile,
style: textTheme.titleMedium!,
// fallback to `ListTile` M3 default style
style: theme.listTileTheme.titleTextStyle ?? theme.textTheme.bodyLarge!.copyWith(color: theme.colorScheme.onSurface),
),
if (hasSubtitle)
Text(
l10n.drawerCollectionAll,
style: textTheme.bodyMedium!.copyWith(color: theme.colorScheme.onSurfaceVariant),
),
if (hasSubtitle) AvesCaption(l10n.drawerCollectionAll),
],
),
const Spacer(),

View file

@ -20,6 +20,7 @@ enum ChipSetAction {
unpin,
lockVault,
showCountryStates,
showCollection,
// selecting (single filter)
rename,
setCover,
@ -57,6 +58,7 @@ class ChipSetActions {
ChipSetAction.showCountryStates,
ChipSetAction.hide,
null,
ChipSetAction.showCollection,
ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats,