Atualização do quiz e videos(incompleto)

This commit is contained in:
Carlos Correia
2026-05-26 16:21:11 +01:00
parent ea009af0d3
commit e292256a98
59 changed files with 351 additions and 405 deletions

View File

@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:lottie/lottie.dart';
import 'dart:async';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import 'dart:math' as math;
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
@@ -413,8 +412,8 @@ class _RiskArcGauge extends StatelessWidget {
builder: (context, value, _) {
final shown = (value * 100).round();
return SizedBox(
width: 178,
height: 94,
width: 120,
height: 60,
child: Stack(
alignment: Alignment.center,
children: [
@@ -424,7 +423,7 @@ class _RiskArcGauge extends StatelessWidget {
),
),
Positioned(
bottom: 8,
bottom: 4,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -432,17 +431,17 @@ class _RiskArcGauge extends StatelessWidget {
'$shown%',
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontSize: 20,
fontWeight: FontWeight.w900,
height: 1,
),
),
const SizedBox(height: 4),
const SizedBox(height: 2),
Text(
'Risco de Má Oclusão',
'',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.92),
fontSize: 9,
fontSize: 8,
fontWeight: FontWeight.w900,
),
),
@@ -464,10 +463,10 @@ class _RiskArcGaugePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(16, 12, size.width - 32, size.height * 1.55);
final rect = Rect.fromLTWH(10, 8, size.width - 20, size.height * 1.7);
const startAngle = math.pi;
const sweepAngle = math.pi;
final strokeWidth = size.width * 0.14;
final strokeWidth = size.width * 0.12;
final backgroundPaint = Paint()
..color = Colors.white.withValues(alpha: 0.72)
@@ -652,7 +651,7 @@ class _VideoLibraryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final item = VideoScreen.library.isEmpty ? null : VideoScreen.library.first;
const Object? item = null;
return Material(
color: Colors.white,
@@ -685,7 +684,9 @@ class _VideoLibraryCard extends StatelessWidget {
child: Stack(
fit: StackFit.expand,
children: [
_VideoThumbnail(url: item.url),
Container(
color: const Color(0xFF2F9E94).withValues(alpha: 0.14),
),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -767,27 +768,6 @@ class _VideoLibraryCard extends StatelessWidget {
}
}
class _VideoThumbnail extends StatelessWidget {
const _VideoThumbnail({required this.url});
final String url;
@override
Widget build(BuildContext context) {
final id = YoutubePlayer.convertUrlToId(url);
final thumb = id == null ? null : 'https://img.youtube.com/vi/$id/0.jpg';
if (thumb == null) {
return Container(color: Colors.white.withValues(alpha: 0.12));
}
return Image.network(
thumb,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Container(color: Colors.white.withValues(alpha: 0.12)),
);
}
}
class _PerfilTab extends StatefulWidget {
const _PerfilTab({
required this.selectedChildIndex,

View File

@@ -36,6 +36,7 @@ class QuizQuestionScreen extends StatefulWidget {
this.isFinal = false,
this.showBackButton = false,
this.answerType = QuizAnswerType.text,
this.questionImagePaths = const [],
});
final String title;
@@ -47,6 +48,7 @@ class QuizQuestionScreen extends StatefulWidget {
final bool isFinal;
final bool showBackButton;
final QuizAnswerType answerType;
final List<String> questionImagePaths;
@override
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
@@ -136,6 +138,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
const SizedBox(height: 6),
if (widget.questionImagePaths.isNotEmpty) ...[
const SizedBox(height: 6),
_QuestionReferenceImages(
paths: widget.questionImagePaths,
),
const SizedBox(height: 10),
],
Text(
widget.question,
textAlign: TextAlign.center,
@@ -312,6 +321,28 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
),
),
],
const SizedBox(height: 10),
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'),
),
),
],
),
),
@@ -425,6 +456,28 @@ class _QuizAnswerTile extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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),
],
Row(
children: [
Expanded(
@@ -437,38 +490,8 @@ class _QuizAnswerTile extends StatelessWidget {
),
),
),
AnimatedRotation(
turns: selected ? 0.5 : 0.0,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
child: Icon(
Icons.expand_more_rounded,
color: Colors.black.withValues(alpha: 0.55),
),
),
],
),
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(
answer.description,
style: TextStyle(
color: Colors.black.withValues(alpha: 0.72),
fontWeight: FontWeight.w600,
height: 1.25,
),
),
),
crossFadeState: selected
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 220),
firstCurve: Curves.easeIn,
secondCurve: Curves.easeOut,
sizeCurve: Curves.easeOutCubic,
),
],
),
),
@@ -477,3 +500,56 @@ class _QuizAnswerTile extends StatelessWidget {
);
}
}
class _QuestionReferenceImages extends StatelessWidget {
const _QuestionReferenceImages({required this.paths});
final List<String> 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,
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],
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),
),
);
}
}

View File

@@ -25,32 +25,28 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//1.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_a.png',
imagePath: 'assets/mockup_images/1.jpeg',
),
QuizAnswer(
//2.jpeg
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_b.png',
imagePath: 'assets/mockup_images/2.jpeg',
),
QuizAnswer(
//3.jpeg
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha à face do seu filho/a',
weight: 2,
imagePath: 'assets/images/face_d.png',
imagePath: 'assets/mockup_images/3.jpeg',
),
],
),
@@ -58,36 +54,24 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
id: 2,
title: 'Quiz 2/26',
question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
'Qual das seguintes imagens se assemelha à posição boca do seu filho/a habitualmente?',
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//4.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_a.png',
imagePath: 'assets/mockup_images/4.jpeg',
),
QuizAnswer(
//5.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_b.png',
),
QuizAnswer(
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth_d.png',
imagePath: 'assets/mockup_images/5.png',
),
],
),
@@ -99,32 +83,20 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//8.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_a.png',
imagePath: 'assets/mockup_images/8.jpeg',
),
QuizAnswer(
//9.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_b.png',
),
QuizAnswer(
title: 'Opção C',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
weight: 2,
imagePath: 'assets/images/dark_circles_d.png',
imagePath: 'assets/mockup_images/9.png',
),
],
),
@@ -136,48 +108,55 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//6.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_a.png',
imagePath: 'assets/mockup_images/6.jpeg',
),
QuizAnswer(
//7.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_b.png',
),
QuizAnswer(
title: 'Opção C',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
weight: 2,
imagePath: 'assets/images/chin_d.png',
imagePath: 'assets/mockup_images/7.png',
),
],
),
QuizQuestion(
id: 5,
title: 'Quiz 5/26',
question: 'Quantos dentes tem o seu filho/a em cima na boca?',
answerType: QuizAnswerType.number,
answers: const [],
),
QuizQuestion(
id: 6,
title: 'Quiz 6/26',
question: 'Quantos dentes tem o seu filho/a em baixo na boca?',
answerType: QuizAnswerType.number,
answers: const [],
question:
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//14.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/mockup_images/14.jpeg',
),
QuizAnswer(
//15.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/mockup_images/15.png',
),
QuizAnswer(
//16.png
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/mockup_images/16.png',
),
],
),
QuizQuestion(
id: 7,
@@ -187,32 +166,28 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//10.png
title: 'Opção A',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_a.png',
imagePath: 'assets/mockup_images/10.png',
),
QuizAnswer(
//11.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_b.png',
imagePath: 'assets/mockup_images/11.png',
),
QuizAnswer(
//13.png
title: 'Opção C',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_c.png',
),
QuizAnswer(
title: 'Opção D',
description:
'Selecione se a imagem se assemelha à boca do seu filho/a',
weight: 2,
imagePath: 'assets/images/mouth2_d.png',
imagePath: 'assets/mockup_images/13.png',
),
],
),
@@ -224,32 +199,45 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
//17.png
title: 'Opção A',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_a.png',
imagePath: 'assets/mockup_images/17.png',
),
QuizAnswer(
//18.jpeg
title: 'Opção B',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_b.png',
imagePath: 'assets/mockup_images/18.jpeg',
),
],
),
QuizQuestion(
id: 8,
title: 'Quiz 8/26',
question:
'Qual das seguintes imagens se assemelha ao freio do seu filho/a?',
answerType: QuizAnswerType.image,
answers: const [
QuizAnswer(
title: 'Opção C',
//19.jpeg
title: 'Opção A',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_c.png',
imagePath: 'assets/mockup_images/19.jpeg',
),
QuizAnswer(
title: 'Opção D',
//20.png
title: 'Opção B',
description:
'Selecione se a imagem se assemelha ao freio do seu filho/a',
weight: 2,
imagePath: 'assets/images/frenulum_d.png',
imagePath: 'assets/mockup_images/20.png',
),
],
),
@@ -652,6 +640,7 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
isFinal: isLastQuestion,
showBackButton: _currentQuestionIndex > 0,
answerType: currentQuestion.answerType,
questionImagePaths: currentQuestion.questionImagePaths,
onFinished: isLastQuestion
? () {
Navigator.of(context).pushReplacement(
@@ -675,6 +664,8 @@ class QuizQuestion {
final String question;
final List<QuizAnswer> answers;
final QuizAnswerType answerType;
// Reference images shown ABOVE the question text (visualization only).
final List<String> questionImagePaths;
QuizQuestion({
required this.id,
@@ -682,5 +673,41 @@ class QuizQuestion {
required this.question,
required this.answers,
this.answerType = QuizAnswerType.text,
this.questionImagePaths = const [],
});
}
// Helper: returns the asset path for a numbered mockup image (0..27).
// Use in QuizAnswer.imagePath or QuizQuestion.questionImagePaths.
String mockup(int n) => 'assets/mockup_images/$n${_mockupExt[n] ?? '.png'}';
const Map<int, String> _mockupExt = {
0: '.png',
1: '.jpeg',
2: '.jpeg',
3: '.jpeg',
4: '.jpeg',
5: '.png',
6: '.jpeg',
7: '.png',
8: '.jpeg',
9: '.png',
10: '.png',
11: '.png',
12: '.png',
13: '.png',
14: '.jpeg',
15: '.png',
16: '.png',
17: '.png',
18: '.jpeg',
19: '.jpeg',
20: '.png',
21: '.jpeg',
22: '.png',
23: '.jpeg',
24: '.png',
25: '.jpg',
26: '.png',
27: '.png',
};

View File

@@ -62,17 +62,20 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
scope.startsWith('${userId}_')) {
final childId = scope.substring(userId.length + 1).trim();
if (childId.isNotEmpty) {
await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('children')
.doc(childId)
.set({
'lastScore': widget.finalScore,
'lastMaxScore': widget.maxScore,
'lastQuizAt': FieldValue.serverTimestamp(),
}, SetOptions(merge: true))
.catchError((_) {});
// Fire-and-forget: avoid blocking UI on Firestore (may hang offline).
unawaited(
FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('children')
.doc(childId)
.set({
'lastScore': widget.finalScore,
'lastMaxScore': widget.maxScore,
'lastQuizAt': FieldValue.serverTimestamp(),
}, SetOptions(merge: true))
.catchError((_) {}),
);
}
}
}
@@ -222,8 +225,11 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
fontWeight: FontWeight.w900,
),
),
onPressed: () =>
Navigator.of(context).popUntil((r) => r.isFirst),
onPressed: () async {
await _saveResultFuture;
if (!context.mounted) return;
Navigator.of(context).popUntil((r) => r.isFirst);
},
child: const Text('Avançar'),
),
),

View File

@@ -2,14 +2,6 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
class VideoItem {
const VideoItem({required this.title, required this.url});
final String title;
final String url;
}
class VideoScreen extends StatelessWidget {
const VideoScreen({super.key});
@@ -17,10 +9,6 @@ class VideoScreen extends StatelessWidget {
static const Color _teal = Color(0xFF2F9E94);
static const Color _accentPink = Color(0xFFFF55A7);
static const List<VideoItem> library = [
VideoItem(title: 'Como escovar da maneira certa', url: 'https://www.youtube.com/watch?v=uH8dBWkD__0'),
];
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
@@ -29,9 +17,8 @@ class VideoScreen extends StatelessWidget {
backgroundColor: _teal,
foregroundColor: Colors.white,
elevation: 0,
title: const Text(
'Vídeos Educativos',
'Videos Educativos',
style: TextStyle(fontWeight: FontWeight.w900),
),
),
@@ -44,10 +31,7 @@ class VideoScreen extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFE6F1),
Color(0xFFFFC9DF),
],
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
),
),
),
@@ -74,63 +58,48 @@ class VideoScreen extends StatelessWidget {
),
),
SafeArea(
child: Align(
alignment: Alignment.topCenter,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
constraints: const BoxConstraints(maxWidth: 480),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
),
child: const Row(
children: [
Icon(Icons.play_circle_fill_rounded, color: _accentPink),
SizedBox(width: 10),
Expanded(
child: Text(
'Vídeos Educativos',
style: TextStyle(
fontWeight: FontWeight.w900,
color: _accentPink,
),
),
),
],
),
padding: const EdgeInsets.all(24),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.black.withValues(alpha: 0.08),
),
const SizedBox(height: 14),
Expanded(
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.92,
children: library.map((item) {
final videoId = YoutubePlayer.convertUrlToId(item.url);
final thumb = videoId == null ? null : 'https://img.youtube.com/vi/$videoId/0.jpg';
return _VideoCard(
title: item.title,
thumbnailUrl: thumb,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => VideoPlayerScreen(url: item.url, title: item.title),
),
);
},
);
}).toList(),
),
child: const Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_circle_fill_rounded,
size: 64,
color: _accentPink,
),
),
],
SizedBox(height: 12),
Text(
'Em breve',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w900,
color: _teal,
),
),
SizedBox(height: 8),
Text(
'Os videos educativos serao disponibilizados em breve.',
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
),
),
),
@@ -141,175 +110,3 @@ class VideoScreen extends StatelessWidget {
);
}
}
class _VideoCard extends StatelessWidget {
const _VideoCard({required this.title, required this.thumbnailUrl, required this.onTap});
final String title;
final String? thumbnailUrl;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.white.withValues(alpha: 0.80),
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
child: Stack(
fit: StackFit.expand,
children: [
if (thumbnailUrl != null)
Image.network(
thumbnailUrl!,
fit: BoxFit.cover,
)
else
Container(color: Colors.black.withValues(alpha: 0.06)),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.10),
Colors.black.withValues(alpha: 0.42),
],
),
),
),
const Align(
alignment: Alignment.center,
child: Icon(Icons.play_circle_fill_rounded, size: 54, color: Colors.white),
),
],
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 12),
child: Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w900,
color: Color(0xFF2F9E94),
),
),
),
],
),
),
);
}
}
class VideoPlayerScreen extends StatefulWidget {
const VideoPlayerScreen({super.key, required this.url, required this.title});
final String url;
final String title;
@override
State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
static const Color _teal = Color(0xFF2F9E94);
static const Color _bg = Color(0xFFFFE6F1);
late final YoutubePlayerController _controller;
@override
void initState() {
super.initState();
final id = YoutubePlayer.convertUrlToId(widget.url);
_controller = YoutubePlayerController(
initialVideoId: id ?? '',
flags: const YoutubePlayerFlags(
autoPlay: true,
mute: false,
enableCaption: true,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final hasVideo = _controller.initialVideoId.isNotEmpty;
return Scaffold(
appBar: AppBar(
backgroundColor: _teal,
foregroundColor: Colors.white,
elevation: 0,
title: Text(
widget.title,
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
body: Container(
color: _bg,
child: SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AspectRatio(
aspectRatio: 16 / 9,
child: hasVideo
? YoutubePlayer(
controller: _controller,
showVideoProgressIndicator: true,
progressIndicatorColor: Colors.white,
)
: Container(
color: Colors.black.withValues(alpha: 0.10),
child: const Center(
child: Text(
'Link de vídeo inválido',
style: TextStyle(fontWeight: FontWeight.w800),
),
),
),
),
),
const SizedBox(height: 14),
Text(
widget.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
color: Color(0xFFFF55A7),
),
),
const SizedBox(height: 6),
Text(
'Assista ao vídeo e aprenda mais sobre saúde bucal.',
style: TextStyle(
color: Colors.black.withValues(alpha: 0.70),
fontWeight: FontWeight.w600,
),
),
],
),
),
),
);
}
}