Atulização do quiz | Porcentagem na base de dados | Telas de Contrato com o usuario

This commit is contained in:
Carlos Correia
2026-07-13 22:28:01 +01:00
parent 824fdb8089
commit 4fcf3a475a
23 changed files with 2061 additions and 369 deletions

View File

@@ -19,6 +19,8 @@ class QuizAnswer {
required this.description,
required this.weight,
this.imagePath,
this.imageBuilder,
this.hideTitle = false,
this.value,
this.helpVideoId,
});
@@ -27,6 +29,18 @@ class QuizAnswer {
final String description;
final int weight;
final String? imagePath;
/// Quando definido, substitui [imagePath] na renderização — usado para
/// mostrar um recorte/zoom de uma imagem partilhada (ex.: metade
/// esquerda/direita de uma foto de comparação) sem precisar de gerar
/// novos ficheiros de imagem.
final WidgetBuilder? imageBuilder;
/// Quando true, esconde o texto [title] por baixo da imagem — usado nas
/// perguntas "escolha a imagem" do quiz, onde a própria imagem já é a
/// resposta e uma legenda seria redundante.
final bool hideTitle;
final String? value;
/// Quando definido (normalmente só na resposta "Não sei"), identifica um
@@ -55,6 +69,7 @@ class QuizQuestionScreen extends StatefulWidget {
this.categoryIcon,
this.fallbackIcon,
this.fallbackColor,
this.answerImageAspectRatio,
});
final String title;
@@ -71,6 +86,12 @@ class QuizQuestionScreen extends StatefulWidget {
final String? suggestedYoutubeId;
final String? suggestedVideoTitle;
/// Substitui o cálculo automático (baseado no número de opções) do
/// aspect ratio dos blocos de imagem em [QuizAnswerType.image] — usado
/// quando as fotos já vêm pré-recortadas num formato específico (ex.:
/// retrato, para a postura).
final double? answerImageAspectRatio;
/// Rótulo pequeno do tema da pergunta (ex.: "Avaliação facial"), mostrado
/// num badge acima da imagem/pergunta.
final String? category;
@@ -147,9 +168,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
body: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Container(color: const Color(0xFFFAFAF7)),
),
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
Positioned(
left: -size.width * 0.40,
bottom: -size.width * 0.45,
@@ -282,9 +301,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 520,
),
constraints: const BoxConstraints(maxWidth: 520),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 16,
@@ -316,7 +333,8 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
: _FallbackIconBlock(
icon:
widget.fallbackIcon ??
Icons.info_outline_rounded,
Icons
.info_outline_rounded,
color:
widget.fallbackColor ??
const Color(0xFF2F9E94),
@@ -350,8 +368,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
'Ver vídeo (opcional)',
style: const TextStyle(
color: Color(0xFF2F9E94),
fontWeight:
FontWeight.w800,
fontWeight: FontWeight.w800,
),
),
),
@@ -378,8 +395,9 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
: 'Escolha apenas uma opção',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black
.withValues(alpha: 0.55),
color: Colors.black.withValues(
alpha: 0.55,
),
fontWeight: FontWeight.w700,
),
),
@@ -387,7 +405,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
),
const SizedBox(height: 18),
SizedBox(
height:
widget.answerType ==
QuizAnswerType.image
? 6
: 18,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
@@ -407,7 +431,14 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
i++
) ...[
if (i > 0)
const SizedBox(height: 12),
SizedBox(
height:
widget.answerType ==
QuizAnswerType
.image
? 5
: 12,
),
FadeSlideIn(
delay: Duration(
milliseconds: 60 * i,
@@ -437,6 +468,22 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
.answers[i],
selected:
_selected == i,
// Perguntas com mais opções de
// imagem precisam de blocos mais
// compactos para caber sem rolar
// — com 2 opções sobra espaço
// para um enquadramento mais
// vertical (ex.: postura), a não
// ser que a pergunta imponha um
// aspect ratio específico.
imageAspectRatio:
widget.answerImageAspectRatio ??
(widget
.answers
.length >=
3
? 2.3
: 1.5),
onTap: () =>
setState(
() =>
@@ -449,7 +496,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
],
),
),
const SizedBox(height: 24),
SizedBox(
height:
widget.answerType ==
QuizAnswerType.image
? 8
: 24,
),
Padding(
padding: const EdgeInsets.fromLTRB(
20,
@@ -462,7 +515,11 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
TapBounce(
child: SizedBox(
width: size.width * 0.62,
height: 46,
height:
widget.answerType ==
QuizAnswerType.image
? 38
: 46,
child: FilledButton(
style:
FilledButton.styleFrom(
@@ -477,8 +534,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
textStyle:
const TextStyle(
fontWeight:
FontWeight
.w900,
FontWeight.w900,
),
).copyWith(
animationDuration:
@@ -491,16 +547,14 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
WidgetStateProperty.resolveWith<
Color?
>((states) {
if (states
.contains(
WidgetState
.pressed,
)) {
if (states.contains(
WidgetState
.pressed,
)) {
return Colors
.white
.withValues(
alpha:
0.14,
alpha: 0.14,
);
}
if (states.contains(
@@ -514,8 +568,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
return Colors
.white
.withValues(
alpha:
0.08,
alpha: 0.08,
);
}
return null;
@@ -529,8 +582,7 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
true,
);
int nextScore =
widget
.currentScore;
widget.currentScore;
if (widget.answerType ==
QuizAnswerType
.number) {
@@ -587,7 +639,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
),
const SizedBox(height: 6),
SizedBox(
height:
widget.answerType ==
QuizAnswerType.image
? 0
: 6,
),
TextButton(
style: TextButton.styleFrom(
foregroundColor: const Color(
@@ -596,10 +654,19 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
textStyle: const TextStyle(
fontWeight: FontWeight.w800,
),
padding:
widget.answerType ==
QuizAnswerType.image
? const EdgeInsets.symmetric(
vertical: 4,
horizontal: 12,
)
: null,
),
onPressed: () => Navigator.of(
context,
).popUntil((route) => route.isFirst),
onPressed: () =>
Navigator.of(context).popUntil(
(route) => route.isFirst,
),
child: const Text(
'Voltar para homepage',
),
@@ -764,11 +831,13 @@ class _QuizAnswerTile extends StatelessWidget {
required this.answer,
required this.selected,
required this.onTap,
this.imageAspectRatio = 4 / 3,
});
final QuizAnswer answer;
final bool selected;
final VoidCallback onTap;
final double imageAspectRatio;
@override
Widget build(BuildContext context) {
@@ -810,47 +879,56 @@ class _QuizAnswerTile extends StatelessWidget {
onTap: onTap,
splashFactory: InkSparkle.splashFactory,
child: Padding(
padding: const EdgeInsets.symmetric(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
vertical: answer.hideTitle ? 8 : 14,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (answer.imagePath != null) ...[
if (answer.imagePath != null ||
answer.imageBuilder != 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,
),
),
aspectRatio: imageAspectRatio,
child: answer.imageBuilder != null
? answer.imageBuilder!(context)
: 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),
if (!answer.hideTitle) const SizedBox(height: 10),
],
Text(
answer.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
color: Color(0xFF2F9E94),
if (!answer.hideTitle)
Text(
answer.title,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
color: Color(0xFF2F9E94),
),
),
),
],
),
),
@@ -983,13 +1061,13 @@ class _FallbackIconBlock extends StatelessWidget {
Widget build(BuildContext context) {
return Center(
child: Container(
width: 64,
height: 64,
width: 52,
height: 52,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(18),
borderRadius: BorderRadius.circular(16),
),
child: Icon(icon, size: 32, color: Colors.white),
child: Icon(icon, size: 26, color: Colors.white),
),
);
}