diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ChatGPT Image 15_05_2026, 18_43_47.png b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ChatGPT Image 15_05_2026, 18_43_47.png deleted file mode 100644 index 748f0cd..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ChatGPT Image 15_05_2026, 18_43_47.png and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ChatGPT Image 15_05_2026, 18_52_57.png b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ChatGPT Image 15_05_2026, 18_52_57.png deleted file mode 100644 index a496a17..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ChatGPT Image 15_05_2026, 18_52_57.png and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_4igv2b4igv2b4igv.png b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_4igv2b4igv2b4igv.png deleted file mode 100644 index 0785c2e..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_4igv2b4igv2b4igv.png and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_c12xbmc12xbmc12x.png b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_c12xbmc12xbmc12x.png deleted file mode 100644 index f5904b6..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_c12xbmc12xbmc12x.png and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_h6nzpwh6nzpwh6nz.png b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_h6nzpwh6nzpwh6nz.png deleted file mode 100644 index df0871e..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/Gemini_Generated_Image_h6nzpwh6nzpwh6nz.png and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/IMG_2421.jpg b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/IMG_2421.jpg deleted file mode 100644 index 4f5e2df..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/IMG_2421.jpg and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ScreenRecording_04-01-2026 22-28-03_1.MP4 b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ScreenRecording_04-01-2026 22-28-03_1.MP4 deleted file mode 100644 index 2061263..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ScreenRecording_04-01-2026 22-28-03_1.MP4 and /dev/null differ diff --git a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ScreenRecording_12-25-2025 23-30-10_1.mov b/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ScreenRecording_12-25-2025 23-30-10_1.mov deleted file mode 100644 index 63c8c04..0000000 Binary files a/assets/Vídeos_APP_Check-Teeth-Kids/Videos/Outros conteúdos utilizados/ScreenRecording_12-25-2025 23-30-10_1.mov and /dev/null differ diff --git a/assets/logo2.png b/assets/logo2.png new file mode 100644 index 0000000..c6e975d Binary files /dev/null and b/assets/logo2.png differ diff --git a/assets/mockup_images/12.png b/assets/mockup_images/12.png deleted file mode 100644 index 73302ae..0000000 Binary files a/assets/mockup_images/12.png and /dev/null differ diff --git a/assets/mockup_images/21.jpeg b/assets/mockup_images/21.jpeg deleted file mode 100644 index 000f0d5..0000000 Binary files a/assets/mockup_images/21.jpeg and /dev/null differ diff --git a/assets/mockup_images/22.png b/assets/mockup_images/22.png deleted file mode 100644 index 367f11e..0000000 Binary files a/assets/mockup_images/22.png and /dev/null differ diff --git a/assets/mockup_images/23.jpeg b/assets/mockup_images/23.jpeg deleted file mode 100644 index 3e0a17a..0000000 Binary files a/assets/mockup_images/23.jpeg and /dev/null differ diff --git a/assets/mockup_images/24.png b/assets/mockup_images/24.png deleted file mode 100644 index d51d6a8..0000000 Binary files a/assets/mockup_images/24.png and /dev/null differ diff --git a/assets/mockup_images/25.jpg b/assets/mockup_images/25.jpg deleted file mode 100644 index d5e4806..0000000 Binary files a/assets/mockup_images/25.jpg and /dev/null differ diff --git a/lib/_debug_harness.dart b/lib/_debug_harness.dart deleted file mode 100644 index c89b604..0000000 --- a/lib/_debug_harness.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'logged_home.dart'; - -class DebugHarness extends StatelessWidget { - const DebugHarness({super.key}); - - @override - Widget build(BuildContext context) { - return const LoggedHomeScreen(); - } -} diff --git a/lib/logged_home.dart b/lib/logged_home.dart index 1c86a71..f596911 100644 --- a/lib/logged_home.dart +++ b/lib/logged_home.dart @@ -11,6 +11,7 @@ import 'brushing_prefs.dart'; import 'main.dart' show supabase; import 'quiz/quiz1.dart'; import 'quiz/quiz_prefs.dart'; +import 'quiz/quiz_result.dart' show kSignsMax, kFactorsMax; import 'screens/settings_screen.dart'; import 'screens/video_screen.dart'; import 'watched_videos_prefs.dart'; @@ -61,7 +62,7 @@ class _LoggedHomeScreenState extends State static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1'; static const double _collapsedAppBarHeight = kToolbarHeight; - static const double _expandedAppBarHeight = 216; + static const double _expandedAppBarHeight = 226; static const double _nameOnlyAppBarHeight = 160; int _index = 0; @@ -426,14 +427,18 @@ class _LoggedHomeScreenState extends State mainAxisSize: MainAxisSize.min, children: [ _RiskArcGauge( + // Usa sempre o máximo atual do quiz (não + // o que foi gravado na última avaliação) + // para não mostrar um denominador antigo + // quando o número de perguntas muda. value: result.signs, - max: result.signsMax, + max: kSignsMax, label: 'Sinais', ), const SizedBox(width: 18), _RiskArcGauge( value: result.factors, - max: result.factorsMax, + max: kFactorsMax, label: 'Fatores de risco', ), ], @@ -603,7 +608,7 @@ class _RiskArcGauge extends StatelessWidget { builder: (context, animatedProgress, _) { return SizedBox( width: 108, - height: 84, + height: 92, child: Stack( clipBehavior: Clip.none, children: [ @@ -633,7 +638,7 @@ class _RiskArcGauge extends StatelessWidget { ), ), Positioned( - top: 58, + top: 68, left: 0, right: 0, child: Text( @@ -1331,7 +1336,7 @@ class _HeroQuizCard extends StatelessWidget { ), const SizedBox(height: 5), Text( - '27 perguntas rápidas · menos de 3 minutos', + '28 perguntas rápidas · menos de 3 minutos', style: TextStyle( color: Colors.white.withValues(alpha: 0.92), fontWeight: FontWeight.w600, @@ -2223,7 +2228,7 @@ class _PerfilTabState extends State<_PerfilTab> { final result = snap.data; final text = result == null ? '--' - : '${result.signs}/${result.signsMax} · ${result.factors}/${result.factorsMax}'; + : '${result.signs}/$kSignsMax · ${result.factors}/$kFactorsMax'; return Container( padding: const EdgeInsets.symmetric( horizontal: 10, diff --git a/lib/main.dart b/lib/main.dart index 71b9865..676bc3f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:async'; import 'gates/debug_launch_gate.dart'; -import '_debug_harness.dart'; const String supabaseUrl = 'https://mannjismlhlwaqqqnvog.supabase.co'; const String supabaseAnonKey = @@ -68,7 +67,7 @@ class MyApp extends StatelessWidget { ], supportedLocales: const [Locale('pt', 'PT'), Locale('pt', 'BR')], locale: const Locale('pt', 'PT'), - home: const DebugHarness(), + home: const DebugLaunchGate(), ); } } diff --git a/lib/quiz/quiz1.dart b/lib/quiz/quiz1.dart index 598571f..ff17459 100644 --- a/lib/quiz/quiz1.dart +++ b/lib/quiz/quiz1.dart @@ -27,11 +27,34 @@ Widget _zoomCrop( ); } +/// Transição entre perguntas do quiz — fade + leve deslize para cima, no +/// mesmo estilo do [FadeSlideIn] já usado dentro de cada ecrã, em vez do +/// slide-from-right abrupto padrão do Material ao navegar entre perguntas. +Route quizPageRoute({required WidgetBuilder builder}) { + return PageRouteBuilder( + transitionDuration: const Duration(milliseconds: 320), + reverseTransitionDuration: const Duration(milliseconds: 220), + pageBuilder: (context, animation, secondaryAnimation) => + builder(context), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final fade = CurvedAnimation(parent: animation, curve: Curves.easeOut); + final slide = Tween( + begin: const Offset(0, 0.04), + end: Offset.zero, + ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)); + return FadeTransition( + opacity: fade, + child: SlideTransition(position: slide, child: child), + ); + }, + ); +} + /// Rota inicial do quiz: mostra primeiro o ecrã de checklist explicativo, /// só depois a pergunta 1 — usada em todos os pontos de entrada do quiz /// em vez de navegar diretamente para [Quiz1Screen]. -MaterialPageRoute quizStartRoute({String? scopeId}) { - return MaterialPageRoute( +Route quizStartRoute({String? scopeId}) { + return quizPageRoute( builder: (_) => QuizChecklistScreen( heading: 'Vamos ajudá-lo/a a compreender:', items: const [ @@ -40,7 +63,7 @@ MaterialPageRoute quizStartRoute({String? scopeId}) { 'Quando deve procurar um dentista (urgência vs vigilância)', ], onAdvance: (context) => Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), + quizPageRoute(builder: (_) => Quiz1Screen(scopeId: scopeId)), ), ), ); @@ -60,7 +83,7 @@ class Quiz1Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 1/27', + title: 'Quiz 1/28', category: 'Saúde respiratória', categoryIcon: Icons.medical_information_rounded, fallbackIcon: Icons.medical_information_rounded, @@ -88,7 +111,7 @@ class Quiz1Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -107,7 +130,7 @@ class Quiz2Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 2/27', + title: 'Quiz 2/28', category: 'Respiração', categoryIcon: Icons.air_rounded, fallbackIcon: Icons.air_rounded, @@ -135,7 +158,7 @@ class Quiz2Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -154,7 +177,7 @@ class Quiz3Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 3/27', + title: 'Quiz 3/28', category: 'Sono', categoryIcon: Icons.bedtime_rounded, fallbackIcon: Icons.bedtime_rounded, @@ -182,7 +205,7 @@ class Quiz3Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -201,7 +224,7 @@ class Quiz4Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 4/27', + title: 'Quiz 4/28', category: 'Respiração', categoryIcon: Icons.sick_rounded, fallbackIcon: Icons.sick_rounded, @@ -229,7 +252,7 @@ class Quiz4Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -248,7 +271,7 @@ class Quiz5Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 5/27', + title: 'Quiz 5/28', category: 'Sono', categoryIcon: Icons.nights_stay_rounded, fallbackIcon: Icons.nights_stay_rounded, @@ -278,7 +301,7 @@ class Quiz5Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz6Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -297,7 +320,7 @@ class Quiz6Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 6/27', + title: 'Quiz 6/28', category: 'Hábitos noturnos', categoryIcon: Icons.nights_stay_rounded, fallbackIcon: Icons.nights_stay_rounded, @@ -318,7 +341,7 @@ class Quiz6Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz7Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -337,7 +360,7 @@ class Quiz7Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 7/27', + title: 'Quiz 7/28', category: 'Saúde geral', categoryIcon: Icons.local_florist_rounded, fallbackIcon: Icons.local_florist_rounded, @@ -365,7 +388,7 @@ class Quiz7Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -384,7 +407,7 @@ class Quiz8Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 8/27', + title: 'Quiz 8/28', category: 'Sono', categoryIcon: Icons.water_drop_rounded, fallbackIcon: Icons.water_drop_rounded, @@ -412,7 +435,7 @@ class Quiz8Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -431,7 +454,7 @@ class Quiz9Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 9/27', + title: 'Quiz 9/28', category: 'Saúde geral', categoryIcon: Icons.hearing_rounded, fallbackIcon: Icons.hearing_rounded, @@ -459,7 +482,7 @@ class Quiz9Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz10Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -478,7 +501,7 @@ class Quiz10Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 10/27', + title: 'Quiz 10/28', category: 'Saúde geral', categoryIcon: Icons.healing_rounded, fallbackIcon: Icons.healing_rounded, @@ -506,7 +529,7 @@ class Quiz10Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz11Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -525,7 +548,7 @@ class Quiz11Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 11/27', + title: 'Quiz 11/28', category: 'Saúde respiratória', categoryIcon: Icons.air_rounded, fallbackIcon: Icons.air_rounded, @@ -554,7 +577,7 @@ class Quiz11Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz12Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -573,7 +596,7 @@ class Quiz12Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 12/27', + title: 'Quiz 12/28', category: 'Hábitos alimentares', categoryIcon: Icons.restaurant_rounded, fallbackIcon: Icons.restaurant_rounded, @@ -594,7 +617,7 @@ class Quiz12Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz13Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -613,7 +636,7 @@ class Quiz13Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 13/27', + title: 'Quiz 13/28', category: 'Hábitos alimentares', categoryIcon: Icons.schedule_rounded, fallbackIcon: Icons.schedule_rounded, @@ -634,7 +657,7 @@ class Quiz13Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz14Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -653,7 +676,7 @@ class Quiz14Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 14/27', + title: 'Quiz 14/28', category: 'Hábitos alimentares', categoryIcon: Icons.restaurant_menu_rounded, fallbackIcon: Icons.restaurant_menu_rounded, @@ -674,7 +697,7 @@ class Quiz14Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz15Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -693,7 +716,7 @@ class Quiz15Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 15/27', + title: 'Quiz 15/28', category: 'Hábitos alimentares', categoryIcon: Icons.local_drink_rounded, fallbackIcon: Icons.local_drink_rounded, @@ -721,7 +744,7 @@ class Quiz15Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz16Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -740,7 +763,7 @@ class Quiz16Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 16/27', + title: 'Quiz 16/28', category: 'Hábitos orais', categoryIcon: Icons.child_care_rounded, fallbackIcon: Icons.child_care_rounded, @@ -768,7 +791,7 @@ class Quiz16Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz17Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.yesNo, @@ -787,7 +810,7 @@ class Quiz17Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 17/27', + title: 'Quiz 17/28', category: 'Hábitos orais', categoryIcon: Icons.back_hand_rounded, fallbackIcon: Icons.back_hand_rounded, @@ -815,11 +838,11 @@ class Quiz17Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => QuizVideoGuideScreen( youtubeId: 'W2BcK9nSyt0', onAdvance: (ctx) => Navigator.of(ctx).pushReplacement( - MaterialPageRoute( + quizPageRoute( builder: (_) => Quiz18Screen(currentScore: nextScore, scopeId: scopeId), ), @@ -842,7 +865,7 @@ class Quiz18Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 18/27', + title: 'Quiz 18/28', category: 'Avaliação postural', categoryIcon: Icons.accessibility_new_rounded, fallbackIcon: Icons.accessibility_new_rounded, @@ -869,7 +892,7 @@ class Quiz18Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz19Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -888,7 +911,7 @@ class Quiz19Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 19/27', + title: 'Quiz 19/28', category: 'Avaliação facial', categoryIcon: Icons.face_rounded, fallbackIcon: Icons.face_rounded, @@ -934,7 +957,7 @@ class Quiz19Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz20Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -953,7 +976,7 @@ class Quiz20Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 20/27', + title: 'Quiz 20/28', category: 'Avaliação facial', categoryIcon: Icons.sentiment_neutral_rounded, fallbackIcon: Icons.sentiment_neutral_rounded, @@ -982,11 +1005,11 @@ class Quiz20Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => QuizVideoGuideScreen( youtubeId: 'msKYr7nPxcw', onAdvance: (ctx) => Navigator.of(ctx).pushReplacement( - MaterialPageRoute( + quizPageRoute( builder: (_) => Quiz21Screen(currentScore: nextScore, scopeId: scopeId), ), @@ -1009,7 +1032,7 @@ class Quiz21Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 21/27', + title: 'Quiz 21/28', category: 'Avaliação facial', categoryIcon: Icons.visibility_rounded, fallbackIcon: Icons.visibility_rounded, @@ -1043,7 +1066,7 @@ class Quiz21Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz22Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -1062,7 +1085,7 @@ class Quiz22Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 22/27', + title: 'Quiz 22/28', category: 'Avaliação facial', categoryIcon: Icons.face_rounded, fallbackIcon: Icons.face_rounded, @@ -1094,7 +1117,7 @@ class Quiz22Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz23Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -1113,7 +1136,7 @@ class Quiz23Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 23/27', + title: 'Quiz 23/28', category: 'Avaliação facial', categoryIcon: Icons.sentiment_neutral_rounded, fallbackIcon: Icons.sentiment_neutral_rounded, @@ -1148,7 +1171,7 @@ class Quiz23Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz24Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -1167,7 +1190,7 @@ class Quiz24Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 24/27', + title: 'Quiz 24/28', category: 'Avaliação facial', categoryIcon: Icons.zoom_in_rounded, fallbackIcon: Icons.zoom_in_rounded, @@ -1202,7 +1225,7 @@ class Quiz24Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz25Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -1221,7 +1244,7 @@ class Quiz25Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 25/27', + title: 'Quiz 25/28', category: 'Avaliação facial', categoryIcon: Icons.record_voice_over_rounded, fallbackIcon: Icons.record_voice_over_rounded, @@ -1247,7 +1270,7 @@ class Quiz25Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz26Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -1266,7 +1289,7 @@ class Quiz26Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 26/27', + title: 'Quiz 26/28', category: 'Avaliação facial', categoryIcon: Icons.record_voice_over_rounded, fallbackIcon: Icons.record_voice_over_rounded, @@ -1292,7 +1315,7 @@ class Quiz26Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => Quiz27Screen(currentScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, @@ -1301,7 +1324,7 @@ class Quiz26Screen extends StatelessWidget { } } -// Quiz 27: Céu da boca (Image-choice, final) +// Quiz 27: Boca / dentição (Image-choice) class Quiz27Screen extends StatelessWidget { const Quiz27Screen({super.key, required this.currentScore, this.scopeId}); @@ -1311,7 +1334,53 @@ class Quiz27Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 27/27', + title: 'Quiz 27/28', + category: 'Avaliação facial', + categoryIcon: Icons.sentiment_neutral_rounded, + fallbackIcon: Icons.sentiment_neutral_rounded, + fallbackColor: const Color(0xFF2F9E94), + isSignQuestion: true, + question: + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + answers: const [ + QuizAnswer( + title: 'Dentição alinhada', + description: 'Dentição bem alinhada, sem apinhamento', + weight: 1, + hideTitle: true, + value: 'dentes_alinhados_2', + imagePath: 'assets/mockup_images/29.jpeg', + ), + QuizAnswer( + title: 'Dentição desalinhada', + description: 'Dentes sobrepostos/apinhados', + weight: 2, + hideTitle: true, + value: 'dentes_desalinhados_2', + imagePath: 'assets/mockup_images/28.jpeg', + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => quizPageRoute( + builder: (_) => Quiz28Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.image, + showBackButton: true, + ); + } +} + +// Quiz 28: Céu da boca (Image-choice, final) +class Quiz28Screen extends StatelessWidget { + const Quiz28Screen({super.key, required this.currentScore, this.scopeId}); + + final QuizScore currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 28/28', category: 'Avaliação facial', categoryIcon: Icons.architecture_rounded, fallbackIcon: Icons.architecture_rounded, @@ -1338,7 +1407,7 @@ class Quiz27Screen extends StatelessWidget { ), ], currentScore: currentScore, - nextRoute: (context, nextScore) => MaterialPageRoute( + nextRoute: (context, nextScore) => quizPageRoute( builder: (_) => QuizResultScreen(finalScore: nextScore, scopeId: scopeId), ), answerType: QuizAnswerType.image, diff --git a/lib/quiz/quiz_question_screen.dart b/lib/quiz/quiz_question_screen.dart index 5fc60e5..97e3334 100644 --- a/lib/quiz/quiz_question_screen.dart +++ b/lib/quiz/quiz_question_screen.dart @@ -140,6 +140,10 @@ class _QuizQuestionScreenState extends State { bool _numberDontKnow = false; bool _navigating = false; + /// Nas perguntas com imagem, ao avançar mostra-se por 2 segundos qual era + /// a imagem certa/errada antes de navegar — fins educativos. + bool _revealed = false; + /// O título ainda chega como texto livre ("Quiz 3/25") em vez de números /// separados — extraímos daqui em vez de acrescentar mais dois parâmetros /// obrigatórios a cada uma das 25 telas de pergunta. @@ -180,6 +184,12 @@ class _QuizQuestionScreenState extends State { (_numberValue != null && _numberValue! >= 0 && _numberValue! <= _maxTeethCount)); + } else if (widget.answerType == QuizAnswerType.yesNo && + _selected != null && + widget.answers[_selected!].value == 'nao_sei') { + // "Não sei" não é uma resposta válida para avançar — obriga a + // criança/responsável a decidir Sim ou Não antes de continuar. + canProceed = false; } final bool hasSuggestedVideo = @@ -485,12 +495,15 @@ class _QuizQuestionScreenState extends State { 3 ? 2.3 : 1.5), - onTap: () => - setState( - () => - _selected = - i, - ), + reveal: _revealed, + onTap: _revealed + ? null + : () => + setState( + () => + _selected = + i, + ), ), ), ], @@ -582,6 +595,25 @@ class _QuizQuestionScreenState extends State { () => _navigating = true, ); + + if (widget.answerType == + QuizAnswerType + .image) { + setState( + () => _revealed = + true, + ); + await Future.delayed( + const Duration( + seconds: 2, + ), + ); + if (!context + .mounted) { + return; + } + } + QuizScore nextScore = widget.currentScore; if (widget.answerType != @@ -838,16 +870,24 @@ class _QuizAnswerTile extends StatelessWidget { required this.selected, required this.onTap, this.imageAspectRatio = 4 / 3, + this.reveal = false, }); final QuizAnswer answer; final bool selected; - final VoidCallback onTap; + final VoidCallback? onTap; final double imageAspectRatio; + /// Quando true, mostra um selo indicando se esta era a resposta certa ou + /// errada — ver [_QuizQuestionScreenState._revealed]. + final bool reveal; + @override Widget build(BuildContext context) { - final borderColor = selected + final bool isCorrect = answer.weight == 1; + final borderColor = reveal + ? (isCorrect ? const Color(0xFF2F9E94) : const Color(0xFFFF55A7)) + : selected ? const Color(0xFF2F9E94) : Colors.black.withValues(alpha: 0.12); final bg = selected @@ -868,7 +908,7 @@ class _QuizAnswerTile extends StatelessWidget { borderRadius: BorderRadius.circular(16), border: Border.all( color: borderColor, - width: selected ? 1.4 : 1.0, + width: (reveal || selected) ? 1.6 : 1.0, ), boxShadow: [ BoxShadow( @@ -965,6 +1005,56 @@ class _QuizAnswerTile extends StatelessWidget { ), ), ), + if (reveal) + Positioned( + left: 8, + right: 8, + bottom: 8, + child: IgnorePointer( + child: Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: isCorrect + ? const Color(0xFF2F9E94) + : const Color(0xFFFF55A7), + borderRadius: BorderRadius.circular(999), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isCorrect + ? Icons.check_rounded + : Icons.close_rounded, + size: 14, + color: Colors.white, + ), + const SizedBox(width: 4), + Text( + isCorrect ? 'Resposta certa' : 'Resposta inadequada', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w800, + fontSize: 11.5, + ), + ), + ], + ), + ), + ), + ), + ), ], ), ); diff --git a/lib/quiz/quiz_result.dart b/lib/quiz/quiz_result.dart index 3bca4ff..28a67a0 100644 --- a/lib/quiz/quiz_result.dart +++ b/lib/quiz/quiz_result.dart @@ -12,11 +12,11 @@ import 'quiz_video_guide.dart'; const String _resultGuideYoutubeId = '3q7C7txH1dE'; -// Fase 3 tem 27 perguntas no total: 4 são sinais de má oclusão já -// instalados (queixo, boca/dentição, apinhamento, céu da boca) — as -// restantes 23 são fatores de risco associados. Ver o diagrama de -// planeamento partilhado com o utilizador para o racional completo. -const int kSignsMax = 4; +// Fase 3 tem 28 perguntas no total: 5 são sinais de má oclusão já +// instalados (queixo, boca/dentição, apinhamento, boca/dentição 2, céu da +// boca) — as restantes 23 são fatores de risco associados. Ver o diagrama +// de planeamento partilhado com o utilizador para o racional completo. +const int kSignsMax = 5; const int kFactorsMax = 23; /// A partir de quantos sinais/fatores presentes se recomenda avaliação. diff --git a/lib/screens/credits_screen.dart b/lib/screens/credits_screen.dart new file mode 100644 index 0000000..2fc1f17 --- /dev/null +++ b/lib/screens/credits_screen.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; + +import '../widgets/app_gradients.dart'; +import '../widgets/entrance.dart'; + +const Color _pink = Color(0xFFFF55A7); +const Color _teal = Color(0xFF2F9E94); + +class _CreditPerson { + const _CreditPerson(this.name, this.role, {this.logoPath}); + + final String name; + final String role; + + /// Quando definido, mostra este logótipo em vez do círculo com a + /// inicial do nome — usado na instituição colaboradora. + final String? logoPath; +} + +class _CreditSection { + const _CreditSection(this.label, this.people); + + final String label; + final List<_CreditPerson> people; +} + +const List<_CreditSection> _kCreditSections = [ + _CreditSection('Desenvolvimento', [ + _CreditPerson('Carlos Correia', 'Desenvolvedor'), + _CreditPerson('Fabio Ceia', 'Desenvolvedor'), + _CreditPerson('Ruben Grandra', 'Desenvolvedor'), + _CreditPerson('Dinis Maria', 'Desenvolvedor'), + ]), + _CreditSection('Idealização', [ + _CreditPerson('Francisca Pacheco Silva', 'Idealizadora/Formanda'), + ]), + _CreditSection('Orientação', [ + _CreditPerson( + 'João Carlos Rodrigues L. Nunes Miranda', + 'Professor supervisor', + ), + ]), + _CreditSection('Instituição colaboradora', [ + _CreditPerson( + 'Escola Profissional da Vila do Conde', + '', + logoPath: 'assets/logo2.png', + ), + ]), +]; + +class CreditsScreen extends StatelessWidget { + const CreditsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Container( + decoration: const BoxDecoration(gradient: kAppBarGradient), + child: AppBar( + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + title: const Text( + 'Criadores e Colaboradores', + style: TextStyle(fontWeight: FontWeight.w900), + ), + ), + ), + ), + body: Container( + color: const Color(0xFFFAFAF7), + child: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), + child: Column( + children: [ + FadeSlideIn( + child: Column( + children: [ + Container( + width: 76, + height: 76, + decoration: BoxDecoration( + color: _pink.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.diversity_3_rounded, + color: _pink, + size: 36, + ), + ), + const SizedBox(height: 16), + const Text( + 'Quem fez a Check-Teeth Kids', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: Colors.black, + ), + ), + ], + ), + ), + const SizedBox(height: 22), + for (var s = 0; s < _kCreditSections.length; s++) ...[ + if (s > 0) const SizedBox(height: 18), + FadeSlideIn( + delay: Duration(milliseconds: 60 * (s + 1)), + child: _CreditSectionCard(section: _kCreditSections[s]), + ), + ], + ], + ), + ), + ), + ), + ); + } +} + +class _CreditSectionCard extends StatelessWidget { + const _CreditSectionCard({required this.section}); + + final _CreditSection section; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + section.label, + style: const TextStyle( + color: _teal, + fontWeight: FontWeight.w900, + fontSize: 14, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 14, + offset: const Offset(0, 6), + ), + ], + ), + child: Column( + children: [ + for (var i = 0; i < section.people.length; i++) ...[ + if (i > 0) const Divider(height: 1, indent: 18, endIndent: 18), + _CreditPersonRow(person: section.people[i]), + ], + ], + ), + ), + ], + ); + } +} + +class _CreditPersonRow extends StatelessWidget { + const _CreditPersonRow({required this.person}); + + final _CreditPerson person; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: _teal.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + clipBehavior: Clip.antiAlias, + child: person.logoPath != null + ? Padding( + padding: const EdgeInsets.all(6), + child: Image.asset( + person.logoPath!, + fit: BoxFit.contain, + ), + ) + : Text( + person.name[0].toUpperCase(), + style: const TextStyle( + color: _teal, + fontWeight: FontWeight.w900, + fontSize: 16, + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + person.name, + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 14.5, + color: Colors.black87, + ), + ), + if (person.role.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + person.role, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 12.5, + color: Colors.black.withValues(alpha: 0.55), + ), + ), + ], + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/hello_splash_screen.dart b/lib/screens/hello_splash_screen.dart index a0c8bf3..e787e60 100644 --- a/lib/screens/hello_splash_screen.dart +++ b/lib/screens/hello_splash_screen.dart @@ -90,7 +90,7 @@ class _HelloSplashScreenState extends State with TickerProvid style: TextStyle( fontSize: 64, fontWeight: FontWeight.w900, - color: const Color(0xFFFF9AD0), + color: Color(0xFFFF9AD0), height: 1.0, ), ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index a42338e..b1a7517 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -5,6 +5,7 @@ import '../widgets/app_dialogs.dart'; import '../widgets/entrance.dart'; import '../widgets/pill_snackbar.dart'; import '../widgets/tap_bounce.dart'; +import 'credits_screen.dart'; import 'terms_screen.dart'; const Color _teal = Color(0xFF2F9E94); @@ -120,6 +121,16 @@ class _SettingsBodyState extends State { ), ), const Divider(height: 1), + _ActionTile( + icon: Icons.diversity_3_outlined, + title: 'Criadores e colaboradores', + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const CreditsScreen(), + ), + ), + ), + const Divider(height: 1), const _InfoTile( icon: Icons.info_outline_rounded, title: 'Versão do app', diff --git a/pubspec.yaml b/pubspec.yaml index 7d56207..1585e8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,7 @@ flutter: # - images/a_dot_ham.jpeg - lottie/ - assets/Check-theeth.png + - assets/logo2.png - assets/mockup_images/ flutter_launcher_icons: