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'; // 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: VideoScreen._teal, foregroundColor: Colors.white, elevation: 0, title: const Text( 'Videos Educativos', style: TextStyle(fontWeight: FontWeight.w900), ), ), body: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], ), ), ), ), Positioned( left: -size.width * 0.40, bottom: -size.width * 0.45, child: IgnorePointer( child: SizedBox( width: size.width * 1.05, height: size.width * 1.05, child: Transform.rotate( angle: 35 * math.pi / 180, child: Opacity( opacity: 0.95, child: Lottie.asset( 'lottie/Liquid waves.json', fit: BoxFit.cover, repeat: true, ), ), ), ), ), ), SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // Search bar Container( decoration: BoxDecoration( 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], ); }, ), ), ], ), ), ), ], ), ); } } 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), ), ], ), ); } }