diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 7510f5ac3..e9f509671 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -16,9 +16,6 @@ class Durations { static const iconAnimation = Duration(milliseconds: 300); static const sweeperOpacityAnimation = Duration(milliseconds: 150); static const sweepingAnimation = Duration(milliseconds: 650); - - // static const staggeredAnimation = Duration(milliseconds: 375); - // static const staggeredAnimationPageTarget = Duration(milliseconds: 800); static const dialogFieldReachAnimation = Duration(milliseconds: 300); static const appBarTitleAnimation = Duration(milliseconds: 300); @@ -43,9 +40,6 @@ class Durations { static const filterRowExpandAnimation = Duration(milliseconds: 300); // viewer animations - static const viewerVerticalPageScrollAnimation = Duration(milliseconds: 500); - static const viewerOverlayAnimation = Duration(milliseconds: 200); - static const viewerOverlayChangeAnimation = Duration(milliseconds: 150); static const thumbnailScrollerScrollAnimation = Duration(milliseconds: 200); static const thumbnailScrollerShadeAnimation = Duration(milliseconds: 150); static const viewerVideoPlayerTransition = Duration(milliseconds: 500); @@ -67,8 +61,6 @@ class Durations { static const highlightScrollInitDelay = Duration(milliseconds: 800); static const videoOverlayHideDelay = Duration(milliseconds: 500); static const videoProgressTimerInterval = Duration(milliseconds: 300); - - // static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); static const searchDebounceDelay = Duration(milliseconds: 250); @@ -107,6 +99,11 @@ class DurationsData { final Duration staggeredAnimation; final Duration staggeredAnimationPageTarget; + // viewer animations + final Duration viewerVerticalPageScrollAnimation; + final Duration viewerOverlayAnimation; + final Duration viewerOverlayChangeAnimation; + // delays & refresh intervals final Duration staggeredAnimationDelay; @@ -114,13 +111,20 @@ class DurationsData { this.expansionTileAnimation = const Duration(milliseconds: 200), this.staggeredAnimation = const Duration(milliseconds: 375), this.staggeredAnimationPageTarget = const Duration(milliseconds: 800), + this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500), + this.viewerOverlayAnimation = const Duration(milliseconds: 200), + this.viewerOverlayChangeAnimation = const Duration(milliseconds: 150), }) : staggeredAnimationDelay = staggeredAnimation ~/ 6; factory DurationsData.noAnimation() { return DurationsData( - expansionTileAnimation: const Duration(microseconds: 1), // as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero + // as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero + expansionTileAnimation: const Duration(microseconds: 1), staggeredAnimation: Duration.zero, staggeredAnimationPageTarget: Duration.zero, + viewerVerticalPageScrollAnimation: Duration.zero, + viewerOverlayAnimation: Duration.zero, + viewerOverlayChangeAnimation: Duration.zero, ); } } diff --git a/lib/widgets/collection/grid/thumbnail.dart b/lib/widgets/collection/grid/thumbnail.dart index 6e9c51380..97d4db0f7 100644 --- a/lib/widgets/collection/grid/thumbnail.dart +++ b/lib/widgets/collection/grid/thumbnail.dart @@ -72,7 +72,7 @@ class InteractiveThumbnail extends StatelessWidget { context, TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), - pageBuilder: (c, a, sa) { + pageBuilder: (context, a, sa) { final viewerCollection = CollectionLens( source: collection.source, filters: collection.filters, diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index cca2f4edc..3e829eda0 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -14,8 +14,8 @@ class BottomGestureAreaProtector extends StatelessWidget { @override Widget build(BuildContext context) { return Selector( - selector: (c, mq) => mq.systemGestureInsets.bottom, - builder: (c, systemGestureBottom, child) { + selector: (context, mq) => mq.systemGestureInsets.bottom, + builder: (context, systemGestureBottom, child) { return Positioned( left: 0, right: 0, diff --git a/lib/widgets/common/behaviour/routes.dart b/lib/widgets/common/behaviour/routes.dart index 7b21b8e91..4e90b00cb 100644 --- a/lib/widgets/common/behaviour/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -19,7 +19,7 @@ class DirectMaterialPageRoute extends PageRouteBuilder { }) : super( settings: settings, transitionDuration: Duration.zero, - pageBuilder: (c, a, sa) => builder(c), + pageBuilder: (context, a, sa) => builder(context), ); @override diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons.dart index 37aea47a2..a4234f602 100644 --- a/lib/widgets/common/map/buttons.dart +++ b/lib/widgets/common/map/buttons.dart @@ -88,11 +88,12 @@ class MapButtonPanel extends StatelessWidget { builder: (context, bounds, child) { final degrees = bounds.rotation; final opacity = degrees == 0 ? .0 : 1.0; + final animationDuration = context.select((v) => v.viewerOverlayAnimation); return IgnorePointer( ignoring: opacity == 0, child: AnimatedOpacity( opacity: opacity, - duration: Durations.viewerOverlayAnimation, + duration: animationDuration, child: MapOverlayButton( icon: Transform( origin: iconSize.center(Offset.zero), diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index f75fd6874..d5d8ca9cb 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -70,8 +70,8 @@ class _AppDrawerState extends State { child: ListTileTheme.merge( selectedColor: Theme.of(context).colorScheme.secondary, child: Selector( - selector: (c, mq) => mq.effectiveBottomPadding, - builder: (c, mqPaddingBottom, child) { + selector: (context, mq) => mq.effectiveBottomPadding, + builder: (context, mqPaddingBottom, child) { final iconTheme = IconTheme.of(context); return SingleChildScrollView( padding: EdgeInsets.only(bottom: mqPaddingBottom), diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index d3522a026..157c39c35 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -108,7 +108,7 @@ class _MapPageContentState extends State with SingleTickerProvid _dotEntryNotifier.addListener(_updateInfoEntry); _overlayAnimationController = AnimationController( - duration: Durations.viewerOverlayAnimation, + duration: context.read().viewerOverlayAnimation, vsync: this, ); _overlayScale = CurvedAnimation( @@ -238,8 +238,8 @@ class _MapPageContentState extends State with SingleTickerProvid ), const SizedBox(height: 8), Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) => ValueListenableBuilder( + selector: (context, mq) => mq.size.width, + builder: (context, mqWidth, child) => ValueListenableBuilder( valueListenable: _regionCollectionNotifier, builder: (context, regionCollection, child) { final regionEntries = regionCollection?.sortedEntries ?? []; @@ -333,7 +333,7 @@ class _MapPageContentState extends State with SingleTickerProvid context, TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), - pageBuilder: (c, a, sa) { + pageBuilder: (context, a, sa) { return EntryViewerPage( collection: regionCollection, initialEntry: initialEntry, diff --git a/lib/widgets/viewer/embedded/embedded_data_opener.dart b/lib/widgets/viewer/embedded/embedded_data_opener.dart index a72bab817..f09a5f04d 100644 --- a/lib/widgets/viewer/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/embedded/embedded_data_opener.dart @@ -75,7 +75,7 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { context, TransparentMaterialPageRoute( settings: const RouteSettings(name: EntryViewerPage.routeName), - pageBuilder: (c, a, sa) => EntryViewerPage( + pageBuilder: (context, a, sa) => EntryViewerPage( initialEntry: tempEntry, ), ), diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 222f0cdee..85334e619 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -101,7 +101,7 @@ class _EntryViewerStackState extends State with FeedbackMixin, _horizontalPager = PageController(initialPage: _currentHorizontalPage); _verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange); _overlayAnimationController = AnimationController( - duration: Durations.viewerOverlayAnimation, + duration: context.read().viewerOverlayAnimation, vsync: this, ); _topOverlayScale = CurvedAnimation( @@ -355,8 +355,8 @@ class _EntryViewerStackState extends State with FeedbackMixin, ); child = Selector( - selector: (c, mq) => mq.size.height, - builder: (c, mqHeight, child) { + selector: (context, mq) => mq.size.height, + builder: (context, mqHeight, child) { // when orientation change, the `PageController` offset is not updated right away // and it does not trigger its listeners when it does, so we force a refresh in the next frame WidgetsBinding.instance!.addPostFrameCallback((_) => _onVerticalPageControllerChange()); @@ -412,13 +412,18 @@ class _EntryViewerStackState extends State with FeedbackMixin, ); } - Future _goToVerticalPage(int page) { - // duration & curve should feel similar to changing page by vertical fling - return _verticalPager.animateToPage( - page, - duration: Durations.viewerVerticalPageScrollAnimation, - curve: Curves.easeOutQuart, - ); + Future _goToVerticalPage(int page) async { + final animationDuration = context.read().viewerVerticalPageScrollAnimation; + if (animationDuration > Duration.zero) { + // duration & curve should feel similar to changing page by vertical fling + await _verticalPager.animateToPage( + page, + duration: animationDuration, + curve: Curves.easeOutQuart, + ); + } else { + _verticalPager.jumpToPage(page); + } } void _onVerticalPageChanged(int page) { diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart index 28142bd06..1eadb7439 100644 --- a/lib/widgets/viewer/info/info_page.dart +++ b/lib/widgets/viewer/info/info_page.dart @@ -47,8 +47,8 @@ class _InfoPageState extends State { child: NotificationListener( onNotification: _handleTopScroll, child: Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) { + selector: (context, mq) => mq.size.width, + builder: (context, mqWidth, child) { return ValueListenableBuilder( valueListenable: widget.entryNotifier, builder: (context, mainEntry, child) { @@ -109,10 +109,11 @@ class _InfoPageState extends State { } void _goToViewer() { + final animationDuration = context.read().viewerVerticalPageScrollAnimation; ShowImageNotification().dispatch(context); _scrollController.animateTo( 0, - duration: Durations.viewerVerticalPageScrollAnimation, + duration: animationDuration, curve: Curves.easeInOut, ); } diff --git a/lib/widgets/viewer/overlay/bottom/common.dart b/lib/widgets/viewer/overlay/bottom/common.dart index dfb1bc308..921cd7e95 100644 --- a/lib/widgets/viewer/overlay/bottom/common.dart +++ b/lib/widgets/viewer/overlay/bottom/common.dart @@ -82,8 +82,8 @@ class _ViewerBottomOverlayState extends State { return BlurredRect( enabled: hasEdgeContent && blurred, child: Selector>( - selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding), - builder: (c, mq, child) { + selector: (context, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding), + builder: (context, mq, child) { final mqWidth = mq.item1; final mqViewInsets = mq.item2; final mqViewPadding = mq.item3; @@ -176,12 +176,12 @@ class _BottomOverlayContent extends AnimatedWidget { child: SizedBox( width: availableWidth, child: Selector( - selector: (c, mq) => mq.orientation, - builder: (c, orientation, child) { + selector: (context, mq) => mq.orientation, + builder: (context, orientation, child) { Widget? infoColumn; if (settings.showOverlayInfo) { - infoColumn = _buildInfoColumn(orientation); + infoColumn = _buildInfoColumn(context, orientation); } if (mainEntry.isMultiPage && multiPageController != null) { @@ -205,12 +205,13 @@ class _BottomOverlayContent extends AnimatedWidget { ); } - Widget _buildInfoColumn(Orientation orientation) { + Widget _buildInfoColumn(BuildContext context, Orientation orientation) { final infoMaxWidth = availableWidth - infoPadding.horizontal; final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth; final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth; final positionTitle = _PositionTitleRow(entry: pageEntry, collectionPosition: position, multiPageController: multiPageController); final hasShootingDetails = details != null && !details!.isEmpty && settings.showOverlayShootingDetails; + final animationDuration = context.select((v) => v.viewerOverlayChangeAnimation); return Padding( padding: infoPadding, @@ -219,7 +220,7 @@ class _BottomOverlayContent extends AnimatedWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (positionTitle.isNotEmpty) positionTitle, - _buildSoloLocationRow(), + _buildSoloLocationRow(animationDuration), if (twoColumns) Padding( padding: const EdgeInsets.only(top: _interRowPadding), @@ -231,7 +232,7 @@ class _BottomOverlayContent extends AnimatedWidget { entry: pageEntry, multiPageController: multiPageController, )), - _buildDuoShootingRow(subRowWidth, hasShootingDetails), + _buildDuoShootingRow(subRowWidth, hasShootingDetails, animationDuration), ], ), ) @@ -244,15 +245,15 @@ class _BottomOverlayContent extends AnimatedWidget { multiPageController: multiPageController, ), ), - _buildSoloShootingRow(subRowWidth, hasShootingDetails), + _buildSoloShootingRow(subRowWidth, hasShootingDetails, animationDuration), ], ], ), ); } - Widget _buildSoloLocationRow() => AnimatedSwitcher( - duration: Durations.viewerOverlayChangeAnimation, + Widget _buildSoloLocationRow(Duration animationDuration) => AnimatedSwitcher( + duration: animationDuration, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, transitionBuilder: _soloTransition, @@ -264,8 +265,8 @@ class _BottomOverlayContent extends AnimatedWidget { : const SizedBox(), ); - Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher( - duration: Durations.viewerOverlayChangeAnimation, + Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails, Duration animationDuration) => AnimatedSwitcher( + duration: animationDuration, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, transitionBuilder: _soloTransition, @@ -278,8 +279,8 @@ class _BottomOverlayContent extends AnimatedWidget { : const SizedBox(), ); - Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher( - duration: Durations.viewerOverlayChangeAnimation, + Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails, Duration animationDuration) => AnimatedSwitcher( + duration: animationDuration, switchInCurve: Curves.easeInOutCubic, switchOutCurve: Curves.easeInOutCubic, transitionBuilder: (child, animation) => FadeTransition( diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 7ba77ab50..5809c2b38 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -81,8 +81,8 @@ class _VideoControlOverlayState extends State with SingleTi ); } else { child = Selector( - selector: (c, mq) => mq.size.width - mq.padding.horizontal, - builder: (c, mqWidth, child) { + selector: (context, mq) => mq.size.width - mq.padding.horizontal, + builder: (context, mqWidth, child) { final buttonWidth = OverlayButton.getSize(context); final availableCount = ((mqWidth - outerPadding * 2) / (buttonWidth + innerPadding)).floor(); final quickActions = settings.videoQuickActions.take(availableCount - 1).toList(); diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index db300afb7..378fc6747 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -45,8 +45,8 @@ class ViewerTopOverlay extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(outerPadding), child: Selector( - selector: (c, mq) => mq.size.width - mq.padding.horizontal, - builder: (c, mqWidth, child) { + selector: (context, mq) => mq.size.width - mq.padding.horizontal, + builder: (context, mqWidth, child) { final buttonWidth = OverlayButton.getSize(context); final availableCount = ((mqWidth - outerPadding * 2 - buttonWidth) / (buttonWidth + innerPadding)).floor(); diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index 95758520c..b2e0e4653 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -90,8 +90,8 @@ class _PanoramaPageState extends State { return Visibility( visible: overlayVisible, child: Selector( - selector: (c, mq) => mq.viewPadding + mq.viewInsets, - builder: (c, mqPadding, child) { + selector: (context, mq) => mq.viewPadding + mq.viewInsets, + builder: (context, mqPadding, child) { return Padding( padding: const EdgeInsets.all(8) + EdgeInsets.only(right: mqPadding.right, bottom: mqPadding.bottom), child: OverlayButton( diff --git a/lib/widgets/viewer/visual/subtitle/subtitle.dart b/lib/widgets/viewer/visual/subtitle/subtitle.dart index 280038833..6695fc51e 100644 --- a/lib/widgets/viewer/visual/subtitle/subtitle.dart +++ b/lib/widgets/viewer/visual/subtitle/subtitle.dart @@ -48,8 +48,8 @@ class VideoSubtitles extends StatelessWidget { ); return Selector( - selector: (c, mq) => mq.orientation, - builder: (c, orientation, child) { + selector: (context, mq) => mq.orientation, + builder: (context, orientation, child) { final bottom = orientation == Orientation.portrait ? .5 : .8; final viewportSize = context.read().size;