filter grid scaling: border radius by extent, shared extent for album list & pick, fixed rebuild on query change, fixed pinned item sort, scroll to scaled item

This commit is contained in:
Thibault Deckers 2020-11-27 10:40:36 +09:00
parent e218afc6b6
commit f86eb078a4
10 changed files with 143 additions and 138 deletions

View file

@ -43,7 +43,7 @@ class ThumbnailCollection extends StatelessWidget {
if (viewportSize.isEmpty) return SizedBox.shrink();
final tileExtentManager = TileExtentManager(
routeName: context.currentRouteName,
settingsRouteKey: context.currentRouteName,
columnCountMin: columnCountMin,
columnCountDefault: columnCountDefault,
extentMin: extentMin,

View file

@ -18,8 +18,9 @@ class AvesFilterChip extends StatefulWidget {
final HeroType heroType;
final FilterCallback onTap;
final OffsetFilterCallback onLongPress;
final BorderRadius borderRadius;
static final BorderRadius borderRadius = BorderRadius.circular(32);
static const double defaultRadius = 32;
static const double outlineWidth = 2;
static const double minChipHeight = kMinInteractiveDimension;
static const double minChipWidth = 80;
@ -33,6 +34,7 @@ class AvesFilterChip extends StatefulWidget {
this.showGenericIcon = true,
this.background,
this.details,
this.borderRadius = const BorderRadius.all(Radius.circular(defaultRadius)),
this.padding = 6.0,
this.heroType = HeroType.onTap,
this.onTap,
@ -52,6 +54,8 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
CollectionFilter get filter => widget.filter;
BorderRadius get borderRadius => widget.borderRadius;
double get padding => widget.padding;
@override
@ -141,8 +145,6 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
);
}
final borderRadius = AvesFilterChip.borderRadius;
Widget chip = Container(
constraints: BoxConstraints(
minWidth: AvesFilterChip.minChipWidth,

View file

@ -4,13 +4,13 @@ import 'package:aves/model/settings/settings.dart';
import 'package:flutter/widgets.dart';
class TileExtentManager {
final String routeName;
final String settingsRouteKey;
final int columnCountMin, columnCountDefault;
final double spacing, extentMin;
final ValueNotifier<double> extentNotifier;
const TileExtentManager({
@required this.routeName,
@required this.settingsRouteKey,
@required this.columnCountMin,
@required this.columnCountDefault,
@required this.extentMin,
@ -26,7 +26,7 @@ class TileExtentManager {
final viewportSizeMin = Size.square(extentMin * columnCountMin);
viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height));
final oldUserPreferredExtent = settings.getTileExtent(routeName);
final oldUserPreferredExtent = settings.getTileExtent(settingsRouteKey);
final currentExtent = extentNotifier.value;
final targetExtent = userPreferredExtent > 0
? userPreferredExtent
@ -38,7 +38,7 @@ class TileExtentManager {
final newExtent = _extentForColumnCount(viewportSize, columnCount);
if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) {
settings.setTileExtent(routeName, newExtent);
settings.setTileExtent(settingsRouteKey, newExtent);
}
if (extentNotifier.value != newExtent) {
extentNotifier.value = newExtent;

View file

@ -32,7 +32,7 @@ class AlbumPickPage extends StatefulWidget {
}
class _AlbumPickPageState extends State<AlbumPickPage> {
final _filterNotifier = ValueNotifier('');
final _queryNotifier = ValueNotifier('');
CollectionSource get source => widget.source;
@ -41,26 +41,29 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
Widget appBar = AlbumPickAppBar(
copy: widget.copy,
actionDelegate: AlbumChipSetActionDelegate(source: source),
filterNotifier: _filterNotifier,
queryNotifier: _queryNotifier,
);
return Selector<Settings, ChipSortFactor>(
selector: (context, s) => s.albumSortFactor,
builder: (context, sortFactor, child) {
return ValueListenableBuilder<String>(
valueListenable: _filterNotifier,
builder: (context, filter, child) => FilterGridPage(
source: source,
appBar: appBar,
filterEntries: AlbumListPage.getAlbumEntries(source, filter: filter),
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: 'No albums',
),
appBarHeight: AlbumPickAppBar.preferredHeight,
onTap: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter)?.album),
return FilterGridPage<AlbumFilter>(
source: source,
appBar: appBar,
filterEntries: AlbumListPage.getAlbumEntries(source),
applyQuery: (filters, query) {
if (query == null || query.isEmpty) return filters;
query = query.toUpperCase();
return filters.where((filter) => filter.uniqueName.toUpperCase().contains(query)).toList();
},
queryNotifier: _queryNotifier,
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: 'No albums',
),
settingsRouteKey: AlbumListPage.routeName,
appBarHeight: AlbumPickAppBar.preferredHeight,
onTap: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter)?.album),
);
},
);
@ -70,14 +73,14 @@ class _AlbumPickPageState extends State<AlbumPickPage> {
class AlbumPickAppBar extends StatelessWidget {
final bool copy;
final AlbumChipSetActionDelegate actionDelegate;
final ValueNotifier<String> filterNotifier;
final ValueNotifier<String> queryNotifier;
static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight;
const AlbumPickAppBar({
@required this.copy,
@required this.actionDelegate,
@required this.filterNotifier,
@required this.queryNotifier,
});
@override
@ -86,7 +89,7 @@ class AlbumPickAppBar extends StatelessWidget {
leading: BackButton(),
title: Text(copy ? 'Copy to Album' : 'Move to Album'),
bottom: AlbumFilterBar(
filterNotifier: filterNotifier,
filterNotifier: queryNotifier,
),
actions: [
IconButton(

View file

@ -33,7 +33,7 @@ class AlbumListPage extends StatelessWidget {
animation: androidFileUtils.appNameChangeNotifier,
builder: (context, child) => StreamBuilder(
stream: source.eventBus.on<AlbumsChangedEvent>(),
builder: (context, snapshot) => FilterNavigationPage(
builder: (context, snapshot) => FilterNavigationPage<AlbumFilter>(
source: source,
title: 'Albums',
chipSetActionDelegate: AlbumChipSetActionDelegate(source: source),
@ -44,7 +44,6 @@ class AlbumListPage extends StatelessWidget {
ChipAction.delete,
],
filterEntries: getAlbumEntries(source),
filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)),
emptyBuilder: () => EmptyContent(
icon: AIcons.album,
text: 'No albums',
@ -58,56 +57,53 @@ class AlbumListPage extends StatelessWidget {
// common with album selection page to move/copy entries
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source, {String filter}) {
final pinned = settings.pinnedFilters.whereType<AlbumFilter>().map((f) => f.album);
static Map<AlbumFilter, ImageEntry> getAlbumEntries(CollectionSource source) {
final pinned = settings.pinnedFilters.whereType<AlbumFilter>();
final entriesByDate = source.sortedEntriesForFilterList;
AlbumFilter _buildFilter(String album) => AlbumFilter(album, source.getUniqueAlbumName(album));
// albums are initially sorted by name at the source level
var sortedAlbums = source.sortedAlbums;
if (filter != null && filter.isNotEmpty) {
filter = filter.toUpperCase();
sortedAlbums = sortedAlbums.where((album) => source.getUniqueAlbumName(album).toUpperCase().contains(filter)).toList();
}
var sortedFilters = source.sortedAlbums.map(_buildFilter);
if (settings.albumSortFactor == ChipSortFactor.name) {
final pinnedAlbums = <String>[], regularAlbums = <String>[], appAlbums = <String>[], specialAlbums = <String>[];
for (var album in sortedAlbums) {
if (pinned.contains(album)) {
pinnedAlbums.add(album);
final pinnedAlbums = <AlbumFilter>[], regularAlbums = <AlbumFilter>[], appAlbums = <AlbumFilter>[], specialAlbums = <AlbumFilter>[];
for (var filter in sortedFilters) {
if (pinned.contains(filter)) {
pinnedAlbums.add(filter);
} else {
switch (androidFileUtils.getAlbumType(album)) {
switch (androidFileUtils.getAlbumType(filter.album)) {
case AlbumType.regular:
regularAlbums.add(album);
regularAlbums.add(filter);
break;
case AlbumType.app:
appAlbums.add(album);
appAlbums.add(filter);
break;
default:
specialAlbums.add(album);
specialAlbums.add(filter);
break;
}
}
}
return Map.fromEntries([...pinnedAlbums, ...specialAlbums, ...appAlbums, ...regularAlbums].map((album) {
return Map.fromEntries([...pinnedAlbums, ...specialAlbums, ...appAlbums, ...regularAlbums].map((filter) {
return MapEntry(
album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
filter,
entriesByDate.firstWhere((entry) => entry.directory == filter.album, orElse: () => null),
);
}));
}
if (settings.albumSortFactor == ChipSortFactor.count) {
CollectionFilter _buildFilter(String album) => AlbumFilter(album, source.getUniqueAlbumName(album));
var filtersWithCount = List.of(sortedAlbums.map((s) => MapEntry(s, source.count(_buildFilter(s)))));
final filtersWithCount = List.of(sortedFilters.map((filter) => MapEntry(filter, source.count(filter))));
filtersWithCount.sort(FilterNavigationPage.compareChipsByEntryCount);
sortedAlbums = filtersWithCount.map((kv) => kv.key).toList();
sortedFilters = filtersWithCount.map((kv) => kv.key).toList();
}
final allMapEntries = sortedAlbums.map((album) => MapEntry(
album,
entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null),
final allMapEntries = sortedFilters.map((filter) => MapEntry(
filter,
entriesByDate.firstWhere((entry) => entry.directory == filter.album, orElse: () => null),
));
final byPin = groupBy<MapEntry<String, ImageEntry>, bool>(allMapEntries, (e) => pinned.contains(e.key));
final byPin = groupBy<MapEntry<AlbumFilter, ImageEntry>, bool>(allMapEntries, (e) => pinned.contains(e.key));
final pinnedMapEntries = (byPin[true] ?? []);
final unpinnedMapEntries = (byPin[false] ?? []);

View file

@ -49,12 +49,14 @@ class DecoratedFilterChip extends StatelessWidget {
entry: entry,
extent: extent,
);
final borderRadius = min<double>(AvesFilterChip.defaultRadius, extent / 4);
final titlePadding = min<double>(6.0, extent / 16);
return AvesFilterChip(
filter: filter,
showGenericIcon: false,
background: backgroundImage,
details: _buildDetails(filter),
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
padding: titlePadding,
onTap: onTap,
onLongPress: onLongPress,

View file

@ -20,12 +20,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
class FilterGridPage extends StatelessWidget {
class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source;
final Widget appBar;
final Map<String, ImageEntry> filterEntries;
final CollectionFilter Function(String key) filterBuilder;
final Map<T, ImageEntry> filterEntries;
final ValueNotifier<String> queryNotifier;
final Widget Function() emptyBuilder;
final String settingsRouteKey;
final Iterable<T> Function(Iterable<T> filters, String query) applyQuery;
final FilterCallback onTap;
final OffsetFilterCallback onLongPress;
@ -39,8 +41,10 @@ class FilterGridPage extends StatelessWidget {
@required this.source,
@required this.appBar,
@required this.filterEntries,
@required this.filterBuilder,
@required this.queryNotifier,
this.applyQuery,
@required this.emptyBuilder,
this.settingsRouteKey,
double appBarHeight = kToolbarHeight,
@required this.onTap,
this.onLongPress,
@ -48,13 +52,8 @@ class FilterGridPage extends StatelessWidget {
_appBarHeightNotifier.value = appBarHeight;
}
List<String> get filterKeys => filterEntries.keys.toList();
static const Color detailColor = Color(0xFFE0E0E0);
// TODO TLAD enforce max extent?
// static const double maxCrossAxisExtent = 180;
@override
Widget build(BuildContext context) {
return MediaQueryDataProvider(
@ -68,7 +67,7 @@ class FilterGridPage extends StatelessWidget {
if (viewportSize.isEmpty) return SizedBox.shrink();
final tileExtentManager = TileExtentManager(
routeName: context.currentRouteName,
settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
columnCountMin: 2,
columnCountDefault: 2,
extentMin: 60,
@ -80,38 +79,51 @@ class FilterGridPage extends StatelessWidget {
valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) {
final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent);
final scrollView = AnimationLimiter(
child: _buildDraggableScrollView(_buildScrollView(context, columnCount)),
);
return GridScaleGestureDetector<FilterGridItem>(
tileExtentManager: tileExtentManager,
scrollableKey: _scrollableKey,
appBarHeightNotifier: _appBarHeightNotifier,
viewportSize: viewportSize,
showScaledGrid: false,
scaledBuilder: (item, extent) {
final filter = item.filter;
return SizedBox(
width: extent,
height: extent,
child: DecoratedFilterChip(
source: source,
filter: filter,
entry: item.entry,
extent: extent,
pinned: settings.pinnedFilters.contains(filter),
),
return ValueListenableBuilder<String>(
valueListenable: queryNotifier,
builder: (context, query, child) {
final allFilters = filterEntries.keys;
final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList();
final scrollView = AnimationLimiter(
child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)),
);
return GridScaleGestureDetector<FilterGridItem>(
tileExtentManager: tileExtentManager,
scrollableKey: _scrollableKey,
appBarHeightNotifier: _appBarHeightNotifier,
viewportSize: viewportSize,
showScaledGrid: false,
scaledBuilder: (item, extent) {
final filter = item.filter;
return SizedBox(
width: extent,
height: extent,
child: DecoratedFilterChip(
source: source,
filter: filter,
entry: item.entry,
extent: extent,
pinned: settings.pinnedFilters.contains(filter),
),
);
},
getScaledItemTileRect: (context, item) {
final index = visibleFilters.indexOf(item.filter);
final column = index % columnCount;
final row = (index / columnCount).floor();
final left = tileExtent * column + spacing * (column - 1);
final top = tileExtent * row + spacing * (row - 1);
return Rect.fromLTWH(left, top, tileExtent, tileExtent);
},
onScaled: (item) {
// TODO TLAD highlight scaled item
},
child: scrollView,
);
},
getScaledItemTileRect: (context, item) {
// TODO TLAD
return Rect.zero;
},
onScaled: (item) {
// TODO TLAD
},
child: scrollView,
);
},
);
@ -148,14 +160,14 @@ class FilterGridPage extends StatelessWidget {
);
}
ScrollView _buildScrollView(BuildContext context, int columnCount) {
ScrollView _buildScrollView(BuildContext context, int columnCount, List<T> visibleFilters) {
final pinnedFilters = settings.pinnedFilters;
return CustomScrollView(
key: _scrollableKey,
controller: PrimaryScrollController.of(context),
slivers: [
appBar,
filterKeys.isEmpty
visibleFilters.isEmpty
? SliverFillRemaining(
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.viewInsets.bottom,
@ -171,13 +183,12 @@ class FilterGridPage extends StatelessWidget {
: SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, i) {
final key = filterKeys[i];
final filter = filterBuilder(key);
final entry = filterEntries[key];
final filter = visibleFilters[i];
final entry = filterEntries[filter];
final child = MetaData(
metaData: ScalerMetadata(FilterGridItem(filter, entry)),
child: DecoratedFilterChip(
key: Key(key),
key: Key(filter.key),
source: source,
filter: filter,
entry: entry,
@ -200,7 +211,7 @@ class FilterGridPage extends StatelessWidget {
),
);
},
childCount: filterKeys.length,
childCount: visibleFilters.length,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
@ -221,8 +232,8 @@ class FilterGridPage extends StatelessWidget {
}
}
class FilterGridItem {
final CollectionFilter filter;
class FilterGridItem<T extends CollectionFilter> {
final T filter;
final ImageEntry entry;
const FilterGridItem(this.filter, this.entry);

View file

@ -18,20 +18,18 @@ import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
import 'package:aves/widgets/search/search_button.dart';
import 'package:aves/widgets/search/search_delegate.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class FilterNavigationPage extends StatelessWidget {
class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source;
final String title;
final ChipSetActionDelegate chipSetActionDelegate;
final ChipActionDelegate chipActionDelegate;
final Map<String, ImageEntry> filterEntries;
final CollectionFilter Function(String key) filterBuilder;
final Map<T, ImageEntry> filterEntries;
final Widget Function() emptyBuilder;
final List<ChipAction> Function(CollectionFilter filter) chipActionsBuilder;
final List<ChipAction> Function(T filter) chipActionsBuilder;
const FilterNavigationPage({
@required this.source,
@ -40,13 +38,12 @@ class FilterNavigationPage extends StatelessWidget {
@required this.chipActionDelegate,
@required this.chipActionsBuilder,
@required this.filterEntries,
@required this.filterBuilder,
@required this.emptyBuilder,
});
@override
Widget build(BuildContext context) {
return FilterGridPage(
return FilterGridPage<T>(
source: source,
appBar: SliverAppBar(
title: TappableAppBarTitle(
@ -61,7 +58,7 @@ class FilterNavigationPage extends StatelessWidget {
floating: true,
),
filterEntries: filterEntries,
filterBuilder: filterBuilder,
queryNotifier: ValueNotifier(''),
emptyBuilder: () => ValueListenableBuilder<SourceState>(
valueListenable: source.stateNotifier,
builder: (context, sourceState, child) {
@ -84,7 +81,7 @@ class FilterNavigationPage extends StatelessWidget {
);
}
Future<void> _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
Future<void> _showMenu(BuildContext context, T filter, Offset tapPosition) async {
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
final touchArea = Size(40, 40);
final selectedAction = await showMenu<ChipAction>(
@ -144,13 +141,13 @@ class FilterNavigationPage extends StatelessWidget {
));
}
static int compareChipsByDate(MapEntry<String, ImageEntry> a, MapEntry<String, ImageEntry> b) {
static int compareChipsByDate(MapEntry<CollectionFilter, ImageEntry> a, MapEntry<CollectionFilter, ImageEntry> b) {
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
return c != 0 ? c : a.key.compareTo(b.key);
}
static int compareChipsByEntryCount(MapEntry<String, num> a, MapEntry<String, num> b) {
static int compareChipsByEntryCount(MapEntry<CollectionFilter, num> a, MapEntry<CollectionFilter, num> b) {
final c = b.value.compareTo(a.value) ?? -1;
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
return c != 0 ? c : a.key.compareTo(b.key);
}
}

View file

@ -39,7 +39,6 @@ class CountryListPage extends StatelessWidget {
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
],
filterEntries: _getCountryEntries(),
filterBuilder: _buildFilter,
emptyBuilder: () => EmptyContent(
icon: AIcons.location,
text: 'No countries',
@ -50,31 +49,29 @@ class CountryListPage extends StatelessWidget {
);
}
CollectionFilter _buildFilter(String location) => LocationFilter(LocationLevel.country, location);
Map<String, ImageEntry> _getCountryEntries() {
final pinned = settings.pinnedFilters.whereType<LocationFilter>().map((f) => f.countryNameAndCode);
Map<LocationFilter, ImageEntry> _getCountryEntries() {
final pinned = settings.pinnedFilters.whereType<LocationFilter>();
final entriesByDate = source.sortedEntriesForFilterList;
// countries are initially sorted by name at the source level
var sortedCountries = source.sortedCountries;
var sortedFilters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location));
if (settings.countrySortFactor == ChipSortFactor.count) {
var filtersWithCount = List.of(sortedCountries.map((s) => MapEntry(s, source.count(_buildFilter(s)))));
final filtersWithCount = List.of(sortedFilters.map((filter) => MapEntry(filter, source.count(filter))));
filtersWithCount.sort(FilterNavigationPage.compareChipsByEntryCount);
sortedCountries = filtersWithCount.map((kv) => kv.key).toList();
sortedFilters = filtersWithCount.map((kv) => kv.key).toList();
}
final locatedEntries = entriesByDate.where((entry) => entry.isLocated);
final allMapEntries = sortedCountries.map((countryNameAndCode) {
final split = countryNameAndCode.split(LocationFilter.locationSeparator);
final allMapEntries = sortedFilters.map((filter) {
final split = filter.countryNameAndCode.split(LocationFilter.locationSeparator);
ImageEntry entry;
if (split.length > 1) {
final countryCode = split[1];
entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null);
}
return MapEntry(countryNameAndCode, entry);
return MapEntry(filter, entry);
});
final byPin = groupBy<MapEntry<String, ImageEntry>, bool>(allMapEntries, (e) => pinned.contains(e.key));
final byPin = groupBy<MapEntry<LocationFilter, ImageEntry>, bool>(allMapEntries, (e) => pinned.contains(e.key));
final pinnedMapEntries = (byPin[true] ?? []);
final unpinnedMapEntries = (byPin[false] ?? []);

View file

@ -39,7 +39,6 @@ class TagListPage extends StatelessWidget {
settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
],
filterEntries: _getTagEntries(),
filterBuilder: _buildFilter,
emptyBuilder: () => EmptyContent(
icon: AIcons.tag,
text: 'No tags',
@ -50,25 +49,23 @@ class TagListPage extends StatelessWidget {
);
}
CollectionFilter _buildFilter(String tag) => TagFilter(tag);
Map<String, ImageEntry> _getTagEntries() {
final pinned = settings.pinnedFilters.whereType<TagFilter>().map((f) => f.tag);
Map<TagFilter, ImageEntry> _getTagEntries() {
final pinned = settings.pinnedFilters.whereType<TagFilter>();
final entriesByDate = source.sortedEntriesForFilterList;
// tags are initially sorted by name at the source level
var sortedTags = source.sortedTags;
var sortedFilters = source.sortedTags.map((tag) => TagFilter(tag));
if (settings.tagSortFactor == ChipSortFactor.count) {
var filtersWithCount = List.of(sortedTags.map((s) => MapEntry(s, source.count(_buildFilter(s)))));
final filtersWithCount = List.of(sortedFilters.map((filter) => MapEntry(filter, source.count(filter))));
filtersWithCount.sort(FilterNavigationPage.compareChipsByEntryCount);
sortedTags = filtersWithCount.map((kv) => kv.key).toList();
sortedFilters = filtersWithCount.map((kv) => kv.key).toList();
}
final allMapEntries = sortedTags.map((tag) => MapEntry(
tag,
entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null),
final allMapEntries = sortedFilters.map((filter) => MapEntry(
filter,
entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(filter.tag), orElse: () => null),
));
final byPin = groupBy<MapEntry<String, ImageEntry>, bool>(allMapEntries, (e) => pinned.contains(e.key));
final byPin = groupBy<MapEntry<TagFilter, ImageEntry>, bool>(allMapEntries, (e) => pinned.contains(e.key));
final pinnedMapEntries = (byPin[true] ?? []);
final unpinnedMapEntries = (byPin[false] ?? []);