Atulização do quiz | Porcentagem na base de dados | Telas de Contrato com o usuario
This commit is contained in:
282
lib/quiz/quiz_video_guide.dart
Normal file
282
lib/quiz/quiz_video_guide.dart
Normal file
@@ -0,0 +1,282 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
import '../widgets/entrance.dart';
|
||||
import '../widgets/tap_bounce.dart';
|
||||
|
||||
const Color _pink = Color(0xFFFF55A7);
|
||||
const Color _teal = Color(0xFF2F9E94);
|
||||
|
||||
/// Player do YouTube incorporado (sem AppBar/tela cheia própria) — usado
|
||||
/// tanto no ecrã intersticial [QuizVideoGuideScreen] como embutido
|
||||
/// diretamente no ecrã de resultado do quiz.
|
||||
class QuizGuideVideoCard extends StatefulWidget {
|
||||
const QuizGuideVideoCard({super.key, required this.youtubeId, this.onEnded});
|
||||
|
||||
final String youtubeId;
|
||||
|
||||
/// Chamado uma única vez quando o vídeo chega ao fim.
|
||||
final VoidCallback? onEnded;
|
||||
|
||||
@override
|
||||
State<QuizGuideVideoCard> createState() => _QuizGuideVideoCardState();
|
||||
}
|
||||
|
||||
class _QuizGuideVideoCardState extends State<QuizGuideVideoCard> {
|
||||
late final YoutubePlayerController _controller = YoutubePlayerController(
|
||||
initialVideoId: widget.youtubeId,
|
||||
flags: const YoutubePlayerFlags(autoPlay: true, mute: false),
|
||||
);
|
||||
bool _endedNotified = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(_handleValueChange);
|
||||
}
|
||||
|
||||
void _handleValueChange() {
|
||||
if (_endedNotified) return;
|
||||
if (_controller.value.playerState == PlayerState.ended) {
|
||||
_endedNotified = true;
|
||||
widget.onEnded?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_handleValueChange);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: YoutubePlayer(
|
||||
controller: _controller,
|
||||
showVideoProgressIndicator: true,
|
||||
progressColors: const ProgressBarColors(
|
||||
playedColor: _pink,
|
||||
handleColor: _pink,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ecrã intersticial mostrado a meio do quiz, convidando a ver um vídeo
|
||||
/// educativo antes de continuar. Tanto "Avançar" como "Pular" seguem para
|
||||
/// [onAdvance] — o vídeo é um convite, não um bloqueio.
|
||||
class QuizVideoGuideScreen extends StatefulWidget {
|
||||
const QuizVideoGuideScreen({
|
||||
super.key,
|
||||
required this.youtubeId,
|
||||
required this.onAdvance,
|
||||
this.caption =
|
||||
'Assista para compreender melhor as próximas perguntas do questionário.',
|
||||
this.extraHeading,
|
||||
this.extraSubtitle,
|
||||
});
|
||||
|
||||
final String youtubeId;
|
||||
final void Function(BuildContext context) onAdvance;
|
||||
final String caption;
|
||||
|
||||
/// Quando definidos, mostram um bloco de destaque extra por baixo da
|
||||
/// legenda do vídeo (ex.: um aviso a preparar o utilizador para as
|
||||
/// próximas perguntas de observação).
|
||||
final String? extraHeading;
|
||||
final String? extraSubtitle;
|
||||
|
||||
@override
|
||||
State<QuizVideoGuideScreen> createState() => _QuizVideoGuideScreenState();
|
||||
}
|
||||
|
||||
class _QuizVideoGuideScreenState extends State<QuizVideoGuideScreen> {
|
||||
// Evita que um duplo-toque (Pular + Avançar, ou o mesmo botão duas vezes
|
||||
// seguidas antes da navegação trocar de ecrã) dispare onAdvance duas
|
||||
// vezes sobre uma rota que já foi substituída.
|
||||
bool _advanced = false;
|
||||
|
||||
// "Avançar" só fica disponível depois de o vídeo chegar ao fim; "Pular"
|
||||
// continua livre a qualquer momento.
|
||||
bool _videoWatched = false;
|
||||
|
||||
void _advance() {
|
||||
if (_advanced) return;
|
||||
_advanced = true;
|
||||
widget.onAdvance(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(color: const Color(0xFFFAFAF7)),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.40,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 4, 24, 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: TextButton(
|
||||
onPressed: _advance,
|
||||
child: const Text(
|
||||
'Pular',
|
||||
style: TextStyle(
|
||||
color: Colors.black45,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const FadeSlideIn(
|
||||
child: Text(
|
||||
'Antes de continuar, assista ao vídeo educativo...',
|
||||
style: TextStyle(
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.black87,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 80),
|
||||
child: QuizGuideVideoCard(
|
||||
youtubeId: widget.youtubeId,
|
||||
onEnded: () => setState(() => _videoWatched = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 120),
|
||||
child: Text(
|
||||
widget.caption,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.extraHeading != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 140),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _pink.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.extraHeading!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: _pink,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
if (widget.extraSubtitle != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
widget.extraSubtitle!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
FadeSlideIn(
|
||||
delay: const Duration(milliseconds: 160),
|
||||
child: TapBounce(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: _teal,
|
||||
disabledBackgroundColor: _teal.withValues(
|
||||
alpha: 0.35,
|
||||
),
|
||||
foregroundColor: Colors.white,
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
onPressed: _videoWatched ? _advance : null,
|
||||
child: const Text('Avançar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user