import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lottie/lottie.dart'; import '../screens/video_screen.dart'; import '../widgets/entrance.dart'; import '../widgets/tap_bounce.dart'; typedef QuizNextBuilder = Route Function(BuildContext context, int nextScore); enum QuizAnswerType { text, image, number, yesNo } class QuizAnswer { const QuizAnswer({ required this.title, required this.description, required this.weight, this.imagePath, this.value, }); final String title; final String description; final int weight; final String? imagePath; final String? value; } class QuizQuestionScreen extends StatefulWidget { const QuizQuestionScreen({ super.key, required this.title, required this.question, required this.answers, required this.nextRoute, this.currentScore = 0, this.onFinished, this.isFinal = false, this.showBackButton = false, this.answerType = QuizAnswerType.text, this.questionImagePaths = const [], this.suggestedVideoPath, this.suggestedYoutubeId, this.suggestedVideoTitle, }); final String title; final String question; final List answers; final QuizNextBuilder nextRoute; final int currentScore; final VoidCallback? onFinished; final bool isFinal; final bool showBackButton; final QuizAnswerType answerType; final List questionImagePaths; final String? suggestedVideoPath; final String? suggestedYoutubeId; final String? suggestedVideoTitle; @override State createState() => _QuizQuestionScreenState(); } class _QuizQuestionScreenState extends State { // Uma criança não tem, na prática, mais do que ~20 dentes numa arcada // (dentição decídua + permanente em transição já é um caso extremo). // Serve para barrar valores absurdos como "400". static const int _maxTeethCount = 20; int? _selected; TextEditingController? _numberController; int? _numberValue; bool _navigating = false; @override void initState() { super.initState(); if (widget.answerType == QuizAnswerType.number) { _numberController = TextEditingController(); } } @override void dispose() { _numberController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); bool canProceed = _selected != null && !_navigating; if (widget.answerType == QuizAnswerType.number) { canProceed = _numberValue != null && _numberValue! >= 0 && _numberValue! <= _maxTeethCount && !_navigating; } final bool hasSuggestedVideo = (widget.suggestedVideoPath?.isNotEmpty ?? false) || (widget.suggestedYoutubeId?.isNotEmpty ?? false); return Scaffold( body: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], ), ), ), ), 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: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: 44, child: Stack( alignment: Alignment.center, children: [ Text( widget.title, textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues(alpha: 0.55), fontWeight: FontWeight.w800, ), ), if (widget.showBackButton) Positioned( left: 4, child: TapBounce( scale: 0.9, child: Material( color: Colors.white.withValues(alpha: 0.85), shape: const CircleBorder(), elevation: 4, shadowColor: Colors.black.withValues( alpha: 0.15, ), child: InkWell( customBorder: const CircleBorder(), onTap: () => Navigator.of(context).maybePop(), child: const Padding( padding: EdgeInsets.all(10), child: Icon( Icons.arrow_back_rounded, color: Color(0xFF2F9E94), size: 22, ), ), ), ), ), ), ], ), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( minHeight: constraints.maxHeight, ), child: Center( child: ConstrainedBox( constraints: const BoxConstraints( maxWidth: 520, ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 16, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ FadeSlideIn( child: Padding( padding: const EdgeInsets.fromLTRB( 20, 4, 20, 10, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget .questionImagePaths .isNotEmpty) ...[ const SizedBox(height: 6), _QuestionReferenceImages( paths: widget.questionImagePaths, ), const SizedBox(height: 10), ], if (hasSuggestedVideo) ...[ TextButton.icon( onPressed: () => showVideoPlayerDialog( context, VideoData( id: 0, title: widget .suggestedVideoTitle ?? 'Vídeo', description: '', videoPath: widget .suggestedVideoPath, youtubeId: widget .suggestedYoutubeId, ), ), icon: const Icon( Icons .play_circle_outline_rounded, color: Color(0xFF2F9E94), ), label: Text( widget.suggestedVideoTitle ?? 'Ver vídeo (opcional)', style: const TextStyle( color: Color(0xFF2F9E94), fontWeight: FontWeight.w800, ), ), ), const SizedBox(height: 4), ], Text( widget.question, textAlign: TextAlign.center, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), height: 1.2, ), ), const SizedBox(height: 8), Text( widget.answerType == QuizAnswerType.number ? 'Insira o número' : widget.answerType == QuizAnswerType.yesNo ? 'Escolha uma opção' : 'Escolha apenas uma opção', textAlign: TextAlign.center, style: TextStyle( color: Colors.black .withValues(alpha: 0.55), fontWeight: FontWeight.w700, ), ), ], ), ), ), const SizedBox(height: 18), Padding( padding: const EdgeInsets.symmetric( horizontal: 20, ), child: widget.answerType == QuizAnswerType.number ? _buildNumberInput() : Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ for ( int i = 0; i < widget.answers.length; i++ ) ...[ if (i > 0) const SizedBox(height: 12), FadeSlideIn( delay: Duration( milliseconds: 60 * i, ), child: _QuizAnswerTile( answer: widget.answers[i], selected: _selected == i, onTap: () => setState( () => _selected = i, ), ), ), ], ], ), ), const SizedBox(height: 24), Padding( padding: const EdgeInsets.fromLTRB( 20, 0, 20, 0, ), child: Column( children: [ TapBounce( child: SizedBox( width: size.width * 0.62, height: 46, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: const Color( 0xFF2F9E94, ), foregroundColor: Colors.white, shape: const StadiumBorder(), textStyle: const TextStyle( fontWeight: FontWeight .w900, ), ).copyWith( animationDuration: const Duration( milliseconds: 180, ), splashFactory: InkSparkle .splashFactory, overlayColor: WidgetStateProperty.resolveWith< Color? >((states) { if (states .contains( WidgetState .pressed, )) { return Colors .white .withValues( alpha: 0.14, ); } if (states.contains( WidgetState .hovered, ) || states.contains( WidgetState .focused, )) { return Colors .white .withValues( alpha: 0.08, ); } return null; }), ), onPressed: !canProceed ? null : () async { setState( () => _navigating = true, ); int nextScore = widget .currentScore; if (widget.answerType == QuizAnswerType .number) { nextScore = widget .currentScore + (_numberValue ?? 0); } else { final picked = widget .answers[_selected!]; nextScore = widget .currentScore + picked.weight; } if (widget.isFinal) { final finishedRoute = widget.nextRoute( context, nextScore, ); Navigator.of( context, ).pushReplacement( finishedRoute, ); return; } await Navigator.of( context, ).push( widget.nextRoute( context, nextScore, ), ); if (mounted) { setState( () => _navigating = false, ); } }, child: Text( widget.isFinal ? 'Concluir' : 'Avançar', ), ), ), ), const SizedBox(height: 10), TapBounce( child: 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', ), ), ), ), ], ), ), ], ), ), ), ), ), ); }, ), ), ], ), ), ], ), ); } Widget _buildNumberInput() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 150, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.70), borderRadius: BorderRadius.circular(16), border: Border.all( color: Colors.black.withValues(alpha: 0.12), width: 1.0, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 18, offset: const Offset(0, 10), ), ], ), child: TextField( controller: _numberController, keyboardType: TextInputType.number, textAlign: TextAlign.center, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2), ], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w900, color: Color(0xFF2F9E94), ), decoration: const InputDecoration( border: InputBorder.none, hintText: '0', hintStyle: TextStyle( fontSize: 24, fontWeight: FontWeight.w900, color: Colors.grey, ), contentPadding: EdgeInsets.symmetric(vertical: 20), ), onChanged: (value) { setState(() { _numberValue = int.tryParse(value); }); }, ), ), if (_numberValue != null && _numberValue! > _maxTeethCount) ...[ const SizedBox(height: 10), Text( 'Um número tão alto assim não é possível.\nO máximo é $_maxTeethCount.', textAlign: TextAlign.center, style: const TextStyle( color: Color(0xFFFF55A7), fontWeight: FontWeight.w700, fontSize: 12, ), ), ], ], ), ); } } class _QuizAnswerTile extends StatelessWidget { const _QuizAnswerTile({ required this.answer, required this.selected, required this.onTap, }); final QuizAnswer answer; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final borderColor = selected ? const Color(0xFF2F9E94) : Colors.black.withValues(alpha: 0.12); final bg = selected ? Colors.white.withValues(alpha: 0.88) : Colors.white.withValues(alpha: 0.70); return TapBounce( scale: 0.97, child: Stack( clipBehavior: Clip.none, fit: StackFit.passthrough, children: [ AnimatedContainer( duration: const Duration(milliseconds: 220), curve: Curves.easeOutCubic, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(16), border: Border.all( color: borderColor, width: selected ? 1.4 : 1.0, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 18, offset: const Offset(0, 10), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), onTap: onTap, splashFactory: InkSparkle.splashFactory, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, 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), ], Text( answer.title, textAlign: TextAlign.center, style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 15, color: Color(0xFF2F9E94), ), ), ], ), ), ), ), ), Positioned( top: 8, right: 8, child: IgnorePointer( child: AnimatedScale( scale: selected ? 1.0 : 0.0, duration: const Duration(milliseconds: 220), curve: Curves.easeOutBack, child: Container( width: 22, height: 22, decoration: const BoxDecoration( color: Color(0xFF2F9E94), shape: BoxShape.circle, ), child: const Icon( Icons.check_rounded, size: 15, color: Colors.white, ), ), ), ), ), ], ), ); } } class _QuestionReferenceImages extends StatelessWidget { const _QuestionReferenceImages({required this.paths}); final List 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, cacheWidth: 800, 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], cacheWidth: 300, 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), ), ); } }