Munça da UI de inicio | escovagem semanal adicionada | quantidade de videos assistidos | todos os videos na nuvem | possibilidade de continuar o video de onde parou
This commit is contained in:
@@ -7,6 +7,7 @@ import 'package:lottie/lottie.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
import '../watched_videos_prefs.dart';
|
||||
import '../widgets/app_gradients.dart';
|
||||
import '../widgets/entrance.dart';
|
||||
import '../widgets/tap_bounce.dart';
|
||||
@@ -97,19 +98,19 @@ final List<VideoData> videoList = [
|
||||
id: 11,
|
||||
title: 'Episódio 11',
|
||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
||||
videoPath: 'assets/videos/episodio_11.mp4',
|
||||
youtubeId: '6sYoBUjks_I',
|
||||
),
|
||||
VideoData(
|
||||
id: 12,
|
||||
title: 'Episódio 12',
|
||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
||||
videoPath: 'assets/videos/episodio_12.mp4',
|
||||
youtubeId: 'eznKrErQbHo',
|
||||
),
|
||||
VideoData(
|
||||
id: 13,
|
||||
title: 'Episódio 13',
|
||||
description: 'Aprenda sobre saúde bucal neste episódio',
|
||||
videoPath: 'assets/videos/episodio_13.mp4',
|
||||
youtubeId: 'VO9CNqHRdeM',
|
||||
),
|
||||
];
|
||||
|
||||
@@ -156,7 +157,20 @@ void _evictAllVideoControllers() {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showVideoPlayerDialog(BuildContext context, VideoData video) {
|
||||
/// Regista o episódio como assistido (localmente, por criança). Chamado
|
||||
/// quando um player deteta que o vídeo chegou ao fim — sem [scopeId] (nenhuma
|
||||
/// criança selecionada) não há onde guardar, por isso não faz nada.
|
||||
void markVideoWatched(String? scopeId, int videoId) {
|
||||
final scope = (scopeId ?? '').trim();
|
||||
if (scope.isEmpty) return;
|
||||
WatchedVideosPrefs.markWatched(scope, videoId);
|
||||
}
|
||||
|
||||
Future<void> showVideoPlayerDialog(
|
||||
BuildContext context,
|
||||
VideoData video, {
|
||||
String? scopeId,
|
||||
}) {
|
||||
if (video.youtubeId != null) {
|
||||
if (video.youtubeId!.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
@@ -165,17 +179,23 @@ Future<void> showVideoPlayerDialog(BuildContext context, VideoData video) {
|
||||
return Future.value();
|
||||
}
|
||||
return Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(builder: (context) => _YoutubePlayerPage(video: video)),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _YoutubePlayerPage(video: video, scopeId: scopeId),
|
||||
),
|
||||
);
|
||||
}
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => _VideoPlayerDialog(video: video),
|
||||
builder: (context) => _VideoPlayerDialog(video: video, scopeId: scopeId),
|
||||
);
|
||||
}
|
||||
|
||||
class VideoScreen extends StatefulWidget {
|
||||
const VideoScreen({super.key});
|
||||
const VideoScreen({super.key, this.scopeId});
|
||||
|
||||
/// Identifica a criança selecionada (`'${uid}_${childId}'`), usado para
|
||||
/// guardar localmente quais episódios ela já assistiu até ao fim.
|
||||
final String? scopeId;
|
||||
|
||||
static const Color _teal = Color(0xFF2F9E94);
|
||||
static const Color _accentPink = Color(0xFFFF55A7);
|
||||
@@ -329,6 +349,7 @@ class _VideoScreenState extends State<VideoScreen> {
|
||||
),
|
||||
child: _VideoButton(
|
||||
video: _filteredVideos[index],
|
||||
scopeId: widget.scopeId,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -509,9 +530,10 @@ class _VideoThumbnailState extends State<VideoThumbnail> {
|
||||
}
|
||||
|
||||
class _VideoButton extends StatelessWidget {
|
||||
const _VideoButton({required this.video});
|
||||
const _VideoButton({required this.video, this.scopeId});
|
||||
|
||||
final VideoData video;
|
||||
final String? scopeId;
|
||||
|
||||
void _showVideoPlayer(BuildContext context, VideoData video) {
|
||||
if (video.youtubeId == null) {
|
||||
@@ -519,7 +541,7 @@ class _VideoButton extends StatelessWidget {
|
||||
// em dialog, que precisa dos seus próprios decoders de vídeo/áudio.
|
||||
_evictAllVideoControllers();
|
||||
}
|
||||
showVideoPlayerDialog(context, video);
|
||||
showVideoPlayerDialog(context, video, scopeId: scopeId);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -588,9 +610,10 @@ class _VideoButton extends StatelessWidget {
|
||||
/// 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});
|
||||
const _YoutubePlayerPage({required this.video, this.scopeId});
|
||||
|
||||
final VideoData video;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
State<_YoutubePlayerPage> createState() => _YoutubePlayerPageState();
|
||||
@@ -599,6 +622,7 @@ class _YoutubePlayerPage extends StatefulWidget {
|
||||
class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
with WidgetsBindingObserver {
|
||||
late final YoutubePlayerController _controller;
|
||||
bool _markedWatched = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -607,9 +631,18 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
initialVideoId: widget.video.youtubeId!,
|
||||
flags: const YoutubePlayerFlags(autoPlay: true, mute: false),
|
||||
);
|
||||
_controller.addListener(_onControllerValueChanged);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
void _onControllerValueChanged() {
|
||||
if (_markedWatched) return;
|
||||
if (_controller.value.playerState == PlayerState.ended) {
|
||||
_markedWatched = true;
|
||||
markVideoWatched(widget.scopeId, widget.video.id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
final isLandscape =
|
||||
@@ -628,6 +661,7 @@ class _YoutubePlayerPageState extends State<_YoutubePlayerPage>
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
SystemChrome.restoreSystemUIOverlays();
|
||||
_controller.removeListener(_onControllerValueChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -716,9 +750,10 @@ class _CoverYoutubePlayer extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _VideoPlayerDialog extends StatefulWidget {
|
||||
const _VideoPlayerDialog({required this.video});
|
||||
const _VideoPlayerDialog({required this.video, this.scopeId});
|
||||
|
||||
final VideoData video;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
State<_VideoPlayerDialog> createState() => _VideoPlayerDialogState();
|
||||
@@ -727,6 +762,7 @@ class _VideoPlayerDialog extends StatefulWidget {
|
||||
class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
||||
late VideoPlayerController _controller;
|
||||
bool _isInitialized = false;
|
||||
bool _markedWatched = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -738,6 +774,7 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
||||
_controller = VideoPlayerController.asset(widget.video.videoPath!);
|
||||
try {
|
||||
await _controller.initialize();
|
||||
_controller.addListener(_onControllerValueChanged);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInitialized = true;
|
||||
@@ -755,8 +792,19 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onControllerValueChanged() {
|
||||
if (_markedWatched) return;
|
||||
final value = _controller.value;
|
||||
if (!value.isInitialized || value.duration == Duration.zero) return;
|
||||
if (value.position >= value.duration - const Duration(milliseconds: 300)) {
|
||||
_markedWatched = true;
|
||||
markVideoWatched(widget.scopeId, widget.video.id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_onControllerValueChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -802,6 +850,8 @@ class _VideoPlayerDialogState extends State<_VideoPlayerDialog> {
|
||||
_VideoControls(
|
||||
controller: _controller,
|
||||
videoPath: widget.video.videoPath!,
|
||||
videoId: widget.video.id,
|
||||
scopeId: widget.scopeId,
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
@@ -817,11 +867,15 @@ class _VideoControls extends StatefulWidget {
|
||||
const _VideoControls({
|
||||
required this.controller,
|
||||
required this.videoPath,
|
||||
required this.videoId,
|
||||
required this.onClose,
|
||||
this.scopeId,
|
||||
});
|
||||
|
||||
final VideoPlayerController controller;
|
||||
final String videoPath;
|
||||
final int videoId;
|
||||
final String? scopeId;
|
||||
final VoidCallback onClose;
|
||||
|
||||
@override
|
||||
@@ -926,6 +980,8 @@ class _VideoControlsState extends State<_VideoControls> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _FullscreenVideoPlayer(
|
||||
videoPath: widget.videoPath,
|
||||
videoId: widget.videoId,
|
||||
scopeId: widget.scopeId,
|
||||
),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
@@ -949,9 +1005,15 @@ class _VideoControlsState extends State<_VideoControls> {
|
||||
}
|
||||
|
||||
class _FullscreenVideoPlayer extends StatefulWidget {
|
||||
const _FullscreenVideoPlayer({required this.videoPath});
|
||||
const _FullscreenVideoPlayer({
|
||||
required this.videoPath,
|
||||
required this.videoId,
|
||||
this.scopeId,
|
||||
});
|
||||
|
||||
final String videoPath;
|
||||
final int videoId;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
State<_FullscreenVideoPlayer> createState() => _FullscreenVideoPlayerState();
|
||||
@@ -960,6 +1022,7 @@ class _FullscreenVideoPlayer extends StatefulWidget {
|
||||
class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> {
|
||||
late VideoPlayerController _controller;
|
||||
bool _isInitialized = false;
|
||||
bool _markedWatched = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -994,6 +1057,16 @@ class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> {
|
||||
}
|
||||
|
||||
void _onControllerUpdate() {
|
||||
if (!_markedWatched) {
|
||||
final value = _controller.value;
|
||||
if (value.isInitialized &&
|
||||
value.duration != Duration.zero &&
|
||||
value.position >=
|
||||
value.duration - const Duration(milliseconds: 300)) {
|
||||
_markedWatched = true;
|
||||
markVideoWatched(widget.scopeId, widget.videoId);
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user