Atualização do quiz e videos(incompleto)

This commit is contained in:
Carlos Correia
2026-05-26 16:21:11 +01:00
parent ea009af0d3
commit e292256a98
59 changed files with 351 additions and 405 deletions

View File

@@ -36,6 +36,7 @@ class QuizQuestionScreen extends StatefulWidget {
this.isFinal = false,
this.showBackButton = false,
this.answerType = QuizAnswerType.text,
this.questionImagePaths = const [],
});
final String title;
@@ -47,6 +48,7 @@ class QuizQuestionScreen extends StatefulWidget {
final bool isFinal;
final bool showBackButton;
final QuizAnswerType answerType;
final List<String> questionImagePaths;
@override
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
@@ -136,6 +138,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
const SizedBox(height: 6),
if (widget.questionImagePaths.isNotEmpty) ...[
const SizedBox(height: 6),
_QuestionReferenceImages(
paths: widget.questionImagePaths,
),
const SizedBox(height: 10),
],
Text(
widget.question,
textAlign: TextAlign.center,
@@ -312,6 +321,28 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
],
const SizedBox(height: 10),
SizedBox(
width: size.width * 0.62,
height: 42,
child: OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF2F9E94),
side: const BorderSide(
color: Color(0xFF2F9E94),
width: 1.3,
),
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontWeight: FontWeight.w900,
),
),
onPressed: () => Navigator.of(
context,
).popUntil((route) => route.isFirst),
child: const Text('Voltar para homepage'),
),
),
],
),
),
@@ -425,6 +456,28 @@ class _QuizAnswerTile extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (answer.imagePath != null) ...[
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 4 / 3,
child: Image.asset(
answer.imagePath!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
color: Colors.black.withValues(alpha: 0.06),
child: const Center(
child: Icon(
Icons.image_not_supported_outlined,
color: Colors.black38,
),
),
),
),
),
),
const SizedBox(height: 10),
],
Row(
children: [
Expanded(
@@ -437,38 +490,8 @@ class _QuizAnswerTile extends StatelessWidget {
),
),
),
AnimatedRotation(
turns: selected ? 0.5 : 0.0,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
child: Icon(
Icons.expand_more_rounded,
color: Colors.black.withValues(alpha: 0.55),
),
),
],
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
answer.description,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.72),
fontWeight: FontWeight.w600,
height: 1.25,
),
),
),
crossFadeState: selected
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 220),
firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut,
sizeCurve: Curves.easeOutCubic,
),
],
),
),
@@ -477,3 +500,56 @@ class _QuizAnswerTile extends StatelessWidget {
);
}
}
class _QuestionReferenceImages extends StatelessWidget {
const _QuestionReferenceImages({required this.paths});
final List<String> paths;
@override
Widget build(BuildContext context) {
if (paths.length == 1) {
return ClipRRect(
borderRadius: BorderRadius.circular(14),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Image.asset(
paths.first,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _placeholder(),
),
),
);
}
return SizedBox(
height: 120,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: paths.length,
separatorBuilder: (context, index) => const SizedBox(width: 10),
itemBuilder: (context, i) {
return AspectRatio(
aspectRatio: 4 / 3,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
paths[i],
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _placeholder(),
),
),
);
},
),
);
}
Widget _placeholder() {
return Container(
color: Colors.black.withValues(alpha: 0.06),
child: const Center(
child: Icon(Icons.image_not_supported_outlined, color: Colors.black38),
),
);
}
}

View File

@@ -25,32 +25,28 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//1.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_a.png',
imagePath: 'assets/mockup_images/1.jpeg',
),
QuizAnswer(
//2.jpeg
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_b.png',
imagePath: 'assets/mockup_images/2.jpeg',
),
QuizAnswer(
//3.jpeg
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_d.png',
imagePath: 'assets/mockup_images/3.jpeg',
),
],
),
@@ -58,36 +54,24 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
id: 2,
title: 'Quiz 2/26',
question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
'Qual das seguintes imagens se assemelha à posição boca do seu filho/a habitualmente?',
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//4.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_a.png',
imagePath: 'assets/mockup_images/4.jpeg',
),
QuizAnswer(
//5.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_b.png',
),
QuizAnswer(
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_d.png',
imagePath: 'assets/mockup_images/5.png',
),
],
),
@@ -99,32 +83,20 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//8.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_a.png',
imagePath: 'assets/mockup_images/8.jpeg',
),
QuizAnswer(
//9.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_b.png',
),
QuizAnswer(
title: 'Opção C',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_d.png',
imagePath: 'assets/mockup_images/9.png',
),
],
),
@@ -136,48 +108,55 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//6.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_a.png',
imagePath: 'assets/mockup_images/6.jpeg',
),
QuizAnswer(
//7.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_b.png',
),
QuizAnswer(
title: 'Opção C',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_d.png',
imagePath: 'assets/mockup_images/7.png',
),
],
),
QuizQuestion(
id: 5,
title: 'Quiz 5/26',
question: 'Quantos dentes tem o seu filho/a em cima na boca?',
answerType: QuizAnswerType.number,
answers: const [],
),
QuizQuestion(
id: 6,
title: 'Quiz 6/26',
question: 'Quantos dentes tem o seu filho/a em baixo na boca?',
answerType: QuizAnswerType.number,
answers: const [],
question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//14.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/mockup_images/14.jpeg',
),
QuizAnswer(
//15.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/mockup_images/15.png',
),
QuizAnswer(
//16.png
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/mockup_images/16.png',
),
],
),
QuizQuestion(
id: 7,
@@ -187,32 +166,28 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//10.png
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_a.png',
imagePath: 'assets/mockup_images/10.png',
),
QuizAnswer(
//11.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_b.png',
imagePath: 'assets/mockup_images/11.png',
),
QuizAnswer(
//13.png
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_d.png',
imagePath: 'assets/mockup_images/13.png',
),
],
),
@@ -224,32 +199,45 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//17.png
title: 'Opção A',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_a.png',
imagePath: 'assets/mockup_images/17.png',
),
QuizAnswer(
//18.jpeg
title: 'Opção B',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_b.png',
imagePath: 'assets/mockup_images/18.jpeg',
),
],
),
QuizQuestion(
id: 8,
title: 'Quiz 8/26',
question:
'Qual das seguintes imagens se assemelha ao freio do seu filho/a?',
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
title: 'Opção C',
//19.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_c.png',
imagePath: 'assets/mockup_images/19.jpeg',
),
QuizAnswer(
title: 'Opção D',
//20.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_d.png',
imagePath: 'assets/mockup_images/20.png',
),
],
),
@@ -652,6 +640,7 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
isFinal: isLastQuestion,
showBackButton: _currentQuestionIndex > 0,
answerType: currentQuestion.answerType,
questionImagePaths: currentQuestion.questionImagePaths,
onFinished: isLastQuestion
? () {
Navigator.of(context).pushReplacement(
@@ -675,6 +664,8 @@ class QuizQuestion {
final String question;
final List<QuizAnswer> answers;
final QuizAnswerType answerType;
// Reference images shown ABOVE the question text (visualization only).
final List<String> questionImagePaths;
QuizQuestion({
required this.id,
@@ -682,5 +673,41 @@ class QuizQuestion {
required this.question,
required this.answers,
this.answerType = QuizAnswerType.text,
this.questionImagePaths = const [],
});
}
// Helper: returns the asset path for a numbered mockup image (0..27).
// Use in QuizAnswer.imagePath or QuizQuestion.questionImagePaths.
String mockup(int n) => 'assets/mockup_images/$n${_mockupExt[n] ?? '.png'}';
const Map<int, String> _mockupExt = {
0: '.png',
1: '.jpeg',
2: '.jpeg',
3: '.jpeg',
4: '.jpeg',
5: '.png',
6: '.jpeg',
7: '.png',
8: '.jpeg',
9: '.png',
10: '.png',
11: '.png',
12: '.png',
13: '.png',
14: '.jpeg',
15: '.png',
16: '.png',
17: '.png',
18: '.jpeg',
19: '.jpeg',
20: '.png',
21: '.jpeg',
22: '.png',
23: '.jpeg',
24: '.png',
25: '.jpg',
26: '.png',
27: '.png',
};

View File

@@ -62,17 +62,20 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
scope.startsWith('${userId}_')) {
final childId = scope.substring(userId.length + 1).trim();
if (childId.isNotEmpty) {
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('children')
.doc(childId)
.set({
'lastScore': widget.finalScore,
'lastMaxScore': widget.maxScore,
'lastQuizAt': FieldValue.serverTimestamp(),
}, SetOptions(merge: true))
.catchError((_) {});
// Fire-and-forget: avoid blocking UI on Firestore (may hang offline).
unawaited(
FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('children')
.doc(childId)
.set({
'lastScore': widget.finalScore,
'lastMaxScore': widget.maxScore,
'lastQuizAt': FieldValue.serverTimestamp(),
}, SetOptions(merge: true))
.catchError((_) {}),
);
}
}
}
@@ -222,8 +225,11 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
fontWeight: FontWeight.w900,
),
),
onPressed: () =>
Navigator.of(context).popUntil((r) => r.isFirst),
onPressed: () async {
await _saveResultFuture;
if (!context.mounted) return;
Navigator.of(context).popUntil((r) => r.isFirst);
},
child: const Text('Avançar'),
),
),