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:
parent
e218afc6b6
commit
f86eb078a4
10 changed files with 143 additions and 138 deletions
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
source: source,
|
||||||
builder: (context, filter, child) => FilterGridPage(
|
appBar: appBar,
|
||||||
source: source,
|
filterEntries: AlbumListPage.getAlbumEntries(source),
|
||||||
appBar: appBar,
|
applyQuery: (filters, query) {
|
||||||
filterEntries: AlbumListPage.getAlbumEntries(source, filter: filter),
|
if (query == null || query.isEmpty) return filters;
|
||||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
query = query.toUpperCase();
|
||||||
emptyBuilder: () => EmptyContent(
|
return filters.where((filter) => filter.uniqueName.toUpperCase().contains(query)).toList();
|
||||||
icon: AIcons.album,
|
},
|
||||||
text: 'No albums',
|
queryNotifier: _queryNotifier,
|
||||||
),
|
emptyBuilder: () => EmptyContent(
|
||||||
appBarHeight: AlbumPickAppBar.preferredHeight,
|
icon: AIcons.album,
|
||||||
onTap: (filter) => Navigator.pop<String>(context, (filter as AlbumFilter)?.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 {
|
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(
|
||||||
|
|
|
@ -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] ?? []);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,38 +79,51 @@ 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);
|
||||||
final scrollView = AnimationLimiter(
|
|
||||||
child: _buildDraggableScrollView(_buildScrollView(context, columnCount)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return GridScaleGestureDetector<FilterGridItem>(
|
return ValueListenableBuilder<String>(
|
||||||
tileExtentManager: tileExtentManager,
|
valueListenable: queryNotifier,
|
||||||
scrollableKey: _scrollableKey,
|
builder: (context, query, child) {
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
final allFilters = filterEntries.keys;
|
||||||
viewportSize: viewportSize,
|
final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList();
|
||||||
showScaledGrid: false,
|
|
||||||
scaledBuilder: (item, extent) {
|
final scrollView = AnimationLimiter(
|
||||||
final filter = item.filter;
|
child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)),
|
||||||
return SizedBox(
|
);
|
||||||
width: extent,
|
|
||||||
height: extent,
|
return GridScaleGestureDetector<FilterGridItem>(
|
||||||
child: DecoratedFilterChip(
|
tileExtentManager: tileExtentManager,
|
||||||
source: source,
|
scrollableKey: _scrollableKey,
|
||||||
filter: filter,
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
entry: item.entry,
|
viewportSize: viewportSize,
|
||||||
extent: extent,
|
showScaledGrid: false,
|
||||||
pinned: settings.pinnedFilters.contains(filter),
|
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;
|
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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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] ?? []);
|
||||||
|
|
||||||
|
|
|
@ -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] ?? []);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue