Atualização do quiz e videos(incompleto)
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 185 KiB |
BIN
assets/mockup_images/0.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/mockup_images/1.jpeg
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
assets/mockup_images/10.png
Normal file
|
After Width: | Height: | Size: 744 KiB |
BIN
assets/mockup_images/11.png
Normal file
|
After Width: | Height: | Size: 669 KiB |
BIN
assets/mockup_images/12.png
Normal file
|
After Width: | Height: | Size: 736 KiB |
BIN
assets/mockup_images/13.png
Normal file
|
After Width: | Height: | Size: 870 KiB |
BIN
assets/mockup_images/14.jpeg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
assets/mockup_images/15.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/mockup_images/16.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/mockup_images/17.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/mockup_images/18.jpeg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
assets/mockup_images/19.jpeg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
assets/mockup_images/2.jpeg
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
assets/mockup_images/20.png
Normal file
|
After Width: | Height: | Size: 800 KiB |
BIN
assets/mockup_images/21.jpeg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
assets/mockup_images/22.png
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
assets/mockup_images/23.jpeg
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
assets/mockup_images/24.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/mockup_images/25.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/mockup_images/26.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/mockup_images/27.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/mockup_images/3.jpeg
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
assets/mockup_images/4.jpeg
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
assets/mockup_images/5.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/mockup_images/6.jpeg
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
assets/mockup_images/7.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/mockup_images/8.jpeg
Normal file
|
After Width: | Height: | Size: 312 KiB |
BIN
assets/mockup_images/9.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets/videos/episodio_01.mp4
Normal file
BIN
assets/videos/episodio_02.mp4
Normal file
BIN
assets/videos/episodio_03.mp4
Normal file
BIN
assets/videos/episodio_04.mp4
Normal file
BIN
assets/videos/episodio_05.mp4
Normal file
BIN
assets/videos/episodio_06.mp4
Normal file
BIN
assets/videos/episodio_07.mp4
Normal file
BIN
assets/videos/episodio_08.mp4
Normal file
BIN
assets/videos/episodio_09.mp4
Normal file
BIN
assets/videos/episodio_10.mp4
Normal file
BIN
assets/videos/episodio_11.mp4
Normal file
BIN
assets/videos/episodio_12.mp4
Normal file
BIN
assets/videos/episodio_13.mp4
Normal file
@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -413,8 +412,8 @@ class _RiskArcGauge extends StatelessWidget {
|
|||||||
builder: (context, value, _) {
|
builder: (context, value, _) {
|
||||||
final shown = (value * 100).round();
|
final shown = (value * 100).round();
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 178,
|
width: 120,
|
||||||
height: 94,
|
height: 60,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -424,7 +423,7 @@ class _RiskArcGauge extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 8,
|
bottom: 4,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -432,17 +431,17 @@ class _RiskArcGauge extends StatelessWidget {
|
|||||||
'$shown%',
|
'$shown%',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 26,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'Risco de Má Oclusão',
|
'',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.92),
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
fontSize: 9,
|
fontSize: 8,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -464,10 +463,10 @@ class _RiskArcGaugePainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
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 startAngle = math.pi;
|
||||||
const sweepAngle = math.pi;
|
const sweepAngle = math.pi;
|
||||||
final strokeWidth = size.width * 0.14;
|
final strokeWidth = size.width * 0.12;
|
||||||
|
|
||||||
final backgroundPaint = Paint()
|
final backgroundPaint = Paint()
|
||||||
..color = Colors.white.withValues(alpha: 0.72)
|
..color = Colors.white.withValues(alpha: 0.72)
|
||||||
@@ -652,7 +651,7 @@ class _VideoLibraryCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final item = VideoScreen.library.isEmpty ? null : VideoScreen.library.first;
|
const Object? item = null;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -685,7 +684,9 @@ class _VideoLibraryCard extends StatelessWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
_VideoThumbnail(url: item.url),
|
Container(
|
||||||
|
color: const Color(0xFF2F9E94).withValues(alpha: 0.14),
|
||||||
|
),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
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 {
|
class _PerfilTab extends StatefulWidget {
|
||||||
const _PerfilTab({
|
const _PerfilTab({
|
||||||
required this.selectedChildIndex,
|
required this.selectedChildIndex,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class QuizQuestionScreen extends StatefulWidget {
|
|||||||
this.isFinal = false,
|
this.isFinal = false,
|
||||||
this.showBackButton = false,
|
this.showBackButton = false,
|
||||||
this.answerType = QuizAnswerType.text,
|
this.answerType = QuizAnswerType.text,
|
||||||
|
this.questionImagePaths = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -47,6 +48,7 @@ class QuizQuestionScreen extends StatefulWidget {
|
|||||||
final bool isFinal;
|
final bool isFinal;
|
||||||
final bool showBackButton;
|
final bool showBackButton;
|
||||||
final QuizAnswerType answerType;
|
final QuizAnswerType answerType;
|
||||||
|
final List<String> questionImagePaths;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
|
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
|
||||||
@@ -136,6 +138,13 @@ class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
if (widget.questionImagePaths.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
_QuestionReferenceImages(
|
||||||
|
paths: widget.questionImagePaths,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
Text(
|
Text(
|
||||||
widget.question,
|
widget.question,
|
||||||
textAlign: TextAlign.center,
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,32 +25,28 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
|
|||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.image,
|
||||||
answers: const [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//1.jpeg
|
||||||
title: 'Opção A',
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à face do seu filho/a',
|
'Selecione se a imagem se assemelha à face do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/face_a.png',
|
imagePath: 'assets/mockup_images/1.jpeg',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//2.jpeg
|
||||||
title: 'Opção B',
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à face do seu filho/a',
|
'Selecione se a imagem se assemelha à face do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/face_b.png',
|
imagePath: 'assets/mockup_images/2.jpeg',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//3.jpeg
|
||||||
title: 'Opção C',
|
title: 'Opção C',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à face do seu filho/a',
|
'Selecione se a imagem se assemelha à face do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/face_c.png',
|
imagePath: 'assets/mockup_images/3.jpeg',
|
||||||
),
|
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -58,36 +54,24 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
|
|||||||
id: 2,
|
id: 2,
|
||||||
title: 'Quiz 2/26',
|
title: 'Quiz 2/26',
|
||||||
question:
|
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,
|
answerType: QuizAnswerType.image,
|
||||||
answers: const [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//4.jpeg
|
||||||
title: 'Opção A',
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth_a.png',
|
imagePath: 'assets/mockup_images/4.jpeg',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//5.png
|
||||||
title: 'Opção B',
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth_b.png',
|
imagePath: 'assets/mockup_images/5.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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -99,32 +83,20 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
|
|||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.image,
|
||||||
answers: const [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//8.jpeg
|
||||||
title: 'Opção A',
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
|
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/dark_circles_a.png',
|
imagePath: 'assets/mockup_images/8.jpeg',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//9.png
|
||||||
title: 'Opção B',
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
|
'Selecione se a imagem se assemelha às olheiras do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/dark_circles_b.png',
|
imagePath: 'assets/mockup_images/9.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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -136,48 +108,55 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
|
|||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.image,
|
||||||
answers: const [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//6.jpeg
|
||||||
title: 'Opção A',
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
|
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/chin_a.png',
|
imagePath: 'assets/mockup_images/6.jpeg',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//7.png
|
||||||
title: 'Opção B',
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
|
'Selecione se a imagem se assemelha ao queixo do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/chin_b.png',
|
imagePath: 'assets/mockup_images/7.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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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(
|
QuizQuestion(
|
||||||
id: 6,
|
id: 6,
|
||||||
title: 'Quiz 6/26',
|
title: 'Quiz 6/26',
|
||||||
question: 'Quantos dentes tem o seu filho/a em baixo na boca?',
|
question:
|
||||||
answerType: QuizAnswerType.number,
|
'Qual das seguintes imagens se assemelha à boca do seu filho/a?',
|
||||||
answers: const [],
|
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(
|
QuizQuestion(
|
||||||
id: 7,
|
id: 7,
|
||||||
@@ -187,32 +166,28 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
|
|||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.image,
|
||||||
answers: const [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//10.png
|
||||||
title: 'Opção A',
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth2_a.png',
|
imagePath: 'assets/mockup_images/10.png',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//11.png
|
||||||
title: 'Opção B',
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth2_b.png',
|
imagePath: 'assets/mockup_images/11.png',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//13.png
|
||||||
title: 'Opção C',
|
title: 'Opção C',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
'Selecione se a imagem se assemelha à boca do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/mouth2_c.png',
|
imagePath: 'assets/mockup_images/13.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',
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -224,32 +199,45 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
|
|||||||
answerType: QuizAnswerType.image,
|
answerType: QuizAnswerType.image,
|
||||||
answers: const [
|
answers: const [
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//17.png
|
||||||
title: 'Opção A',
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/frenulum_a.png',
|
imagePath: 'assets/mockup_images/17.png',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
|
//18.jpeg
|
||||||
title: 'Opção B',
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
||||||
weight: 2,
|
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(
|
QuizAnswer(
|
||||||
title: 'Opção C',
|
//19.jpeg
|
||||||
|
title: 'Opção A',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
imagePath: 'assets/images/frenulum_c.png',
|
imagePath: 'assets/mockup_images/19.jpeg',
|
||||||
),
|
),
|
||||||
QuizAnswer(
|
QuizAnswer(
|
||||||
title: 'Opção D',
|
//20.png
|
||||||
|
title: 'Opção B',
|
||||||
description:
|
description:
|
||||||
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
'Selecione se a imagem se assemelha ao freio do seu filho/a',
|
||||||
weight: 2,
|
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,
|
isFinal: isLastQuestion,
|
||||||
showBackButton: _currentQuestionIndex > 0,
|
showBackButton: _currentQuestionIndex > 0,
|
||||||
answerType: currentQuestion.answerType,
|
answerType: currentQuestion.answerType,
|
||||||
|
questionImagePaths: currentQuestion.questionImagePaths,
|
||||||
onFinished: isLastQuestion
|
onFinished: isLastQuestion
|
||||||
? () {
|
? () {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
@@ -675,6 +664,8 @@ class QuizQuestion {
|
|||||||
final String question;
|
final String question;
|
||||||
final List<QuizAnswer> answers;
|
final List<QuizAnswer> answers;
|
||||||
final QuizAnswerType answerType;
|
final QuizAnswerType answerType;
|
||||||
|
// Reference images shown ABOVE the question text (visualization only).
|
||||||
|
final List<String> questionImagePaths;
|
||||||
|
|
||||||
QuizQuestion({
|
QuizQuestion({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -682,5 +673,41 @@ class QuizQuestion {
|
|||||||
required this.question,
|
required this.question,
|
||||||
required this.answers,
|
required this.answers,
|
||||||
this.answerType = QuizAnswerType.text,
|
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',
|
||||||
|
};
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
|
|||||||
scope.startsWith('${userId}_')) {
|
scope.startsWith('${userId}_')) {
|
||||||
final childId = scope.substring(userId.length + 1).trim();
|
final childId = scope.substring(userId.length + 1).trim();
|
||||||
if (childId.isNotEmpty) {
|
if (childId.isNotEmpty) {
|
||||||
await FirebaseFirestore.instance
|
// Fire-and-forget: avoid blocking UI on Firestore (may hang offline).
|
||||||
|
unawaited(
|
||||||
|
FirebaseFirestore.instance
|
||||||
.collection('users')
|
.collection('users')
|
||||||
.doc(userId)
|
.doc(userId)
|
||||||
.collection('children')
|
.collection('children')
|
||||||
@@ -72,7 +74,8 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
|
|||||||
'lastMaxScore': widget.maxScore,
|
'lastMaxScore': widget.maxScore,
|
||||||
'lastQuizAt': FieldValue.serverTimestamp(),
|
'lastQuizAt': FieldValue.serverTimestamp(),
|
||||||
}, SetOptions(merge: true))
|
}, SetOptions(merge: true))
|
||||||
.catchError((_) {});
|
.catchError((_) {}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,8 +225,11 @@ class _QuizResultScreenState extends State<QuizResultScreen> {
|
|||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () =>
|
onPressed: () async {
|
||||||
Navigator.of(context).popUntil((r) => r.isFirst),
|
await _saveResultFuture;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).popUntil((r) => r.isFirst);
|
||||||
|
},
|
||||||
child: const Text('Avançar'),
|
child: const Text('Avançar'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lottie/lottie.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 {
|
class VideoScreen extends StatelessWidget {
|
||||||
const VideoScreen({super.key});
|
const VideoScreen({super.key});
|
||||||
@@ -17,10 +9,6 @@ class VideoScreen extends StatelessWidget {
|
|||||||
static const Color _teal = Color(0xFF2F9E94);
|
static const Color _teal = Color(0xFF2F9E94);
|
||||||
static const Color _accentPink = Color(0xFFFF55A7);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
@@ -29,9 +17,8 @@ class VideoScreen extends StatelessWidget {
|
|||||||
backgroundColor: _teal,
|
backgroundColor: _teal,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Vídeos Educativos',
|
'Videos Educativos',
|
||||||
style: TextStyle(fontWeight: FontWeight.w900),
|
style: TextStyle(fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -44,10 +31,7 @@ class VideoScreen extends StatelessWidget {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)],
|
||||||
Color(0xFFFFE6F1),
|
|
||||||
Color(0xFFFFC9DF),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -74,242 +58,55 @@ class VideoScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Align(
|
child: Center(
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Container(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
padding: const EdgeInsets.all(24),
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withValues(alpha: 0.65),
|
color: Colors.white.withValues(alpha: 0.85),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
|
border: Border.all(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
),
|
),
|
||||||
child: const Row(
|
),
|
||||||
|
child: const Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.play_circle_fill_rounded, color: _accentPink),
|
Icon(
|
||||||
SizedBox(width: 10),
|
Icons.play_circle_fill_rounded,
|
||||||
Expanded(
|
size: 64,
|
||||||
child: Text(
|
|
||||||
'Vídeos Educativos',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
color: _accentPink,
|
color: _accentPink,
|
||||||
),
|
),
|
||||||
),
|
SizedBox(height: 12),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
Text(
|
||||||
widget.title,
|
'Em breve',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: Color(0xFFFF55A7),
|
color: _teal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Assista ao vídeo e aprenda mais sobre saúde bucal.',
|
'Os videos educativos serao disponibilizados em breve.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black.withValues(alpha: 0.70),
|
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import firebase_core
|
|||||||
import firebase_storage
|
import firebase_storage
|
||||||
import flutter_inappwebview_macos
|
import flutter_inappwebview_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
import video_player_avfoundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
|
||||||
@@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
|
FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin"))
|
||||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
|
VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
58
pubspec.lock
@@ -121,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -360,6 +368,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -717,6 +733,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
video_player:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_player
|
||||||
|
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.11.1"
|
||||||
|
video_player_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_android
|
||||||
|
sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.5"
|
||||||
|
video_player_avfoundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_avfoundation
|
||||||
|
sha256: "9338f3ec22774f88146b22f13273a446719b1da010fd200c4d1d97802156ac58"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.9.7"
|
||||||
|
video_player_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_platform_interface
|
||||||
|
sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.7.0"
|
||||||
|
video_player_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: video_player_web
|
||||||
|
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -767,4 +823,4 @@ packages:
|
|||||||
version: "9.1.3"
|
version: "9.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.4 <4.0.0"
|
dart: ">=3.10.4 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.38.0"
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dependencies:
|
|||||||
lottie: ^3.3.1
|
lottie: ^3.3.1
|
||||||
shared_preferences: ^2.3.2
|
shared_preferences: ^2.3.2
|
||||||
youtube_player_flutter: ^9.0.0
|
youtube_player_flutter: ^9.0.0
|
||||||
|
video_player: ^2.9.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -72,6 +73,7 @@ flutter:
|
|||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
- lottie/
|
- lottie/
|
||||||
- assets/
|
- assets/
|
||||||
|
- assets/mockup_images/
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
|
|||||||