Files
CheckTheethKids/lib/quiz/quiz_video_guide.dart
Carlos Correia fb39e1f722 CTK 1.0.0
2026-07-14 22:19:26 +01:00

258 lines
9.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../widgets/entrance.dart';
import '../widgets/liquid_waves_background.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 =
'Veja o vídeo 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) {
return Scaffold(
body: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(child: Container(color: const Color(0xFFFAFAF7))),
const LiquidWavesBackground(),
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, veja o 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'),
),
),
),
),
],
),
),
),
],
),
);
}
}