diff --git a/lib/model/settings/modules/app.dart b/lib/model/settings/modules/app.dart index e3bb24f23..60f968d21 100644 --- a/lib/model/settings/modules/app.dart +++ b/lib/model/settings/modules/app.dart @@ -6,7 +6,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; mixin AppSettings on SettingsAccess { - static const int _recentFilterHistoryMax = 10; + static const int recentFilterHistoryMax = 20; bool get hasAcceptedTerms => getBool(SettingKeys.hasAcceptedTermsKey) ?? SettingsDefaults.hasAcceptedTerms; @@ -105,9 +105,9 @@ mixin AppSettings on SettingsAccess { List get recentDestinationAlbums => getStringList(SettingKeys.recentDestinationAlbumsKey) ?? []; - set recentDestinationAlbums(List newValue) => set(SettingKeys.recentDestinationAlbumsKey, newValue.take(_recentFilterHistoryMax).toList()); + set recentDestinationAlbums(List newValue) => set(SettingKeys.recentDestinationAlbumsKey, newValue.take(recentFilterHistoryMax).toList()); List get recentTags => (getStringList(SettingKeys.recentTagsKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList(); - set recentTags(List newValue) => set(SettingKeys.recentTagsKey, newValue.take(_recentFilterHistoryMax).map((filter) => filter.toJson()).toList()); + set recentTags(List newValue) => set(SettingKeys.recentTagsKey, newValue.take(recentFilterHistoryMax).map((filter) => filter.toJson()).toList()); } diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 1aa37db63..8397cf412 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -162,6 +162,8 @@ class AIcons { static const zoomOut = Icons.remove_outlined; static const collapse = Icons.expand_less_outlined; static const expand = Icons.expand_more_outlined; + static const up = Icons.keyboard_arrow_up_outlined; + static const down = Icons.keyboard_arrow_down_outlined; static const previous = Icons.chevron_left_outlined; static const next = Icons.chevron_right_outlined; diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index 013a39601..3a9ee4b29 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -142,7 +142,7 @@ class _Chip extends StatelessWidget { key: ValueKey(filter), filter: filter, maxWidth: single - ? AvesFilterChip.computeMaxWidth( + ? AvesFilterChip.computeMaxWidthForRow( context, minChipPerRow: 1, chipPadding: FilterBar.chipPadding.horizontal, diff --git a/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart index f1b5b7ee5..e705d7670 100644 --- a/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart +++ b/lib/widgets/common/action_controls/quick_choosers/album_chooser.dart @@ -1,14 +1,17 @@ import 'dart:async'; import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class AlbumQuickChooser extends StatelessWidget { +class AlbumQuickChooser extends StatelessWidget with FilterQuickChooserMixin { final ValueNotifier valueNotifier; + @override final List options; final bool blurred; final PopupMenuPosition chooserPosition; @@ -25,7 +28,6 @@ class AlbumQuickChooser extends StatelessWidget { @override Widget build(BuildContext context) { - final source = context.read(); return MenuQuickChooser( valueNotifier: valueNotifier, options: options, @@ -33,10 +35,17 @@ class AlbumQuickChooser extends StatelessWidget { blurred: blurred, chooserPosition: chooserPosition, pointerGlobalPosition: pointerGlobalPosition, - itemBuilder: (context, album) => AvesFilterChip( - filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), - allowGenericIcon: false, - ), + maxTotalOptionCount: FilterQuickChooserMixin.maxTotalOptionCount, + itemHeight: computeItemHeight(context), + contentWidth: computeLargestItemWidth, + itemBuilder: itemBuilder, + emptyBuilder: (context) => Text(context.l10n.albumEmpty), ); } + + @override + CollectionFilter buildFilter(BuildContext context, String option) { + final source = context.read(); + return AlbumFilter(option, source.getAlbumDisplayName(context, option)); + } } diff --git a/lib/widgets/common/action_controls/quick_choosers/common/menu.dart b/lib/widgets/common/action_controls/quick_choosers/common/menu.dart index aff175503..b0c1a8d36 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/menu.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/menu.dart @@ -1,9 +1,10 @@ import 'dart:async'; +import 'dart:math'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/quick_chooser.dart'; import 'package:aves_ui/aves_ui.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; @@ -16,9 +17,13 @@ class MenuQuickChooser extends StatefulWidget { final bool blurred; final PopupMenuPosition chooserPosition; final Stream pointerGlobalPosition; + final int maxTotalOptionCount; + final double itemHeight; + final double? Function(BuildContext context)? contentWidth; final Widget Function(BuildContext context, T menuItem) itemBuilder; + final WidgetBuilder? emptyBuilder; - static const int maxOptionCount = 5; + static const int maxVisibleOptionCount = 5; MenuQuickChooser({ super.key, @@ -28,8 +33,12 @@ class MenuQuickChooser extends StatefulWidget { required this.blurred, required this.chooserPosition, required this.pointerGlobalPosition, + this.maxTotalOptionCount = maxVisibleOptionCount, + this.itemHeight = kMinInteractiveDimension, + this.contentWidth, required this.itemBuilder, - }) : options = options.take(maxOptionCount).toList(); + this.emptyBuilder, + }) : options = options.take(maxTotalOptionCount).toList(); @override State> createState() => _MenuQuickChooserState(); @@ -38,6 +47,10 @@ class MenuQuickChooser extends StatefulWidget { class _MenuQuickChooserState extends State> { final List _subscriptions = []; final ValueNotifier _selectedRowRect = ValueNotifier(Rect.zero); + final ScrollController _scrollController = ScrollController(); + int _scrollDirection = 0; + Timer? _scrollUpdateTimer; + Offset _globalPosition = Offset.zero; ValueNotifier get valueNotifier => widget.valueNotifier; @@ -45,12 +58,26 @@ class _MenuQuickChooserState extends State> { bool get reversed => widget.autoReverse && widget.chooserPosition == PopupMenuPosition.over; - static const double intraPadding = 8; + bool get scrollable => options.length > MenuQuickChooser.maxVisibleOptionCount; + + int get visibleOptionCount => min(MenuQuickChooser.maxVisibleOptionCount, options.length); + + double get itemHeight => widget.itemHeight; + + double get contentHeight => max(0, itemHeight * visibleOptionCount + _intraPadding * (visibleOptionCount - 1)); + + static const double _selectorMargin = 24; + static const double _intraPadding = 8; + static const double _nonScrollablePaddingHeight = _intraPadding; + static const double _scrollerAreaHeight = kMinInteractiveDimension; + static const double scrollMaxPixelPerSecond = 600.0; + static const Duration scrollUpdateInterval = Duration(milliseconds: 100); @override void initState() { super.initState(); _registerWidget(widget); + WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {})); } @override @@ -69,6 +96,9 @@ class _MenuQuickChooserState extends State> { @override void dispose() { _unregisterWidget(widget); + _selectedRowRect.dispose(); + _scrollController.dispose(); + _scrollUpdateTimer?.cancel(); super.dispose(); } @@ -91,28 +121,11 @@ class _MenuQuickChooserState extends State> { builder: (context, selectedValue, child) { final durations = context.watch(); - List optionChildren = options.mapIndexed((index, value) { - final isFirst = index == (reversed ? options.length - 1 : 0); + if (options.isEmpty) { return Padding( - padding: EdgeInsets.only(top: isFirst ? intraPadding : 0, bottom: intraPadding), - child: widget.itemBuilder(context, value), + padding: const EdgeInsets.all(16), + child: widget.emptyBuilder?.call(context) ?? const SizedBox(), ); - }).toList(); - - optionChildren = AnimationConfiguration.toStaggeredList( - duration: durations.staggeredAnimation * .5, - delay: durations.staggeredAnimationDelay * .5 * timeDilation, - childAnimationBuilder: (child) => SlideAnimation( - verticalOffset: 50.0 * (widget.chooserPosition == PopupMenuPosition.over ? 1 : -1), - child: FadeInAnimation( - child: child, - ), - ), - children: optionChildren, - ); - - if (reversed) { - optionChildren = optionChildren.reversed.toList(); } return Stack( @@ -137,12 +150,67 @@ class _MenuQuickChooserState extends State> { return child; }, ), - Padding( - padding: const EdgeInsetsDirectional.only(start: 24), + Container( + width: widget.contentWidth?.call(context), + margin: const EdgeInsetsDirectional.only(start: _selectorMargin), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: optionChildren, + children: [ + scrollable + ? ListenableBuilder( + listenable: _scrollController, + builder: (context, child) => Opacity( + opacity: canGoUp ? 1 : .5, + child: child, + ), + child: _buildScrollerArea(AIcons.up), + ) + : const SizedBox(height: _nonScrollablePaddingHeight), + ConstrainedBox( + constraints: BoxConstraints.tightFor(height: contentHeight), + child: ListView.separated( + reverse: reversed, + controller: _scrollController, + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + final child = Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints.tightFor(height: itemHeight), + child: widget.itemBuilder(context, options[index]), + ); + if (index < MenuQuickChooser.maxVisibleOptionCount) { + // only animate items visible on first render + return AnimationConfiguration.staggeredList( + position: index, + duration: durations.staggeredAnimation * .5, + delay: durations.staggeredAnimationDelay * .5 * timeDilation, + child: SlideAnimation( + verticalOffset: 50.0 * (widget.chooserPosition == PopupMenuPosition.over ? 1 : -1), + child: FadeInAnimation( + child: child, + ), + ), + ); + } else { + return child; + } + }, + separatorBuilder: (context, index) => const SizedBox(height: _intraPadding), + itemCount: options.length, + ), + ), + scrollable + ? ListenableBuilder( + listenable: _scrollController, + builder: (context, child) => Opacity( + opacity: canGoDown ? 1 : .5, + child: child, + ), + child: _buildScrollerArea(AIcons.down), + ) + : const SizedBox(height: _nonScrollablePaddingHeight), + ], ), ), ], @@ -152,30 +220,97 @@ class _MenuQuickChooserState extends State> { ); } - void _onPointerMove(Offset globalPosition) { - final padding = QuickChooser.margin.vertical + QuickChooser.padding.vertical; + bool get canGoUp { + if (!_scrollController.hasClients) return false; + final position = _scrollController.position; + return reversed ? position.pixels < position.maxScrollExtent : 0 < position.pixels; + } + + bool get canGoDown { + if (!_scrollController.hasClients) return false; + final position = _scrollController.position; + return reversed ? 0 < position.pixels : position.pixels < position.maxScrollExtent; + } + + Widget _buildScrollerArea(IconData icon) { + return Container( + alignment: Alignment.center, + height: _scrollerAreaHeight, + margin: const EdgeInsetsDirectional.only(end: _selectorMargin), + child: Icon(icon), + ); + } + + void _onPointerMove(Offset globalPosition) { + _globalPosition = globalPosition; + final chooserBox = context.findRenderObject() as RenderBox?; + if (chooserBox == null) return; - final chooserBox = context.findRenderObject() as RenderBox; final chooserSize = chooserBox.size; final contentWidth = chooserSize.width; - final contentHeight = chooserSize.height - padding; + final chooserBoxEdgeHeight = (QuickChooser.margin.vertical + QuickChooser.padding.vertical) / 2; - final optionCount = options.length; - final itemHeight = (contentHeight - (optionCount + 1) * intraPadding) / optionCount; + final localPosition = chooserBox.globalToLocal(globalPosition); + final dx = localPosition.dx; + if (!(0 < dx && dx < contentWidth)) { + valueNotifier.value = null; + return; + } - final local = chooserBox.globalToLocal(globalPosition); - final dx = local.dx; - final dy = local.dy - padding / 2; + double dy = localPosition.dy - chooserBoxEdgeHeight; + int scrollDirection = 0; + if (scrollable) { + dy -= _scrollerAreaHeight; + if (-_scrollerAreaHeight < dy && dy < 0) { + scrollDirection = reversed ? 1 : -1; + } else if (contentHeight < dy && dy < contentHeight + _scrollerAreaHeight) { + scrollDirection = reversed ? -1 : 1; + } + _scroll(scrollDirection); + } else { + dy -= _nonScrollablePaddingHeight; + } T? selectedValue; - if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) { - final index = (optionCount * dy / contentHeight).floor(); - if (0 <= index && index < optionCount) { - selectedValue = options[reversed ? optionCount - 1 - index : index]; - final top = index * (itemHeight + intraPadding) + intraPadding; + if (scrollDirection == 0 && 0 < dy && dy < contentHeight) { + final visibleOffset = reversed ? contentHeight - dy : dy; + final fullItemHeight = itemHeight + _intraPadding; + final scrollOffset = _scrollController.offset; + final index = (visibleOffset + _intraPadding + scrollOffset) ~/ (fullItemHeight); + if (0 <= index && index < options.length) { + selectedValue = options[index]; + double fromEdge = fullItemHeight * index; + fromEdge += (scrollable ? _scrollerAreaHeight - scrollOffset : _nonScrollablePaddingHeight); + final top = reversed ? chooserSize.height - chooserBoxEdgeHeight - fromEdge - fullItemHeight : fromEdge; _selectedRowRect.value = Rect.fromLTWH(0, top, contentWidth, itemHeight); } } valueNotifier.value = selectedValue; } + + void _scroll(int scrollDirection) { + if (scrollDirection == _scrollDirection) return; + _scrollDirection = scrollDirection; + _scrollUpdateTimer?.cancel(); + + final current = _scrollController.offset; + if (scrollDirection == 0) { + _scrollController.jumpTo(current); + return; + } + + final target = scrollDirection > 0 ? _scrollController.position.maxScrollExtent : .0; + if (target != current) { + final distance = target - current; + final millis = distance * 1000 / scrollMaxPixelPerSecond / scrollDirection; + _scrollController.animateTo( + target, + duration: Duration(milliseconds: millis.round()), + curve: Curves.linear, + ); + // use a timer to update the selection, because `_onPointerMove` + // is not called when the pointer stays still while the view is scrolling + _scrollUpdateTimer = Timer.periodic(scrollUpdateInterval, (_) => _onPointerMove(_globalPosition)); + } + } } diff --git a/lib/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart b/lib/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart new file mode 100644 index 000000000..93c9eeb22 --- /dev/null +++ b/lib/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart @@ -0,0 +1,56 @@ +import 'dart:math'; + +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/modules/app.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +mixin FilterQuickChooserMixin { + List get options; + + static const int maxTotalOptionCount = AppSettings.recentFilterHistoryMax; + static const double _chipPadding = AvesFilterChip.defaultPadding; + static const bool _chipAllowGenericIcon = false; + + CollectionFilter buildFilter(BuildContext context, T option); + + Widget itemBuilder(BuildContext context, T option) { + return AvesFilterChip( + filter: buildFilter(context, option), + allowGenericIcon: _chipAllowGenericIcon, + padding: _chipPadding, + maxWidth: double.infinity, + ); + } + + double computeItemHeight(BuildContext context) => AvesFilterChip.minChipHeight; + + double? computeLargestItemWidth(BuildContext context) { + if (options.isEmpty) return null; + + final textStyle = DefaultTextStyle.of(context).style.copyWith( + fontSize: AvesFilterChip.fontSize, + ); + final textDirection = Directionality.of(context); + final textScaler = MediaQuery.textScalerOf(context); + final iconSize = textScaler.scale(AvesFilterChip.iconSize); + + return options.map((option) { + final filter = buildFilter(context, option); + final icon = filter.iconBuilder(context, iconSize, allowGenericIcon: _chipAllowGenericIcon); + final label = filter.getLabel(context); + final paragraph = RenderParagraph( + TextSpan(text: label, style: textStyle), + textDirection: textDirection, + textScaler: textScaler, + )..layout(const BoxConstraints(), parentUsesSize: true); + final labelWidth = paragraph.getMaxIntrinsicWidth(double.infinity); + double chipWidth = labelWidth + _chipPadding * 4; + if (icon != null) { + chipWidth += iconSize + _chipPadding; + } + return max(AvesFilterChip.minChipWidth, chipWidth); + }).reduce(max); + } +} diff --git a/lib/widgets/common/action_controls/quick_choosers/move_button.dart b/lib/widgets/common/action_controls/quick_choosers/move_button.dart index 0d5ac1b66..f15cca1c3 100644 --- a/lib/widgets/common/action_controls/quick_choosers/move_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/move_button.dart @@ -5,7 +5,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/album_chooser.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; -import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; +import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:aves_model/aves_model.dart'; @@ -42,7 +42,7 @@ class _MoveButtonState extends ChooserQuickButtonState { final source = context.read(); final rawAlbums = source.rawAlbums; final options = settings.recentDestinationAlbums.where(rawAlbums.contains).toList(); - final takeCount = MenuQuickChooser.maxOptionCount - options.length; + final takeCount = FilterQuickChooserMixin.maxTotalOptionCount - options.length; if (takeCount > 0) { final filters = rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet(); final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList(); diff --git a/lib/widgets/common/action_controls/quick_choosers/share_button.dart b/lib/widgets/common/action_controls/quick_choosers/share_button.dart index 009594358..df22d7b17 100644 --- a/lib/widgets/common/action_controls/quick_choosers/share_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/share_button.dart @@ -51,7 +51,6 @@ class _ShareButtonState extends ChooserQuickButtonState valueNotifier; final List options; - final bool autoReverse; final bool blurred; final PopupMenuPosition chooserPosition; final Stream pointerGlobalPosition; + static const _itemPadding = EdgeInsetsDirectional.only(end: 8); + const ShareQuickChooser({ super.key, required this.valueNotifier, required this.options, - required this.autoReverse, required this.blurred, required this.chooserPosition, required this.pointerGlobalPosition, @@ -29,20 +31,39 @@ class ShareQuickChooser extends StatelessWidget { return MenuQuickChooser( valueNotifier: valueNotifier, options: options, - autoReverse: autoReverse, + autoReverse: false, blurred: blurred, chooserPosition: chooserPosition, pointerGlobalPosition: pointerGlobalPosition, - itemBuilder: (context, action) => ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Padding( - padding: const EdgeInsetsDirectional.only(end: 8), - child: MenuRow( - text: action.getText(context), - icon: action.getIcon(), - ), + itemHeight: kMinInteractiveDimension, + contentWidth: _computeLargestItemWidth, + itemBuilder: (context, action) => Padding( + padding: _itemPadding, + child: MenuRow( + text: action.getText(context), + icon: action.getIcon(), ), ), ); } + + double? _computeLargestItemWidth(BuildContext context) { + if (options.isEmpty) return null; + + final textStyle = DefaultTextStyle.of(context).style; + final textDirection = Directionality.of(context); + final textScaler = MediaQuery.textScalerOf(context); + final iconSize = IconTheme.of(context).size ?? 24; + + return options.map((action) { + final text = action.getText(context); + final paragraph = RenderParagraph( + TextSpan(text: text, style: textStyle), + textDirection: textDirection, + textScaler: textScaler, + )..layout(const BoxConstraints(), parentUsesSize: true); + final labelWidth = paragraph.getMaxIntrinsicWidth(double.infinity); + return iconSize + MenuRow.leadingPadding.horizontal + labelWidth + _itemPadding.horizontal; + }).reduce(max); + } } diff --git a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart index 565773837..9437c1f24 100644 --- a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart @@ -4,7 +4,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/view/view.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/button.dart'; -import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; +import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/tag_chooser.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; @@ -38,7 +38,7 @@ class _TagButtonState extends ChooserQuickButtonState animation, PopupMenuPosition chooserPosition) { final options = settings.recentTags; - final takeCount = MenuQuickChooser.maxOptionCount - options.length; + final takeCount = FilterQuickChooserMixin.maxTotalOptionCount - options.length; if (takeCount > 0) { final source = context.read(); final filters = source.sortedTags.map(TagFilter.new).whereNot(options.contains).toSet(); diff --git a/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart index 1947814f2..1b139e507 100644 --- a/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart +++ b/lib/widgets/common/action_controls/quick_choosers/tag_chooser.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/widgets/common/action_controls/quick_choosers/common/menu.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/action_controls/quick_choosers/filter_quick_chooser_mixin.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; -class TagQuickChooser extends StatelessWidget { +class TagQuickChooser extends StatelessWidget with FilterQuickChooserMixin { final ValueNotifier valueNotifier; + @override final List options; final bool blurred; final PopupMenuPosition chooserPosition; @@ -30,10 +32,14 @@ class TagQuickChooser extends StatelessWidget { blurred: blurred, chooserPosition: chooserPosition, pointerGlobalPosition: pointerGlobalPosition, - itemBuilder: (context, filter) => AvesFilterChip( - filter: filter, - allowGenericIcon: false, - ), + maxTotalOptionCount: FilterQuickChooserMixin.maxTotalOptionCount, + itemHeight: computeItemHeight(context), + contentWidth: computeLargestItemWidth, + itemBuilder: itemBuilder, + emptyBuilder: (context) => Text(context.l10n.tagEmpty), ); } + + @override + CollectionFilter buildFilter(BuildContext context, CollectionFilter option) => option; } diff --git a/lib/widgets/common/basic/popup/menu_row.dart b/lib/widgets/common/basic/popup/menu_row.dart index 33773c04f..23f77b47e 100644 --- a/lib/widgets/common/basic/popup/menu_row.dart +++ b/lib/widgets/common/basic/popup/menu_row.dart @@ -10,6 +10,8 @@ class MenuRow extends StatelessWidget { this.icon, }); + static const leadingPadding = EdgeInsetsDirectional.only(end: 12); + @override Widget build(BuildContext context) { return Row( @@ -17,7 +19,7 @@ class MenuRow extends StatelessWidget { children: [ if (icon != null) Padding( - padding: const EdgeInsetsDirectional.only(end: 12), + padding: leadingPadding, child: IconTheme.merge( data: IconThemeData( color: ListTileTheme.of(context).iconColor, diff --git a/lib/widgets/common/grid/selector.dart b/lib/widgets/common/grid/selector.dart index dc7058347..45aa02cab 100644 --- a/lib/widgets/common/grid/selector.dart +++ b/lib/widgets/common/grid/selector.dart @@ -55,9 +55,9 @@ class _GridSelectionGestureDetectorState extends State extends State extends State 0 ? scrollController.position.maxScrollExtent : .0; if (target != current) { final distance = target - current; - final millis = distance * 1000 / scrollMaxPixelPerSecond / speedFactor; + final millis = distance * 1000 / _scrollMaxPixelPerSecond / speedFactor; scrollController.animateTo( target, duration: Duration(milliseconds: millis.round()), @@ -193,7 +193,7 @@ class _GridSelectionGestureDetectorState extends State _onLongPressUpdate()); + _selectionUpdateTimer = Timer.periodic(_scrollUpdateInterval, (_) => _onLongPressUpdate()); } } diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index def2ceaf6..4617a7eed 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -59,6 +59,7 @@ class AvesFilterChip extends StatefulWidget { final FilterCallback? onTap, onRemove; final OffsetFilterCallback? onLongPress; + static const double defaultPadding = 6.0; static const double defaultRadius = 32; static const double outlineWidth = 2; static const double minChipHeight = kMinInteractiveDimension; @@ -79,7 +80,7 @@ class AvesFilterChip extends StatefulWidget { this.banner, this.leadingOverride, this.details, - this.padding = 6.0, + this.padding = defaultPadding, this.maxWidth, this.heroType = HeroType.onTap, this.onTap, @@ -87,7 +88,7 @@ class AvesFilterChip extends StatefulWidget { this.onLongPress = showDefaultLongPressMenu, }); - static double computeMaxWidth( + static double computeMaxWidthForRow( BuildContext context, { required int minChipPerRow, required double chipPadding, @@ -347,7 +348,7 @@ class _AvesFilterChipState extends State { maxWidth: max( AvesFilterChip.minChipWidth, widget.maxWidth ?? - AvesFilterChip.computeMaxWidth( + AvesFilterChip.computeMaxWidthForRow( context, minChipPerRow: 2, chipPadding: FilterBar.chipPadding.horizontal,