From d528913e843261264d200ba3b40263fb9ed3dbbb Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 16 Jun 2026 17:40:57 +0200 Subject: [PATCH] update media view to new mybutton design --- lib/src/providers/routing.provider.dart | 7 +- .../elements/my_icon_button.element.dart | 145 +++++ lib/src/visual/elements/my_input.element.dart | 7 +- .../entries/chat_media_entry.dart | 12 +- .../visual/views/chats/media_viewer.view.dart | 561 ++++++++++-------- .../reaction_buttons.comp.dart | 14 +- 6 files changed, 477 insertions(+), 269 deletions(-) create mode 100644 lib/src/visual/elements/my_icon_button.element.dart diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 55b3ef1e..a2c4b46f 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -85,9 +85,12 @@ final routerProvider = GoRouter( ), GoRoute( path: 'media_viewer', - builder: (context, state) { + pageBuilder: (context, state) { final group = state.extra! as Group; - return MediaViewerView(group); + return MediaViewerView.buildPage( + key: state.pageKey, + group: group, + ); }, ), GoRoute( diff --git a/lib/src/visual/elements/my_icon_button.element.dart b/lib/src/visual/elements/my_icon_button.element.dart new file mode 100644 index 00000000..8ec26b70 --- /dev/null +++ b/lib/src/visual/elements/my_icon_button.element.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +enum MyIconButtonVariant { + primary, + secondary, +} + +class MyIconButton extends StatefulWidget { + const MyIconButton({ + required this.icon, + required this.onPressed, + this.onLongPress, + this.variant = MyIconButtonVariant.primary, + super.key, + }); + + final Widget icon; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final MyIconButtonVariant variant; + + @override + State createState() => _MyIconButtonState(); +} + +class _MyIconButtonState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + lowerBound: double.negativeInfinity, + upperBound: double.infinity, + value: 0, + )..addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _onTapDown(TapDownDetails details) { + if (widget.onPressed != null || widget.onLongPress != null) { + _controller.animateTo( + 1, + duration: const Duration(milliseconds: 60), + curve: Curves.easeOut, + ); + } + } + + void _onTapUp(TapUpDetails details) { + if (widget.onPressed != null || widget.onLongPress != null) { + _bounce(); + } + } + + void _onTapCancel() { + if (widget.onPressed != null || widget.onLongPress != null) { + _bounce(); + } + } + + void _bounce() { + const spring = SpringDescription( + mass: 1, + stiffness: 400, + damping: 15, + ); + final simulation = SpringSimulation( + spring, + _controller.value, + 0, + _controller.velocity, + ); + _controller.animateWith(simulation); + } + + @override + Widget build(BuildContext context) { + final scale = 1.0 - (_controller.value * 0.02); + final isEnabled = widget.onPressed != null || widget.onLongPress != null; + final isDark = isDarkMode(context); + final disabledBgColor = isDark + ? const Color(0xFF353535) + : const Color(0xFFE0E0E0); + final disabledFgColor = isDark + ? const Color(0xFF757575) + : const Color(0xFF9E9E9E); + + late final Color bgColor; + late final Color fgColor; + + if (widget.variant == MyIconButtonVariant.primary) { + bgColor = primaryColor; + fgColor = Colors.black87; + } else { + bgColor = isDark ? Colors.grey[800]! : Colors.grey[200]!; + fgColor = isDark ? Colors.white : Colors.black87; + } + + final childButton = FilledButton( + style: FilledButton.styleFrom( + backgroundColor: bgColor, + foregroundColor: fgColor, + disabledBackgroundColor: disabledBgColor, + disabledForegroundColor: disabledFgColor, + minimumSize: const Size(72, 52), + fixedSize: const Size(72, 52), + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + elevation: 0, + ), + onPressed: isEnabled ? () {} : null, + child: widget.icon, + ); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: isEnabled ? _onTapDown : null, + onTapUp: isEnabled ? _onTapUp : null, + onTapCancel: isEnabled ? _onTapCancel : null, + onTap: widget.onPressed, + onLongPress: widget.onLongPress, + child: Transform.scale( + scale: scale, + child: AbsorbPointer( + child: childButton, + ), + ), + ); + } +} diff --git a/lib/src/visual/elements/my_input.element.dart b/lib/src/visual/elements/my_input.element.dart index 8ee00385..c79dfce9 100644 --- a/lib/src/visual/elements/my_input.element.dart +++ b/lib/src/visual/elements/my_input.element.dart @@ -17,6 +17,7 @@ class MyInput extends StatefulWidget { this.autofocus = false, this.errorText, this.obscureText = false, + this.dense = false, super.key, }); @@ -31,6 +32,7 @@ class MyInput extends StatefulWidget { final bool autofocus; final String? errorText; final bool obscureText; + final bool dense; @override State createState() => _MyInputState(); @@ -165,14 +167,15 @@ class _MyInputState extends State with SingleTickerProviderStateMixin { color: isDark ? Colors.white : Colors.black87, ), decoration: InputDecoration( + isDense: widget.dense, hintText: widget.hintText, hintStyle: TextStyle( color: inputHintColor, ), filled: true, fillColor: inputFillColor, - contentPadding: const EdgeInsets.symmetric( - vertical: 18, + contentPadding: EdgeInsets.symmetric( + vertical: widget.dense ? 14 : 18, horizontal: 24, ), border: OutlineInputBorder( diff --git a/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart b/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart index 28b5c949..f917975b 100644 --- a/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart +++ b/lib/src/visual/views/chats/chat_messages_components/entries/chat_media_entry.dart @@ -95,14 +95,10 @@ class _ChatMediaEntryState extends State { if (!mounted) return; await Navigator.push( context, - MaterialPageRoute( - builder: (context) { - return MediaViewerView( - widget.group, - initialMessage: widget.message, - ); - }, - ), + MediaViewerView.buildPage( + group: widget.group, + initialMessage: widget.message, + ).createRoute(context), ); } else if (widget.mediaService.mediaFile.downloadState == DownloadState.pending) { diff --git a/lib/src/visual/views/chats/media_viewer.view.dart b/lib/src/visual/views/chats/media_viewer.view.dart index 6b6d4559..2cafc3b2 100644 --- a/lib/src/visual/views/chats/media_viewer.view.dart +++ b/lib/src/visual/views/chats/media_viewer.view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:math'; import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; @@ -27,7 +28,8 @@ import 'package:twonly/src/services/notifications/background.notifications.dart' import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/animate_icon.comp.dart'; -import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; +import 'package:twonly/src/visual/elements/my_icon_button.element.dart'; +import 'package:twonly/src/visual/elements/my_input.element.dart'; import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; @@ -41,6 +43,30 @@ class MediaViewerView extends StatefulWidget { final Message? initialMessage; + static Page buildPage({ + required Group group, + LocalKey? key, + Message? initialMessage, + }) { + return CustomTransitionPage( + key: key, + opaque: false, + barrierColor: Colors.transparent, + transitionDuration: const Duration(milliseconds: 250), + reverseTransitionDuration: const Duration(milliseconds: 250), + child: MediaViewerView( + group, + initialMessage: initialMessage, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ); + } + @override State createState() => _MediaViewerViewState(); } @@ -81,6 +107,9 @@ class _MediaViewerViewState extends State { final HashSet _alreadyOpenedMediaIds = HashSet(); bool _isTransitioning = false; + bool _isZoomed = false; + late PageController _verticalPager; + final ValueNotifier _backdropOpacityNotifier = ValueNotifier(1); @override void initState() { @@ -91,6 +120,11 @@ class _MediaViewerViewState extends State { allMediaFiles = [widget.initialMessage!]; } + _verticalPager = PageController(initialPage: 1); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated); + }); + asyncLoadNextMedia(true); } @@ -118,9 +152,29 @@ class _MediaViewerViewState extends State { ); textMessageController.dispose(); + _verticalPager + ..removeListener(_onVerticalScrollUpdated) + ..dispose(); + _backdropOpacityNotifier.dispose(); + super.dispose(); } + void _onVerticalScrollUpdated() { + if (!_verticalPager.hasClients) return; + final page = _verticalPager.page ?? 1.0; + final linearFraction = min(1, max(0, page)).toDouble(); + _backdropOpacityNotifier.value = linearFraction * linearFraction; + } + + void _onPageSnapped(int index) { + if (index == 0) { + if (mounted) { + Navigator.pop(context); + } + } + } + void _disposeVideoController() { final listener = _videoListener; final controller = videoController; @@ -549,30 +603,18 @@ class _MediaViewerViewState extends State { if (currentMedia != null && !currentMedia!.mediaFile.requiresAuthentication && currentMedia!.mediaFile.displayLimitInMilliseconds == null) - OutlinedButton( - style: OutlinedButton.styleFrom( - iconColor: imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - foregroundColor: imageSaved - ? Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, - ), + MyIconButton( + variant: MyIconButtonVariant.secondary, onPressed: (currentMedia == null) ? null : onPressedSaveToGallery, - child: Row( - children: [ - if (imageSaving) - const SizedBox( - width: 10, - height: 10, - child: CircularProgressIndicator.adaptive(strokeWidth: 1), + icon: imageSaving + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), ) - else - imageSaved - ? const Icon(Icons.check) - : const FaIcon(FontAwesomeIcons.floppyDisk), - ], - ), + : imageSaved + ? const Icon(Icons.check) + : const FaIcon(FontAwesomeIcons.floppyDisk, size: 20), ), const SizedBox(width: 10), IconButton( @@ -614,23 +656,21 @@ class _MediaViewerViewState extends State { ), ), const SizedBox(width: 10), - IconButton.outlined( - icon: const FaIcon(FontAwesomeIcons.message), + MyIconButton( + variant: MyIconButtonVariant.secondary, onPressed: () async { displayShortReactions(); setState(() { showSendTextMessageInput = true; }); }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - ), + icon: const FaIcon( + FontAwesomeIcons.message, + size: 20, ), ), const SizedBox(width: 10), - IconButton.outlined( - icon: const FaIcon(FontAwesomeIcons.camera), + MyIconButton( onPressed: () async { nextMediaTimer?.cancel(); progressTimer?.cancel(); @@ -651,11 +691,7 @@ class _MediaViewerViewState extends State { await videoController?.play(); } }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 10, horizontal: 20), - ), - ), + icon: const FaIcon(FontAwesomeIcons.camera, size: 24), ), ], ); @@ -688,221 +724,256 @@ class _MediaViewerViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - child: Stack( - fit: StackFit.expand, + backgroundColor: Colors.transparent, + body: ValueListenableBuilder( + valueListenable: _backdropOpacityNotifier, + builder: (context, opacity, child) { + final baseColor = isDarkMode(context) ? Colors.black : Colors.white; + return ColoredBox( + color: baseColor.withValues(alpha: opacity), + child: child, + ); + }, + child: PageView( + controller: _verticalPager, + scrollDirection: Axis.vertical, + physics: _isZoomed + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + onPageChanged: _onPageSnapped, children: [ - if (_showDownloadingLoader) _loader(), - if ((currentMedia != null || videoController != null) && - (canBeSeenUntil == null || progress.value >= 0)) - GestureDetector( - onTap: onTap, - onDoubleTap: (videoController == null) ? null : onTap, - child: MediaViewSizingHelper( - bottomNavigation: bottomNavigation(), - requiredHeight: 55, - child: Stack( - children: [ - if (videoController != null) - Positioned.fill( - child: PhotoView.customChild( - initialScale: PhotoViewComputedScale.contained, - minScale: PhotoViewComputedScale.contained, - child: VideoPlayer( - videoController!, - ), - ), - ) - else if (currentMedia != null && - (currentMedia!.mediaFile.type == MediaType.image || - currentMedia!.mediaFile.type == MediaType.gif)) - Positioned.fill( - child: PhotoView( - imageProvider: FileImage( - currentMedia!.tempPath, - ), - loadingBuilder: (context, event) => _loader(), - initialScale: PhotoViewComputedScale.contained, - minScale: PhotoViewComputedScale.contained, - errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon( - Icons.broken_image_outlined, - color: Colors.white38, - size: 64, - ), - ); - }, - ), - ), - ], - ), - ), - ), - if (displayTwonlyPresent) - Positioned.fill( - child: GestureDetector( - onTap: () => loadCurrentMediaFile(showTwonly: true), - child: Column( - children: [ - Expanded( - child: Lottie.asset( - 'assets/animations/present.lottie.lottie', - ), - ), - Container( - padding: const EdgeInsets.only(bottom: 200), - child: Text(context.lang.mediaViewerTwonlyTapToOpen), - ), - ], - ), - ), - ), - Positioned( - left: 10, - top: 10, - child: Row( + const SizedBox.expand(), + SafeArea( + child: Stack( + fit: StackFit.expand, children: [ - IconButton( - icon: const Icon(Icons.close, size: 30), - color: Colors.white, - onPressed: () => Navigator.pop(context), + if (_showDownloadingLoader) _loader(), + if ((currentMedia != null || videoController != null) && + (canBeSeenUntil == null || progress.value >= 0)) + GestureDetector( + onTap: onTap, + onDoubleTap: (videoController == null) ? null : onTap, + child: MediaViewSizingHelper( + bottomNavigation: bottomNavigation(), + requiredHeight: 55, + child: Stack( + children: [ + if (videoController != null) + Positioned.fill( + child: PhotoView.customChild( + initialScale: + PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + scaleStateChangedCallback: (state) { + final zoomed = + state != PhotoViewScaleState.initial; + if (_isZoomed != zoomed) { + setState(() { + _isZoomed = zoomed; + }); + } + }, + child: VideoPlayer( + videoController!, + ), + ), + ) + else if (currentMedia != null && + (currentMedia!.mediaFile.type == + MediaType.image || + currentMedia!.mediaFile.type == + MediaType.gif)) + Positioned.fill( + child: PhotoView( + imageProvider: FileImage( + currentMedia!.tempPath, + ), + loadingBuilder: (context, event) => _loader(), + backgroundDecoration: const BoxDecoration( + color: Colors.transparent, + ), + initialScale: + PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + scaleStateChangedCallback: (state) { + final zoomed = + state != PhotoViewScaleState.initial; + if (_isZoomed != zoomed) { + setState(() { + _isZoomed = zoomed; + }); + } + }, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon( + Icons.broken_image_outlined, + color: Colors.white38, + size: 64, + ), + ); + }, + ), + ), + ], + ), + ), + ), + if (displayTwonlyPresent) + Positioned.fill( + child: GestureDetector( + onTap: () => loadCurrentMediaFile(showTwonly: true), + child: Column( + children: [ + Expanded( + child: Lottie.asset( + 'assets/animations/present.lottie.lottie', + ), + ), + Container( + padding: const EdgeInsets.only(bottom: 200), + child: Text( + context.lang.mediaViewerTwonlyTapToOpen, + ), + ), + ], + ), + ), + ), + if (currentMedia != null && + currentMedia?.mediaFile.downloadState != + DownloadState.ready) + Positioned.fill(child: _loader()), + if (canBeSeenUntil != null || progress.value >= 0) + Positioned( + right: 20, + top: 27, + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, child) { + return CircularProgressIndicator( + value: value, + strokeWidth: 2, + ); + }, + ), + ), + ], + ), + ), + Positioned( + top: 10, + left: showSendTextMessageInput ? 0 : null, + right: showSendTextMessageInput ? 0 : 15, + child: Text( + _currentMediaSender, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: showSendTextMessageInput ? 24 : 14, + fontWeight: FontWeight.bold, + color: showSendTextMessageInput + ? null + : const Color.fromARGB(255, 126, 126, 126), + shadows: const [ + Shadow( + color: Color.fromARGB(122, 0, 0, 0), + blurRadius: 5, + ), + ], + ), + ), + ), + if (showSendTextMessageInput) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + color: context.color.surface, + padding: const EdgeInsets.only( + bottom: 10, + left: 20, + right: 20, + top: 10, + ), + child: Row( + children: [ + Expanded( + child: MyInput( + dense: true, + autofocus: true, + controller: textMessageController, + hintText: context.lang.chatListDetailInput, + onChanged: (value) { + setState(() {}); + }, + onSubmitted: (value) { + setState(() { + showSendTextMessageInput = false; + showShortReactions = false; + }); + }, + ), + ), + const SizedBox(width: 10), + MyIconButton( + icon: const FaIcon( + FontAwesomeIcons.solidPaperPlane, + size: 20, + ), + onPressed: () async { + if (textMessageController.text.isNotEmpty) { + await insertAndSendTextMessage( + widget.group.groupId, + textMessageController.text, + currentMessage!.messageId, + ); + textMessageController.clear(); + } + setState(() { + showSendTextMessageInput = false; + showShortReactions = false; + }); + }, + ), + ], + ), + ), + ), + if (currentMessage != null) + AdditionalMessageContent(currentMessage!), + if (currentMedia != null) + ReactionButtons( + show: showShortReactions, + textInputFocused: showSendTextMessageInput, + mediaViewerDistanceFromBottom: + mediaViewerDistanceFromBottom, + groupId: widget.group.groupId, + messageId: currentMessage!.messageId, + emojiKey: emojiKey, + hide: () { + setState(() { + showShortReactions = false; + showSendTextMessageInput = false; + }); + }, + ), + Positioned.fill( + child: EmojiFloatWidget(key: emojiKey), ), ], ), ), - if (currentMedia != null && - currentMedia?.mediaFile.downloadState != DownloadState.ready) - Positioned.fill(child: _loader()), - if (canBeSeenUntil != null || progress.value >= 0) - Positioned( - right: 20, - top: 27, - child: Row( - children: [ - SizedBox( - width: 20, - height: 20, - child: ValueListenableBuilder( - valueListenable: progress, - builder: (context, value, child) { - return CircularProgressIndicator( - value: value, - strokeWidth: 2, - ); - }, - ), - ), - ], - ), - ), - Positioned( - top: 10, - left: showSendTextMessageInput ? 0 : null, - right: showSendTextMessageInput ? 0 : 15, - child: Text( - _currentMediaSender, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: showSendTextMessageInput ? 24 : 14, - fontWeight: FontWeight.bold, - color: showSendTextMessageInput - ? null - : const Color.fromARGB(255, 126, 126, 126), - shadows: const [ - Shadow( - color: Color.fromARGB(122, 0, 0, 0), - blurRadius: 5, - ), - ], - ), - ), - ), - if (showSendTextMessageInput) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - color: context.color.surface, - padding: const EdgeInsets.only( - bottom: 10, - left: 20, - right: 20, - top: 10, - ), - child: Row( - children: [ - IconButton( - icon: const FaIcon(FontAwesomeIcons.xmark), - onPressed: () { - setState(() { - showShortReactions = false; - showSendTextMessageInput = false; - }); - }, - ), - Expanded( - child: TextField( - autofocus: true, - controller: textMessageController, - textCapitalization: TextCapitalization.sentences, - onChanged: (value) { - setState(() {}); - }, - onEditingComplete: () { - setState(() { - showSendTextMessageInput = false; - showShortReactions = false; - }); - }, - decoration: inputTextMessageDeco( - context, - context.lang.chatListDetailInput, - ), - ), - ), - IconButton( - icon: const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () async { - if (textMessageController.text.isNotEmpty) { - await insertAndSendTextMessage( - widget.group.groupId, - textMessageController.text, - currentMessage!.messageId, - ); - textMessageController.clear(); - } - setState(() { - showSendTextMessageInput = false; - showShortReactions = false; - }); - }, - ), - ], - ), - ), - ), - if (currentMessage != null) - AdditionalMessageContent(currentMessage!), - if (currentMedia != null) - ReactionButtons( - show: showShortReactions, - textInputFocused: showSendTextMessageInput, - mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom, - groupId: widget.group.groupId, - messageId: currentMessage!.messageId, - emojiKey: emojiKey, - hide: () { - setState(() { - showShortReactions = false; - showSendTextMessageInput = false; - }); - }, - ), - Positioned.fill( - child: EmojiFloatWidget(key: emojiKey), - ), ], ), ), diff --git a/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart b/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart index 1bdffc4b..ac7f34e8 100644 --- a/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart +++ b/lib/src/visual/views/chats/media_viewer_components/reaction_buttons.comp.dart @@ -56,17 +56,7 @@ class _ReactionButtonsState extends State { void didUpdateWidget(ReactionButtons oldWidget) { super.didUpdateWidget(oldWidget); if (widget.show != oldWidget.show) { - if (widget.show) { - _renderAnimations = true; - } else { - Future.delayed(const Duration(milliseconds: 150), () { - if (mounted && !widget.show) { - setState(() { - _renderAnimations = false; - }); - } - }); - } + _renderAnimations = widget.show; } } @@ -98,7 +88,7 @@ class _ReactionButtonsState extends State { ignoring: !widget.show, child: AnimatedOpacity( opacity: widget.show ? 1.0 : 0.0, // Fade in/out - duration: const Duration(milliseconds: 150), + duration: Duration(milliseconds: widget.show ? 150 : 50), child: Container( color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, padding: const EdgeInsets.symmetric(vertical: 32),