diff --git a/lib/home_screen.dart b/lib/home_screen.dart index d27c982..e5071c6 100644 --- a/lib/home_screen.dart +++ b/lib/home_screen.dart @@ -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 { } } 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? 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( diff --git a/lib/logged_home.dart b/lib/logged_home.dart index 2bc6b72..8464f94 100644 --- a/lib/logged_home.dart +++ b/lib/logged_home.dart @@ -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?> _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 _addAnotherChild(BuildContext context, String uid) async { if (_addingChild) return; - final messenger = ScaffoldMessenger.of(context); final result = await showModalBottomSheet?>( 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', ), diff --git a/lib/main.dart b/lib/main.dart index 1233bed..16dcf0e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ); } diff --git a/lib/quiz/quiz1.dart b/lib/quiz/quiz1.dart index 523a468..c452370 100644 --- a/lib/quiz/quiz1.dart +++ b/lib/quiz/quiz1.dart @@ -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( @@ -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( @@ -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( @@ -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( @@ -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, diff --git a/lib/quiz/quiz_question_screen.dart b/lib/quiz/quiz_question_screen.dart index c218058..9dd497f 100644 --- a/lib/quiz/quiz_question_screen.dart +++ b/lib/quiz/quiz_question_screen.dart @@ -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, + ), + ], ), ), ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 3e5648e..a42338e 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -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 { } Future _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 { 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); } diff --git a/lib/screens/video_screen.dart b/lib/screens/video_screen.dart index 0933207..e51e8c9 100644 --- a/lib/screens/video_screen.dart +++ b/lib/screens/video_screen.dart @@ -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 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( @@ -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'); } } } diff --git a/lib/widgets/name_input_formatter.dart b/lib/widgets/name_input_formatter.dart new file mode 100644 index 0000000..595b07a --- /dev/null +++ b/lib/widgets/name_input_formatter.dart @@ -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); + } +} diff --git a/lib/widgets/pill_snackbar.dart b/lib/widgets/pill_snackbar.dart new file mode 100644 index 0000000..32b49a5 --- /dev/null +++ b/lib/widgets/pill_snackbar.dart @@ -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( + 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, + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 081dbc6..bf01ee9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -294,6 +294,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -432,6 +437,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" json_annotation: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c4c15be..7d56207 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.