Videos na nuvem | atualização de desing (Appbar) | mudança no quiz

This commit is contained in:
Carlos Correia
2026-07-08 22:23:38 +01:00
parent a7b6d35026
commit 67b580778a
12 changed files with 885 additions and 660 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -11,8 +12,9 @@ import '../widgets/entrance.dart';
import '../widgets/tap_bounce.dart';
// Video data structure - easily editable for future updates.
// Episódios 1-7 tocam via YouTube (não listado); preencha youtubeId ao subir
// cada vídeo. Episódios 8-13 continuam embutidos no app (assets/videos).
// Episódios 1-10 tocam via YouTube (não listado). Episódios 11-13 ainda não
// foram subidos ao YouTube e continuam embutidos no app (assets/videos) até
// lá — depois de subidos, troque videoPath por youtubeId e apague o mp4.
class VideoData {
final int id;
final String title;
@@ -35,61 +37,61 @@ final List<VideoData> videoList = [
id: 1,
title: 'Episódio 1',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'PJ58CZv4ECw',
),
VideoData(
id: 2,
title: 'Episódio 2',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'y4_kWmZtAtg',
),
VideoData(
id: 3,
title: 'Episódio 3',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'nD75Y5PuKTo',
),
VideoData(
id: 4,
title: 'Episódio 4',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'yvFllWYeuLw',
),
VideoData(
id: 5,
title: 'Episódio 5',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'DnhUa-T8_Ps',
),
VideoData(
id: 6,
title: 'Episódio 6',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'zKt_iwkrjvo',
),
VideoData(
id: 7,
title: 'Episódio 7',
description: 'Aprenda sobre saúde bucal neste episódio',
youtubeId: '', // TODO: colar o ID do vídeo do YouTube (não listado)
youtubeId: 'NpmQ2brap5A',
),
VideoData(
id: 8,
title: 'Episódio 8',
description: 'Aprenda sobre saúde bucal neste episódio',
videoPath: 'assets/videos/episodio_08.mp4',
youtubeId: 'Wj3KYw9pBi0',
),
VideoData(
id: 9,
title: 'Episódio 9',
description: 'Aprenda sobre saúde bucal neste episódio',
videoPath: 'assets/videos/episodio_09.mp4',
youtubeId: 'bOm9t61cT_U',
),
VideoData(
id: 10,
title: 'Episódio 10',
description: 'Aprenda sobre saúde bucal neste episódio',
videoPath: 'assets/videos/episodio_10.mp4',
youtubeId: 'fAitMizbcms',
),
VideoData(
id: 11,
@@ -162,9 +164,8 @@ Future<void> showVideoPlayerDialog(BuildContext context, VideoData video) {
).showSnackBar(const SnackBar(content: Text('Vídeo ainda não disponível')));
return Future.value();
}
return showDialog<void>(
context: context,
builder: (context) => _YoutubePlayerDialog(video: video),
return Navigator.of(context).push<void>(
MaterialPageRoute(builder: (context) => _YoutubePlayerPage(video: video)),
);
}
return showDialog<void>(
@@ -577,16 +578,26 @@ class _VideoButton extends StatelessWidget {
}
}
class _YoutubePlayerDialog extends StatefulWidget {
const _YoutubePlayerDialog({required this.video});
/// Página cheia (não diálogo) para o player do YouTube. O botão de tela cheia
/// do próprio player força a rotação para paisagem via
/// `SystemChrome.setPreferredOrientations`; um `Dialog` de largura fixa não
/// se adapta a essa rotação e causa overflow gráfico.
///
/// Em paisagem/tela cheia, o vídeo é recortado ("cover", como Instagram/
/// TikTok) para preencher o ecrã todo sem barras pretas — em vez de manter
/// a proporção 16:9 do YouTube e sobrar espaço vazio quando o ecrã tem uma
/// proporção mais larga que 16:9 (ex.: a maioria dos telemóveis atuais).
class _YoutubePlayerPage extends StatefulWidget {
const _YoutubePlayerPage({required this.video});
final VideoData video;
@override
State<_YoutubePlayerDialog> createState() => _YoutubePlayerDialogState();
State<_YoutubePlayerPage> createState() => _YoutubePlayerPageState();
}
class _YoutubePlayerDialogState extends State<_YoutubePlayerDialog> {
class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
with WidgetsBindingObserver {
late final YoutubePlayerController _controller;
@override
@@ -596,48 +607,110 @@ class _YoutubePlayerDialogState extends State<_YoutubePlayerDialog> {
initialVideoId: widget.video.youtubeId!,
flags: const YoutubePlayerFlags(autoPlay: true, mute: false),
);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeMetrics() {
final isLandscape =
PlatformDispatcher.instance.views.first.physicalSize.width >
PlatformDispatcher.instance.views.first.physicalSize.height;
if (isLandscape == _controller.value.isFullScreen) return;
_controller.updateValue(_controller.value.copyWith(isFullScreen: isLandscape));
if (isLandscape) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
} else {
SystemChrome.restoreSystemUIOverlays();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
SystemChrome.restoreSystemUIOverlays();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
color: VideoScreen._accentPink.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: VideoScreen._accentPink, width: 3),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(21),
child: SizedBox(
width: size.width * 0.9,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
YoutubePlayer(controller: _controller),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: VideoScreen._accentPink.withValues(alpha: 0.15),
alignment: Alignment.centerRight,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
return ValueListenableBuilder<YoutubePlayerValue>(
valueListenable: _controller,
builder: (context, value, _) {
return PopScope(
canPop: !value.isFullScreen,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) _controller.toggleFullScreenMode();
},
child: Scaffold(
backgroundColor: Colors.black,
appBar: value.isFullScreen
? null
: AppBar(
backgroundColor: VideoScreen._teal,
foregroundColor: Colors.white,
elevation: 0,
title: Text(
widget.video.title,
style: const TextStyle(fontWeight: FontWeight.w900),
),
),
),
],
body: value.isFullScreen
? _CoverYoutubePlayer(controller: _controller)
: Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: YoutubePlayer(controller: _controller),
),
),
),
);
},
);
}
}
/// Preenche todo o espaço disponível recortando o vídeo (mantém a proporção
/// 16:9 real do YouTube, mas amplia e corta o excesso nas laterais ou em
/// cima/baixo em vez de deixar barras pretas). O player é sempre desenhado
/// no seu tamanho real (nunca reduzido a uma caixa minúscula), por isso o
/// recorte fica nítido, sem perda de qualidade.
class _CoverYoutubePlayer extends StatelessWidget {
const _CoverYoutubePlayer({required this.controller});
final YoutubePlayerController controller;
static const double _videoAspectRatio = 16 / 9;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final parentAspectRatio = constraints.maxWidth / constraints.maxHeight;
final double boxWidth;
final double boxHeight;
if (parentAspectRatio > _videoAspectRatio) {
boxWidth = constraints.maxWidth;
boxHeight = boxWidth / _videoAspectRatio;
} else {
boxHeight = constraints.maxHeight;
boxWidth = boxHeight * _videoAspectRatio;
}
return ClipRect(
child: OverflowBox(
maxWidth: boxWidth,
maxHeight: boxHeight,
child: SizedBox(
width: boxWidth,
height: boxHeight,
child: YoutubePlayer(
controller: controller,
aspectRatio: _videoAspectRatio,
),
),
),
),
),
);
},
);
}
}