quiz redirecionado aos videos | redesing da notificação

This commit is contained in:
Carlos Correia
2026-07-10 13:06:40 +01:00
parent 7ddc76d393
commit 30068d1501
11 changed files with 323 additions and 142 deletions

View File

@@ -2,12 +2,15 @@ import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lottie/lottie.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'main.dart' show supabase;
import 'widgets/app_gradients.dart';
import 'widgets/entrance.dart';
import 'widgets/name_input_formatter.dart';
import 'widgets/pill_snackbar.dart';
import 'widgets/tap_bounce.dart';
const Color _teal = Color(0xFF2F9E94);
@@ -117,32 +120,22 @@ class _HomeScreenState extends State<HomeScreen> {
}
} on _AccountNotFoundException {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
),
),
showPillSnackBar(
context,
'Esta conta não existe mais. Verifique o email ou crie uma nova conta.',
);
} on AuthException catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(_friendlyAuthError(e))));
showPillSnackBar(context, _friendlyAuthError(e));
} on TimeoutException {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Tempo esgotado. Verifique sua conexão e tente novamente.',
),
),
showPillSnackBar(
context,
'Tempo esgotado. Verifique sua conexão e tente novamente.',
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro: $e')));
showPillSnackBar(context, 'Erro: $e');
} finally {
if (mounted) setState(() => _loading = false);
}
@@ -403,6 +396,8 @@ class _AuthForm extends StatelessWidget {
hintText: 'Digite seu nome',
icon: Icons.person_outline_rounded,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
inputFormatters: [CapitalizeFirstLetterFormatter()],
validator: (v) {
final value = (v ?? '').trim();
if (value.isEmpty) return 'Informe seu nome';
@@ -518,6 +513,8 @@ class _AuthTextField extends StatelessWidget {
this.obscureText = false,
this.keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.inputFormatters,
});
final TextEditingController controller;
@@ -527,6 +524,8 @@ class _AuthTextField extends StatelessWidget {
final bool obscureText;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final TextCapitalization textCapitalization;
final List<TextInputFormatter>? inputFormatters;
@override
Widget build(BuildContext context) {
@@ -547,6 +546,8 @@ class _AuthTextField extends StatelessWidget {
obscureText: obscureText,
keyboardType: keyboardType,
textInputAction: textInputAction,
textCapitalization: textCapitalization,
inputFormatters: inputFormatters,
validator: validator,
style: const TextStyle(fontWeight: FontWeight.w700),
decoration: InputDecoration(

View File

@@ -19,6 +19,8 @@ import 'widgets/animated_nav_icon.dart';
import 'widgets/app_dialogs.dart';
import 'widgets/app_gradients.dart';
import 'widgets/entrance.dart';
import 'widgets/name_input_formatter.dart';
import 'widgets/pill_snackbar.dart';
import 'widgets/tap_bounce.dart';
/// Nomes só podem ter letras (incluindo acentuadas) e espaços — sem números.
@@ -806,18 +808,20 @@ class _InicioTab extends StatelessWidget {
await _requireFirstChild(context, uid);
return;
}
final messenger = ScaffoldMessenger.of(context);
if (!(await BrushingPrefs.canLogMore(scope))) {
messenger.showSnackBar(
const SnackBar(
content: Text('Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!'),
),
);
if (context.mounted) {
showPillSnackBar(
context,
'Já registou as ${BrushingPrefs.maxPerDay} escovagens de hoje!',
);
}
return;
}
await BrushingPrefs.logToday(scope);
await state?.refreshStats();
messenger.showSnackBar(const SnackBar(content: Text('Escovagem registada!')));
if (context.mounted) {
showPillSnackBar(context, 'Escovagem registada!');
}
}
}
@@ -1009,9 +1013,7 @@ Future<Map<String, dynamic>?> _createChildViaSheet(
return inserted;
} catch (e) {
if (!context.mounted) return null;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro ao adicionar criança: $e')));
showPillSnackBar(context, 'Erro ao adicionar criança: $e');
return null;
}
}
@@ -1550,9 +1552,7 @@ class _PerfilTabState extends State<_PerfilTab> {
}
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro ao enviar foto: $e')));
showPillSnackBar(context, 'Erro ao enviar foto: $e');
} finally {
if (mounted) setState(() => _updatingPhoto = false);
}
@@ -1563,7 +1563,6 @@ class _PerfilTabState extends State<_PerfilTab> {
required String childId,
required String childName,
}) async {
final messenger = ScaffoldMessenger.of(context);
final confirmed = await showConfirmDialog(
context,
title: 'Remover criança',
@@ -1589,9 +1588,11 @@ class _PerfilTabState extends State<_PerfilTab> {
widget.onChildSelected(0, null, null);
await _loadPerfilData();
messenger.showSnackBar(const SnackBar(content: Text('Criança removida')));
if (context.mounted) showPillSnackBar(context, 'Criança removida');
} catch (e) {
messenger.showSnackBar(SnackBar(content: Text('Erro ao remover: $e')));
if (context.mounted) {
showPillSnackBar(context, 'Erro ao remover: $e');
}
}
}
@@ -1666,7 +1667,6 @@ class _PerfilTabState extends State<_PerfilTab> {
Future<void> _addAnotherChild(BuildContext context, String uid) async {
if (_addingChild) return;
final messenger = ScaffoldMessenger.of(context);
final result = await showModalBottomSheet<Map<String, dynamic>?>(
context: context,
isScrollControlled: true,
@@ -1693,9 +1693,7 @@ class _PerfilTabState extends State<_PerfilTab> {
if (!mounted) return;
await _loadPerfilData();
messenger.showSnackBar(
const SnackBar(content: Text('Criança adicionada')),
);
if (context.mounted) showPillSnackBar(context, 'Criança adicionada');
if (mounted) {
setState(() => _addingChild = false);
@@ -1721,15 +1719,11 @@ class _PerfilTabState extends State<_PerfilTab> {
await _addAnotherChild(context, uid);
}
} on TimeoutException {
if (!mounted) return;
messenger.showSnackBar(
const SnackBar(
content: Text('Tempo esgotado ao adicionar. Tente novamente.'),
),
);
if (!mounted || !context.mounted) return;
showPillSnackBar(context, 'Tempo esgotado ao adicionar. Tente novamente.');
} catch (e) {
if (!mounted) return;
messenger.showSnackBar(SnackBar(content: Text('Erro ao adicionar: $e')));
if (!mounted || !context.mounted) return;
showPillSnackBar(context, 'Erro ao adicionar: $e');
} finally {
if (mounted) setState(() => _addingChild = false);
}
@@ -2220,6 +2214,7 @@ class _AddChildSheetState extends State<_AddChildSheet> {
helpText: 'Data de nascimento',
cancelText: 'Cancelar',
confirmText: 'Confirmar',
locale: const Locale('pt', 'PT'),
);
if (picked == null) return;
setState(() {
@@ -2278,6 +2273,8 @@ class _AddChildSheetState extends State<_AddChildSheet> {
TextFormField(
controller: _nameController,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
inputFormatters: [CapitalizeFirstLetterFormatter()],
decoration: const InputDecoration(
labelText: 'Nome da criança',
),

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:async';
@@ -42,6 +43,13 @@ class MyApp extends StatelessWidget {
scaffoldBackgroundColor: const Color(0xFFFFE2EF),
useMaterial3: true,
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('pt', 'PT'), Locale('pt', 'BR')],
locale: const Locale('pt', 'PT'),
home: const DebugLaunchGate(),
);
}

View File

@@ -37,6 +37,7 @@ class Quiz1Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 6,
),
],
currentScore: currentScore,
@@ -83,6 +84,7 @@ class Quiz2Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 10,
),
],
currentScore: currentScore,
@@ -129,6 +131,7 @@ class Quiz3Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 8,
),
],
currentScore: currentScore,
@@ -175,6 +178,7 @@ class Quiz4Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 1,
),
],
currentScore: currentScore,
@@ -223,6 +227,7 @@ class Quiz5Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 7,
),
],
currentScore: currentScore,
@@ -264,12 +269,6 @@ class Quiz6Screen extends StatelessWidget {
weight: 1,
value: 'nao',
),
QuizAnswer(
title: 'Não sei',
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
@@ -315,6 +314,7 @@ class Quiz7Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 2,
),
],
currentScore: currentScore,
@@ -361,6 +361,7 @@ class Quiz8Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 9,
),
],
currentScore: currentScore,
@@ -407,6 +408,7 @@ class Quiz9Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 3,
),
],
currentScore: currentScore,
@@ -453,6 +455,7 @@ class Quiz10Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 4,
),
],
currentScore: currentScore,
@@ -500,6 +503,7 @@ class Quiz11Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 5,
),
],
currentScore: currentScore,
@@ -541,12 +545,6 @@ class Quiz12Screen extends StatelessWidget {
weight: 1,
value: 'nao',
),
QuizAnswer(
title: 'Não sei',
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
@@ -587,12 +585,6 @@ class Quiz13Screen extends StatelessWidget {
weight: 1,
value: 'nao',
),
QuizAnswer(
title: 'Não sei',
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
@@ -633,12 +625,6 @@ class Quiz14Screen extends StatelessWidget {
weight: 1,
value: 'nao',
),
QuizAnswer(
title: 'Não sei',
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
@@ -684,6 +670,7 @@ class Quiz15Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 11,
),
],
currentScore: currentScore,
@@ -730,6 +717,7 @@ class Quiz16Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 12,
),
],
currentScore: currentScore,
@@ -776,6 +764,7 @@ class Quiz17Screen extends StatelessWidget {
description: 'Não tenho a certeza',
weight: 1,
value: 'nao_sei',
helpVideoId: 13,
),
],
currentScore: currentScore,

View File

@@ -20,6 +20,7 @@ class QuizAnswer {
required this.weight,
this.imagePath,
this.value,
this.helpVideoId,
});
final String title;
@@ -27,6 +28,11 @@ class QuizAnswer {
final int weight;
final String? imagePath;
final String? value;
/// Quando definido (normalmente só na resposta "Não sei"), identifica um
/// vídeo em [videoList] que ajuda a responder a esta pergunta — mostrado
/// como um botão que expande quando esta resposta é selecionada.
final int? helpVideoId;
}
class QuizQuestionScreen extends StatefulWidget {
@@ -1013,6 +1019,15 @@ class _QuizAnswerPill extends StatelessWidget {
bool get _isYes => (answer.value ?? '').trim().toLowerCase() == 'sim';
bool get _isNo => (answer.value ?? '').trim().toLowerCase() == 'nao';
VideoData? get _helpVideo {
final id = answer.helpVideoId;
if (id == null) return null;
for (final v in videoList) {
if (v.id == id) return v;
}
return null;
}
@override
Widget build(BuildContext context) {
final accent = _isYes
@@ -1023,6 +1038,8 @@ class _QuizAnswerPill extends StatelessWidget {
final borderColor = selected
? const Color(0xFF2F9E94)
: Colors.black.withValues(alpha: 0.10);
final helpVideo = _helpVideo;
final showHelp = selected && helpVideo != null;
return TapBounce(
scale: 0.97,
@@ -1033,7 +1050,7 @@ class _QuizAnswerPill extends StatelessWidget {
color: selected
? Colors.white.withValues(alpha: 0.92)
: Colors.white.withValues(alpha: 0.70),
borderRadius: BorderRadius.circular(999),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: borderColor, width: selected ? 1.4 : 1.0),
boxShadow: [
BoxShadow(
@@ -1045,71 +1062,138 @@ class _QuizAnswerPill extends StatelessWidget {
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: onTap,
splashFactory: InkSparkle.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
child: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: accent,
shape: BoxShape.circle,
),
child: Icon(
_isYes
? Icons.check_rounded
: _isNo
? Icons.close_rounded
: Icons.help_outline_rounded,
color: Colors.white,
size: 17,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
InkWell(
borderRadius: BorderRadius.circular(22),
onTap: onTap,
splashFactory: InkSparkle.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
const SizedBox(width: 12),
Expanded(
child: Text(
answer.title,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
color: Color(0xFF2F9E94),
child: Row(
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: accent,
shape: BoxShape.circle,
),
child: Icon(
_isYes
? Icons.check_rounded
: _isNo
? Icons.close_rounded
: Icons.help_outline_rounded,
color: Colors.white,
size: 17,
),
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 220),
width: 22,
height: 22,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected
? const Color(0xFF2F9E94)
: Colors.transparent,
border: Border.all(
color: selected
? const Color(0xFF2F9E94)
: Colors.black.withValues(alpha: 0.25),
width: 1.6,
const SizedBox(width: 12),
Expanded(
child: Text(
answer.title,
style: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15,
color: Color(0xFF2F9E94),
),
),
),
),
child: selected
? const Icon(
Icons.check_rounded,
size: 14,
color: Colors.white,
)
: null,
AnimatedContainer(
duration: const Duration(milliseconds: 220),
width: 22,
height: 22,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected
? const Color(0xFF2F9E94)
: Colors.transparent,
border: Border.all(
color: selected
? const Color(0xFF2F9E94)
: Colors.black.withValues(alpha: 0.25),
width: 1.6,
),
),
child: selected
? const Icon(
Icons.check_rounded,
size: 14,
color: Colors.white,
)
: null,
),
],
),
],
),
),
AnimatedSize(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
alignment: Alignment.topCenter,
child: !showHelp
? const SizedBox.shrink()
: Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
child: _HelpVideoButton(video: helpVideo),
),
),
],
),
),
),
);
}
}
/// Botão que aparece quando a resposta "Não sei" é selecionada, sugerindo o
/// episódio que pode ajudar a esclarecer a dúvida antes de responder.
class _HelpVideoButton extends StatelessWidget {
const _HelpVideoButton({required this.video});
final VideoData video;
@override
Widget build(BuildContext context) {
return TapBounce(
scale: 0.97,
child: Material(
color: const Color(0xFF2F9E94).withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () => showVideoPlayerDialog(context, video),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
const Icon(
Icons.play_circle_fill_rounded,
color: Color(0xFF2F9E94),
size: 22,
),
const SizedBox(width: 10),
Expanded(
child: Text(
'Não tem a certeza? Veja o "${video.title}" para ajudar a responder',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 12.5,
color: Color(0xFF2F9E94),
),
),
),
const Icon(
Icons.chevron_right_rounded,
color: Color(0xFF2F9E94),
size: 20,
),
],
),
),
),

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../main.dart' show supabase;
import '../widgets/app_dialogs.dart';
import '../widgets/entrance.dart';
import '../widgets/pill_snackbar.dart';
import '../widgets/tap_bounce.dart';
import 'terms_screen.dart';
@@ -28,7 +29,6 @@ class _SettingsBodyState extends State<SettingsBody> {
}
Future<void> _confirmDeleteAccountData() async {
final messenger = ScaffoldMessenger.of(context);
final confirmed = await showConfirmDialog(
context,
title: 'Apagar dados da conta',
@@ -63,7 +63,7 @@ class _SettingsBodyState extends State<SettingsBody> {
if (!mounted) return;
Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
messenger.showSnackBar(SnackBar(content: Text('Erro ao apagar: $e')));
if (mounted) showPillSnackBar(context, 'Erro ao apagar: $e');
} finally {
if (mounted) setState(() => _deletingAccount = false);
}

View File

@@ -10,6 +10,7 @@ import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../watched_videos_prefs.dart';
import '../widgets/app_gradients.dart';
import '../widgets/entrance.dart';
import '../widgets/pill_snackbar.dart';
import '../widgets/tap_bounce.dart';
// Video data structure - easily editable for future updates.
@@ -173,9 +174,7 @@ Future<void> showVideoPlayerDialog(
}) {
if (video.youtubeId != null) {
if (video.youtubeId!.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Vídeo ainda não disponível')));
showPillSnackBar(context, 'Vídeo ainda não disponível');
return Future.value();
}
return Navigator.of(context).push<void>(
@@ -785,9 +784,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
setState(() {
_isInitialized = false;
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e')));
showPillSnackBar(context, 'Erro ao carregar vídeo: $e');
}
}
}
@@ -1049,9 +1046,7 @@ class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> {
setState(() {
_isInitialized = false;
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e')));
showPillSnackBar(context, 'Erro ao carregar vídeo: $e');
}
}
}

View File

@@ -0,0 +1,19 @@
import 'package:flutter/services.dart';
/// Capitaliza automaticamente a primeira letra à medida que o utilizador
/// escreve — usado nos campos de nome (criança e conta) para não depender
/// só da sugestão do teclado (que o utilizador pode ignorar ou que pode
/// não existir em todos os teclados).
class CapitalizeFirstLetterFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text;
if (text.isEmpty) return newValue;
final capitalized = text[0].toUpperCase() + text.substring(1);
if (capitalized == text) return newValue;
return newValue.copyWith(text: capitalized, selection: newValue.selection);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
/// Notificação em formato de pílula flutuante (fundo branco, texto rosa,
/// cantos totalmente arredondados, com um pequeno "pop" de entrada), usada
/// em toda a app em vez do SnackBar padrão do Material (barra preta a
/// ocupar a largura toda). O SnackBar continua a ser o mecanismo por trás
/// — só o visual muda: fica transparente/sem elevação e o conteúdo real é
/// este contentor central que abraça o texto em vez de esticar de ponta a
/// ponta.
void showPillSnackBar(BuildContext context, String message) {
final messenger = ScaffoldMessenger.of(context);
messenger
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.transparent,
elevation: 0,
duration: const Duration(seconds: 2),
padding: EdgeInsets.zero,
content: Center(child: _AnimatedPill(message: message)),
),
);
}
/// Pequeno "pop" de entrada (escala + fade, com leve ultrapassagem via
/// [Curves.easeOutBack]) para a pílula de notificação — em vez de aparecer
/// estática, dá-lhe uma sensação de vida ao surgir. Corre uma única vez,
/// automaticamente, assim que este widget é construído (cada chamada a
/// [showPillSnackBar] cria uma instância nova).
class _AnimatedPill extends StatelessWidget {
const _AnimatedPill({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 380),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Opacity(
opacity: value.clamp(0.0, 1.0),
child: Transform.scale(scale: value, child: child),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(999),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFFFF55A7),
fontWeight: FontWeight.w800,
fontSize: 14,
),
),
),
);
}
}