update media view to new mybutton design
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-06-16 17:40:57 +02:00
parent f22e9086ed
commit d528913e84
6 changed files with 477 additions and 269 deletions

View file

@ -85,9 +85,12 @@ final routerProvider = GoRouter(
), ),
GoRoute( GoRoute(
path: 'media_viewer', path: 'media_viewer',
builder: (context, state) { pageBuilder: (context, state) {
final group = state.extra! as Group; final group = state.extra! as Group;
return MediaViewerView(group); return MediaViewerView.buildPage(
key: state.pageKey,
group: group,
);
}, },
), ),
GoRoute( GoRoute(

View file

@ -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<MyIconButton> createState() => _MyIconButtonState();
}
class _MyIconButtonState extends State<MyIconButton>
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,
),
),
);
}
}

View file

@ -17,6 +17,7 @@ class MyInput extends StatefulWidget {
this.autofocus = false, this.autofocus = false,
this.errorText, this.errorText,
this.obscureText = false, this.obscureText = false,
this.dense = false,
super.key, super.key,
}); });
@ -31,6 +32,7 @@ class MyInput extends StatefulWidget {
final bool autofocus; final bool autofocus;
final String? errorText; final String? errorText;
final bool obscureText; final bool obscureText;
final bool dense;
@override @override
State<MyInput> createState() => _MyInputState(); State<MyInput> createState() => _MyInputState();
@ -165,14 +167,15 @@ class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
color: isDark ? Colors.white : Colors.black87, color: isDark ? Colors.white : Colors.black87,
), ),
decoration: InputDecoration( decoration: InputDecoration(
isDense: widget.dense,
hintText: widget.hintText, hintText: widget.hintText,
hintStyle: TextStyle( hintStyle: TextStyle(
color: inputHintColor, color: inputHintColor,
), ),
filled: true, filled: true,
fillColor: inputFillColor, fillColor: inputFillColor,
contentPadding: const EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
vertical: 18, vertical: widget.dense ? 14 : 18,
horizontal: 24, horizontal: 24,
), ),
border: OutlineInputBorder( border: OutlineInputBorder(

View file

@ -95,14 +95,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
if (!mounted) return; if (!mounted) return;
await Navigator.push( await Navigator.push(
context, context,
MaterialPageRoute( MediaViewerView.buildPage<void>(
builder: (context) { group: widget.group,
return MediaViewerView( initialMessage: widget.message,
widget.group, ).createRoute(context),
initialMessage: widget.message,
);
},
),
); );
} else if (widget.mediaService.mediaFile.downloadState == } else if (widget.mediaService.mediaFile.downloadState ==
DownloadState.pending) { DownloadState.pending) {

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:math';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; 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/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/animate_icon.comp.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/helpers/media_view_sizing.helper.dart';
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart'; import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
@ -41,6 +43,30 @@ class MediaViewerView extends StatefulWidget {
final Message? initialMessage; final Message? initialMessage;
static Page<T> buildPage<T>({
required Group group,
LocalKey? key,
Message? initialMessage,
}) {
return CustomTransitionPage<T>(
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 @override
State<MediaViewerView> createState() => _MediaViewerViewState(); State<MediaViewerView> createState() => _MediaViewerViewState();
} }
@ -81,6 +107,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final HashSet<String> _alreadyOpenedMediaIds = HashSet(); final HashSet<String> _alreadyOpenedMediaIds = HashSet();
bool _isTransitioning = false; bool _isTransitioning = false;
bool _isZoomed = false;
late PageController _verticalPager;
final ValueNotifier<double> _backdropOpacityNotifier = ValueNotifier(1);
@override @override
void initState() { void initState() {
@ -91,6 +120,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
allMediaFiles = [widget.initialMessage!]; allMediaFiles = [widget.initialMessage!];
} }
_verticalPager = PageController(initialPage: 1);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated);
});
asyncLoadNextMedia(true); asyncLoadNextMedia(true);
} }
@ -118,9 +152,29 @@ class _MediaViewerViewState extends State<MediaViewerView> {
); );
textMessageController.dispose(); textMessageController.dispose();
_verticalPager
..removeListener(_onVerticalScrollUpdated)
..dispose();
_backdropOpacityNotifier.dispose();
super.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() { void _disposeVideoController() {
final listener = _videoListener; final listener = _videoListener;
final controller = videoController; final controller = videoController;
@ -549,30 +603,18 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (currentMedia != null && if (currentMedia != null &&
!currentMedia!.mediaFile.requiresAuthentication && !currentMedia!.mediaFile.requiresAuthentication &&
currentMedia!.mediaFile.displayLimitInMilliseconds == null) currentMedia!.mediaFile.displayLimitInMilliseconds == null)
OutlinedButton( MyIconButton(
style: OutlinedButton.styleFrom( variant: MyIconButtonVariant.secondary,
iconColor: imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
foregroundColor: imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
),
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery, onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
child: Row( icon: imageSaving
children: [ ? const SizedBox(
if (imageSaving) width: 16,
const SizedBox( height: 16,
width: 10, child: CircularProgressIndicator.adaptive(strokeWidth: 2),
height: 10,
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
) )
else : imageSaved
imageSaved ? const Icon(Icons.check)
? const Icon(Icons.check) : const FaIcon(FontAwesomeIcons.floppyDisk, size: 20),
: const FaIcon(FontAwesomeIcons.floppyDisk),
],
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton( IconButton(
@ -614,23 +656,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton.outlined( MyIconButton(
icon: const FaIcon(FontAwesomeIcons.message), variant: MyIconButtonVariant.secondary,
onPressed: () async { onPressed: () async {
displayShortReactions(); displayShortReactions();
setState(() { setState(() {
showSendTextMessageInput = true; showSendTextMessageInput = true;
}); });
}, },
style: ButtonStyle( icon: const FaIcon(
padding: WidgetStateProperty.all<EdgeInsets>( FontAwesomeIcons.message,
const EdgeInsets.symmetric(vertical: 10, horizontal: 20), size: 20,
),
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
IconButton.outlined( MyIconButton(
icon: const FaIcon(FontAwesomeIcons.camera),
onPressed: () async { onPressed: () async {
nextMediaTimer?.cancel(); nextMediaTimer?.cancel();
progressTimer?.cancel(); progressTimer?.cancel();
@ -651,11 +691,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
await videoController?.play(); await videoController?.play();
} }
}, },
style: ButtonStyle( icon: const FaIcon(FontAwesomeIcons.camera, size: 24),
padding: WidgetStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
), ),
], ],
); );
@ -688,221 +724,256 @@ class _MediaViewerViewState extends State<MediaViewerView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( backgroundColor: Colors.transparent,
child: Stack( body: ValueListenableBuilder<double>(
fit: StackFit.expand, 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: [ children: [
if (_showDownloadingLoader) _loader(), const SizedBox.expand(),
if ((currentMedia != null || videoController != null) && SafeArea(
(canBeSeenUntil == null || progress.value >= 0)) child: Stack(
GestureDetector( fit: StackFit.expand,
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(
children: [ children: [
IconButton( if (_showDownloadingLoader) _loader(),
icon: const Icon(Icons.close, size: 30), if ((currentMedia != null || videoController != null) &&
color: Colors.white, (canBeSeenUntil == null || progress.value >= 0))
onPressed: () => Navigator.pop(context), 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<double>(
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<double>(
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),
),
], ],
), ),
), ),

View file

@ -56,17 +56,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
void didUpdateWidget(ReactionButtons oldWidget) { void didUpdateWidget(ReactionButtons oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.show != oldWidget.show) { if (widget.show != oldWidget.show) {
if (widget.show) { _renderAnimations = widget.show;
_renderAnimations = true;
} else {
Future.delayed(const Duration(milliseconds: 150), () {
if (mounted && !widget.show) {
setState(() {
_renderAnimations = false;
});
}
});
}
} }
} }
@ -98,7 +88,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
ignoring: !widget.show, ignoring: !widget.show,
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: widget.show ? 1.0 : 0.0, // Fade in/out opacity: widget.show ? 1.0 : 0.0, // Fade in/out
duration: const Duration(milliseconds: 150), duration: Duration(milliseconds: widget.show ? 150 : 50),
child: Container( child: Container(
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent, color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 32), padding: const EdgeInsets.symmetric(vertical: 32),