283 lines
9.7 KiB
Dart
283 lines
9.7 KiB
Dart
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'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|