filter grid scaling (WIP)
This commit is contained in:
parent
a4fab7339d
commit
e218afc6b6
15 changed files with 469 additions and 323 deletions
|
@ -29,11 +29,11 @@ class Settings extends ChangeNotifier {
|
||||||
static const keepScreenOnKey = 'keep_screen_on';
|
static const keepScreenOnKey = 'keep_screen_on';
|
||||||
static const homePageKey = 'home_page';
|
static const homePageKey = 'home_page';
|
||||||
static const catalogTimeZoneKey = 'catalog_time_zone';
|
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||||
|
static const tileExtentPrefixKey = 'tile_extent_';
|
||||||
|
|
||||||
// collection
|
// collection
|
||||||
static const collectionGroupFactorKey = 'collection_group_factor';
|
static const collectionGroupFactorKey = 'collection_group_factor';
|
||||||
static const collectionSortFactorKey = 'collection_sort_factor';
|
static const collectionSortFactorKey = 'collection_sort_factor';
|
||||||
static const collectionTileExtentKey = 'collection_tile_extent';
|
|
||||||
static const showThumbnailLocationKey = 'show_thumbnail_location';
|
static const showThumbnailLocationKey = 'show_thumbnail_location';
|
||||||
static const showThumbnailRawKey = 'show_thumbnail_raw';
|
static const showThumbnailRawKey = 'show_thumbnail_raw';
|
||||||
static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration';
|
static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration';
|
||||||
|
@ -112,6 +112,12 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
|
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
|
// collection
|
||||||
|
|
||||||
EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values);
|
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());
|
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);
|
bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, true);
|
||||||
|
|
||||||
set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue);
|
set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'dart:math';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/collection/grid/header_generic.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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class SectionedListLayoutProvider extends StatelessWidget {
|
||||||
@required this.thumbnailBuilder,
|
@required this.thumbnailBuilder,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
}) : assert(scrollableWidth != 0),
|
}) : assert(scrollableWidth != 0),
|
||||||
columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin);
|
columnCount = max((scrollableWidth / tileExtent).round(), ThumbnailCollection.columnCountMin);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/services/viewer_service.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_known_extent.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_section_layout.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/collection/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||||
|
|
|
@ -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<double> 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;
|
|
||||||
}
|
|
|
@ -3,19 +3,20 @@ import 'dart:async';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/image_entry.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_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.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/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/app_bar.dart';
|
import 'package:aves/widgets/collection/app_bar.dart';
|
||||||
import 'package:aves/widgets/collection/empty.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_section_layout.dart';
|
||||||
import 'package:aves/widgets/collection/grid/list_sliver.dart';
|
import 'package:aves/widgets/collection/grid/list_sliver.dart';
|
||||||
import 'package:aves/widgets/collection/grid/scaling.dart';
|
import 'package:aves/widgets/common/scaling.dart';
|
||||||
import 'package:aves/widgets/collection/grid/tile_extent_manager.dart';
|
import 'package:aves/widgets/common/tile_extent_manager.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/decorated.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/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
|
||||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -28,6 +29,10 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isScrollingNotifier = ValueNotifier(false);
|
||||||
final GlobalKey _scrollableKey = GlobalKey();
|
final GlobalKey _scrollableKey = GlobalKey();
|
||||||
|
|
||||||
|
static const columnCountMin = 2;
|
||||||
|
static const columnCountDefault = 4;
|
||||||
|
static const extentMin = 46.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
@ -37,8 +42,15 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
||||||
if (viewportSize.isEmpty) return SizedBox.shrink();
|
if (viewportSize.isEmpty) return SizedBox.shrink();
|
||||||
|
|
||||||
TileExtentManager.applyTileExtent(viewportSize, _tileExtentNotifier);
|
final tileExtentManager = TileExtentManager(
|
||||||
final cacheExtent = TileExtentManager.extentMaxForSize(viewportSize) * 2;
|
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<CollectionLens>
|
// do not replace by Provider.of<CollectionLens>
|
||||||
// so that view updates on collection filter changes
|
// so that view updates on collection filter changes
|
||||||
|
@ -58,16 +70,17 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
final scaler = GridScaleGestureDetector<ImageEntry>(
|
final scaler = GridScaleGestureDetector<ImageEntry>(
|
||||||
|
tileExtentManager: tileExtentManager,
|
||||||
scrollableKey: _scrollableKey,
|
scrollableKey: _scrollableKey,
|
||||||
appBarHeightNotifier: _appBarHeightNotifier,
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
extentNotifier: _tileExtentNotifier,
|
|
||||||
viewportSize: viewportSize,
|
viewportSize: viewportSize,
|
||||||
|
showScaledGrid: true,
|
||||||
scaledBuilder: (entry, extent) => DecoratedThumbnail(
|
scaledBuilder: (entry, extent) => DecoratedThumbnail(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: extent,
|
extent: extent,
|
||||||
showOverlay: false,
|
showOverlay: false,
|
||||||
),
|
),
|
||||||
getScaledItemTileRect: (entry) {
|
getScaledItemTileRect: (context, entry) {
|
||||||
final sectionedListLayout = Provider.of<SectionedListLayout>(context, listen: false);
|
final sectionedListLayout = Provider.of<SectionedListLayout>(context, listen: false);
|
||||||
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,7 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
final bool showGenericIcon;
|
final bool showGenericIcon;
|
||||||
final Widget background;
|
final Widget background;
|
||||||
final Widget details;
|
final Widget details;
|
||||||
|
final double padding;
|
||||||
final HeroType heroType;
|
final HeroType heroType;
|
||||||
final FilterCallback onTap;
|
final FilterCallback onTap;
|
||||||
final OffsetFilterCallback onLongPress;
|
final OffsetFilterCallback onLongPress;
|
||||||
|
@ -24,7 +25,6 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
static const double minChipWidth = 80;
|
static const double minChipWidth = 80;
|
||||||
static const double maxChipWidth = 160;
|
static const double maxChipWidth = 160;
|
||||||
static const double iconSize = 20;
|
static const double iconSize = 20;
|
||||||
static const double padding = 6;
|
|
||||||
|
|
||||||
const AvesFilterChip({
|
const AvesFilterChip({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -33,8 +33,9 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
this.showGenericIcon = true,
|
this.showGenericIcon = true,
|
||||||
this.background,
|
this.background,
|
||||||
this.details,
|
this.details,
|
||||||
|
this.padding = 6.0,
|
||||||
this.heroType = HeroType.onTap,
|
this.heroType = HeroType.onTap,
|
||||||
@required this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
}) : assert(filter != null),
|
}) : assert(filter != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
@ -51,6 +52,8 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
|
|
||||||
CollectionFilter get filter => widget.filter;
|
CollectionFilter get filter => widget.filter;
|
||||||
|
|
||||||
|
double get padding => widget.padding;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -80,9 +83,11 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
const iconSize = AvesFilterChip.iconSize;
|
||||||
|
|
||||||
final hasBackground = widget.background != null;
|
final hasBackground = widget.background != null;
|
||||||
final leading = filter.iconBuilder(context, AvesFilterChip.iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground);
|
final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground);
|
||||||
final trailing = widget.removable ? Icon(AIcons.clear, size: AvesFilterChip.iconSize) : null;
|
final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null;
|
||||||
|
|
||||||
Widget content = Row(
|
Widget content = Row(
|
||||||
mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min,
|
mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min,
|
||||||
|
@ -90,7 +95,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
children: [
|
children: [
|
||||||
if (leading != null) ...[
|
if (leading != null) ...[
|
||||||
leading,
|
leading,
|
||||||
SizedBox(width: AvesFilterChip.padding),
|
SizedBox(width: padding),
|
||||||
],
|
],
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
@ -101,7 +106,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (trailing != null) ...[
|
if (trailing != null) ...[
|
||||||
SizedBox(width: AvesFilterChip.padding),
|
SizedBox(width: padding),
|
||||||
trailing,
|
trailing,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -118,7 +123,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
}
|
}
|
||||||
|
|
||||||
content = Padding(
|
content = Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.padding * 2, vertical: 2),
|
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2),
|
||||||
child: content,
|
child: content,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ import 'dart:math';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
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/collection/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.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/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
@ -16,20 +16,22 @@ class ScalerMetadata<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class GridScaleGestureDetector<T> extends StatefulWidget {
|
class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||||
|
final TileExtentManager tileExtentManager;
|
||||||
final GlobalKey scrollableKey;
|
final GlobalKey scrollableKey;
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
final ValueNotifier<double> extentNotifier;
|
|
||||||
final Size viewportSize;
|
final Size viewportSize;
|
||||||
|
final bool showScaledGrid;
|
||||||
final Widget Function(T item, double extent) scaledBuilder;
|
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 void Function(T item) onScaled;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const GridScaleGestureDetector({
|
const GridScaleGestureDetector({
|
||||||
this.scrollableKey,
|
@required this.tileExtentManager,
|
||||||
|
@required this.scrollableKey,
|
||||||
@required this.appBarHeightNotifier,
|
@required this.appBarHeightNotifier,
|
||||||
@required this.extentNotifier,
|
|
||||||
@required this.viewportSize,
|
@required this.viewportSize,
|
||||||
|
@required this.showScaledGrid,
|
||||||
@required this.scaledBuilder,
|
@required this.scaledBuilder,
|
||||||
@required this.getScaledItemTileRect,
|
@required this.getScaledItemTileRect,
|
||||||
@required this.onScaled,
|
@required this.onScaled,
|
||||||
|
@ -40,14 +42,16 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||||
_GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState<T>();
|
_GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector> {
|
class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T>> {
|
||||||
double _startExtent, _extentMin, _extentMax;
|
double _startExtent, _extentMin, _extentMax;
|
||||||
bool _applyingScale = false;
|
bool _applyingScale = false;
|
||||||
ValueNotifier<double> _scaledExtentNotifier;
|
ValueNotifier<double> _scaledExtentNotifier;
|
||||||
OverlayEntry _overlayEntry;
|
OverlayEntry _overlayEntry;
|
||||||
ScalerMetadata<T> _metadata;
|
ScalerMetadata<T> _metadata;
|
||||||
|
|
||||||
ValueNotifier<double> get tileExtentNotifier => widget.extentNotifier;
|
TileExtentManager get tileExtentManager => widget.tileExtentManager;
|
||||||
|
|
||||||
|
Size get viewportSize => widget.viewportSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -72,13 +76,15 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector>
|
||||||
// abort if we cannot find an image to show on overlay
|
// abort if we cannot find an image to show on overlay
|
||||||
if (renderMetaData == null) return;
|
if (renderMetaData == null) return;
|
||||||
_metadata = renderMetaData.metaData;
|
_metadata = renderMetaData.metaData;
|
||||||
_startExtent = tileExtentNotifier.value;
|
_startExtent = renderMetaData.size.width;
|
||||||
_scaledExtentNotifier = ValueNotifier(_startExtent);
|
_scaledExtentNotifier = ValueNotifier(_startExtent);
|
||||||
|
|
||||||
// not the same as `MediaQuery.size.width`, because of screen insets/padding
|
// not the same as `MediaQuery.size.width`, because of screen insets/padding
|
||||||
final gridWidth = scrollableBox.size.width;
|
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 halfExtent = _startExtent / 2;
|
||||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
|
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent));
|
||||||
_overlayEntry = OverlayEntry(
|
_overlayEntry = OverlayEntry(
|
||||||
|
@ -87,6 +93,7 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector>
|
||||||
center: thumbnailCenter,
|
center: thumbnailCenter,
|
||||||
gridWidth: gridWidth,
|
gridWidth: gridWidth,
|
||||||
scaledExtentNotifier: _scaledExtentNotifier,
|
scaledExtentNotifier: _scaledExtentNotifier,
|
||||||
|
showScaledGrid: widget.showScaledGrid,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Overlay.of(scrollableContext).insert(_overlayEntry);
|
Overlay.of(scrollableContext).insert(_overlayEntry);
|
||||||
|
@ -104,11 +111,10 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector>
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyingScale = true;
|
_applyingScale = true;
|
||||||
final oldExtent = tileExtentNotifier.value;
|
final oldExtent = tileExtentManager.extentNotifier.value;
|
||||||
// sanitize and update grid layout if necessary
|
// sanitize and update grid layout if necessary
|
||||||
final newExtent = TileExtentManager.applyTileExtent(
|
final newExtent = tileExtentManager.applyTileExtent(
|
||||||
widget.viewportSize,
|
viewportSize: widget.viewportSize,
|
||||||
tileExtentNotifier,
|
|
||||||
userPreferredExtent: _scaledExtentNotifier.value,
|
userPreferredExtent: _scaledExtentNotifier.value,
|
||||||
);
|
);
|
||||||
_scaledExtentNotifier = null;
|
_scaledExtentNotifier = null;
|
||||||
|
@ -137,7 +143,7 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector>
|
||||||
void _scrollToItem(T item) {
|
void _scrollToItem(T item) {
|
||||||
final scrollableContext = widget.scrollableKey.currentContext;
|
final scrollableContext = widget.scrollableKey.currentContext;
|
||||||
final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height;
|
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,
|
// most of the time the app bar will be scrolled away after scaling,
|
||||||
// so we compensate for it to center the focal point thumbnail
|
// so we compensate for it to center the focal point thumbnail
|
||||||
final appBarHeight = widget.appBarHeightNotifier.value;
|
final appBarHeight = widget.appBarHeightNotifier.value;
|
||||||
|
@ -152,12 +158,14 @@ class ScaleOverlay extends StatefulWidget {
|
||||||
final Offset center;
|
final Offset center;
|
||||||
final double gridWidth;
|
final double gridWidth;
|
||||||
final ValueNotifier<double> scaledExtentNotifier;
|
final ValueNotifier<double> scaledExtentNotifier;
|
||||||
|
final bool showScaledGrid;
|
||||||
|
|
||||||
const ScaleOverlay({
|
const ScaleOverlay({
|
||||||
@required this.builder,
|
@required this.builder,
|
||||||
@required this.center,
|
@required this.center,
|
||||||
@required this.gridWidth,
|
@required this.gridWidth,
|
||||||
@required this.scaledExtentNotifier,
|
@required this.scaledExtentNotifier,
|
||||||
|
@required this.showScaledGrid,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -217,24 +225,29 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
||||||
}
|
}
|
||||||
final clampedCenter = center.translate(dx, 0);
|
final clampedCenter = center.translate(dx, 0);
|
||||||
|
|
||||||
return CustomPaint(
|
var child = widget.builder(extent);
|
||||||
painter: GridPainter(
|
child = Stack(
|
||||||
center: clampedCenter,
|
children: [
|
||||||
extent: extent,
|
Positioned(
|
||||||
),
|
left: clampedCenter.dx - extent / 2,
|
||||||
child: Stack(
|
top: clampedCenter.dy - extent / 2,
|
||||||
children: [
|
child: DefaultTextStyle(
|
||||||
Positioned(
|
style: TextStyle(),
|
||||||
left: clampedCenter.dx - extent / 2,
|
child: child,
|
||||||
top: clampedCenter.dy - extent / 2,
|
|
||||||
child: DefaultTextStyle(
|
|
||||||
style: TextStyle(),
|
|
||||||
child: widget.builder(extent),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
|
if (widget.showScaledGrid) {
|
||||||
|
child = CustomPaint(
|
||||||
|
painter: GridPainter(
|
||||||
|
center: clampedCenter,
|
||||||
|
extent: extent,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
70
lib/widgets/common/tile_extent_manager.dart
Normal file
70
lib/widgets/common/tile_extent_manager.dart
Normal file
|
@ -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<double> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
import 'package:aves/model/settings/settings.dart';
|
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/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:aves/widgets/fullscreen/info/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -27,7 +31,10 @@ class DebugSettingsSection extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
child: InfoRowGroup({
|
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}',
|
'infoMapZoom': '${settings.infoMapZoom}',
|
||||||
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
||||||
'searchHistory': toMultiline(settings.searchHistory),
|
'searchHistory': toMultiline(settings.searchHistory),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/model/actions/chip_actions.dart';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.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/album.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.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/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/collection/empty.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/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/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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/source/collection_source.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/android_file_utils.dart';
|
||||||
import 'package:aves/utils/constants.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/raster.dart';
|
||||||
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.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:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||||
import 'package:decorated_icon/decorated_icon.dart';
|
import 'package:decorated_icon/decorated_icon.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -19,6 +20,7 @@ class DecoratedFilterChip extends StatelessWidget {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final CollectionFilter filter;
|
final CollectionFilter filter;
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
final double extent;
|
||||||
final bool pinned;
|
final bool pinned;
|
||||||
final FilterCallback onTap;
|
final FilterCallback onTap;
|
||||||
final OffsetFilterCallback onLongPress;
|
final OffsetFilterCallback onLongPress;
|
||||||
|
@ -28,8 +30,9 @@ class DecoratedFilterChip extends StatelessWidget {
|
||||||
@required this.source,
|
@required this.source,
|
||||||
@required this.filter,
|
@required this.filter,
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
|
@required this.extent,
|
||||||
this.pinned = false,
|
this.pinned = false,
|
||||||
@required this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -40,57 +43,60 @@ class DecoratedFilterChip extends StatelessWidget {
|
||||||
: entry.isSvg
|
: entry.isSvg
|
||||||
? ThumbnailVectorImage(
|
? ThumbnailVectorImage(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: FilterGridPage.maxCrossAxisExtent,
|
extent: extent,
|
||||||
)
|
)
|
||||||
: ThumbnailRasterImage(
|
: ThumbnailRasterImage(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
extent: FilterGridPage.maxCrossAxisExtent,
|
extent: extent,
|
||||||
);
|
);
|
||||||
|
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),
|
||||||
|
padding: titlePadding,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetails(CollectionFilter filter) {
|
Widget _buildDetails(CollectionFilter filter) {
|
||||||
final count = Text(
|
final padding = min<double>(8.0, extent / 16);
|
||||||
'${source.count(filter)}',
|
final iconSize = min<double>(14.0, extent / 8);
|
||||||
style: TextStyle(color: FilterGridPage.detailColor),
|
final fontSize = min<double>(14.0, (extent / 6).roundToDouble());
|
||||||
);
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
AnimatedCrossFade(
|
if (pinned)
|
||||||
firstChild: Padding(
|
AnimatedPadding(
|
||||||
padding: EdgeInsets.only(right: 8),
|
padding: EdgeInsets.only(right: padding),
|
||||||
child: DecoratedIcon(
|
child: DecoratedIcon(
|
||||||
AIcons.pin,
|
AIcons.pin,
|
||||||
color: FilterGridPage.detailColor,
|
color: FilterGridPage.detailColor,
|
||||||
shadows: [Constants.embossShadow],
|
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))
|
if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album))
|
||||||
Padding(
|
AnimatedPadding(
|
||||||
padding: EdgeInsets.only(right: 8),
|
padding: EdgeInsets.only(right: padding),
|
||||||
child: DecoratedIcon(
|
child: DecoratedIcon(
|
||||||
AIcons.removableStorage,
|
AIcons.removableStorage,
|
||||||
color: FilterGridPage.detailColor,
|
color: FilterGridPage.detailColor,
|
||||||
shadows: [Constants.embossShadow],
|
shadows: [Constants.embossShadow],
|
||||||
size: 16,
|
size: iconSize,
|
||||||
),
|
),
|
||||||
|
duration: Durations.chipDecorationAnimation,
|
||||||
),
|
),
|
||||||
count,
|
Text(
|
||||||
|
'${source.count(filter)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: FilterGridPage.detailColor,
|
||||||
|
fontSize: fontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,193 +1,59 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/main.dart';
|
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings/settings.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/model/source/collection_source.dart';
|
||||||
import 'package:aves/theme/durations.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/widgets/common/behaviour/double_back_pop.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu_row.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
|
||||||
import 'package:aves/widgets/common/identity/scroll_thumb.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/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/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:draggable_scrollbar/draggable_scrollbar.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_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 FilterNavigationPage extends StatelessWidget {
|
|
||||||
final CollectionSource source;
|
|
||||||
final String title;
|
|
||||||
final ChipSetActionDelegate chipSetActionDelegate;
|
|
||||||
final ChipActionDelegate chipActionDelegate;
|
|
||||||
final Map<String, ImageEntry> filterEntries;
|
|
||||||
final CollectionFilter Function(String key) filterBuilder;
|
|
||||||
final Widget Function() emptyBuilder;
|
|
||||||
final List<ChipAction> 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<SourceState>(
|
|
||||||
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<void> _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<ChipAction>(
|
|
||||||
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<Widget> _buildActions(BuildContext context) {
|
|
||||||
return [
|
|
||||||
SearchButton(source),
|
|
||||||
PopupMenuButton<ChipSetAction>(
|
|
||||||
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<String, ImageEntry> a, MapEntry<String, ImageEntry> b) {
|
|
||||||
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
|
|
||||||
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int compareChipsByEntryCount(MapEntry<String, num> a, MapEntry<String, num> b) {
|
|
||||||
final c = b.value.compareTo(a.value) ?? -1;
|
|
||||||
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FilterGridPage extends StatelessWidget {
|
class FilterGridPage extends StatelessWidget {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final Widget appBar;
|
final Widget appBar;
|
||||||
final Map<String, ImageEntry> filterEntries;
|
final Map<String, ImageEntry> filterEntries;
|
||||||
final CollectionFilter Function(String key) filterBuilder;
|
final CollectionFilter Function(String key) filterBuilder;
|
||||||
final Widget Function() emptyBuilder;
|
final Widget Function() emptyBuilder;
|
||||||
final double appBarHeight;
|
|
||||||
final FilterCallback onTap;
|
final FilterCallback onTap;
|
||||||
final OffsetFilterCallback onLongPress;
|
final OffsetFilterCallback onLongPress;
|
||||||
|
|
||||||
const FilterGridPage({
|
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||||
|
final ValueNotifier<double> _tileExtentNotifier = ValueNotifier(0);
|
||||||
|
final GlobalKey _scrollableKey = GlobalKey();
|
||||||
|
|
||||||
|
static const spacing = 8.0;
|
||||||
|
|
||||||
|
FilterGridPage({
|
||||||
@required this.source,
|
@required this.source,
|
||||||
@required this.appBar,
|
@required this.appBar,
|
||||||
@required this.filterEntries,
|
@required this.filterEntries,
|
||||||
@required this.filterBuilder,
|
@required this.filterBuilder,
|
||||||
@required this.emptyBuilder,
|
@required this.emptyBuilder,
|
||||||
this.appBarHeight = kToolbarHeight,
|
double appBarHeight = kToolbarHeight,
|
||||||
@required this.onTap,
|
@required this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
});
|
}) {
|
||||||
|
_appBarHeightNotifier.value = appBarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> get filterKeys => filterEntries.keys.toList();
|
List<String> get filterKeys => filterEntries.keys.toList();
|
||||||
|
|
||||||
static const Color detailColor = Color(0xFFE0E0E0);
|
static const Color detailColor = Color(0xFFE0E0E0);
|
||||||
static const double maxCrossAxisExtent = 180;
|
|
||||||
|
// TODO TLAD enforce max extent?
|
||||||
|
// static const double maxCrossAxisExtent = 180;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -195,13 +61,59 @@ class FilterGridPage extends StatelessWidget {
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: DoubleBackPopScope(
|
body: DoubleBackPopScope(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Selector<MediaQueryData, double>(
|
child: LayoutBuilder(
|
||||||
selector: (c, mq) => mq.size.width,
|
builder: (context, constraints) {
|
||||||
builder: (c, mqWidth, child) {
|
final viewportSize = constraints.biggest;
|
||||||
final columnCount = (mqWidth / maxCrossAxisExtent).ceil();
|
assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.');
|
||||||
final scrollView = _buildScrollView(context, columnCount);
|
if (viewportSize.isEmpty) return SizedBox.shrink();
|
||||||
return AnimationLimiter(
|
|
||||||
child: _buildDraggableScrollView(scrollView),
|
final tileExtentManager = TileExtentManager(
|
||||||
|
routeName: context.currentRouteName,
|
||||||
|
columnCountMin: 2,
|
||||||
|
columnCountDefault: 2,
|
||||||
|
extentMin: 60,
|
||||||
|
extentNotifier: _tileExtentNotifier,
|
||||||
|
spacing: spacing,
|
||||||
|
)..applyTileExtent(viewportSize: viewportSize);
|
||||||
|
|
||||||
|
return ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _tileExtentNotifier,
|
||||||
|
builder: (context, tileExtent, child) {
|
||||||
|
final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent);
|
||||||
|
final scrollView = AnimationLimiter(
|
||||||
|
child: _buildDraggableScrollView(_buildScrollView(context, columnCount)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GridScaleGestureDetector<FilterGridItem>(
|
||||||
|
tileExtentManager: tileExtentManager,
|
||||||
|
scrollableKey: _scrollableKey,
|
||||||
|
appBarHeightNotifier: _appBarHeightNotifier,
|
||||||
|
viewportSize: viewportSize,
|
||||||
|
showScaledGrid: false,
|
||||||
|
scaledBuilder: (item, extent) {
|
||||||
|
final filter = item.filter;
|
||||||
|
return SizedBox(
|
||||||
|
width: extent,
|
||||||
|
height: extent,
|
||||||
|
child: DecoratedFilterChip(
|
||||||
|
source: source,
|
||||||
|
filter: filter,
|
||||||
|
entry: item.entry,
|
||||||
|
extent: extent,
|
||||||
|
pinned: settings.pinnedFilters.contains(filter),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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),
|
controller: PrimaryScrollController.of(context),
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
// padding to keep scroll thumb between app bar above and nav bar below
|
// padding to keep scroll thumb between app bar above and nav bar below
|
||||||
top: appBarHeight,
|
top: _appBarHeightNotifier.value,
|
||||||
bottom: mqViewInsetsBottom,
|
bottom: mqViewInsetsBottom,
|
||||||
),
|
),
|
||||||
child: scrollView,
|
child: scrollView,
|
||||||
|
@ -239,6 +151,7 @@ class FilterGridPage extends StatelessWidget {
|
||||||
ScrollView _buildScrollView(BuildContext context, int columnCount) {
|
ScrollView _buildScrollView(BuildContext context, int columnCount) {
|
||||||
final pinnedFilters = settings.pinnedFilters;
|
final pinnedFilters = settings.pinnedFilters;
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
|
key: _scrollableKey,
|
||||||
controller: PrimaryScrollController.of(context),
|
controller: PrimaryScrollController.of(context),
|
||||||
slivers: [
|
slivers: [
|
||||||
appBar,
|
appBar,
|
||||||
|
@ -255,42 +168,44 @@ class FilterGridPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
hasScrollBody: false,
|
hasScrollBody: false,
|
||||||
)
|
)
|
||||||
: SliverPadding(
|
: SliverGrid(
|
||||||
padding: EdgeInsets.all(AvesFilterChip.outlineWidth),
|
delegate: SliverChildBuilderDelegate(
|
||||||
sliver: SliverGrid(
|
(context, i) {
|
||||||
delegate: SliverChildBuilderDelegate(
|
final key = filterKeys[i];
|
||||||
(context, i) {
|
final filter = filterBuilder(key);
|
||||||
final key = filterKeys[i];
|
final entry = filterEntries[key];
|
||||||
final filter = filterBuilder(key);
|
final child = MetaData(
|
||||||
final child = DecoratedFilterChip(
|
metaData: ScalerMetadata(FilterGridItem(filter, entry)),
|
||||||
|
child: DecoratedFilterChip(
|
||||||
key: Key(key),
|
key: Key(key),
|
||||||
source: source,
|
source: source,
|
||||||
filter: filter,
|
filter: filter,
|
||||||
entry: filterEntries[key],
|
entry: entry,
|
||||||
|
extent: _tileExtentNotifier.value,
|
||||||
pinned: pinnedFilters.contains(filter),
|
pinned: pinnedFilters.contains(filter),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
);
|
),
|
||||||
return AnimationConfiguration.staggeredGrid(
|
);
|
||||||
position: i,
|
return AnimationConfiguration.staggeredGrid(
|
||||||
columnCount: columnCount,
|
position: i,
|
||||||
duration: Durations.staggeredAnimation,
|
columnCount: columnCount,
|
||||||
delay: Durations.staggeredAnimationDelay,
|
duration: Durations.staggeredAnimation,
|
||||||
child: SlideAnimation(
|
delay: Durations.staggeredAnimationDelay,
|
||||||
verticalOffset: 50.0,
|
child: SlideAnimation(
|
||||||
child: FadeInAnimation(
|
verticalOffset: 50.0,
|
||||||
child: child,
|
child: FadeInAnimation(
|
||||||
),
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
childCount: filterKeys.length,
|
},
|
||||||
),
|
childCount: filterKeys.length,
|
||||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
),
|
||||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
mainAxisSpacing: 8,
|
crossAxisCount: columnCount,
|
||||||
crossAxisSpacing: 8,
|
mainAxisSpacing: spacing,
|
||||||
),
|
crossAxisSpacing: spacing,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
|
@ -305,3 +220,10 @@ class FilterGridPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FilterGridItem {
|
||||||
|
final CollectionFilter filter;
|
||||||
|
final ImageEntry entry;
|
||||||
|
|
||||||
|
const FilterGridItem(this.filter, this.entry);
|
||||||
|
}
|
||||||
|
|
156
lib/widgets/filter_grids/common/filter_nav_page.dart
Normal file
156
lib/widgets/filter_grids/common/filter_nav_page.dart
Normal file
|
@ -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<String, ImageEntry> filterEntries;
|
||||||
|
final CollectionFilter Function(String key) filterBuilder;
|
||||||
|
final Widget Function() emptyBuilder;
|
||||||
|
final List<ChipAction> 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<SourceState>(
|
||||||
|
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<void> _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<ChipAction>(
|
||||||
|
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<Widget> _buildActions(BuildContext context) {
|
||||||
|
return [
|
||||||
|
SearchButton(source),
|
||||||
|
PopupMenuButton<ChipSetAction>(
|
||||||
|
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<String, ImageEntry> a, MapEntry<String, ImageEntry> b) {
|
||||||
|
final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1;
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int compareChipsByEntryCount(MapEntry<String, num> a, MapEntry<String, num> b) {
|
||||||
|
final c = b.value.compareTo(a.value) ?? -1;
|
||||||
|
return c != 0 ? c : compareAsciiUpperCase(a.key, b.key);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/collection/empty.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_action_delegate.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/chip_set_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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.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/filters.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/image_entry.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/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/widgets/collection/empty.dart';
|
|
||||||
import 'package:aves/theme/icons.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/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/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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
Loading…
Reference in a new issue