diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index ef44fe919..228a2c239 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -79,18 +79,17 @@ class _CollectionGridContent extends StatelessWidget { final sectionedListLayoutProvider = ValueListenableBuilder( valueListenable: context.select>((controller) => controller.extentNotifier), builder: (context, tileExtent, child) { - return GridTheme( - extent: tileExtent, - child: Selector>( - selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), - builder: (context, c, child) { - final scrollableWidth = c.item1; - final columnCount = c.item2; - final tileSpacing = c.item3; - // do not listen for animation delay change - final controller = Provider.of(context, listen: false); - final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget); - return SectionedEntryListLayoutProvider( + return Selector>( + selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), + builder: (context, c, child) { + final scrollableWidth = c.item1; + final columnCount = c.item2; + final tileSpacing = c.item3; + // do not listen for animation delay change + final tileAnimationDelay = context.read().getTileAnimationDelay(Durations.staggeredAnimationPageTarget); + return GridTheme( + extent: tileExtent, + child: SectionedEntryListLayoutProvider( collection: collection, scrollableWidth: scrollableWidth, columnCount: columnCount, @@ -104,16 +103,18 @@ class _CollectionGridContent extends StatelessWidget { isScrollingNotifier: _isScrollingNotifier, ), tileAnimationDelay: tileAnimationDelay, - child: _CollectionSectionedContent( - collection: collection, - isScrollingNotifier: _isScrollingNotifier, - scrollController: PrimaryScrollController.of(context)!, - ), - ); - }, - ), + child: child!, + ), + ); + }, + child: child, ); }, + child: _CollectionSectionedContent( + collection: collection, + isScrollingNotifier: _isScrollingNotifier, + scrollController: PrimaryScrollController.of(context)!, + ), ); return sectionedListLayoutProvider; }, @@ -199,10 +200,11 @@ class _CollectionScaler extends StatelessWidget { final tileSpacing = context.select((controller) => controller.spacing); return GridScaleGestureDetector( scrollableKey: scrollableKey, - gridBuilder: (center, extent, child) => CustomPaint( + heightForWidth: (width) => width, + gridBuilder: (center, tileSize, child) => CustomPaint( painter: GridPainter( center: center, - extent: extent, + tileSize: tileSize, spacing: tileSpacing, borderWidth: DecoratedThumbnail.borderWidth, borderRadius: Radius.zero, @@ -210,7 +212,7 @@ class _CollectionScaler extends StatelessWidget { ), child: child, ), - scaledBuilder: (entry, extent) => DecoratedThumbnail( + scaledBuilder: (entry, tileSize) => DecoratedThumbnail( entry: entry, tileExtent: context.read().effectiveExtentMax, selectable: false, diff --git a/lib/widgets/collection/grid/section_layout.dart b/lib/widgets/collection/grid/section_layout.dart index bc69a9961..99ba1dbd4 100644 --- a/lib/widgets/collection/grid/section_layout.dart +++ b/lib/widgets/collection/grid/section_layout.dart @@ -23,7 +23,8 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider extends StatelessWidget { final section = sll.sections[sectionLayout.sectionKey]!; final dy = offsetY - (sectionLayout.minOffset + sectionLayout.headerExtent); - final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileExtent + sll.spacing)) * sll.columnCount; + final itemIndex = dy < 0 ? 0 : (dy ~/ (sll.tileHeight + sll.spacing)) * sll.columnCount; final item = section[itemIndex]; final lines = lineBuilder(context, item); diff --git a/lib/widgets/common/grid/section_layout.dart b/lib/widgets/common/grid/section_layout.dart index 7f8d141bd..727b81f95 100644 --- a/lib/widgets/common/grid/section_layout.dart +++ b/lib/widgets/common/grid/section_layout.dart @@ -13,7 +13,7 @@ import 'package:provider/provider.dart'; abstract class SectionedListLayoutProvider extends StatelessWidget { final double scrollableWidth; final int columnCount; - final double spacing, tileExtent; + final double spacing, tileWidth, tileHeight; final Widget Function(T item) tileBuilder; final Duration tileAnimationDelay; final Widget child; @@ -23,7 +23,8 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { required this.scrollableWidth, required this.columnCount, required this.spacing, - required this.tileExtent, + required this.tileWidth, + required this.tileHeight, required this.tileBuilder, required this.tileAnimationDelay, required this.child, @@ -60,7 +61,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { final sectionLastIndex = currentIndex - 1; final sectionMinOffset = currentOffset; - currentOffset += headerExtent + tileExtent * rowCount + spacing * (rowCount - 1); + currentOffset += headerExtent + tileHeight * rowCount + spacing * (rowCount - 1); final sectionMaxOffset = currentOffset; sectionLayouts.add( @@ -71,7 +72,7 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { minOffset: sectionMinOffset, maxOffset: sectionMaxOffset, headerExtent: headerExtent, - tileExtent: tileExtent, + tileHeight: tileHeight, spacing: spacing, builder: (context, listIndex) => _buildInSection( context, @@ -89,7 +90,8 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { sections: _sections, showHeaders: _showHeaders, columnCount: columnCount, - tileExtent: tileExtent, + tileWidth: tileWidth, + tileHeight: tileHeight, spacing: spacing, sectionLayouts: sectionLayouts, ); @@ -123,7 +125,8 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { children.add(animate ? _buildAnimation(itemGridIndex, item) : item); } return _GridRow( - extent: tileExtent, + width: tileWidth, + height: tileHeight, spacing: spacing, children: children, ); @@ -158,7 +161,8 @@ abstract class SectionedListLayoutProvider extends StatelessWidget { properties.add(DoubleProperty('scrollableWidth', scrollableWidth)); properties.add(IntProperty('columnCount', columnCount)); properties.add(DoubleProperty('spacing', spacing)); - properties.add(DoubleProperty('tileExtent', tileExtent)); + properties.add(DoubleProperty('tileWidth', tileWidth)); + properties.add(DoubleProperty('tileHeight', tileHeight)); properties.add(DiagnosticsProperty('showHeaders', showHeaders)); } } @@ -167,14 +171,15 @@ class SectionedListLayout { final Map> sections; final bool showHeaders; final int columnCount; - final double tileExtent, spacing; + final double tileWidth, tileHeight, spacing; final List sectionLayouts; const SectionedListLayout({ required this.sections, required this.showHeaders, required this.columnCount, - required this.tileExtent, + required this.tileWidth, + required this.tileHeight, required this.spacing, required this.sectionLayouts, }); @@ -192,9 +197,9 @@ class SectionedListLayout { final row = (sectionItemIndex / columnCount).floor(); final listIndex = sectionLayout.firstIndex + 1 + row; - final left = tileExtent * column + spacing * (column - 1); + final left = tileWidth * column + spacing * (column - 1); final top = sectionLayout.indexToLayoutOffset(listIndex); - return Rect.fromLTWH(left, top, tileExtent, tileExtent); + return Rect.fromLTWH(left, top, tileWidth, tileHeight); } SectionLayout? getSectionAt(double offsetY) => sectionLayouts.firstWhereOrNull((sl) => offsetY < sl.maxOffset); @@ -210,8 +215,8 @@ class SectionedListLayout { dy -= sectionLayout.minOffset + sectionLayout.headerExtent; if (dy < 0) return null; - final row = dy ~/ (tileExtent + spacing); - final column = position.dx ~/ (tileExtent + spacing); + final row = dy ~/ (tileHeight + spacing); + final column = position.dx ~/ (tileWidth + spacing); final index = row * columnCount + column; if (index >= section.length) return null; @@ -219,7 +224,7 @@ class SectionedListLayout { } @override - String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileExtent=$tileExtent}'; + String toString() => '$runtimeType#${shortHash(this)}{sectionCount=${sections.length} columnCount=$columnCount, tileWidth=$tileWidth, tileHeight=$tileHeight}'; } @immutable @@ -227,11 +232,11 @@ class SectionLayout extends Equatable { final SectionKey sectionKey; final int firstIndex, lastIndex, bodyFirstIndex; final double minOffset, maxOffset, bodyMinOffset; - final double headerExtent, tileExtent, spacing, mainAxisStride; + final double headerExtent, tileHeight, spacing, mainAxisStride; final IndexedWidgetBuilder builder; @override - List get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileExtent, spacing]; + List get props => [sectionKey, firstIndex, lastIndex, minOffset, maxOffset, headerExtent, tileHeight, spacing]; const SectionLayout({ required this.sectionKey, @@ -240,12 +245,12 @@ class SectionLayout extends Equatable { required this.minOffset, required this.maxOffset, required this.headerExtent, - required this.tileExtent, + required this.tileHeight, required this.spacing, required this.builder, }) : bodyFirstIndex = firstIndex + 1, bodyMinOffset = minOffset + headerExtent, - mainAxisStride = tileExtent + spacing; + mainAxisStride = tileHeight + spacing; bool hasChild(int index) => firstIndex <= index && index <= lastIndex; @@ -271,11 +276,12 @@ class SectionLayout extends Equatable { } class _GridRow extends MultiChildRenderObjectWidget { - final double extent, spacing; + final double width, height, spacing; _GridRow({ Key? key, - required this.extent, + required this.width, + required this.height, required this.spacing, required List children, }) : super(key: key, children: children); @@ -283,21 +289,24 @@ class _GridRow extends MultiChildRenderObjectWidget { @override RenderObject createRenderObject(BuildContext context) { return _RenderGridRow( - extent: extent, + width: width, + height: height, spacing: spacing, ); } @override void updateRenderObject(BuildContext context, _RenderGridRow renderObject) { - renderObject.extent = extent; + renderObject.width = width; + renderObject.height = height; renderObject.spacing = spacing; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DoubleProperty('extent', extent)); + properties.add(DoubleProperty('width', width)); + properties.add(DoubleProperty('height', height)); properties.add(DoubleProperty('spacing', spacing)); } } @@ -307,19 +316,30 @@ class _GridRowParentData extends ContainerBoxParentData {} class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { _RenderGridRow({ List? children, - required double extent, + required double width, + required double height, required double spacing, - }) : _extent = extent, + }) : _width = width, + _height = height, _spacing = spacing { addAll(children); } - double get extent => _extent; - double _extent; + double get width => _width; + double _width; - set extent(double value) { - if (_extent == value) return; - _extent = value; + set width(double value) { + if (_width == value) return; + _width = value; + markNeedsLayout(); + } + + double get height => _height; + double _height; + + set height(double value) { + if (_height == value) return; + _height = value; markNeedsLayout(); } @@ -339,7 +359,7 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin extent * childCount + spacing * (childCount - 1); + double get intrinsicWidth => width * childCount + spacing * (childCount - 1); @override double computeMinIntrinsicWidth(double height) => intrinsicWidth; @@ -348,10 +368,10 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin intrinsicWidth; @override - double computeMinIntrinsicHeight(double width) => extent; + double computeMinIntrinsicHeight(double width) => height; @override - double computeMaxIntrinsicHeight(double width) => extent; + double computeMaxIntrinsicHeight(double width) => height; @override void performLayout() { @@ -360,14 +380,14 @@ class _RenderGridRow extends RenderBox with ContainerRenderObjectMixin BorderRadius.vertical(bottom: radius); + + BorderRadius get chipBorderRadius => BorderRadius.all(radius); +} + class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; final bool removable; final bool showGenericIcon; - final Widget? background; + final AvesFilterDecoration? decoration; final String? banner; final Widget? details; - final BorderRadius? borderRadius; final double padding; final HeroType heroType; final FilterCallback? onTap; @@ -37,16 +51,18 @@ class AvesFilterChip extends StatefulWidget { static const double minChipHeight = kMinInteractiveDimension; static const double minChipWidth = 80; static const double maxChipWidth = 160; + static const double iconSize = 18; + static const double fontSize = 14; + static const double decoratedContentVerticalPadding = 5; const AvesFilterChip({ Key? key, required this.filter, this.removable = false, this.showGenericIcon = true, - this.background, + this.decoration, this.banner, this.details, - this.borderRadius, this.padding = 6.0, this.heroType = HeroType.onTap, this.onTap, @@ -140,15 +156,15 @@ class _AvesFilterChipState extends State { @override Widget build(BuildContext context) { + final chipBackground = Theme.of(context).scaffoldBackgroundColor; final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final iconSize = 20 * textScaleFactor; - - final hasBackground = widget.background != null; - final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground); + final iconSize = AvesFilterChip.iconSize * textScaleFactor; + final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon); final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null; + final decoration = widget.decoration; Widget content = Row( - mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min, + mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (leading != null) ...[ @@ -158,6 +174,9 @@ class _AvesFilterChipState extends State { Flexible( child: Text( filter.getLabel(context), + style: const TextStyle( + fontSize: AvesFilterChip.fontSize, + ), softWrap: false, overflow: TextOverflow.fade, maxLines: 1, @@ -170,36 +189,37 @@ class _AvesFilterChipState extends State { ], ); - if (widget.details != null) { + final details = widget.details; + if (details != null) { content = Column( mainAxisSize: MainAxisSize.min, children: [ content, - Flexible(child: widget.details!), + Flexible(child: details), ], ); } - content = Padding( - padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2), - child: content, - ); - - if (hasBackground) { - content = Center( - child: ColoredBox( - color: Colors.black54, - child: DefaultTextStyle( - style: Theme.of(context).textTheme.bodyText2!.copyWith( - shadows: Constants.embossShadows, - ), + if (decoration != null) { + content = Align( + alignment: Alignment.bottomCenter, + child: ClipRRect( + borderRadius: decoration.textBorderRadius, + child: Container( + padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: AvesFilterChip.decoratedContentVerticalPadding), + color: chipBackground, child: content, ), ), ); + } else { + content = Padding( + padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2), + child: content, + ); } - final borderRadius = widget.borderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)); + final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)); final banner = widget.banner; Widget chip = Container( constraints: const BoxConstraints( @@ -210,13 +230,13 @@ class _AvesFilterChipState extends State { child: Stack( fit: StackFit.passthrough, children: [ - if (hasBackground) + if (decoration != null) ClipRRect( - borderRadius: borderRadius, - child: widget.background, + borderRadius: decoration.chipBorderRadius, + child: decoration.widget, ), Material( - color: hasBackground ? Colors.transparent : Theme.of(context).scaffoldBackgroundColor, + color: decoration != null ? Colors.transparent : chipBackground, shape: RoundedRectangleBorder( borderRadius: borderRadius, ), @@ -248,7 +268,7 @@ class _AvesFilterChipState extends State { ), position: DecorationPosition.foreground, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: EdgeInsets.symmetric(vertical: decoration != null ? 0 : 8), child: content, ), ); @@ -279,9 +299,11 @@ class _AvesFilterChipState extends State { chip = Hero( tag: filter, transitionOnUserGestures: true, - child: DefaultTextStyle( - style: const TextStyle(), - child: chip, + child: MediaQueryDataProvider( + child: DefaultTextStyle( + style: const TextStyle(), + child: chip, + ), ), ); } diff --git a/lib/widgets/common/scaling.dart b/lib/widgets/common/scaling.dart index 8cf79fc6e..88156ebda 100644 --- a/lib/widgets/common/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -19,14 +19,16 @@ class ScalerMetadata { class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; - final Widget Function(Offset center, double extent, Widget child) gridBuilder; - final Widget Function(T item, double extent) scaledBuilder; + final double Function(double width) heightForWidth; + final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; + final Widget Function(T item, Size tileSize) scaledBuilder; final Object Function(T item)? highlightItem; final Widget child; const GridScaleGestureDetector({ Key? key, required this.scrollableKey, + required this.heightForWidth, required this.gridBuilder, required this.scaledBuilder, this.highlightItem, @@ -38,9 +40,10 @@ class GridScaleGestureDetector extends StatefulWidget { } class _GridScaleGestureDetectorState extends State> { - double? _startExtent, _extentMin, _extentMax; + Size? _startSize; + double? _extentMin, _extentMax; bool _applyingScale = false; - ValueNotifier? _scaledExtentNotifier; + ValueNotifier? _scaledSizeNotifier; OverlayEntry? _overlayEntry; ScalerMetadata? _metadata; @@ -63,8 +66,8 @@ class _GridScaleGestureDetectorState extends State extends State ScaleOverlay( - builder: (extent) => SizedBox( - width: extent, - height: extent, + builder: (scaledTileSize) => SizedBox.fromSize( + size: scaledTileSize, child: GridTheme( - extent: extent, - child: widget.scaledBuilder(_metadata!.item, extent), + extent: scaledTileSize.width, + child: widget.scaledBuilder(_metadata!.item, scaledTileSize), ), ), center: thumbnailCenter, viewportWidth: gridWidth, gridBuilder: widget.gridBuilder, - scaledExtentNotifier: _scaledExtentNotifier!, + scaledSizeNotifier: _scaledSizeNotifier!, ), ); Overlay.of(scrollableContext)!.insert(_overlayEntry!); }, onScaleUpdate: (details) { - if (_scaledExtentNotifier == null) return; + if (_scaledSizeNotifier == null) return; final s = details.scale; - _scaledExtentNotifier!.value = (_startExtent! * s).clamp(_extentMin!, _extentMax!); + final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!); + _scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth)); }, onScaleEnd: (details) { - if (_scaledExtentNotifier == null) return; + if (_scaledSizeNotifier == null) return; if (_overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; @@ -109,8 +112,8 @@ class _GridScaleGestureDetectorState extends State(); final oldExtent = tileExtentController.extentNotifier.value; // sanitize and update grid layout if necessary - final newExtent = tileExtentController.setUserPreferredExtent(_scaledExtentNotifier!.value); - _scaledExtentNotifier = null; + final newExtent = tileExtentController.setUserPreferredExtent(_scaledSizeNotifier!.value.width); + _scaledSizeNotifier = null; if (newExtent == oldExtent) { _applyingScale = false; } else { @@ -138,18 +141,18 @@ class _GridScaleGestureDetectorState extends State scaledExtentNotifier; - final Widget Function(Offset center, double extent, Widget child) gridBuilder; + final ValueNotifier scaledSizeNotifier; + final Widget Function(Offset center, Size extent, Widget child) gridBuilder; const ScaleOverlay({ Key? key, required this.builder, required this.center, required this.viewportWidth, - required this.scaledExtentNotifier, + required this.scaledSizeNotifier, required this.gridBuilder, }) : super(key: key); @@ -197,26 +200,28 @@ class _ScaleOverlayState extends State { ), ), duration: Durations.collectionScalingBackgroundAnimation, - child: ValueListenableBuilder( - valueListenable: widget.scaledExtentNotifier, - builder: (context, extent, child) { + child: ValueListenableBuilder( + valueListenable: widget.scaledSizeNotifier, + builder: (context, scaledSize, child) { + final width = scaledSize.width; + final height = scaledSize.height; // keep scaled thumbnail within the screen final xMin = context.select((mq) => mq.padding.left); final xMax = xMin + gridWidth; var dx = .0; - if (center.dx - extent / 2 < xMin) { - dx = xMin - (center.dx - extent / 2); - } else if (center.dx + extent / 2 > xMax) { - dx = xMax - (center.dx + extent / 2); + if (center.dx - width / 2 < xMin) { + dx = xMin - (center.dx - width / 2); + } else if (center.dx + width / 2 > xMax) { + dx = xMax - (center.dx + width / 2); } final clampedCenter = center.translate(dx, 0); - var child = widget.builder(extent); + var child = widget.builder(scaledSize); child = Stack( children: [ Positioned( - left: clampedCenter.dx - extent / 2, - top: clampedCenter.dy - extent / 2, + left: clampedCenter.dx - width / 2, + top: clampedCenter.dy - height / 2, child: DefaultTextStyle( style: const TextStyle(), child: child, @@ -224,7 +229,7 @@ class _ScaleOverlayState extends State { ), ], ); - child = widget.gridBuilder(clampedCenter, extent, child); + child = widget.gridBuilder(clampedCenter, scaledSize, child); return child; }, ), @@ -237,13 +242,14 @@ class _ScaleOverlayState extends State { class GridPainter extends CustomPainter { final Offset center; - final double extent, spacing, borderWidth; + final Size tileSize; + final double spacing, borderWidth; final Radius borderRadius; final Color color; const GridPainter({ required this.center, - required this.extent, + required this.tileSize, required this.spacing, required this.borderWidth, required this.borderRadius, @@ -252,12 +258,15 @@ class GridPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + final tileWidth = tileSize.width; + final tileHeight = tileSize.height; + final strokePaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = borderWidth ..shader = ui.Gradient.radial( center, - extent * 2, + tileWidth * 2, [ color, Colors.transparent, @@ -271,17 +280,18 @@ class GridPainter extends CustomPainter { ..style = PaintingStyle.fill ..color = color.withOpacity(.25); - final delta = extent + spacing; + final deltaX = tileWidth + spacing; + final deltaY = tileHeight + spacing; for (var i = -2; i <= 2; i++) { - final dx = delta * i; + final dx = deltaX * i; for (var j = -2; j <= 2; j++) { if (i == 0 && j == 0) continue; - final dy = delta * j; + final dy = deltaY * j; final rect = RRect.fromRectAndRadius( Rect.fromCenter( center: center + Offset(dx, dy), - width: extent, - height: extent, + width: tileWidth, + height: tileHeight, ), borderRadius, ); diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index 7181bd4d3..74d057f81 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -12,6 +12,7 @@ import 'package:aves/model/source/tag.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/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; @@ -40,6 +41,22 @@ class CoveredFilterChip extends StatelessWidget { }) : thumbnailExtent = thumbnailExtent ?? extent, super(key: key); + static double tileHeight({required double extent, required double textScaleFactor}) { + return extent + infoHeight(extent: extent, textScaleFactor: textScaleFactor); + } + + static double infoHeight({required double extent, required double textScaleFactor}) { + // height can actually be a little larger or smaller, when info includes icons or non-latin scripts + // but it's not worth measuring text metrics, as the widget is flexible enough to absorb the difference + return (AvesFilterChip.fontSize + detailFontSize(extent) + 4) * textScaleFactor + AvesFilterChip.decoratedContentVerticalPadding * 2; + } + + static Radius radius(double extent) => Radius.circular(min(AvesFilterChip.defaultRadius, extent / 4)); + + static double detailIconSize(double extent) => min(AvesFilterChip.fontSize, extent / 8); + + static double detailFontSize(double extent) => min(AvesFilterChip.fontSize, extent / 6); + @override Widget build(BuildContext context) { return Consumer( @@ -50,7 +67,7 @@ class CoveredFilterChip extends StatelessWidget { final album = (filter as AlbumFilter).album; return StreamBuilder( stream: source.eventBus.on().where((event) => event.directories == null || event.directories!.contains(album)), - builder: (context, snapshot) => _buildChip(source), + builder: (context, snapshot) => _buildChip(context, source), ); } case LocationFilter: @@ -58,7 +75,7 @@ class CoveredFilterChip extends StatelessWidget { final countryCode = (filter as LocationFilter).countryCode; return StreamBuilder( stream: source.eventBus.on().where((event) => event.countryCodes == null || event.countryCodes!.contains(countryCode)), - builder: (context, snapshot) => _buildChip(source), + builder: (context, snapshot) => _buildChip(context, source), ); } case TagFilter: @@ -66,7 +83,7 @@ class CoveredFilterChip extends StatelessWidget { final tag = (filter as TagFilter).tag; return StreamBuilder( stream: source.eventBus.on().where((event) => event.tags == null || event.tags!.contains(tag)), - builder: (context, snapshot) => _buildChip(source), + builder: (context, snapshot) => _buildChip(context, source), ); } default: @@ -76,38 +93,53 @@ class CoveredFilterChip extends StatelessWidget { ); } - static Radius radius(double extent) => Radius.circular(min(AvesFilterChip.defaultRadius, extent / 4)); - - Widget _buildChip(CollectionSource source) { + Widget _buildChip(BuildContext context, CollectionSource source) { final entry = coverEntry ?? source.coverEntry(filter); - final backgroundImage = entry == null - ? Container(color: Colors.white) - : ThumbnailImage( - entry: entry, - extent: thumbnailExtent, - ); final titlePadding = min(4.0, extent / 32); - return SizedBox( - width: extent, - height: extent, - child: AvesFilterChip( - filter: filter, - showGenericIcon: false, - background: backgroundImage, - banner: banner, - details: _buildDetails(source, filter), - borderRadius: BorderRadius.all(radius(extent)), - padding: titlePadding, - onTap: onTap, - onLongPress: null, + return AvesFilterChip( + filter: filter, + showGenericIcon: false, + decoration: AvesFilterDecoration( + widget: Selector( + selector: (context, mq) => mq.textScaleFactor, + builder: (context, textScaleFactor, child) { + return Padding( + padding: EdgeInsets.only(bottom: infoHeight(extent: extent, textScaleFactor: textScaleFactor)), + child: child, + ); + }, + child: entry == null + ? Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.white, + stringToColor(filter.getLabel(context)), + ], + ), + ), + ) + : ThumbnailImage( + entry: entry, + extent: thumbnailExtent, + ), + ), + radius: radius(extent), ), + banner: banner, + details: _buildDetails(source, filter), + padding: titlePadding, + onTap: onTap, + onLongPress: null, ); } Widget _buildDetails(CollectionSource source, T filter) { final padding = min(8.0, extent / 16); - final iconSize = min(14.0, extent / 8); - final fontSize = min(14.0, extent / 6); + final iconSize = detailIconSize(extent); + final fontSize = detailFontSize(extent); return Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index d7d483b2d..a82b088de 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -238,56 +238,66 @@ class _FilterGridContent extends StatelessWidget { final sectionedListLayoutProvider = ValueListenableBuilder( valueListenable: context.select>((controller) => controller.extentNotifier), builder: (context, tileExtent, child) { - return GridTheme( - extent: tileExtent, - child: Selector>( - selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), - builder: (context, c, child) { - final scrollableWidth = c.item1; - final columnCount = c.item2; - final tileSpacing = c.item3; - // do not listen for animation delay change - final controller = Provider.of(context, listen: false); - final tileAnimationDelay = controller.getTileAnimationDelay(Durations.staggeredAnimationPageTarget); - return SectionedFilterListLayoutProvider( - sections: visibleSections, - showHeaders: showHeaders, - scrollableWidth: scrollableWidth, - columnCount: columnCount, - spacing: tileSpacing, - tileExtent: tileExtent, - tileBuilder: (gridItem) { - final filter = gridItem.filter; - return MetaData( - metaData: ScalerMetadata(gridItem), - child: FilterChipGridDecorator>( - gridItem: gridItem, - extent: tileExtent, - child: CoveredFilterChip( - key: Key(filter.key), - filter: filter, + return Selector>( + selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), + builder: (context, c, child) { + final scrollableWidth = c.item1; + final columnCount = c.item2; + final tileSpacing = c.item3; + // do not listen for animation delay change + final tileAnimationDelay = context.read().getTileAnimationDelay(Durations.staggeredAnimationPageTarget); + return Selector( + selector: (context, mq) => mq.textScaleFactor, + builder: (context, textScaleFactor, child) { + final tileHeight = CoveredFilterChip.tileHeight(extent: tileExtent, textScaleFactor: textScaleFactor); + return GridTheme( + extent: tileExtent, + child: SectionedFilterListLayoutProvider( + sections: visibleSections, + showHeaders: showHeaders, + scrollableWidth: scrollableWidth, + columnCount: columnCount, + spacing: tileSpacing, + tileWidth: tileExtent, + tileHeight: tileHeight, + tileBuilder: (gridItem) { + final filter = gridItem.filter; + return MetaData( + metaData: ScalerMetadata(gridItem), + child: FilterChipGridDecorator>( + gridItem: gridItem, extent: tileExtent, - pinned: pinnedFilters.contains(filter), - banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null, - onTap: onTap, + child: CoveredFilterChip( + key: Key(filter.key), + filter: filter, + extent: tileExtent, + pinned: pinnedFilters.contains(filter), + banner: newFilters.contains(filter) ? context.l10n.newFilterBanner : null, + onTap: onTap, + ), ), - ), - ); - }, - tileAnimationDelay: tileAnimationDelay, - child: _FilterSectionedContent( - appBar: appBar, - appBarHeightNotifier: _appBarHeightNotifier, - visibleSections: visibleSections, - sortFactor: sortFactor, - selectable: selectable, - emptyBuilder: emptyBuilder, - scrollController: PrimaryScrollController.of(context)!, + ); + }, + tileAnimationDelay: tileAnimationDelay, + child: child!, ), ); - }), + }, + child: child, + ); + }, + child: child, ); }, + child: _FilterSectionedContent( + appBar: appBar, + appBarHeightNotifier: _appBarHeightNotifier, + visibleSections: visibleSections, + sortFactor: sortFactor, + selectable: selectable, + emptyBuilder: emptyBuilder, + scrollController: PrimaryScrollController.of(context)!, + ), ); return sectionedListLayoutProvider; }, @@ -399,24 +409,26 @@ class _FilterScaler extends StatelessWidget { Widget build(BuildContext context) { final pinnedFilters = settings.pinnedFilters; final tileSpacing = context.select((controller) => controller.spacing); + final textScaleFactor = context.select((mq) => mq.textScaleFactor); return GridScaleGestureDetector>( scrollableKey: scrollableKey, - gridBuilder: (center, extent, child) => CustomPaint( + heightForWidth: (width) => CoveredFilterChip.tileHeight(extent: width, textScaleFactor: textScaleFactor), + gridBuilder: (center, tileSize, child) => CustomPaint( painter: GridPainter( center: center, - extent: extent, + tileSize: tileSize, spacing: tileSpacing, borderWidth: AvesFilterChip.outlineWidth, - borderRadius: CoveredFilterChip.radius(extent), + borderRadius: CoveredFilterChip.radius(tileSize.width), color: Colors.grey.shade700, ), child: child, ), - scaledBuilder: (item, extent) { + scaledBuilder: (item, tileSize) { final filter = item.filter; return CoveredFilterChip( filter: filter, - extent: extent, + extent: tileSize.width, thumbnailExtent: context.read().effectiveExtentMax, pinned: pinnedFilters.contains(filter), ); diff --git a/lib/widgets/filter_grids/common/section_layout.dart b/lib/widgets/filter_grids/common/section_layout.dart index c945c1642..6c350b6f1 100644 --- a/lib/widgets/filter_grids/common/section_layout.dart +++ b/lib/widgets/filter_grids/common/section_layout.dart @@ -13,7 +13,8 @@ class SectionedFilterListLayoutProvider extends Sect required double scrollableWidth, required int columnCount, required double spacing, - required double tileExtent, + required double tileWidth, + required double tileHeight, required Widget Function(FilterGridItem gridItem) tileBuilder, required Duration tileAnimationDelay, required Widget child, @@ -22,7 +23,8 @@ class SectionedFilterListLayoutProvider extends Sect scrollableWidth: scrollableWidth, columnCount: columnCount, spacing: spacing, - tileExtent: tileExtent, + tileWidth: tileWidth, + tileHeight: tileHeight, tileBuilder: tileBuilder, tileAnimationDelay: tileAnimationDelay, child: child,