From e218afc6b6ceeb5d16b593453559bd673a1f2dc0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 26 Nov 2020 14:44:22 +0900 Subject: [PATCH] filter grid scaling (WIP) --- lib/model/settings/settings.dart | 14 +- .../collection/grid/list_section_layout.dart | 4 +- lib/widgets/collection/grid/list_sliver.dart | 2 +- .../collection/grid/tile_extent_manager.dart | 46 --- .../collection/thumbnail_collection.dart | 29 +- .../common/identity/aves_filter_chip.dart | 19 +- .../{collection/grid => common}/scaling.dart | 75 +++-- lib/widgets/common/tile_extent_manager.dart | 70 ++++ lib/widgets/debug/settings.dart | 9 +- lib/widgets/filter_grids/albums_page.dart | 6 +- .../common/decorated_filter_chip.dart | 52 +-- .../filter_grids/common/filter_grid_page.dart | 302 +++++++----------- .../filter_grids/common/filter_nav_page.dart | 156 +++++++++ lib/widgets/filter_grids/countries_page.dart | 2 +- lib/widgets/filter_grids/tags_page.dart | 6 +- 15 files changed, 469 insertions(+), 323 deletions(-) delete mode 100644 lib/widgets/collection/grid/tile_extent_manager.dart rename lib/widgets/{collection/grid => common}/scaling.dart (83%) create mode 100644 lib/widgets/common/tile_extent_manager.dart create mode 100644 lib/widgets/filter_grids/common/filter_nav_page.dart diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 1e44c52e5..87f6e0380 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -29,11 +29,11 @@ class Settings extends ChangeNotifier { static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; static const catalogTimeZoneKey = 'catalog_time_zone'; + static const tileExtentPrefixKey = 'tile_extent_'; // collection static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; - static const collectionTileExtentKey = 'collection_tile_extent'; static const showThumbnailLocationKey = 'show_thumbnail_location'; static const showThumbnailRawKey = 'show_thumbnail_raw'; static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration'; @@ -112,6 +112,12 @@ class Settings extends ChangeNotifier { set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); + double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0; + + // do not notify, as tile extents are only used internally by `TileExtentManager` + // and should not trigger rebuilding by change notification + void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue, notify: false); + // collection EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values); @@ -122,12 +128,6 @@ class Settings extends ChangeNotifier { set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); - double get collectionTileExtent => _prefs.getDouble(collectionTileExtentKey) ?? 0; - - // do not notify, as `collectionTileExtent` is only used internally by `TileExtentManager` - // and should not trigger rebuilding by change notification - set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue, notify: false); - bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, true); set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue); diff --git a/lib/widgets/collection/grid/list_section_layout.dart b/lib/widgets/collection/grid/list_section_layout.dart index fe88bfd51..540beee50 100644 --- a/lib/widgets/collection/grid/list_section_layout.dart +++ b/lib/widgets/collection/grid/list_section_layout.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/grid/header_generic.dart'; -import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; +import 'package:aves/widgets/collection/thumbnail_collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -22,7 +22,7 @@ class SectionedListLayoutProvider extends StatelessWidget { @required this.thumbnailBuilder, @required this.child, }) : assert(scrollableWidth != 0), - columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin); + columnCount = max((scrollableWidth / tileExtent).round(), ThumbnailCollection.columnCountMin); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/grid/list_sliver.dart b/lib/widgets/collection/grid/list_sliver.dart index e275fc223..91089845c 100644 --- a/lib/widgets/collection/grid/list_sliver.dart +++ b/lib/widgets/collection/grid/list_sliver.dart @@ -4,7 +4,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/widgets/collection/grid/list_known_extent.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; -import 'package:aves/widgets/collection/grid/scaling.dart'; +import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; diff --git a/lib/widgets/collection/grid/tile_extent_manager.dart b/lib/widgets/collection/grid/tile_extent_manager.dart deleted file mode 100644 index 1ddf1f922..000000000 --- a/lib/widgets/collection/grid/tile_extent_manager.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:math'; - -import 'package:aves/model/settings/settings.dart'; -import 'package:flutter/widgets.dart'; - -class TileExtentManager { - static const int columnCountMin = 2; - static const int columnCountDefault = 4; - static const double tileExtentMin = 46.0; - static const viewportSizeMin = Size.square(tileExtentMin * columnCountMin); - - static double applyTileExtent( - Size viewportSize, - ValueNotifier extentNotifier, { - double userPreferredExtent = 0, - }) { - // sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size) - viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height)); - - final oldUserPreferredExtent = settings.collectionTileExtent; - final currentExtent = extentNotifier.value; - final targetExtent = userPreferredExtent > 0 - ? userPreferredExtent - : oldUserPreferredExtent > 0 - ? oldUserPreferredExtent - : currentExtent; - - int columnCount; - if (targetExtent > 0) { - columnCount = max(columnCountMin, (viewportSize.width / targetExtent.clamp(tileExtentMin, extentMaxForSize(viewportSize))).round()); - } else { - columnCount = columnCountDefault; - } - final newExtent = viewportSize.width / columnCount; - - if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { - settings.collectionTileExtent = newExtent; - } - if (extentNotifier.value != newExtent) { - extentNotifier.value = newExtent; - } - return newExtent; - } - - static double extentMaxForSize(Size viewportSize) => viewportSize.shortestSide / columnCountMin; -} diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index e7788cf94..e0c230aff 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -3,19 +3,20 @@ import 'dart:async'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/ref/mime_types.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; import 'package:aves/widgets/collection/grid/list_sliver.dart'; -import 'package:aves/widgets/collection/grid/scaling.dart'; -import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; +import 'package:aves/widgets/common/scaling.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,10 @@ class ThumbnailCollection extends StatelessWidget { final ValueNotifier _isScrollingNotifier = ValueNotifier(false); final GlobalKey _scrollableKey = GlobalKey(); + static const columnCountMin = 2; + static const columnCountDefault = 4; + static const extentMin = 46.0; + @override Widget build(BuildContext context) { return SafeArea( @@ -37,8 +42,15 @@ class ThumbnailCollection extends StatelessWidget { assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); if (viewportSize.isEmpty) return SizedBox.shrink(); - TileExtentManager.applyTileExtent(viewportSize, _tileExtentNotifier); - final cacheExtent = TileExtentManager.extentMaxForSize(viewportSize) * 2; + final tileExtentManager = TileExtentManager( + routeName: context.currentRouteName, + columnCountMin: columnCountMin, + columnCountDefault: columnCountDefault, + extentMin: extentMin, + extentNotifier: _tileExtentNotifier, + spacing: 0, + )..applyTileExtent(viewportSize: viewportSize); + final cacheExtent = tileExtentManager.getEffectiveExtentMax(viewportSize) * 2; // do not replace by Provider.of // so that view updates on collection filter changes @@ -58,16 +70,17 @@ class ThumbnailCollection extends StatelessWidget { ); final scaler = GridScaleGestureDetector( + tileExtentManager: tileExtentManager, scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, - extentNotifier: _tileExtentNotifier, viewportSize: viewportSize, + showScaledGrid: true, scaledBuilder: (entry, extent) => DecoratedThumbnail( entry: entry, extent: extent, showOverlay: false, ), - getScaledItemTileRect: (entry) { + getScaledItemTileRect: (context, entry) { final sectionedListLayout = Provider.of(context, listen: false); return sectionedListLayout.getTileRect(entry) ?? Rect.zero; }, diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 732d27485..bbcfb83c4 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -14,6 +14,7 @@ class AvesFilterChip extends StatefulWidget { final bool showGenericIcon; final Widget background; final Widget details; + final double padding; final HeroType heroType; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -24,7 +25,6 @@ class AvesFilterChip extends StatefulWidget { static const double minChipWidth = 80; static const double maxChipWidth = 160; static const double iconSize = 20; - static const double padding = 6; const AvesFilterChip({ Key key, @@ -33,8 +33,9 @@ class AvesFilterChip extends StatefulWidget { this.showGenericIcon = true, this.background, this.details, + this.padding = 6.0, this.heroType = HeroType.onTap, - @required this.onTap, + this.onTap, this.onLongPress, }) : assert(filter != null), super(key: key); @@ -51,6 +52,8 @@ class _AvesFilterChipState extends State { CollectionFilter get filter => widget.filter; + double get padding => widget.padding; + @override void initState() { super.initState(); @@ -80,9 +83,11 @@ class _AvesFilterChipState extends State { @override Widget build(BuildContext context) { + const iconSize = AvesFilterChip.iconSize; + final hasBackground = widget.background != null; - final leading = filter.iconBuilder(context, AvesFilterChip.iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground); - final trailing = widget.removable ? Icon(AIcons.clear, size: AvesFilterChip.iconSize) : null; + final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground); + final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null; Widget content = Row( mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min, @@ -90,7 +95,7 @@ class _AvesFilterChipState extends State { children: [ if (leading != null) ...[ leading, - SizedBox(width: AvesFilterChip.padding), + SizedBox(width: padding), ], Flexible( child: Text( @@ -101,7 +106,7 @@ class _AvesFilterChipState extends State { ), ), if (trailing != null) ...[ - SizedBox(width: AvesFilterChip.padding), + SizedBox(width: padding), trailing, ], ], @@ -118,7 +123,7 @@ class _AvesFilterChipState extends State { } content = Padding( - padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.padding * 2, vertical: 2), + padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2), child: content, ); diff --git a/lib/widgets/collection/grid/scaling.dart b/lib/widgets/common/scaling.dart similarity index 83% rename from lib/widgets/collection/grid/scaling.dart rename to lib/widgets/common/scaling.dart index fab83f78f..c5bb1603c 100644 --- a/lib/widgets/collection/grid/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -2,9 +2,9 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -16,20 +16,22 @@ class ScalerMetadata { } class GridScaleGestureDetector extends StatefulWidget { + final TileExtentManager tileExtentManager; final GlobalKey scrollableKey; final ValueNotifier appBarHeightNotifier; - final ValueNotifier extentNotifier; final Size viewportSize; + final bool showScaledGrid; final Widget Function(T item, double extent) scaledBuilder; - final Rect Function(T item) getScaledItemTileRect; + final Rect Function(BuildContext context, T item) getScaledItemTileRect; final void Function(T item) onScaled; final Widget child; const GridScaleGestureDetector({ - this.scrollableKey, + @required this.tileExtentManager, + @required this.scrollableKey, @required this.appBarHeightNotifier, - @required this.extentNotifier, @required this.viewportSize, + @required this.showScaledGrid, @required this.scaledBuilder, @required this.getScaledItemTileRect, @required this.onScaled, @@ -40,14 +42,16 @@ class GridScaleGestureDetector extends StatefulWidget { _GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState(); } -class _GridScaleGestureDetectorState extends State { +class _GridScaleGestureDetectorState extends State> { double _startExtent, _extentMin, _extentMax; bool _applyingScale = false; ValueNotifier _scaledExtentNotifier; OverlayEntry _overlayEntry; ScalerMetadata _metadata; - ValueNotifier get tileExtentNotifier => widget.extentNotifier; + TileExtentManager get tileExtentManager => widget.tileExtentManager; + + Size get viewportSize => widget.viewportSize; @override Widget build(BuildContext context) { @@ -72,13 +76,15 @@ class _GridScaleGestureDetectorState extends State // abort if we cannot find an image to show on overlay if (renderMetaData == null) return; _metadata = renderMetaData.metaData; - _startExtent = tileExtentNotifier.value; + _startExtent = renderMetaData.size.width; _scaledExtentNotifier = ValueNotifier(_startExtent); // not the same as `MediaQuery.size.width`, because of screen insets/padding final gridWidth = scrollableBox.size.width; - _extentMin = gridWidth / (gridWidth / TileExtentManager.tileExtentMin).round(); - _extentMax = gridWidth / (gridWidth / TileExtentManager.extentMaxForSize(widget.viewportSize)).round(); + + _extentMin = tileExtentManager.getEffectiveExtentMin(viewportSize); + _extentMax = tileExtentManager.getEffectiveExtentMax(viewportSize); + final halfExtent = _startExtent / 2; final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); _overlayEntry = OverlayEntry( @@ -87,6 +93,7 @@ class _GridScaleGestureDetectorState extends State center: thumbnailCenter, gridWidth: gridWidth, scaledExtentNotifier: _scaledExtentNotifier, + showScaledGrid: widget.showScaledGrid, ), ); Overlay.of(scrollableContext).insert(_overlayEntry); @@ -104,11 +111,10 @@ class _GridScaleGestureDetectorState extends State } _applyingScale = true; - final oldExtent = tileExtentNotifier.value; + final oldExtent = tileExtentManager.extentNotifier.value; // sanitize and update grid layout if necessary - final newExtent = TileExtentManager.applyTileExtent( - widget.viewportSize, - tileExtentNotifier, + final newExtent = tileExtentManager.applyTileExtent( + viewportSize: widget.viewportSize, userPreferredExtent: _scaledExtentNotifier.value, ); _scaledExtentNotifier = null; @@ -137,7 +143,7 @@ class _GridScaleGestureDetectorState extends State void _scrollToItem(T item) { final scrollableContext = widget.scrollableKey.currentContext; final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height; - final tileRect = widget.getScaledItemTileRect(item); + final tileRect = widget.getScaledItemTileRect(context, item); // most of the time the app bar will be scrolled away after scaling, // so we compensate for it to center the focal point thumbnail final appBarHeight = widget.appBarHeightNotifier.value; @@ -152,12 +158,14 @@ class ScaleOverlay extends StatefulWidget { final Offset center; final double gridWidth; final ValueNotifier scaledExtentNotifier; + final bool showScaledGrid; const ScaleOverlay({ @required this.builder, @required this.center, @required this.gridWidth, @required this.scaledExtentNotifier, + @required this.showScaledGrid, }); @override @@ -217,24 +225,29 @@ class _ScaleOverlayState extends State { } final clampedCenter = center.translate(dx, 0); - return CustomPaint( - painter: GridPainter( - center: clampedCenter, - extent: extent, - ), - child: Stack( - children: [ - Positioned( - left: clampedCenter.dx - extent / 2, - top: clampedCenter.dy - extent / 2, - child: DefaultTextStyle( - style: TextStyle(), - child: widget.builder(extent), - ), + var child = widget.builder(extent); + child = Stack( + children: [ + Positioned( + left: clampedCenter.dx - extent / 2, + top: clampedCenter.dy - extent / 2, + child: DefaultTextStyle( + style: TextStyle(), + child: child, ), - ], - ), + ), + ], ); + if (widget.showScaledGrid) { + child = CustomPaint( + painter: GridPainter( + center: clampedCenter, + extent: extent, + ), + child: child, + ); + } + return child; }, ), ), diff --git a/lib/widgets/common/tile_extent_manager.dart b/lib/widgets/common/tile_extent_manager.dart new file mode 100644 index 000000000..7e54ebab3 --- /dev/null +++ b/lib/widgets/common/tile_extent_manager.dart @@ -0,0 +1,70 @@ +import 'dart:math'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/widgets.dart'; + +class TileExtentManager { + final String routeName; + final int columnCountMin, columnCountDefault; + final double spacing, extentMin; + final ValueNotifier extentNotifier; + + const TileExtentManager({ + @required this.routeName, + @required this.columnCountMin, + @required this.columnCountDefault, + @required this.extentMin, + @required this.extentNotifier, + @required this.spacing, + }); + + double applyTileExtent({ + @required Size viewportSize, + double userPreferredExtent = 0, + }) { + // sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size) + final viewportSizeMin = Size.square(extentMin * columnCountMin); + viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height)); + + final oldUserPreferredExtent = settings.getTileExtent(routeName); + final currentExtent = extentNotifier.value; + final targetExtent = userPreferredExtent > 0 + ? userPreferredExtent + : oldUserPreferredExtent > 0 + ? oldUserPreferredExtent + : currentExtent; + + final columnCount = getEffectiveColumnCountForExtent(viewportSize, targetExtent); + final newExtent = _extentForColumnCount(viewportSize, columnCount); + + if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { + settings.setTileExtent(routeName, newExtent); + } + if (extentNotifier.value != newExtent) { + extentNotifier.value = newExtent; + } + return newExtent; + } + + double _extentMax(Size viewportSize) => (viewportSize.shortestSide - spacing * (columnCountMin - 1)) / columnCountMin; + + double _columnCountForExtent(Size viewportSize, double extent) => (viewportSize.width + spacing) / (extent + spacing); + + double _extentForColumnCount(Size viewportSize, int columnCount) => (viewportSize.width - spacing * (columnCount - 1)) / columnCount; + + int _effectiveColumnCountMin(Size viewportSize) => _columnCountForExtent(viewportSize, _extentMax(viewportSize)).ceil(); + + int _effectiveColumnCountMax(Size viewportSize) => _columnCountForExtent(viewportSize, extentMin).floor(); + + double getEffectiveExtentMin(Size viewportSize) => _extentForColumnCount(viewportSize, _effectiveColumnCountMax(viewportSize)); + + double getEffectiveExtentMax(Size viewportSize) => _extentForColumnCount(viewportSize, _effectiveColumnCountMin(viewportSize)); + + int getEffectiveColumnCountForExtent(Size viewportSize, double extent) { + if (extent > 0) { + final columnCount = _columnCountForExtent(viewportSize, extent); + return columnCount.clamp(_effectiveColumnCountMin(viewportSize), _effectiveColumnCountMax(viewportSize)).round(); + } + return columnCountDefault; + } +} diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index 05e58c137..d3a1a7ce4 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -1,5 +1,9 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; +import 'package:aves/widgets/filter_grids/countries_page.dart'; +import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -27,7 +31,10 @@ class DebugSettingsSection extends StatelessWidget { Padding( padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup({ - 'collectionTileExtent': '${settings.collectionTileExtent}', + 'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}', + 'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}', + 'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}', + 'tileExtent - Tags': '${settings.getTileExtent(TagListPage.routeName)}', 'infoMapZoom': '${settings.infoMapZoom}', 'pinnedFilters': toMultiline(settings.pinnedFilters), 'searchHistory': toMultiline(settings.searchHistory), diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index a7604e3ef..642957d60 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; @@ -5,13 +6,12 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/model/actions/chip_actions.dart'; 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_nav_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 245b8cd12..f7690b276 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -1,16 +1,17 @@ +import 'dart:math'; import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; @@ -19,6 +20,7 @@ class DecoratedFilterChip extends StatelessWidget { final CollectionSource source; final CollectionFilter filter; final ImageEntry entry; + final double extent; final bool pinned; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -28,8 +30,9 @@ class DecoratedFilterChip extends StatelessWidget { @required this.source, @required this.filter, @required this.entry, + @required this.extent, this.pinned = false, - @required this.onTap, + this.onTap, this.onLongPress, }) : super(key: key); @@ -40,57 +43,60 @@ class DecoratedFilterChip extends StatelessWidget { : entry.isSvg ? ThumbnailVectorImage( entry: entry, - extent: FilterGridPage.maxCrossAxisExtent, + extent: extent, ) : ThumbnailRasterImage( entry: entry, - extent: FilterGridPage.maxCrossAxisExtent, + extent: extent, ); + final titlePadding = min(6.0, extent / 16); return AvesFilterChip( filter: filter, showGenericIcon: false, background: backgroundImage, details: _buildDetails(filter), + padding: titlePadding, onTap: onTap, onLongPress: onLongPress, ); } Widget _buildDetails(CollectionFilter filter) { - final count = Text( - '${source.count(filter)}', - style: TextStyle(color: FilterGridPage.detailColor), - ); + final padding = min(8.0, extent / 16); + final iconSize = min(14.0, extent / 8); + final fontSize = min(14.0, (extent / 6).roundToDouble()); return Row( mainAxisSize: MainAxisSize.min, children: [ - AnimatedCrossFade( - firstChild: Padding( - padding: EdgeInsets.only(right: 8), + if (pinned) + AnimatedPadding( + padding: EdgeInsets.only(right: padding), child: DecoratedIcon( AIcons.pin, color: FilterGridPage.detailColor, shadows: [Constants.embossShadow], - size: 16, + size: iconSize, ), + duration: Durations.chipDecorationAnimation, ), - secondChild: SizedBox.shrink(), - sizeCurve: Curves.easeInOutCubic, - alignment: AlignmentDirectional.centerEnd, - crossFadeState: pinned ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: Durations.chipDecorationAnimation, - ), if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) - Padding( - padding: EdgeInsets.only(right: 8), + AnimatedPadding( + padding: EdgeInsets.only(right: padding), child: DecoratedIcon( AIcons.removableStorage, color: FilterGridPage.detailColor, shadows: [Constants.embossShadow], - size: 16, + size: iconSize, ), + duration: Durations.chipDecorationAnimation, ), - count, + Text( + '${source.count(filter)}', + style: TextStyle( + color: FilterGridPage.detailColor, + fontSize: fontSize, + ), + ), ], ); } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 1a14338fb..ee75af44a 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -1,193 +1,59 @@ import 'dart:ui'; -import 'package:aves/main.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/app_bar_subtitle.dart'; -import 'package:aves/widgets/common/app_bar_title.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/basic/menu_row.dart'; -import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/scaling.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; -import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/model/actions/chip_actions.dart'; -import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; -import 'package:aves/widgets/search/search_button.dart'; -import 'package:aves/widgets/search/search_delegate.dart'; -import 'package:collection/collection.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; -class FilterNavigationPage extends StatelessWidget { - final CollectionSource source; - final String title; - final ChipSetActionDelegate chipSetActionDelegate; - final ChipActionDelegate chipActionDelegate; - final Map filterEntries; - final CollectionFilter Function(String key) filterBuilder; - final Widget Function() emptyBuilder; - final List Function(CollectionFilter filter) chipActionsBuilder; - - const FilterNavigationPage({ - @required this.source, - @required this.title, - @required this.chipSetActionDelegate, - @required this.chipActionDelegate, - @required this.chipActionsBuilder, - @required this.filterEntries, - @required this.filterBuilder, - @required this.emptyBuilder, - }); - - @override - Widget build(BuildContext context) { - return FilterGridPage( - source: source, - appBar: SliverAppBar( - title: TappableAppBarTitle( - onTap: () => _goToSearch(context), - child: SourceStateAwareAppBarTitle( - title: Text(title), - source: source, - ), - ), - actions: _buildActions(context), - titleSpacing: 0, - floating: true, - ), - filterEntries: filterEntries, - filterBuilder: filterBuilder, - emptyBuilder: () => ValueListenableBuilder( - valueListenable: source.stateNotifier, - builder: (context, sourceState, child) { - return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); - }, - ), - onTap: (filter) => Navigator.push( - context, - MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage(CollectionLens( - source: source, - filters: [filter], - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - )), - ), - ), - onLongPress: AvesApp.mode == AppMode.main ? (filter, tapPosition) => _showMenu(context, filter, tapPosition) : null, - ); - } - - Future _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { - final RenderBox overlay = Overlay.of(context).context.findRenderObject(); - final touchArea = Size(40, 40); - final selectedAction = await showMenu( - context: context, - position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), - items: chipActionsBuilder(filter) - .map((action) => PopupMenuItem( - value: action, - child: MenuRow(text: action.getText(), icon: action.getIcon()), - )) - .toList(), - ); - if (selectedAction != null) { - // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipActionDelegate.onActionSelected(context, filter, selectedAction)); - } - } - - List _buildActions(BuildContext context) { - return [ - SearchButton(source), - PopupMenuButton( - key: Key('appbar-menu-button'), - itemBuilder: (context) { - return [ - PopupMenuItem( - key: Key('menu-sort'), - value: ChipSetAction.sort, - child: MenuRow(text: 'Sort…', icon: AIcons.sort), - ), - if (kDebugMode) - PopupMenuItem( - value: ChipSetAction.refresh, - child: MenuRow(text: 'Refresh', icon: AIcons.refresh), - ), - PopupMenuItem( - value: ChipSetAction.stats, - child: MenuRow(text: 'Stats', icon: AIcons.stats), - ), - ]; - }, - onSelected: (action) { - // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipSetActionDelegate.onActionSelected(context, action)); - }, - ), - ]; - } - - void _goToSearch(BuildContext context) { - Navigator.push( - context, - SearchPageRoute( - delegate: ImageSearchDelegate( - source: source, - ), - )); - } - - static int compareChipsByDate(MapEntry a, MapEntry b) { - final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); - } - - static int compareChipsByEntryCount(MapEntry a, MapEntry b) { - final c = b.value.compareTo(a.value) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); - } -} - class FilterGridPage extends StatelessWidget { final CollectionSource source; final Widget appBar; final Map filterEntries; final CollectionFilter Function(String key) filterBuilder; final Widget Function() emptyBuilder; - final double appBarHeight; final FilterCallback onTap; final OffsetFilterCallback onLongPress; - const FilterGridPage({ + final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); + final ValueNotifier _tileExtentNotifier = ValueNotifier(0); + final GlobalKey _scrollableKey = GlobalKey(); + + static const spacing = 8.0; + + FilterGridPage({ @required this.source, @required this.appBar, @required this.filterEntries, @required this.filterBuilder, @required this.emptyBuilder, - this.appBarHeight = kToolbarHeight, + double appBarHeight = kToolbarHeight, @required this.onTap, this.onLongPress, - }); + }) { + _appBarHeightNotifier.value = appBarHeight; + } List get filterKeys => filterEntries.keys.toList(); static const Color detailColor = Color(0xFFE0E0E0); - static const double maxCrossAxisExtent = 180; + + // TODO TLAD enforce max extent? + // static const double maxCrossAxisExtent = 180; @override Widget build(BuildContext context) { @@ -195,13 +61,59 @@ class FilterGridPage extends StatelessWidget { child: Scaffold( body: DoubleBackPopScope( child: SafeArea( - child: Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) { - final columnCount = (mqWidth / maxCrossAxisExtent).ceil(); - final scrollView = _buildScrollView(context, columnCount); - return AnimationLimiter( - child: _buildDraggableScrollView(scrollView), + child: LayoutBuilder( + builder: (context, constraints) { + final viewportSize = constraints.biggest; + assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); + if (viewportSize.isEmpty) return SizedBox.shrink(); + + final tileExtentManager = TileExtentManager( + routeName: context.currentRouteName, + columnCountMin: 2, + columnCountDefault: 2, + extentMin: 60, + extentNotifier: _tileExtentNotifier, + spacing: spacing, + )..applyTileExtent(viewportSize: viewportSize); + + return ValueListenableBuilder( + valueListenable: _tileExtentNotifier, + builder: (context, tileExtent, child) { + final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); + final scrollView = AnimationLimiter( + child: _buildDraggableScrollView(_buildScrollView(context, columnCount)), + ); + + return GridScaleGestureDetector( + 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) { + // TODO TLAD + return Rect.zero; + }, + onScaled: (item) { + // TODO TLAD + }, + child: scrollView, + ); + }, ); }, ), @@ -228,7 +140,7 @@ class FilterGridPage extends StatelessWidget { controller: PrimaryScrollController.of(context), padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below - top: appBarHeight, + top: _appBarHeightNotifier.value, bottom: mqViewInsetsBottom, ), child: scrollView, @@ -239,6 +151,7 @@ class FilterGridPage extends StatelessWidget { ScrollView _buildScrollView(BuildContext context, int columnCount) { final pinnedFilters = settings.pinnedFilters; return CustomScrollView( + key: _scrollableKey, controller: PrimaryScrollController.of(context), slivers: [ appBar, @@ -255,42 +168,44 @@ class FilterGridPage extends StatelessWidget { ), hasScrollBody: false, ) - : SliverPadding( - padding: EdgeInsets.all(AvesFilterChip.outlineWidth), - sliver: SliverGrid( - delegate: SliverChildBuilderDelegate( - (context, i) { - final key = filterKeys[i]; - final filter = filterBuilder(key); - final child = DecoratedFilterChip( + : SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, i) { + final key = filterKeys[i]; + final filter = filterBuilder(key); + final entry = filterEntries[key]; + final child = MetaData( + metaData: ScalerMetadata(FilterGridItem(filter, entry)), + child: DecoratedFilterChip( key: Key(key), source: source, filter: filter, - entry: filterEntries[key], + entry: entry, + extent: _tileExtentNotifier.value, pinned: pinnedFilters.contains(filter), onTap: onTap, onLongPress: onLongPress, - ); - return AnimationConfiguration.staggeredGrid( - position: i, - columnCount: columnCount, - duration: Durations.staggeredAnimation, - delay: Durations.staggeredAnimationDelay, - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, - ), + ), + ); + return AnimationConfiguration.staggeredGrid( + position: i, + columnCount: columnCount, + duration: Durations.staggeredAnimation, + delay: Durations.staggeredAnimationDelay, + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, ), - ); - }, - childCount: filterKeys.length, - ), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: maxCrossAxisExtent, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), + ), + ); + }, + childCount: filterKeys.length, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columnCount, + mainAxisSpacing: spacing, + crossAxisSpacing: spacing, ), ), SliverToBoxAdapter( @@ -305,3 +220,10 @@ class FilterGridPage extends StatelessWidget { ); } } + +class FilterGridItem { + final CollectionFilter filter; + final ImageEntry entry; + + const FilterGridItem(this.filter, this.entry); +} diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart new file mode 100644 index 000000000..36d65821b --- /dev/null +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -0,0 +1,156 @@ +import 'dart:ui'; + +import 'package:aves/main.dart'; +import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/image_entry.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/app_bar_subtitle.dart'; +import 'package:aves/widgets/common/app_bar_title.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; +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 { + final CollectionSource source; + final String title; + final ChipSetActionDelegate chipSetActionDelegate; + final ChipActionDelegate chipActionDelegate; + final Map filterEntries; + final CollectionFilter Function(String key) filterBuilder; + final Widget Function() emptyBuilder; + final List Function(CollectionFilter filter) chipActionsBuilder; + + const FilterNavigationPage({ + @required this.source, + @required this.title, + @required this.chipSetActionDelegate, + @required this.chipActionDelegate, + @required this.chipActionsBuilder, + @required this.filterEntries, + @required this.filterBuilder, + @required this.emptyBuilder, + }); + + @override + Widget build(BuildContext context) { + return FilterGridPage( + source: source, + appBar: SliverAppBar( + title: TappableAppBarTitle( + onTap: () => _goToSearch(context), + child: SourceStateAwareAppBarTitle( + title: Text(title), + source: source, + ), + ), + actions: _buildActions(context), + titleSpacing: 0, + floating: true, + ), + filterEntries: filterEntries, + filterBuilder: filterBuilder, + emptyBuilder: () => ValueListenableBuilder( + valueListenable: source.stateNotifier, + builder: (context, sourceState, child) { + return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); + }, + ), + onTap: (filter) => Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: CollectionPage.routeName), + builder: (context) => CollectionPage(CollectionLens( + source: source, + filters: [filter], + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + )), + ), + ), + onLongPress: AvesApp.mode == AppMode.main ? (filter, tapPosition) => _showMenu(context, filter, tapPosition) : null, + ); + } + + Future _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { + final RenderBox overlay = Overlay.of(context).context.findRenderObject(); + final touchArea = Size(40, 40); + final selectedAction = await showMenu( + context: context, + position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), + items: chipActionsBuilder(filter) + .map((action) => PopupMenuItem( + value: action, + child: MenuRow(text: action.getText(), icon: action.getIcon()), + )) + .toList(), + ); + if (selectedAction != null) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipActionDelegate.onActionSelected(context, filter, selectedAction)); + } + } + + List _buildActions(BuildContext context) { + return [ + SearchButton(source), + PopupMenuButton( + key: Key('appbar-menu-button'), + itemBuilder: (context) { + return [ + PopupMenuItem( + key: Key('menu-sort'), + value: ChipSetAction.sort, + child: MenuRow(text: 'Sort…', icon: AIcons.sort), + ), + if (kDebugMode) + PopupMenuItem( + value: ChipSetAction.refresh, + child: MenuRow(text: 'Refresh', icon: AIcons.refresh), + ), + PopupMenuItem( + value: ChipSetAction.stats, + child: MenuRow(text: 'Stats', icon: AIcons.stats), + ), + ]; + }, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipSetActionDelegate.onActionSelected(context, action)); + }, + ), + ]; + } + + void _goToSearch(BuildContext context) { + Navigator.push( + context, + SearchPageRoute( + delegate: ImageSearchDelegate( + source: source, + ), + )); + } + + static int compareChipsByDate(MapEntry a, MapEntry b) { + final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; + return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + } + + static int compareChipsByEntryCount(MapEntry a, MapEntry b) { + final c = b.value.compareTo(a.value) ?? -1; + return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + } +} diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 0e5bec313..1c8587477 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -10,7 +10,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; 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_nav_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 312f4d926..1de87a33d 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; @@ -5,12 +6,11 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/tag.dart'; -import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/model/actions/chip_actions.dart'; 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_nav_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart';