From 535d4c0d003a159e50b442fdc0e452337a09e558 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 13 Sep 2024 21:30:35 +0200 Subject: [PATCH] #1177 slideshow: fixed bottom overlay layout on inset transition --- lib/widgets/viewer/entry_viewer_stack.dart | 18 +-- lib/widgets/viewer/overlay/bottom.dart | 46 +++--- .../viewer/overlay/slideshow_buttons.dart | 152 ++++++++++++++---- 3 files changed, 144 insertions(+), 72 deletions(-) diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 9cdd65722..3a09636a5 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -412,17 +412,17 @@ class _EntryViewerStackState extends State with EntryViewContr } Widget _buildSlideshowBottomOverlay(Size availableSize) { - return SizedBox.fromSize( - size: availableSize, + return TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), child: Align( alignment: AlignmentDirectional.bottomEnd, - child: TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: SlideshowButtons( - animationController: _overlayAnimationController, - ), + child: SlideshowBottomOverlay( + animationController: _overlayAnimationController, + availableSize: availableSize, + viewInsets: _frozenViewInsets, + viewPadding: _frozenViewPadding, ), ), ); diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 06f00808b..92523895e 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -21,7 +21,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -class ViewerBottomOverlay extends StatefulWidget { +class ViewerBottomOverlay extends StatelessWidget { final List entries; final int index; final CollectionLens? collection; @@ -33,6 +33,10 @@ class ViewerBottomOverlay extends StatefulWidget { // always keep action buttons in the lower right corner, even with RTL locales static const actionsDirection = TextDirection.ltr; + AvesEntry? get entry { + return index < entries.length ? entries[index] : null; + } + const ViewerBottomOverlay({ super.key, required this.entries, @@ -45,27 +49,6 @@ class ViewerBottomOverlay extends StatefulWidget { required this.multiPageController, }); - @override - State createState() => _ViewerBottomOverlayState(); - - static double actionSafeHeight(BuildContext context) { - final mqPaddingBottom = context.select((mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom)); - final buttonHeight = ViewerButtons.preferredHeight(context); - final thumbnailHeight = (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0); - return mqPaddingBottom + buttonHeight + thumbnailHeight; - } -} - -class _ViewerBottomOverlayState extends State { - List get entries => widget.entries; - - AvesEntry? get entry { - final index = widget.index; - return index < entries.length ? entries[index] : null; - } - - MultiPageController? get multiPageController => widget.multiPageController; - @override Widget build(BuildContext context) { final mainEntry = entry; @@ -73,15 +56,15 @@ class _ViewerBottomOverlayState extends State { Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent( entries: entries, - index: widget.index, + index: index, mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, - collection: widget.collection, - availableSize: widget.availableSize, - viewInsets: widget.viewInsets, - viewPadding: widget.viewPadding, + collection: collection, + availableSize: availableSize, + viewInsets: viewInsets, + viewPadding: viewPadding, multiPageController: multiPageController, - animationController: widget.animationController, + animationController: animationController, ); Widget child = multiPageController != null @@ -102,6 +85,13 @@ class _ViewerBottomOverlayState extends State { child: child, ); } + + static double actionSafeHeight(BuildContext context) { + final mqPaddingBottom = context.select((mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom)); + final buttonHeight = ViewerButtons.preferredHeight(context); + final thumbnailHeight = (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0); + return mqPaddingBottom + buttonHeight + thumbnailHeight; + } } class _BottomOverlayContent extends StatefulWidget { diff --git a/lib/widgets/viewer/overlay/slideshow_buttons.dart b/lib/widgets/viewer/overlay/slideshow_buttons.dart index a34af9a6c..6d4c7b19a 100644 --- a/lib/widgets/viewer/overlay/slideshow_buttons.dart +++ b/lib/widgets/viewer/overlay/slideshow_buttons.dart @@ -1,21 +1,64 @@ +import 'dart:math'; + import 'package:aves/model/settings/settings.dart'; import 'package:aves/view/view.dart'; +import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/controls/intents.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; +import 'package:aves/widgets/viewer/overlay/bottom.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart'; import 'package:aves/widgets/viewer/slideshow_page.dart'; import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class SlideshowBottomOverlay extends StatelessWidget { + final AnimationController animationController; + final Size availableSize; + final EdgeInsets? viewInsets, viewPadding; + + const SlideshowBottomOverlay({ + super.key, + required this.animationController, + required this.availableSize, + this.viewInsets, + this.viewPadding, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom), + builder: (context, mqPaddingBottom, child) { + return Padding( + padding: EdgeInsets.only(bottom: mqPaddingBottom), + child: child, + ); + }, + child: SlideshowButtons( + availableSize: availableSize, + viewInsets: viewInsets, + viewPadding: viewPadding, + animationController: animationController, + ), + ); + } +} class SlideshowButtons extends StatefulWidget { + final Size availableSize; + final EdgeInsets? viewInsets, viewPadding; final AnimationController animationController; const SlideshowButtons({ super.key, + required this.availableSize, + required this.viewInsets, + required this.viewPadding, required this.animationController, }); @@ -70,7 +113,8 @@ class _SlideshowButtonsState extends State { @override Widget build(BuildContext context) { - return FocusableActionDetector( + final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); + final viewerButtonRow = FocusableActionDetector( focusNode: _buttonRowFocusScopeNode, shortcuts: settings.useTvLayout ? const { @@ -80,40 +124,78 @@ class _SlideshowButtonsState extends State { actions: { TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)), }, - child: settings.useTvLayout - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: _actions.map((action) { - return CaptionedButton( - scale: _buttonScale, - icon: action.getIcon(), - caption: action.getText(context), - onPressed: () => _onAction(context, action), - ); - }).toList(), - ) - : SafeArea( - child: Padding( - padding: const EdgeInsets.only(left: _padding / 2, right: _padding / 2, bottom: _padding), - child: Row( - mainAxisSize: MainAxisSize.min, - children: _actions - .map((action) => Padding( - padding: const EdgeInsets.symmetric(horizontal: _padding / 2), - child: OverlayButton( - scale: _buttonScale, - child: IconButton( - icon: action.getIcon(), - onPressed: () => _onAction(context, action), - tooltip: action.getText(context), - ), - ), - )) - .toList(), - ), - ), - ), + child: SafeArea( + top: false, + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: _buildButtons(context), + ), + ); + + final availableWidth = widget.availableSize.width; + return SizedBox( + width: availableWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + viewerButtonRow, + ], + ), + ); + } + + Widget _buildButtons(BuildContext context) { + if (settings.useTvLayout) { + return _buildTvButtonRowContent(context); + } + + return SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.only(left: _padding / 2, right: _padding / 2, bottom: _padding), + child: Row( + textDirection: ViewerBottomOverlay.actionsDirection, + children: [ + const Spacer(), + ..._actions.map((action) => _buildOverlayButton(context, action)), + ], + ), + ), + ); + } + + Widget _buildOverlayButton(BuildContext context, SlideshowAction action) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: _padding / 2), + child: OverlayButton( + scale: _buttonScale, + child: IconButton( + icon: action.getIcon(), + onPressed: () => _onAction(context, action), + tooltip: action.getText(context), + ), + ), + ); + } + + Widget _buildTvButtonRowContent(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + textDirection: ViewerBottomOverlay.actionsDirection, + children: _actions.map((action) { + return CaptionedButton( + scale: _buttonScale, + icon: action.getIcon(), + caption: action.getText(context), + onPressed: () => _onAction(context, action), + ); + }).toList(), ); }