CTK 1.0.2

This commit is contained in:
Carlos Correia
2026-07-15 17:46:14 +01:00
parent 9c21887fc7
commit 3214a07779
9 changed files with 211 additions and 150 deletions

View File

@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="Check Theeth Kids" android:label="Check Teeth Kids"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
assets/mockup_images/24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

BIN
assets/mockup_images/25.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Check Theeth Kids</string> <string>Check Teeth Kids</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Check Theeth Kids</string> <string>Check Teeth Kids</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@@ -62,8 +62,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1'; static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1';
static const double _collapsedAppBarHeight = kToolbarHeight; static const double _collapsedAppBarHeight = kToolbarHeight;
static const double _expandedAppBarHeight = 226; static const double _expandedAppBarHeight = 256;
static const double _nameOnlyAppBarHeight = 160;
int _index = 0; int _index = 0;
@@ -349,9 +348,11 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
// um NestedScrollView em vez do AppBar fixo usado nas outras abas, // um NestedScrollView em vez do AppBar fixo usado nas outras abas,
// para que a altura acompanhe o scroll em vez de ficar sempre cheia // para que a altura acompanhe o scroll em vez de ficar sempre cheia
// (o que cortava o conteúdo do card do quiz contra a barra ao rolar). // (o que cortava o conteúdo do card do quiz contra a barra ao rolar).
final double expandedHeight = hasScore // A app bar mantém sempre a mesma altura e mostra sempre os dois
? _expandedAppBarHeight // gauges — vazios (sem número, sem rótulo) quando ainda não há
: _nameOnlyAppBarHeight; // resultado, para o utilizador já perceber que aquele espaço é para
// o resultado do quiz mesmo antes de o fazer.
const double expandedHeight = _expandedAppBarHeight;
return Scaffold( return Scaffold(
body: NestedScrollView( body: NestedScrollView(
@@ -385,7 +386,7 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
top: kToolbarHeight + 34, top: kToolbarHeight + 50,
child: Center( child: Center(
child: Text( child: Text(
(_selectedChildName ?? '').trim(), (_selectedChildName ?? '').trim(),
@@ -397,31 +398,12 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
), ),
), ),
), ),
)
else if ((_selectedChildName ?? '').trim().isNotEmpty)
Positioned(
left: 0,
right: 0,
top: kToolbarHeight,
bottom: 0,
child: Center(
child: Text(
_selectedChildName!.trim(),
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white.withValues(alpha: 0.92),
fontSize: 14,
),
),
),
), ),
//posição da app bar relativamente ao nome //posição da app bar relativamente ao nome
if (hasScore)
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
top: kToolbarHeight + 60, top: kToolbarHeight + 96,
child: Center( child: Center(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -431,14 +413,14 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
// o que foi gravado na última avaliação) // o que foi gravado na última avaliação)
// para não mostrar um denominador antigo // para não mostrar um denominador antigo
// quando o número de perguntas muda. // quando o número de perguntas muda.
value: result.signs, value: hasScore ? result.signs : null,
max: kSignsMax, max: hasScore ? kSignsMax : null,
label: 'Sinais', label: 'Sinais de má\noclusão',
), ),
const SizedBox(width: 18), const SizedBox(width: 18),
_RiskArcGauge( _RiskArcGauge(
value: result.factors, value: hasScore ? result.factors : null,
max: kFactorsMax, max: hasScore ? kFactorsMax : null,
label: 'Fatores de risco', label: 'Fatores de risco',
), ),
], ],
@@ -587,19 +569,23 @@ class _LoggedHomeScreenState extends State<LoggedHomeScreen>
} }
class _RiskArcGauge extends StatelessWidget { class _RiskArcGauge extends StatelessWidget {
const _RiskArcGauge({ const _RiskArcGauge({required this.value, required this.max, this.label});
required this.value,
required this.max,
required this.label,
});
final int value; /// Quando [value]/[max] são nulos (sem criança selecionada, ou criança
final int max; /// ainda sem quiz feito), o gauge mostra-se vazio — só o anel em branco,
final String label; /// sem número nem rótulo — em vez de desaparecer, para o espaço já
/// indicar onde o resultado vai aparecer assim que existir.
final int? value;
final int? max;
final String? label;
bool get _isEmpty => value == null || max == null;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final progress = max > 0 ? (value / max).clamp(0, 1).toDouble() : 0.0; final progress = _isEmpty || max! <= 0
? 0.0
: (value! / max!).clamp(0, 1).toDouble();
return TweenAnimationBuilder<double>( return TweenAnimationBuilder<double>(
duration: const Duration(milliseconds: 700), duration: const Duration(milliseconds: 700),
@@ -621,6 +607,7 @@ class _RiskArcGauge extends StatelessWidget {
painter: _RiskArcGaugePainter(progress: animatedProgress), painter: _RiskArcGaugePainter(progress: animatedProgress),
), ),
), ),
if (!_isEmpty) ...[
Positioned( Positioned(
top: 38, top: 38,
left: 0, left: 0,
@@ -642,7 +629,7 @@ class _RiskArcGauge extends StatelessWidget {
left: 0, left: 0,
right: 0, right: 0,
child: Text( child: Text(
label, label ?? '',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.85), color: Colors.white.withValues(alpha: 0.85),
@@ -654,6 +641,7 @@ class _RiskArcGauge extends StatelessWidget {
), ),
), ),
], ],
],
), ),
); );
}, },
@@ -1350,7 +1338,7 @@ class _HeroQuizCard extends StatelessWidget {
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
'28 perguntas rápidas · menos de 3 minutos', '29 perguntas rápidas · menos de 3 minutos',
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.92), color: Colors.white.withValues(alpha: 0.92),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@@ -83,7 +83,7 @@ class Quiz1Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 1/28', title: 'Quiz 1/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a tem problemas respiratórios diagnosticados?', question: 'O seu filho/a tem problemas respiratórios diagnosticados?',
answers: const [ answers: const [
@@ -127,7 +127,7 @@ class Quiz2Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 2/28', title: 'Quiz 2/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: 'O seu filho/a respira habitualmente pela boca?', question: 'O seu filho/a respira habitualmente pela boca?',
answers: const [ answers: const [
@@ -171,7 +171,7 @@ class Quiz3Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 3/28', title: 'Quiz 3/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a ressona habitualmente durante a noite?', question: 'O seu filho/a ressona habitualmente durante a noite?',
answers: const [ answers: const [
@@ -215,7 +215,7 @@ class Quiz4Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 4/28', title: 'Quiz 4/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a sente habitualmente o nariz "tapado"?', question: 'O seu filho/a sente habitualmente o nariz "tapado"?',
answers: const [ answers: const [
@@ -259,7 +259,7 @@ class Quiz5Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 5/28', title: 'Quiz 5/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: question:
'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?', 'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?',
@@ -305,7 +305,7 @@ class Quiz6Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 6/28', title: 'Quiz 6/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: 'O seu filho/a range os dentes com frequência?', question: 'O seu filho/a range os dentes com frequência?',
answers: const [ answers: const [
@@ -342,7 +342,7 @@ class Quiz7Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 7/28', title: 'Quiz 7/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a habitualmente tem alergias sazonais?', question: 'O seu filho/a habitualmente tem alergias sazonais?',
answers: const [ answers: const [
@@ -386,7 +386,7 @@ class Quiz8Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 8/28', title: 'Quiz 8/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?', question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?',
answers: const [ answers: const [
@@ -430,7 +430,7 @@ class Quiz9Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 9/28', title: 'Quiz 9/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: 'O seu filho/a teve ou costuma ter com frequência otites?', question: 'O seu filho/a teve ou costuma ter com frequência otites?',
answers: const [ answers: const [
@@ -474,7 +474,7 @@ class Quiz10Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 10/28', title: 'Quiz 10/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?', question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?',
answers: const [ answers: const [
@@ -518,7 +518,7 @@ class Quiz11Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 11/28', title: 'Quiz 11/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: question:
'O seu filho/a teve ou costuma ter com frequência bronquiolites?', 'O seu filho/a teve ou costuma ter com frequência bronquiolites?',
@@ -563,7 +563,7 @@ class Quiz12Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 12/28', title: 'Quiz 12/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a apresenta dificuldades a mastigar?', question: 'O seu filho/a apresenta dificuldades a mastigar?',
answers: const [ answers: const [
@@ -600,7 +600,7 @@ class Quiz13Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 13/28', title: 'Quiz 13/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: 'O seu filho/a habitualmente é lento a comer?', question: 'O seu filho/a habitualmente é lento a comer?',
answers: const [ answers: const [
@@ -637,7 +637,7 @@ class Quiz14Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 14/28', title: 'Quiz 14/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a habitualmente prefere comer alimentos moles?', question: 'O seu filho/a habitualmente prefere comer alimentos moles?',
answers: const [ answers: const [
@@ -674,7 +674,7 @@ class Quiz15Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 15/28', title: 'Quiz 15/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'Em bebé apenas foi alimentado por biberão?', question: 'Em bebé apenas foi alimentado por biberão?',
answers: const [ answers: const [
@@ -718,7 +718,7 @@ class Quiz16Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 16/28', title: 'Quiz 16/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: 'O seu filho/a usa ou usou chupeta com frequência?', question: 'O seu filho/a usa ou usou chupeta com frequência?',
answers: const [ answers: const [
@@ -762,7 +762,7 @@ class Quiz17Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 17/28', title: 'Quiz 17/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?', question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?',
answers: const [ answers: const [
@@ -814,7 +814,7 @@ class Quiz18Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 18/28', title: 'Quiz 18/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
answerImageAspectRatio: 1.5, answerImageAspectRatio: 1.5,
question: question:
@@ -857,7 +857,7 @@ class Quiz19Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 19/28', title: 'Quiz 19/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: question:
'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?', 'Qual das seguintes imagens é mais parecida com o perfil do seu filho/a?',
@@ -919,7 +919,7 @@ class Quiz20Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 20/28', title: 'Quiz 20/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
question: 'Qual é a posição da boca do seu filho/a habitualmente?', question: 'Qual é a posição da boca do seu filho/a habitualmente?',
answers: [ answers: [
@@ -964,7 +964,7 @@ class Quiz21Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 21/28', title: 'Quiz 21/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: question:
'Qual das imagens, na zona abaixo dos olhos, se assemelha mais ao seu filho/a?', 'Qual das imagens, na zona abaixo dos olhos, se assemelha mais ao seu filho/a?',
@@ -1014,7 +1014,7 @@ class Quiz22Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 22/28', title: 'Quiz 22/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
isSignQuestion: true, isSignQuestion: true,
question: question:
@@ -1070,12 +1070,20 @@ class Quiz23Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 23/28', title: 'Quiz 23/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
isSignQuestion: true, isSignQuestion: true,
question: question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?', 'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answers: const [ answers: const [
QuizAnswer(
title: 'Dentição sobreposta',
description: 'Dentes sobrepostos/tortos',
weight: 2,
hideTitle: true,
value: 'dentes_sobrepostos',
imagePath: 'assets/mockup_images/10.png',
),
QuizAnswer( QuizAnswer(
title: 'Dentição alinhada', title: 'Dentição alinhada',
description: 'Dentição bem alinhada, sem apinhamento', description: 'Dentição bem alinhada, sem apinhamento',
@@ -1092,14 +1100,6 @@ class Quiz23Screen extends StatelessWidget {
value: 'dentes_desalinhados', value: 'dentes_desalinhados',
imagePath: 'assets/mockup_images/13.png', imagePath: 'assets/mockup_images/13.png',
), ),
QuizAnswer(
title: 'Dentição sobreposta',
description: 'Dentes sobrepostos/tortos',
weight: 2,
hideTitle: true,
value: 'dentes_sobrepostos',
imagePath: 'assets/mockup_images/10.png',
),
], ],
currentScore: currentScore, currentScore: currentScore,
nextRoute: (context, nextScore) => quizPageRoute( nextRoute: (context, nextScore) => quizPageRoute(
@@ -1121,7 +1121,7 @@ class Quiz24Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 24/28', title: 'Quiz 24/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
isSignQuestion: true, isSignQuestion: true,
question: question:
@@ -1172,7 +1172,7 @@ class Quiz25Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 25/28', title: 'Quiz 25/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: question:
'Qual das seguintes imagens se assemelha ao freio labial do seu filho/a?', 'Qual das seguintes imagens se assemelha ao freio labial do seu filho/a?',
@@ -1214,7 +1214,7 @@ class Quiz26Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 26/28', title: 'Quiz 26/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
question: question:
'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?', 'Qual das seguintes imagens se assemelha ao freio lingual do seu filho/a?',
@@ -1256,7 +1256,58 @@ class Quiz27Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 27/28', title: 'Quiz 27/29',
fallbackColor: const Color(0xFF2F9E94),
isSignQuestion: true,
question:
'Qual das seguintes imagens se assemelha com a boca do seu filho/a?',
answers: const [
QuizAnswer(
title: 'Dentição desalinhada',
description: 'Dentição desalinhada/apinhada',
weight: 2,
hideTitle: true,
value: 'dentes_desalinhados_3',
imagePath: 'assets/mockup_images/23.JPEG',
),
QuizAnswer(
title: 'Dentição sobreposta',
description: 'Dentes sobrepostos/tortos',
weight: 2,
hideTitle: true,
value: 'dentes_sobrepostos_3',
imagePath: 'assets/mockup_images/24.png',
),
QuizAnswer(
title: 'Dentição alinhada',
description: 'Dentição bem alinhada, sem apinhamento',
weight: 1,
hideTitle: true,
value: 'dentes_alinhados_3',
imagePath: 'assets/mockup_images/25.jpg',
),
],
currentScore: currentScore,
nextRoute: (context, nextScore) => quizPageRoute(
builder: (_) => Quiz28Screen(currentScore: nextScore, scopeId: scopeId),
),
answerType: QuizAnswerType.image,
showBackButton: true,
);
}
}
// Quiz 28: Boca / dentição 2 (Image-choice)
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/29',
fallbackColor: const Color(0xFF2F9E94), fallbackColor: const Color(0xFF2F9E94),
isSignQuestion: true, isSignQuestion: true,
question: question:
@@ -1281,7 +1332,7 @@ class Quiz27Screen extends StatelessWidget {
], ],
currentScore: currentScore, currentScore: currentScore,
nextRoute: (context, nextScore) => quizPageRoute( nextRoute: (context, nextScore) => quizPageRoute(
builder: (_) => Quiz28Screen(currentScore: nextScore, scopeId: scopeId), builder: (_) => Quiz29Screen(currentScore: nextScore, scopeId: scopeId),
), ),
answerType: QuizAnswerType.image, answerType: QuizAnswerType.image,
showBackButton: true, showBackButton: true,
@@ -1289,9 +1340,9 @@ class Quiz27Screen extends StatelessWidget {
} }
} }
// Quiz 28: Céu da boca (Image-choice, final) // Quiz 29: Céu da boca (Image-choice, final)
class Quiz28Screen extends StatelessWidget { class Quiz29Screen extends StatelessWidget {
const Quiz28Screen({super.key, required this.currentScore, this.scopeId}); const Quiz29Screen({super.key, required this.currentScore, this.scopeId});
final QuizScore currentScore; final QuizScore currentScore;
final String? scopeId; final String? scopeId;
@@ -1299,9 +1350,10 @@ class Quiz28Screen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return QuizQuestionScreen( return QuizQuestionScreen(
title: 'Quiz 28/28', title: 'Quiz 29/29',
fallbackColor: const Color(0xFFFF55A7), fallbackColor: const Color(0xFFFF55A7),
isSignQuestion: true, isSignQuestion: true,
correctBadgeLabel: 'Posição saudável',
question: question:
'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?', 'Qual das seguintes imagens se assemelha ao céu da boca do seu filho/a?',
answers: const [ answers: const [

View File

@@ -82,6 +82,7 @@ class QuizQuestionScreen extends StatefulWidget {
this.suggestedVideoTitle, this.suggestedVideoTitle,
this.fallbackColor, this.fallbackColor,
this.answerImageAspectRatio, this.answerImageAspectRatio,
this.correctBadgeLabel = 'Posição certa',
}); });
final String title; final String title;
@@ -115,6 +116,11 @@ class QuizQuestionScreen extends StatefulWidget {
/// imagem colapsar, mostra-se um bloco colorido com o ícone da app. /// imagem colapsar, mostra-se um bloco colorido com o ícone da app.
final Color? fallbackColor; final Color? fallbackColor;
/// Texto do selo mostrado sobre a imagem certa ao revelar a resposta —
/// customizável por pergunta (ex.: "Posição saudável" em vez do genérico
/// "Posição certa", quando faz mais sentido para o contexto da pergunta).
final String correctBadgeLabel;
@override @override
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState(); State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
} }
@@ -445,6 +451,8 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
? 2.3 ? 2.3
: 1.5), : 1.5),
reveal: _revealed, reveal: _revealed,
correctLabel: widget
.correctBadgeLabel,
onTap: _revealed onTap: _revealed
? null ? null
: () => : () =>
@@ -820,6 +828,7 @@ class _QuizAnswerTile extends StatelessWidget {
required this.onTap, required this.onTap,
this.imageAspectRatio = 4 / 3, this.imageAspectRatio = 4 / 3,
this.reveal = false, this.reveal = false,
this.correctLabel = 'Posição certa',
}); });
final QuizAnswer answer; final QuizAnswer answer;
@@ -831,6 +840,10 @@ class _QuizAnswerTile extends StatelessWidget {
/// errada — ver [_QuizQuestionScreenState._revealed]. /// errada — ver [_QuizQuestionScreenState._revealed].
final bool reveal; final bool reveal;
/// Texto do selo quando [isCorrect] — customizável por pergunta (ex.:
/// "Posição saudável" em vez do genérico "Posição certa").
final String correctLabel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isCorrect = answer.weight == 1; final bool isCorrect = answer.weight == 1;
@@ -991,7 +1004,7 @@ class _QuizAnswerTile extends StatelessWidget {
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
isCorrect ? 'Resposta certa' : 'Resposta inadequada', isCorrect ? correctLabel : 'Posição inadequada',
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,

View File

@@ -13,11 +13,12 @@ import 'quiz_video_guide.dart';
const String _resultGuideYoutubeId = '3q7C7txH1dE'; const String _resultGuideYoutubeId = '3q7C7txH1dE';
// Fase 3 tem 28 perguntas no total: 5 são sinais de má oclusão já // Fase 3 tem 29 perguntas no total: 6 são sinais de má oclusão já
// instalados (queixo, boca/dentição, apinhamento, boca/dentição 2, céu da // instalados (queixo, boca/dentição, apinhamento, boca/dentição 2,
// boca) — as restantes 23 são fatores de risco associados. Ver o diagrama // boca/dentição 3, céu da boca) — as restantes 23 são fatores de risco
// de planeamento partilhado com o utilizador para o racional completo. // associados. Ver o diagrama de planeamento partilhado com o utilizador
const int kSignsMax = 5; // para o racional completo.
const int kSignsMax = 6;
const int kFactorsMax = 23; const int kFactorsMax = 23;
/// A partir de quantos sinais/fatores presentes se recomenda avaliação. /// A partir de quantos sinais/fatores presentes se recomenda avaliação.
@@ -73,13 +74,16 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
scope.startsWith('${userId}_')) { scope.startsWith('${userId}_')) {
final childId = scope.substring(userId.length + 1).trim(); final childId = scope.substring(userId.length + 1).trim();
if (childId.isNotEmpty) { if (childId.isNotEmpty) {
// Fire-and-forget: avoid blocking UI on erros de rede. Requer as // Aguarda a gravação (em vez de fire-and-forget) para que, ao voltar
// à Home logo a seguir, _loadQuizResult já encontre o valor novo na
// base de dados — caso contrário havia uma corrida em que a Home
// consultava a tabela antes desta escrita terminar, mostrando o
// resultado antigo até trocar de aba ou reabrir a app. Requer as
// colunas last_signs/last_signs_max/last_factors/last_factors_max // colunas last_signs/last_signs_max/last_factors/last_factors_max
// na tabela children — se ainda não existirem na base de dados, // na tabela children — se ainda não existirem, falha silenciosamente
// esta chamada falha silenciosamente e o resultado fica só local // e o resultado fica só local (SharedPreferences, acima).
// (SharedPreferences, acima), tal como já acontecia antes. try {
unawaited( await supabase
supabase
.from('children') .from('children')
.update({ .update({
'last_signs': widget.finalScore.signs, 'last_signs': widget.finalScore.signs,
@@ -87,13 +91,12 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
'last_factors': widget.finalScore.factors, 'last_factors': widget.finalScore.factors,
'last_factors_max': kFactorsMax, 'last_factors_max': kFactorsMax,
}) })
.eq('id', childId) .eq('id', childId);
.catchError((e) { } catch (e) {
debugPrint( debugPrint(
'[QuizResult] Falha ao gravar score na base de dados: $e', '[QuizResult] Falha ao gravar score na base de dados: $e',
); );
}), }
);
} }
} }
} }
@@ -112,7 +115,11 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
'de leite como de permanentes).\n\nO seu propósito é ' 'de leite como de permanentes).\n\nO seu propósito é '
'identificar, prevenir ou reduzir a gravidade de maloclusões ' 'identificar, prevenir ou reduzir a gravidade de maloclusões '
'e problemas no desenvolvimento dos maxilares enquanto os ' 'e problemas no desenvolvimento dos maxilares enquanto os '
'ossos e dentes ainda estão em crescimento.', 'ossos e dentes ainda estão em crescimento.\n\nEsta '
'intervenção aproveita a fase de desenvolvimento craniofacial '
'para influenciar o crescimento ósseo, redirecionar a '
'mandíbula, corrigir o alinhamento dos maxilares ou criar '
'espaço para a erupção correta dos dentes permanentes.',
onAdvance: (context) => onAdvance: (context) =>
Navigator.of(context).popUntil((r) => r.isFirst), Navigator.of(context).popUntil((r) => r.isFirst),
), ),