From e8afe36cd2fb6183bf6f98bf9686751a5001caca Mon Sep 17 00:00:00 2001 From: 240405 <240405@epvc.pt> Date: Tue, 26 May 2026 22:00:07 +0100 Subject: [PATCH] =?UTF-8?q?Interface=20V=C3=ADdeos=20educativos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reports/problems/problems-report.html | 663 ++++++++++++++++ lib/screens/video_screen.dart | 705 +++++++++++++++++- pubspec.lock | 16 +- pubspec.yaml | 1 + 4 files changed, 1336 insertions(+), 49 deletions(-) create mode 100644 android/build/reports/problems/problems-report.html diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..e56bba0 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/lib/screens/video_screen.dart b/lib/screens/video_screen.dart index a2b8127..8e0678a 100644 --- a/lib/screens/video_screen.dart +++ b/lib/screens/video_screen.dart @@ -1,20 +1,157 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:lottie/lottie.dart'; +import 'package:video_player/video_player.dart'; -class VideoScreen extends StatelessWidget { +// Video data structure - easily editable for future updates +class VideoData { + final int id; + final String title; + final String description; + final String videoPath; + + VideoData({ + required this.id, + required this.title, + required this.description, + required this.videoPath, + }); +} + +// List of all videos - edit titles and descriptions here +final List videoList = [ + VideoData( + id: 1, + title: 'Episódio 1', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_01.mp4', + ), + VideoData( + id: 2, + title: 'Episódio 2', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_02.mp4', + ), + VideoData( + id: 3, + title: 'Episódio 3', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_03.mp4', + ), + VideoData( + id: 4, + title: 'Episódio 4', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_04.mp4', + ), + VideoData( + id: 5, + title: 'Episódio 5', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_05.mp4', + ), + VideoData( + id: 6, + title: 'Episódio 6', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_06.mp4', + ), + VideoData( + id: 7, + title: 'Episódio 7', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_07.mp4', + ), + VideoData( + id: 8, + title: 'Episódio 8', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_08.mp4', + ), + VideoData( + id: 9, + title: 'Episódio 9', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_09.mp4', + ), + VideoData( + id: 10, + title: 'Episódio 10', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_10.mp4', + ), + VideoData( + id: 11, + title: 'Episódio 11', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_11.mp4', + ), + VideoData( + id: 12, + title: 'Episódio 12', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_12.mp4', + ), + VideoData( + id: 13, + title: 'Episódio 13', + description: 'Aprenda sobre saúde bucal neste episódio', + videoPath: 'assets/videos/episodio_13.mp4', + ), +]; + +// Cache for video controllers to avoid re-initializing +final Map _videoControllerCache = {}; + +class VideoScreen extends StatefulWidget { const VideoScreen({super.key}); static const Color _teal = Color(0xFF2F9E94); static const Color _accentPink = Color(0xFFFF55A7); + @override + State createState() => _VideoScreenState(); +} + +class _VideoScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + List _filteredVideos = videoList; + + @override + void initState() { + super.initState(); + _filteredVideos = videoList; + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _searchController.dispose(); + // Dispose all cached controllers + for (var controller in _videoControllerCache.values) { + controller.dispose(); + } + _videoControllerCache.clear(); + super.dispose(); + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredVideos = videoList + .where((video) => video.title.toLowerCase().contains(query)) + .toList(); + }); + } + @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); return Scaffold( appBar: AppBar( - backgroundColor: _teal, + backgroundColor: VideoScreen._teal, foregroundColor: Colors.white, elevation: 0, title: const Text( @@ -58,50 +195,70 @@ class VideoScreen extends StatelessWidget { ), ), SafeArea( - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 480), - child: Padding( - padding: const EdgeInsets.all(24), - child: Container( - padding: const EdgeInsets.all(24), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Search bar + Container( decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.black.withValues(alpha: 0.08), - ), - ), - 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, - ), + color: Colors.white.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 8, + offset: const Offset(0, 2), ), ], ), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Pesquisar vídeos...', + prefixIcon: const Icon( + Icons.search, + color: VideoScreen._teal, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + ), + ), ), - ), + const SizedBox(height: 16), + // Video grid + Expanded( + child: _filteredVideos.isEmpty + ? Center( + child: Text( + 'Nenhum vídeo encontrado', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black.withValues(alpha: 0.6), + ), + ), + ) + : GridView.builder( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.85, + ), + itemCount: _filteredVideos.length, + itemBuilder: (context, index) { + return _VideoButton( + video: _filteredVideos[index], + ); + }, + ), + ), + ], ), ), ), @@ -110,3 +267,469 @@ class VideoScreen extends StatelessWidget { ); } } + +class _VideoButton extends StatefulWidget { + const _VideoButton({required this.video}); + + final VideoData video; + + @override + State<_VideoButton> createState() => _VideoButtonState(); +} + +class _VideoButtonState extends State<_VideoButton> { + VideoPlayerController? _controller; + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _initializeVideo(); + } + + Future _initializeVideo() async { + // Check if controller exists in cache + if (_videoControllerCache.containsKey(widget.video.videoPath)) { + _controller = _videoControllerCache[widget.video.videoPath]; + await _controller!.seekTo(const Duration(seconds: 2)); + await _controller!.pause(); + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + return; + } + + // Create new controller and cache it + _controller = VideoPlayerController.asset(widget.video.videoPath); + try { + await _controller!.initialize(); + await _controller!.seekTo(const Duration(seconds: 2)); + await _controller!.pause(); + _videoControllerCache[widget.video.videoPath] = _controller!; + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isInitialized = false; + }); + } + } + } + + @override + void dispose() { + // Don't dispose cached controllers, they will be disposed when screen is disposed + if (_controller != null && + !_videoControllerCache.containsKey(widget.video.videoPath)) { + _controller!.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + elevation: 8, + shadowColor: Colors.black.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(16), + color: Colors.white, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _showVideoPlayer(context, widget.video), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 80, + decoration: BoxDecoration( + color: const Color(0xFFFFE6F1), + borderRadius: BorderRadius.circular(12), + ), + child: _isInitialized && _controller != null + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: _controller!.value.size.width, + height: _controller!.value.size.height, + child: VideoPlayer(_controller!), + ), + ), + ) + : const Center( + child: Icon( + Icons.play_circle_fill_rounded, + size: 48, + color: VideoScreen._accentPink, + ), + ), + ), + const SizedBox(height: 10), + Text( + widget.video.title, + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 14, + color: VideoScreen._teal, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + widget.video.description, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.black.withValues(alpha: 0.6), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + } + + void _showVideoPlayer(BuildContext context, VideoData video) { + // Dispose the cached controller to avoid codec conflict with dialog controller + if (_videoControllerCache.containsKey(video.videoPath)) { + _videoControllerCache[video.videoPath]!.dispose(); + _videoControllerCache.remove(video.videoPath); + } + _controller = null; + showDialog( + context: context, + builder: (context) => _VideoPlayerDialog(video: video), + ); + } +} + +class _VideoPlayerDialog extends StatefulWidget { + const _VideoPlayerDialog({required this.video}); + + final VideoData video; + + @override + State<_VideoPlayerDialog> createState() => _VideoPlayerDialogState(); +} + +class _VideoPlayerDialogState extends State<_VideoPlayerDialog> { + late VideoPlayerController _controller; + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _initializeVideo(); + } + + Future _initializeVideo() async { + _controller = VideoPlayerController.asset(widget.video.videoPath); + try { + await _controller.initialize(); + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isInitialized = false; + }); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e'))); + } + } + } + + @override + void dispose() { + _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: [ + // Video player + if (_isInitialized) + AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + else + const AspectRatio( + aspectRatio: 16 / 9, + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + VideoScreen._accentPink, + ), + ), + ), + ), + // Video controls with close button + if (_isInitialized) + _VideoControls( + controller: _controller, + videoPath: widget.video.videoPath, + onClose: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _VideoControls extends StatefulWidget { + const _VideoControls({ + required this.controller, + required this.videoPath, + required this.onClose, + }); + + final VideoPlayerController controller; + final String videoPath; + final VoidCallback onClose; + + @override + State<_VideoControls> createState() => _VideoControlsState(); +} + +class _VideoControlsState extends State<_VideoControls> { + @override + void initState() { + super.initState(); + widget.controller.addListener(_onControllerUpdate); + } + + @override + void dispose() { + widget.controller.removeListener(_onControllerUpdate); + super.dispose(); + } + + void _onControllerUpdate() { + if (mounted) { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + color: VideoScreen._accentPink.withValues(alpha: 0.15), + child: Row( + children: [ + // Control buttons + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Progress bar + VideoProgressIndicator( + widget.controller, + allowScrubbing: true, + colors: const VideoProgressColors( + playedColor: VideoScreen._teal, + bufferedColor: Colors.white54, + backgroundColor: Colors.white24, + ), + ), + const SizedBox(height: 4), + // Control buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon( + widget.controller.value.isPlaying + ? Icons.pause + : Icons.play_arrow, + color: Colors.white, + ), + onPressed: () { + if (widget.controller.value.isPlaying) { + widget.controller.pause(); + } else { + widget.controller.play(); + } + }, + ), + IconButton( + icon: const Icon(Icons.fullscreen, color: Colors.white), + onPressed: () { + // Dispose the cached controller to avoid codec conflict with fullscreen controller + if (_videoControllerCache.containsKey( + widget.videoPath, + )) { + _videoControllerCache[widget.videoPath]!.dispose(); + _videoControllerCache.remove(widget.videoPath); + } + Navigator.of(context).pop(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => _FullscreenVideoPlayer( + videoPath: widget.videoPath, + ), + fullscreenDialog: true, + ), + ); + }, + ), + ], + ), + ], + ), + ), + // Close button + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: widget.onClose, + ), + ], + ), + ); + } +} + +class _FullscreenVideoPlayer extends StatefulWidget { + const _FullscreenVideoPlayer({required this.videoPath}); + + final String videoPath; + + @override + State<_FullscreenVideoPlayer> createState() => _FullscreenVideoPlayerState(); +} + +class _FullscreenVideoPlayerState extends State<_FullscreenVideoPlayer> { + late VideoPlayerController _controller; + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + _initializeVideo(); + } + + Future _initializeVideo() async { + _controller = VideoPlayerController.asset(widget.videoPath); + try { + await _controller.initialize(); + _controller.addListener(_onControllerUpdate); + if (mounted) { + setState(() { + _isInitialized = true; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isInitialized = false; + }); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Erro ao carregar vídeo: $e'))); + } + } + } + + void _onControllerUpdate() { + if (mounted) { + setState(() {}); + } + } + + @override + void dispose() { + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + _controller.removeListener(_onControllerUpdate); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: _isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + VideoScreen._accentPink, + ), + ), + ), + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isInitialized) + FloatingActionButton( + heroTag: 'play_pause', + backgroundColor: VideoScreen._teal, + onPressed: () { + if (_controller.value.isPlaying) { + _controller.pause(); + } else { + _controller.play(); + } + }, + child: Icon( + _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white, + ), + ), + if (_isInitialized) const SizedBox(height: 16), + FloatingActionButton( + heroTag: 'exit_fullscreen', + backgroundColor: VideoScreen._accentPink, + onPressed: () => Navigator.of(context).pop(), + child: const Icon(Icons.fullscreen_exit, color: Colors.white), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 1dec370..3df9986 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -516,18 +516,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -713,10 +713,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f4a57e9..362acdf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ flutter: - lottie/ - assets/ - assets/mockup_images/ + - assets/videos/ flutter_launcher_icons: android: true