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(); if (viewportSize.isEmpty) return SizedBox.shrink();
final tileExtentManager = TileExtentManager( final tileExtentManager = TileExtentManager(
routeName: context.currentRouteName, settingsRouteKey: context.currentRouteName,
columnCountMin: columnCountMin, columnCountMin: columnCountMin,
columnCountDefault: columnCountDefault, columnCountDefault: columnCountDefault,
extentMin: extentMin, extentMin: extentMin,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,12 +20,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class FilterGridPage extends StatelessWidget { class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source; final CollectionSource source;
final Widget appBar; final Widget appBar;
final Map<String, ImageEntry> filterEntries; final Map<T, ImageEntry> filterEntries;
final CollectionFilter Function(String key) filterBuilder; final ValueNotifier<String> queryNotifier;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final String settingsRouteKey;
final Iterable<T> Function(Iterable<T> filters, String query) applyQuery;
final FilterCallback onTap; final FilterCallback onTap;
final OffsetFilterCallback onLongPress; final OffsetFilterCallback onLongPress;
@ -39,8 +41,10 @@ class FilterGridPage extends StatelessWidget {
@required this.source, @required this.source,
@required this.appBar, @required this.appBar,
@required this.filterEntries, @required this.filterEntries,
@required this.filterBuilder, @required this.queryNotifier,
this.applyQuery,
@required this.emptyBuilder, @required this.emptyBuilder,
this.settingsRouteKey,
double appBarHeight = kToolbarHeight, double appBarHeight = kToolbarHeight,
@required this.onTap, @required this.onTap,
this.onLongPress, this.onLongPress,
@ -48,13 +52,8 @@ class FilterGridPage extends StatelessWidget {
_appBarHeightNotifier.value = appBarHeight; _appBarHeightNotifier.value = appBarHeight;
} }
List<String> get filterKeys => filterEntries.keys.toList();
static const Color detailColor = Color(0xFFE0E0E0); static const Color detailColor = Color(0xFFE0E0E0);
// TODO TLAD enforce max extent?
// static const double maxCrossAxisExtent = 180;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return MediaQueryDataProvider(
@ -68,7 +67,7 @@ class FilterGridPage extends StatelessWidget {
if (viewportSize.isEmpty) return SizedBox.shrink(); if (viewportSize.isEmpty) return SizedBox.shrink();
final tileExtentManager = TileExtentManager( final tileExtentManager = TileExtentManager(
routeName: context.currentRouteName, settingsRouteKey: settingsRouteKey ?? context.currentRouteName,
columnCountMin: 2, columnCountMin: 2,
columnCountDefault: 2, columnCountDefault: 2,
extentMin: 60, extentMin: 60,
@ -80,8 +79,15 @@ class FilterGridPage extends StatelessWidget {
valueListenable: _tileExtentNotifier, valueListenable: _tileExtentNotifier,
builder: (context, tileExtent, child) { builder: (context, tileExtent, child) {
final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent);
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( final scrollView = AnimationLimiter(
child: _buildDraggableScrollView(_buildScrollView(context, columnCount)), child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)),
); );
return GridScaleGestureDetector<FilterGridItem>( return GridScaleGestureDetector<FilterGridItem>(
@ -105,17 +111,23 @@ class FilterGridPage extends StatelessWidget {
); );
}, },
getScaledItemTileRect: (context, item) { getScaledItemTileRect: (context, item) {
// TODO TLAD final index = visibleFilters.indexOf(item.filter);
return Rect.zero; 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) { onScaled: (item) {
// TODO TLAD // TODO TLAD highlight scaled item
}, },
child: scrollView, 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; final pinnedFilters = settings.pinnedFilters;
return CustomScrollView( return CustomScrollView(
key: _scrollableKey, key: _scrollableKey,
controller: PrimaryScrollController.of(context), controller: PrimaryScrollController.of(context),
slivers: [ slivers: [
appBar, appBar,
filterKeys.isEmpty visibleFilters.isEmpty
? SliverFillRemaining( ? SliverFillRemaining(
child: Selector<MediaQueryData, double>( child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.viewInsets.bottom, selector: (context, mq) => mq.viewInsets.bottom,
@ -171,13 +183,12 @@ class FilterGridPage extends StatelessWidget {
: SliverGrid( : SliverGrid(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, i) { (context, i) {
final key = filterKeys[i]; final filter = visibleFilters[i];
final filter = filterBuilder(key); final entry = filterEntries[filter];
final entry = filterEntries[key];
final child = MetaData( final child = MetaData(
metaData: ScalerMetadata(FilterGridItem(filter, entry)), metaData: ScalerMetadata(FilterGridItem(filter, entry)),
child: DecoratedFilterChip( child: DecoratedFilterChip(
key: Key(key), key: Key(filter.key),
source: source, source: source,
filter: filter, filter: filter,
entry: entry, entry: entry,
@ -200,7 +211,7 @@ class FilterGridPage extends StatelessWidget {
), ),
); );
}, },
childCount: filterKeys.length, childCount: visibleFilters.length,
), ),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount, crossAxisCount: columnCount,
@ -221,8 +232,8 @@ class FilterGridPage extends StatelessWidget {
} }
} }
class FilterGridItem { class FilterGridItem<T extends CollectionFilter> {
final CollectionFilter filter; final T filter;
final ImageEntry entry; final ImageEntry entry;
const FilterGridItem(this.filter, this.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/filter_grids/common/filter_grid_page.dart';
import 'package:aves/widgets/search/search_button.dart'; import 'package:aves/widgets/search/search_button.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
class FilterNavigationPage extends StatelessWidget { class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source; final CollectionSource source;
final String title; final String title;
final ChipSetActionDelegate chipSetActionDelegate; final ChipSetActionDelegate chipSetActionDelegate;
final ChipActionDelegate chipActionDelegate; final ChipActionDelegate chipActionDelegate;
final Map<String, ImageEntry> filterEntries; final Map<T, ImageEntry> filterEntries;
final CollectionFilter Function(String key) filterBuilder;
final Widget Function() emptyBuilder; final Widget Function() emptyBuilder;
final List<ChipAction> Function(CollectionFilter filter) chipActionsBuilder; final List<ChipAction> Function(T filter) chipActionsBuilder;
const FilterNavigationPage({ const FilterNavigationPage({
@required this.source, @required this.source,
@ -40,13 +38,12 @@ class FilterNavigationPage extends StatelessWidget {
@required this.chipActionDelegate, @required this.chipActionDelegate,
@required this.chipActionsBuilder, @required this.chipActionsBuilder,
@required this.filterEntries, @required this.filterEntries,
@required this.filterBuilder,
@required this.emptyBuilder, @required this.emptyBuilder,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FilterGridPage( return FilterGridPage<T>(
source: source, source: source,
appBar: SliverAppBar( appBar: SliverAppBar(
title: TappableAppBarTitle( title: TappableAppBarTitle(
@ -61,7 +58,7 @@ class FilterNavigationPage extends StatelessWidget {
floating: true, floating: true,
), ),
filterEntries: filterEntries, filterEntries: filterEntries,
filterBuilder: filterBuilder, queryNotifier: ValueNotifier(''),
emptyBuilder: () => ValueListenableBuilder<SourceState>( emptyBuilder: () => ValueListenableBuilder<SourceState>(
valueListenable: source.stateNotifier, valueListenable: source.stateNotifier,
builder: (context, sourceState, child) { 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 RenderBox overlay = Overlay.of(context).context.findRenderObject();
final touchArea = Size(40, 40); final touchArea = Size(40, 40);
final selectedAction = await showMenu<ChipAction>( 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; 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; 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, settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin,
], ],
filterEntries: _getCountryEntries(), filterEntries: _getCountryEntries(),
filterBuilder: _buildFilter,
emptyBuilder: () => EmptyContent( emptyBuilder: () => EmptyContent(
icon: AIcons.location, icon: AIcons.location,
text: 'No countries', text: 'No countries',
@ -50,31 +49,29 @@ class CountryListPage extends StatelessWidget {
); );
} }
CollectionFilter _buildFilter(String location) => LocationFilter(LocationLevel.country, location); Map<LocationFilter, ImageEntry> _getCountryEntries() {
final pinned = settings.pinnedFilters.whereType<LocationFilter>();
Map<String, ImageEntry> _getCountryEntries() {
final pinned = settings.pinnedFilters.whereType<LocationFilter>().map((f) => f.countryNameAndCode);
final entriesByDate = source.sortedEntriesForFilterList; final entriesByDate = source.sortedEntriesForFilterList;
// countries are initially sorted by name at the source level // 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) { 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); 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 locatedEntries = entriesByDate.where((entry) => entry.isLocated);
final allMapEntries = sortedCountries.map((countryNameAndCode) { final allMapEntries = sortedFilters.map((filter) {
final split = countryNameAndCode.split(LocationFilter.locationSeparator); final split = filter.countryNameAndCode.split(LocationFilter.locationSeparator);
ImageEntry entry; ImageEntry entry;
if (split.length > 1) { if (split.length > 1) {
final countryCode = split[1]; final countryCode = split[1];
entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null); 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 pinnedMapEntries = (byPin[true] ?? []);
final unpinnedMapEntries = (byPin[false] ?? []); final unpinnedMapEntries = (byPin[false] ?? []);

View file

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