import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:youtube_player_iframe/youtube_player_iframe.dart' as yt_iframe; import 'package:url_launcher/url_launcher.dart'; import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; import 'package:chewie/chewie.dart'; import 'dart:async'; import '../models/app_models.dart'; import '../services/i18n_service.dart'; import '../services/toast_service.dart'; import '../screens/pdf_viewer_screen.dart'; class LessonDetailSheet extends StatefulWidget { final QuickLesson lesson; final bool isCompleted; final Function(QuickLesson) onMarkComplete; const LessonDetailSheet({ super.key, required this.lesson, required this.isCompleted, required this.onMarkComplete, }); @override State createState() => _LessonDetailSheetState(); } class _LessonDetailSheetState extends State with WidgetsBindingObserver { // Controllers yt_iframe.YoutubePlayerController? _ytIframeController; VideoPlayerController? _videoPlayerController; ChewieController? _chewieController; StreamSubscription? _ytSubscription; // State flags bool _isLoading = true; bool _useExternalFallback = false; bool _playerInitialized = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initializePlayer(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (_videoPlayerController != null) { try { if (state == AppLifecycleState.paused && _videoPlayerController!.value.isPlaying) { _videoPlayerController!.pause(); } } catch (e) { print('LessonDetailSheet: erro ao pausar VideoPlayer -> $e'); } } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _ytSubscription?.cancel(); _chewieController?.dispose(); _videoPlayerController?.dispose(); super.dispose(); } // MÉTODO FALLBACK: Adicionada verificação `mounted` antes de setState void _activateExternalFallback() { if (!mounted) return; _ytSubscription?.cancel(); _ytIframeController = null; _chewieController?.dispose(); _videoPlayerController?.dispose(); _videoPlayerController = null; _chewieController = null; // ✅ CORRIGIDO: Só chama setState se o widget ainda estiver montado setState(() { _useExternalFallback = true; _isLoading = false; }); } Future _initializePlayer() async { if (_playerInitialized) return; _playerInitialized = true; if (!mounted) return; final rawUrl = widget.lesson.videoUrl; if (rawUrl == null || rawUrl.trim().isEmpty) { print('LessonDetailSheet: videoUrl vazio'); _activateExternalFallback(); return; } final videoUrl = rawUrl.trim(); String? youtubeId; try { youtubeId = yt_iframe.YoutubePlayerController.convertUrlToId(videoUrl); print('LessonDetailSheet: convertUrlToId -> $youtubeId for $videoUrl'); } catch (e) { youtubeId = null; print('LessonDetailSheet: erro convertUrlToId: $e'); } if (youtubeId != null && youtubeId.isNotEmpty) { try { // ✅ CORRIGIDO: autoPlay é passado no construtor, não dentro de params. _ytIframeController = yt_iframe.YoutubePlayerController.fromVideoId( videoId: youtubeId, autoPlay: kIsWeb, // Correção de erro: movemos para o local correto params: const yt_iframe.YoutubePlayerParams( showControls: true, showFullscreenButton: true, strictRelatedVideos: true, playsInline: true, mute: kIsWeb, ), ); _ytSubscription?.cancel(); _ytSubscription = _ytIframeController!.stream.listen((event) { if (event.error != null) { print('LessonDetailSheet: Youtube Player Runtime Error: ${event.error}. State: ${event.playerState}. Forçando fallback.'); // ✅ CORRIGIDO: Garante que o fallback seja chamado apenas se o widget estiver montado. Future.microtask(() { if(mounted) { _activateExternalFallback(); } }); } }); await Future.delayed(const Duration(milliseconds: 200)); if (mounted) { setState(() { _isLoading = false; _useExternalFallback = false; }); } } catch (e) { print('LessonDetailSheet: falha ao inicializar yt iframe controller: $e'); _activateExternalFallback(); } return; } // Tenta VideoPlayer + Chewie try { _videoPlayerController = VideoPlayerController.networkUrl(Uri.parse(videoUrl)); await _videoPlayerController!.initialize(); _chewieController = ChewieController( videoPlayerController: _videoPlayerController!, autoPlay: true, looping: false, allowFullScreen: true, aspectRatio: _videoPlayerController!.value.isInitialized ? _videoPlayerController!.value.aspectRatio : 16 / 9, placeholder: const Center( child: CircularProgressIndicator(color: Color(0xFFE94560)), ), errorBuilder: (context, errorMessage) { print('LessonDetailSheet: Chewie errorBuilder -> $errorMessage'); return const Center( child: Padding( padding: EdgeInsets.all(16.0), child: Text( 'Erro ao carregar o vídeo. Tente o botão abaixo.', style: TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ), ); }, ); if (mounted) { setState(() { _isLoading = false; _useExternalFallback = false; }); } } catch (e) { print('LessonDetailSheet: falha VideoPlayer/Chewie -> $e'); _videoPlayerController?.dispose(); _videoPlayerController = null; _chewieController?.dispose(); _chewieController = null; _activateExternalFallback(); } } Widget _buildExternalPlayerFallback() { final url = widget.lesson.videoUrl; final i18n = Provider.of(context, listen: false); return Container( height: 250, color: const Color(0xFF1A1A2E), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.videocam_off, color: Color(0xFFE94560), size: 50), const SizedBox(height: 16), const Padding( padding: EdgeInsets.symmetric(horizontal: 20.0), child: Text( 'O player nativo falhou (pode ser devido a restrições de rede ou incompatibilidade na Web). Abra o vídeo diretamente no navegador.', style: TextStyle(color: Colors.white, fontSize: 16), textAlign: TextAlign.center, ), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () async { if (url != null && url.isNotEmpty) { final uri = Uri.parse(url); await launchUrl(uri, mode: LaunchMode.externalApplication); } else { ToastService.show( message: i18n.t('URL de vídeo não disponível.'), type: ToastType.error, ); } }, icon: const Icon(Icons.open_in_new), label: const Text('Abrir no Navegador'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFE94560), foregroundColor: Colors.white, ), ), ], ), ), ); } Widget _buildVideoPlayer() { return Container( height: 250, decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFF553C9A), Color(0xFF282A52)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.vertical(top: Radius.circular(30)), ), child: _isLoading ? const Center(child: CircularProgressIndicator(color: Color(0xFFE94560))) : ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), child: Builder( builder: (context) { if (_ytIframeController != null) { return yt_iframe.YoutubePlayer( controller: _ytIframeController!, aspectRatio: 16 / 9, ); } if (_chewieController != null) { return Chewie(controller: _chewieController!); } if (_useExternalFallback) { return _buildExternalPlayerFallback(); } return const Center( child: Text( 'URL de vídeo inválida ou não disponível.', style: TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ); }, ), ), ); } Future _downloadFile(String url, String fileName) async { final i18n = Provider.of(context, listen: false); if (yt_iframe.YoutubePlayerController.convertUrlToId(url) != null) { ToastService.show( message: 'Não é possível fazer o download de vídeos do YouTube diretamente.', type: ToastType.error, ); return; } if (url.isEmpty) { ToastService.show( message: 'A URL do recurso não está disponível.', type: ToastType.error, ); return; } if (Theme.of(context).platform == TargetPlatform.android || Theme.of(context).platform == TargetPlatform.iOS) { final status = await Permission.storage.request(); if (status.isGranted) { await FlutterDownloader.enqueue( url: url, savedDir: '/storage/emulated/0/Download', fileName: fileName, showNotification: true, openFileFromNotification: true, ); ToastService.show(message: 'Download iniciado'); } else { ToastService.show( message: 'Permissão de armazenamento negada', type: ToastType.error, ); } } else { try { if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); ToastService.show(message: 'Download iniciado no navegador'); } else { ToastService.show( message: 'Não foi possível iniciar o download. Verifique a URL.', type: ToastType.error, ); } } catch (e) { ToastService.show( message: 'Ocorreu um erro ao iniciar o download: $e', type: ToastType.error, ); } } } // --- Widgets de UI --- Widget _buildLessonHeader(I18nService i18n) { return ShaderMask( shaderCallback: (bounds) => const LinearGradient( colors: [Color(0xFFE94560), Color(0xFFF07E3F)], ).createShader(bounds), child: Text( widget.lesson.titulo ?? '', style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, ), ), ); } Widget _buildLessonContent() { return Text( widget.lesson.conteudo ?? '', style: TextStyle( fontSize: 16, color: Colors.white.withOpacity(0.8), ), ); } // ✅ Mantido o uso de Expanded para garantir que a Row se ajusta Widget _buildVideoActions(I18nService i18n) { return Row( children: [ // Duração: Ocupa 1/3 do espaço Expanded( flex: 1, child: _buildDurationWidget(i18n), ), const SizedBox(width: 8), // Download: Ocupa 2/3 do espaço Expanded( flex: 2, child: _buildDownloadVideoButton(i18n), ), ], ); } Widget _buildBookSection(I18nService i18n) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 24), Text( 'Recursos Adicionais', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white.withOpacity(0.9), ), ), const SizedBox(height: 12), _buildReadBookOnlineButton(i18n), const SizedBox(height: 16), _buildDownloadBookButton(i18n), ], ); } // ✅ CORREÇÃO FINAL DE LAYOUT: Otimizado para evitar overflow vertical e horizontal Widget _buildDurationWidget(I18nService i18n) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF553C9A), Color(0xFF282A52)], begin: Alignment.centerLeft, end: Alignment.centerRight, ), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.access_time_rounded, size: 20, color: Colors.white), const SizedBox(width: 4), // Força o texto a quebrar ou a usar '...' se o espaço no Expanded for muito pequeno Flexible( child: Text( '${i18n.t('duration')}: ${widget.lesson.duracao ?? ''}', style: const TextStyle( fontSize: 14, color: Colors.white, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } Widget _buildMarkCompleteButton(I18nService i18n) { return Container( width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), gradient: widget.isCompleted ? const LinearGradient( colors: [Colors.green, Colors.greenAccent], begin: Alignment.centerLeft, end: Alignment.centerRight, ) : const LinearGradient( colors: [Color(0xFFE94560), Color(0xFFF07E3F)], begin: Alignment.centerLeft, end: Alignment.centerRight, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.4), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: ElevatedButton.icon( onPressed: widget.isCompleted ? null : () { widget.onMarkComplete(widget.lesson); Navigator.pop(context); ToastService.show(message: i18n.t('lesson_completed')); }, icon: widget.isCompleted ? const Icon(Icons.check_circle_rounded, color: Colors.white) : const Icon(Icons.check_circle_outline_rounded, color: Colors.white), label: Text( widget.isCompleted ? i18n.t('completed') : i18n.t('mark_as_complete'), style: const TextStyle(fontWeight: FontWeight.bold), ), style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.transparent, shadowColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), elevation: 0, ), ), ); } Widget _buildDownloadVideoButton(I18nService i18n) { return InkWell( onTap: () { if (widget.lesson.videoUrl != null) { _downloadFile(widget.lesson.videoUrl!, widget.lesson.titulo ?? 'video'); } else { ToastService.show( message: i18n.t('URL do vídeo não disponível.'), type: ToastType.error); } }, child: Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), gradient: const LinearGradient( colors: [Color(0xFFE94560), Color(0xFFF07E3F)], begin: Alignment.centerLeft, end: Alignment.centerRight, ), boxShadow: [ BoxShadow( color: const Color(0xFFE94560).withOpacity(0.4), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.cloud_download_rounded, color: Colors.white, size: 24), const SizedBox(width: 8), Text( i18n.t('download_video'), style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ), ); } Widget _buildReadBookOnlineButton(I18nService i18n) { return InkWell( onTap: () { if (widget.lesson.bookUrl != null && widget.lesson.bookUrl!.isNotEmpty) { if (kIsWeb) { launchUrl(Uri.parse(widget.lesson.bookUrl!), mode: LaunchMode.platformDefault); } else { Navigator.push( context, MaterialPageRoute( builder: (context) => PdfViewerScreen(pdfUrl: widget.lesson.bookUrl!), ), ); } } else { ToastService.show( message: i18n.t('URL do livro não disponível.'), type: ToastType.error, ); } }, child: Container( height: 60, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), gradient: const LinearGradient( colors: [Color(0xFF553C9A), Color(0xFF4D2C8E)], begin: Alignment.centerLeft, end: Alignment.centerRight, ), boxShadow: [ BoxShadow( color: const Color(0xFF553C9A).withOpacity(0.4), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Center( child: Text( i18n.t('read_book_online'), style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), ); } Widget _buildDownloadBookButton(I18nService i18n) { return InkWell( onTap: () { if (widget.lesson.bookUrl != null) { _downloadFile(widget.lesson.bookUrl!, widget.lesson.titulo ?? 'livro'); } else { ToastService.show( message: i18n.t('URL do livro não disponível.'), type: ToastType.error); } }, child: Container( height: 60, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), gradient: const LinearGradient( colors: [Color(0xFF4D2C8E), Color(0xFF553C9A)], begin: Alignment.centerLeft, end: Alignment.centerRight, ), boxShadow: [ BoxShadow( color: const Color(0xFF4D2C8E).withOpacity(0.4), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Center( child: Text( i18n.t('download_book'), style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), ); } // Método build principal @override Widget build(BuildContext context) { final i18n = Provider.of(context); // O SingleChildScrollView evita que o conteúdo transborde verticalmente return SingleChildScrollView( child: Container( decoration: const BoxDecoration( color: Color(0xFF1A1A2E), borderRadius: BorderRadius.vertical(top: Radius.circular(30)), ), padding: const EdgeInsets.only(bottom: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ _buildVideoPlayer(), Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLessonHeader(i18n), const SizedBox(height: 8), _buildLessonContent(), const SizedBox(height: 24), _buildVideoActions(i18n), const SizedBox(height: 24), _buildBookSection(i18n), const SizedBox(height: 32), _buildMarkCompleteButton(i18n), ], ), ), ], ), ), ); } }