collection: action to show moved/copied items
This commit is contained in:
parent
fa5f30ea7c
commit
9c8d0215c6
11 changed files with 190 additions and 98 deletions
|
@ -18,6 +18,8 @@
|
|||
"@applyButtonLabel": {},
|
||||
"deleteButtonLabel": "DELETE",
|
||||
"@deleteButtonLabel": {},
|
||||
"showButtonLabel": "SHOW",
|
||||
"@showButtonLabel": {},
|
||||
"hideButtonLabel": "HIDE",
|
||||
"@hideButtonLabel": {},
|
||||
"continueButtonLabel": "CONTINUE",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
"applyButtonLabel": "확인",
|
||||
"deleteButtonLabel": "삭제",
|
||||
"showButtonLabel": "보기",
|
||||
"hideButtonLabel": "숨기기",
|
||||
"continueButtonLabel": "다음",
|
||||
"clearTooltip": "초기화",
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class HighlightInfo extends ChangeNotifier {
|
||||
final EventBus eventBus = EventBus();
|
||||
|
||||
void trackItem<T>(
|
||||
T item, {
|
||||
required bool animate,
|
||||
Object? highlight,
|
||||
}) =>
|
||||
eventBus.fire(TrackEvent<T>(item, animate, highlight));
|
||||
|
||||
Object? _item;
|
||||
|
||||
void set(Object item) {
|
||||
|
@ -22,3 +32,12 @@ class HighlightInfo extends ChangeNotifier {
|
|||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{item=$_item}';
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TrackEvent<T> {
|
||||
final T item;
|
||||
final bool animate;
|
||||
final Object? highlight;
|
||||
|
||||
const TrackEvent(this.item, this.animate, this.highlight);
|
||||
}
|
||||
|
|
|
@ -55,9 +55,10 @@ class Durations {
|
|||
|
||||
// delays & refresh intervals
|
||||
static const opToastDisplay = Duration(seconds: 3);
|
||||
static const opToastActionDisplay = Duration(seconds: 5);
|
||||
static const infoScrollMonitoringTimerDelay = Duration(milliseconds: 100);
|
||||
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
|
||||
static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300);
|
||||
static const highlightJumpDelay = Duration(milliseconds: 400);
|
||||
static const highlightScrollInitDelay = Duration(milliseconds: 800);
|
||||
static const videoProgressTimerInterval = Duration(milliseconds: 300);
|
||||
static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation;
|
||||
|
|
|
@ -32,6 +32,7 @@ class Themes {
|
|||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: Colors.grey.shade800,
|
||||
actionTextColor: _accentColor,
|
||||
contentTextStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:aves/app_mode.dart';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
|
@ -22,7 +21,7 @@ import 'package:aves/widgets/common/basic/insets.dart';
|
|||
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||
|
@ -131,33 +130,37 @@ class _CollectionSectionedContent extends StatefulWidget {
|
|||
_CollectionSectionedContentState createState() => _CollectionSectionedContentState();
|
||||
}
|
||||
|
||||
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
|
||||
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> with GridItemTrackerMixin<AvesEntry, _CollectionSectionedContent> {
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
@override
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
final ValueNotifier<double> _appBarHeightNotifier = ValueNotifier(0);
|
||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
||||
@override
|
||||
final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0);
|
||||
|
||||
@override
|
||||
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollView = AnimationLimiter(
|
||||
child: _CollectionScrollView(
|
||||
scrollableKey: _scrollableKey,
|
||||
scrollableKey: scrollableKey,
|
||||
collection: collection,
|
||||
appBar: CollectionAppBar(
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
collection: collection,
|
||||
),
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
isScrollingNotifier: widget.isScrollingNotifier,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
);
|
||||
|
||||
final scaler = _CollectionScaler(
|
||||
scrollableKey: _scrollableKey,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
child: scrollView,
|
||||
);
|
||||
|
||||
|
@ -166,7 +169,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
|||
selectable: isMainMode,
|
||||
collection: collection,
|
||||
scrollController: scrollController,
|
||||
appBarHeightNotifier: _appBarHeightNotifier,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
child: scaler,
|
||||
);
|
||||
|
||||
|
@ -190,7 +193,6 @@ class _CollectionScaler extends StatelessWidget {
|
|||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
return GridScaleGestureDetector<AvesEntry>(
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
gridBuilder: (center, extent, child) => CustomPaint(
|
||||
painter: GridPainter(
|
||||
center: center,
|
||||
|
@ -211,11 +213,6 @@ class _CollectionScaler extends StatelessWidget {
|
|||
highlightable: false,
|
||||
),
|
||||
),
|
||||
getScaledItemTileRect: (context, entry) {
|
||||
final sectionedListLayout = context.read<SectionedListLayout<AvesEntry>>();
|
||||
return sectionedListLayout.getTileRect(entry) ?? Rect.zero;
|
||||
},
|
||||
onScaled: (entry) => context.read<HighlightInfo>().set(entry),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,20 +4,27 @@ import 'package:aves/model/actions/collection_actions.dart';
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/move_type.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_op_events.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/album_pick.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
final CollectionLens collection;
|
||||
|
@ -114,6 +121,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
collection.browse();
|
||||
source.resumeMonitoring();
|
||||
|
||||
// cleanup
|
||||
if (moveType == MoveType.move) {
|
||||
await storageService.deleteEmptyDirectories(selectionDirs);
|
||||
}
|
||||
|
||||
final l10n = context.l10n;
|
||||
final movedCount = movedOps.length;
|
||||
if (movedCount < todoCount) {
|
||||
|
@ -121,12 +133,46 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
showFeedback(context, copy ? l10n.collectionCopyFailureFeedback(count) : l10n.collectionMoveFailureFeedback(count));
|
||||
} else {
|
||||
final count = movedCount;
|
||||
showFeedback(context, copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count));
|
||||
}
|
||||
|
||||
// cleanup
|
||||
if (moveType == MoveType.move) {
|
||||
await storageService.deleteEmptyDirectories(selectionDirs);
|
||||
showFeedback(
|
||||
context,
|
||||
copy ? l10n.collectionCopySuccessFeedback(count) : l10n.collectionMoveSuccessFeedback(count),
|
||||
SnackBarAction(
|
||||
label: context.l10n.showButtonLabel,
|
||||
onPressed: () async {
|
||||
final highlightInfo = context.read<HighlightInfo>();
|
||||
var targetCollection = collection;
|
||||
if (collection.filters.any((f) => f is AlbumFilter)) {
|
||||
final filter = AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum));
|
||||
// we could simply add the filter to the current collection
|
||||
// but navigating makes the change less jarring
|
||||
targetCollection = CollectionLens(
|
||||
source: collection.source,
|
||||
filters: collection.filters,
|
||||
groupFactor: collection.groupFactor,
|
||||
sortFactor: collection.sortFactor,
|
||||
)..addFilter(filter);
|
||||
unawaited(Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) {
|
||||
return CollectionPage(
|
||||
targetCollection,
|
||||
);
|
||||
},
|
||||
),
|
||||
));
|
||||
await Future.delayed(Durations.staggeredAnimationPageTarget);
|
||||
}
|
||||
await Future.delayed(Durations.highlightScrollInitDelay);
|
||||
final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet();
|
||||
final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri));
|
||||
if (targetEntry != null) {
|
||||
highlightInfo.trackItem(targetEntry, animate: true, highlight: targetEntry);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -6,15 +6,16 @@ import 'package:percent_indicator/circular_percent_indicator.dart';
|
|||
mixin FeedbackMixin {
|
||||
void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
void showFeedback(BuildContext context, String message) {
|
||||
showFeedbackWithMessenger(ScaffoldMessenger.of(context), message);
|
||||
void showFeedback(BuildContext context, String message, [SnackBarAction? action]) {
|
||||
showFeedbackWithMessenger(ScaffoldMessenger.of(context), message, action);
|
||||
}
|
||||
|
||||
// provide the messenger if feedback happens as the widget is disposed
|
||||
void showFeedbackWithMessenger(ScaffoldMessengerState messenger, String message) {
|
||||
void showFeedbackWithMessenger(ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(message),
|
||||
duration: Durations.opToastDisplay,
|
||||
action: action,
|
||||
duration: action != null ? Durations.opToastActionDisplay : Durations.opToastDisplay,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
73
lib/widgets/common/grid/item_tracker.dart
Normal file
73
lib/widgets/common/grid/item_tracker.dart
Normal file
|
@ -0,0 +1,73 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U> {
|
||||
ValueNotifier<double> get appBarHeightNotifier;
|
||||
|
||||
GlobalKey get scrollableKey;
|
||||
|
||||
ScrollController get scrollController;
|
||||
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final highlightInfo = context.read<HighlightInfo>();
|
||||
_subscriptions.add(highlightInfo.eventBus.on<TrackEvent<T>>().listen((e) => _trackItem(
|
||||
e.item,
|
||||
animate: e.animate,
|
||||
highlight: e.highlight,
|
||||
)));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// about scrolling & offset retrieval:
|
||||
// `Scrollable.ensureVisible` only works on already rendered objects
|
||||
// `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata`
|
||||
// `RenderViewport.scrollOffsetOf` is a good alternative
|
||||
Future<void> _trackItem(T item, {required bool animate, required Object? highlight}) async {
|
||||
final sectionedListLayout = context.read<SectionedListLayout<T>>();
|
||||
final tileRect = sectionedListLayout.getTileRect(item);
|
||||
if (tileRect == null) return;
|
||||
|
||||
final scrollableContext = scrollableKey.currentContext!;
|
||||
final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height;
|
||||
|
||||
// most of the time the app bar will be scrolled away after scaling,
|
||||
// so we compensate for it to center the focal point thumbnail
|
||||
final appBarHeight = appBarHeightNotifier.value;
|
||||
final scrollOffset = tileRect.top + (tileRect.height - scrollableHeight) / 2 + appBarHeight;
|
||||
|
||||
if (animate) {
|
||||
if (scrollOffset > 0) {
|
||||
await scrollController.animateTo(
|
||||
scrollOffset,
|
||||
duration: Duration(milliseconds: (scrollOffset / 2).round().clamp(Durations.highlightScrollAnimationMinMillis, Durations.highlightScrollAnimationMaxMillis)),
|
||||
curve: Curves.easeInOutCubic,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final maxScrollExtent = scrollController.position.maxScrollExtent;
|
||||
scrollController.jumpTo(scrollOffset.clamp(.0, maxScrollExtent));
|
||||
await Future.delayed(Durations.highlightJumpDelay);
|
||||
}
|
||||
|
||||
if (highlight != null) {
|
||||
context.read<HighlightInfo>().set(highlight);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
|
@ -17,20 +18,16 @@ class ScalerMetadata<T> {
|
|||
|
||||
class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final Widget Function(Offset center, double extent, Widget child)? gridBuilder;
|
||||
final Widget Function(Offset center, double extent, Widget child) gridBuilder;
|
||||
final Widget Function(T item, double extent) scaledBuilder;
|
||||
final Rect Function(BuildContext context, T item) getScaledItemTileRect;
|
||||
final void Function(T item) onScaled;
|
||||
final Object Function(T item)? highlightItem;
|
||||
final Widget child;
|
||||
|
||||
const GridScaleGestureDetector({
|
||||
required this.scrollableKey,
|
||||
required this.appBarHeightNotifier,
|
||||
this.gridBuilder,
|
||||
required this.gridBuilder,
|
||||
required this.scaledBuilder,
|
||||
required this.getScaledItemTileRect,
|
||||
required this.onScaled,
|
||||
this.highlightItem,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
|
@ -114,11 +111,9 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
} else {
|
||||
// scroll to show the focal point thumbnail at its new position
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
final entry = _metadata!.item;
|
||||
_scrollToItem(entry);
|
||||
// warning: posting `onScaled` in the next frame with `addPostFrameCallback`
|
||||
// would trigger only when the scrollable offset actually changes
|
||||
Future.delayed(Durations.collectionScalingCompleteNotificationDelay).then((_) => widget.onScaled(entry));
|
||||
final trackItem = _metadata!.item;
|
||||
final highlightItem = widget.highlightItem?.call(trackItem) ?? trackItem;
|
||||
context.read<HighlightInfo>().trackItem(trackItem, animate: false, highlight: highlightItem);
|
||||
_applyingScale = false;
|
||||
});
|
||||
}
|
||||
|
@ -135,26 +130,6 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
// about scrolling & offset retrieval:
|
||||
// `Scrollable.ensureVisible` only works on already rendered objects
|
||||
// `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata`
|
||||
// `RenderViewport.scrollOffsetOf` is a good alternative
|
||||
void _scrollToItem(T item) {
|
||||
final scrollableContext = widget.scrollableKey.currentContext!;
|
||||
final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height;
|
||||
final tileRect = widget.getScaledItemTileRect(context, item);
|
||||
// most of the time the app bar will be scrolled away after scaling,
|
||||
// so we compensate for it to center the focal point thumbnail
|
||||
final appBarHeight = widget.appBarHeightNotifier.value;
|
||||
final scrollOffset = tileRect.top + (tileRect.height - scrollableHeight) / 2 + appBarHeight;
|
||||
|
||||
final controller = PrimaryScrollController.of(context);
|
||||
if (controller != null) {
|
||||
final maxScrollExtent = controller.position.maxScrollExtent;
|
||||
controller.jumpTo(scrollOffset.clamp(.0, maxScrollExtent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScaleOverlay extends StatefulWidget {
|
||||
|
@ -162,14 +137,14 @@ class ScaleOverlay extends StatefulWidget {
|
|||
final Offset center;
|
||||
final double viewportWidth;
|
||||
final ValueNotifier<double> scaledExtentNotifier;
|
||||
final Widget Function(Offset center, double extent, Widget child)? gridBuilder;
|
||||
final Widget Function(Offset center, double extent, Widget child) gridBuilder;
|
||||
|
||||
const ScaleOverlay({
|
||||
required this.builder,
|
||||
required this.center,
|
||||
required this.viewportWidth,
|
||||
required this.scaledExtentNotifier,
|
||||
this.gridBuilder,
|
||||
required this.gridBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -243,7 +218,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
|||
),
|
||||
],
|
||||
);
|
||||
child = widget.gridBuilder?.call(clampedCenter, extent, child) ?? child;
|
||||
child = widget.gridBuilder(clampedCenter, extent, child);
|
||||
return child;
|
||||
},
|
||||
),
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/widgets/common/basic/insets.dart';
|
|||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
|
@ -280,18 +281,21 @@ class _FilterSectionedContent<T extends CollectionFilter> extends StatefulWidget
|
|||
_FilterSectionedContentState createState() => _FilterSectionedContentState<T>();
|
||||
}
|
||||
|
||||
class _FilterSectionedContentState<T extends CollectionFilter> extends State<_FilterSectionedContent<T>> {
|
||||
class _FilterSectionedContentState<T extends CollectionFilter> extends State<_FilterSectionedContent<T>> with GridItemTrackerMixin<FilterGridItem<T>, _FilterSectionedContent<T>> {
|
||||
Widget get appBar => widget.appBar;
|
||||
|
||||
@override
|
||||
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
|
||||
|
||||
Map<ChipSectionKey, List<FilterGridItem<T>>> get visibleFilterSections => widget.visibleFilterSections;
|
||||
|
||||
Widget Function() get emptyBuilder => widget.emptyBuilder;
|
||||
|
||||
@override
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
final GlobalKey _scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
||||
@override
|
||||
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'filter-grid-page-scrollable');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -303,7 +307,7 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
Widget build(BuildContext context) {
|
||||
final scrollView = AnimationLimiter(
|
||||
child: _FilterScrollView<T>(
|
||||
scrollableKey: _scrollableKey,
|
||||
scrollableKey: scrollableKey,
|
||||
appBar: appBar,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
sortFactor: widget.sortFactor,
|
||||
|
@ -313,7 +317,7 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
);
|
||||
|
||||
final scaler = _FilterScaler<T>(
|
||||
scrollableKey: _scrollableKey,
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
child: scrollView,
|
||||
);
|
||||
|
@ -328,33 +332,10 @@ class _FilterSectionedContentState<T extends CollectionFilter> extends State<_Fi
|
|||
final gridItem = visibleFilterSections.values.expand((list) => list).firstWhereOrNull((gridItem) => gridItem.filter == filter);
|
||||
if (gridItem != null) {
|
||||
await Future.delayed(Durations.highlightScrollInitDelay);
|
||||
final sectionedListLayout = context.read<SectionedListLayout<FilterGridItem<T>>>();
|
||||
final tileRect = sectionedListLayout.getTileRect(gridItem);
|
||||
if (tileRect != null) {
|
||||
await _scrollToItem(tileRect);
|
||||
highlightInfo.set(filter);
|
||||
}
|
||||
highlightInfo.trackItem(gridItem, animate: true, highlight: filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _scrollToItem(Rect tileRect) async {
|
||||
final scrollableContext = _scrollableKey.currentContext!;
|
||||
final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height;
|
||||
|
||||
// most of the time the app bar will be scrolled away after scaling,
|
||||
// so we compensate for it to center the focal point thumbnail
|
||||
final appBarHeight = appBarHeightNotifier.value;
|
||||
final scrollOffset = tileRect.top + (tileRect.height - scrollableHeight) / 2 + appBarHeight;
|
||||
|
||||
if (scrollOffset > 0) {
|
||||
await scrollController.animateTo(
|
||||
scrollOffset,
|
||||
duration: Duration(milliseconds: (scrollOffset / 2).round().clamp(Durations.highlightScrollAnimationMinMillis, Durations.highlightScrollAnimationMaxMillis)),
|
||||
curve: Curves.easeInOutCubic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
||||
|
@ -374,7 +355,6 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
|||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
return GridScaleGestureDetector<FilterGridItem<T>>(
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
gridBuilder: (center, extent, child) => CustomPaint(
|
||||
painter: GridPainter(
|
||||
center: center,
|
||||
|
@ -396,11 +376,7 @@ class _FilterScaler<T extends CollectionFilter> extends StatelessWidget {
|
|||
highlightable: false,
|
||||
);
|
||||
},
|
||||
getScaledItemTileRect: (context, item) {
|
||||
final sectionedListLayout = context.read<SectionedListLayout<FilterGridItem<T>>>();
|
||||
return sectionedListLayout.getTileRect(item) ?? Rect.zero;
|
||||
},
|
||||
onScaled: (item) => context.read<HighlightInfo>().set(item.filter),
|
||||
highlightItem: (item) => item.filter,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue