Atulização do quiz | Porcentagem na base de dados | Telas de Contrato com o usuario

This commit is contained in:
Carlos Correia
2026-07-13 22:28:01 +01:00
parent 824fdb8089
commit 4fcf3a475a
23 changed files with 2061 additions and 369 deletions

View File

@@ -319,7 +319,10 @@ class _VideoScreenState extends State<VideoScreen> {
),
),
const SizedBox(height: 16),
// Video grid
// Lista vertical (um vídeo abaixo do outro, rolando para
// baixo), mas cada card em si é horizontal — miniatura à
// esquerda, título/descrição à direita — em vez da antiga
// grelha de 2 colunas com cards verticais.
Expanded(
child: _filteredVideos.isEmpty
? Center(
@@ -332,15 +335,10 @@ class _VideoScreenState extends State<VideoScreen> {
),
),
)
: GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.85,
),
: ListView.separated(
itemCount: _filteredVideos.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 12),
itemBuilder: (context, index) {
return FadeSlideIn(
delay: Duration(
@@ -546,112 +544,127 @@ class _VideoButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return TapBounce(
scale: 0.95,
scale: 0.97,
child: Material(
elevation: 10,
shadowColor: Colors.black.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(18),
color: Colors.white,
child: InkWell(
elevation: 8,
shadowColor: Colors.black.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(18),
onTap: () => _showVideoPlayer(context, video),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Stack(
fit: StackFit.expand,
children: [
ColoredBox(
color: const Color(0xFFFFE6F1),
child: VideoThumbnail(video: video, borderRadius: 0),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.32),
],
stops: const [0.55, 1.0],
),
color: Colors.white,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: () => _showVideoPlayer(context, video),
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 130,
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child: Stack(
fit: StackFit.expand,
children: [
ColoredBox(
color: const Color(0xFFFFE6F1),
child: VideoThumbnail(video: video, borderRadius: 0),
),
),
Center(
child: Container(
width: 34,
height: 34,
DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow_rounded,
color: VideoScreen._accentPink,
size: 20,
),
),
),
Positioned(
left: 6,
bottom: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(999),
),
child: Text(
'EP. ${video.id}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 9.5,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.32),
],
stops: const [0.55, 1.0],
),
),
),
Center(
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow_rounded,
color: VideoScreen._accentPink,
size: 20,
),
),
),
Positioned(
left: 6,
bottom: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(999),
),
child: Text(
'EP. ${video.id}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 9.5,
),
),
),
),
],
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
video.title,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
color: VideoScreen._teal,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
video.description,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
const SizedBox(height: 8),
Text(
video.title,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 14,
color: VideoScreen._teal,
const SizedBox(width: 4),
Icon(
Icons.chevron_right_rounded,
color: Colors.black.withValues(alpha: 0.3),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
video.description,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.6),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
),
),
);
}
}
@@ -733,24 +746,73 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
if (!didPop) _controller.toggleFullScreenMode();
},
child: Scaffold(
backgroundColor: Colors.black,
backgroundColor: value.isFullScreen
? Colors.black
: const Color(0xFFFAFAF7),
appBar: value.isFullScreen
? null
: AppBar(
backgroundColor: VideoScreen._teal,
foregroundColor: Colors.white,
elevation: 0,
title: Text(
widget.video.title,
style: const TextStyle(fontWeight: FontWeight.w900),
: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Container(
decoration: const BoxDecoration(
gradient: kAppBarGradient,
),
child: AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
title: Text(
widget.video.title,
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
),
),
// Em vez de centrar o vídeo num quadro 16:9 no meio de um ecrã
// preto (o que sobrava como barras pretas em cima/baixo no
// modo retrato), o vídeo fica encostado ao topo, a preencher a
// largura toda, com o título e a descrição do episódio logo
// abaixo — sem nenhum espaço preto sobrando. O modo tela cheia
// (paisagem) continua a recortar o vídeo para preencher tudo.
body: value.isFullScreen
? _CoverYoutubePlayer(controller: _controller)
: Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: YoutubePlayer(controller: _controller),
: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: YoutubePlayer(controller: _controller),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.video.title,
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 19,
color: VideoScreen._teal,
),
),
const SizedBox(height: 8),
Text(
widget.video.description,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black.withValues(alpha: 0.65),
height: 1.4,
),
),
],
),
),
],
),
),
),