diff --git a/assets/referencia/video_player_caro.text b/assets/referencia/video_player_caro.text new file mode 100644 index 0000000..4f25d93 --- /dev/null +++ b/assets/referencia/video_player_caro.text @@ -0,0 +1,75 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:appinio_video_player/appinio_video_player.dart'; + +class VideoScreenNew extends StatefulWidget { + final dynamic videoUrl; + const VideoScreenNew({Key? key, required this.videoUrl}) : super(key: key); + + @override + _VideoScreenNewState createState() => _VideoScreenNewState(); +} + +class _VideoScreenNewState extends State { + late VideoPlayerController _videoPlayerController; + + late CustomVideoPlayerController _customVideoPlayerController; + late CustomVideoPlayerWebController _customVideoPlayerWebController; + + final CustomVideoPlayerSettings _customVideoPlayerSettings = + const CustomVideoPlayerSettings(); + + late CustomVideoPlayerWebSettings _customVideoPlayerWebSettings; + + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + _videoPlayerController = VideoPlayerController.network( + widget.videoUrl, + )..initialize().then((value) => setState(() {})); + _customVideoPlayerController = CustomVideoPlayerController( + context: context, + videoPlayerController: _videoPlayerController, + customVideoPlayerSettings: _customVideoPlayerSettings, + ); + + _customVideoPlayerWebSettings = CustomVideoPlayerWebSettings( + src: widget.videoUrl, + ); + + _customVideoPlayerWebController = CustomVideoPlayerWebController( + webVideoPlayerSettings: _customVideoPlayerWebSettings, + ); + + _controller = VideoPlayerController.network(widget.videoUrl) + ..initialize().then((_) { + setState(() {}); + }); + } + + @override + void dispose() { + _customVideoPlayerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + backgroundColor: Colors.black, + child: SafeArea( + child: Center( + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: CustomVideoPlayerWeb( + customVideoPlayerWebController: _customVideoPlayerWebController, + ), + ), + ), + ), + ); + } +} diff --git a/assets/referencia/video_player_live.text b/assets/referencia/video_player_live.text new file mode 100644 index 0000000..43c6ac8 --- /dev/null +++ b/assets/referencia/video_player_live.text @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:chewie/chewie.dart'; + +class VideoPlayerLive extends StatefulWidget { + final String url; + + const VideoPlayerLive({Key? key, required this.url}) : super(key: key); + + @override + _VideoPlayerLiveState createState() => _VideoPlayerLiveState(); +} + +class _VideoPlayerLiveState extends State { + late ChewieController _chewieController; + bool _isFullScreen = false; + + @override + void initState() { + super.initState(); + _initializePlayer(); + } + + @override + void dispose() { + super.dispose(); + _chewieController.dispose(); + } + + void _initializePlayer() { + final videoPlayerController = VideoPlayerController.network(widget.url); + _chewieController = ChewieController( + videoPlayerController: videoPlayerController, + autoPlay: false, + looping: false, + showControls: true, + allowFullScreen: true, + allowMuting: true, + allowPlaybackSpeedChanging: false, + aspectRatio: videoPlayerController.value.aspectRatio, + customControls: CupertinoControls( + backgroundColor: Color.fromARGB(66, 0, 0, 0), + iconColor: Color.fromARGB(255, 202, 202, 202), + showPlayButton: true), + ); + } + + void _toggleFullScreen() { + if (!_isFullScreen) { + _chewieController.enterFullScreen(); + } else { + _chewieController.exitFullScreen(); + } + setState(() { + _isFullScreen = !_isFullScreen; + }); + } + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.black, + child: Center( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: _toggleFullScreen, + child: AspectRatio( + aspectRatio: + _chewieController.videoPlayerController.value.aspectRatio, + child: Chewie( + controller: _chewieController, + ), + ), + ), + ), + ), + ); + } +} diff --git a/assets/referencia/video_thumbnail.dart b/assets/referencia/video_thumbnail.dart new file mode 100644 index 0000000..6c4fa73 --- /dev/null +++ b/assets/referencia/video_thumbnail.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class VideoScreenThumbnail extends StatefulWidget { + final dynamic video; + const VideoScreenThumbnail({Key? key, required this.video}) : super(key: key); + + @override + State createState() => _VideoScreenThumbnailState(); +} + +class _VideoScreenThumbnailState extends State { + late VideoPlayerController _controllerVideo; + late bool startedPlaying; + + @override + void initState() { + _controllerVideo = VideoPlayerController.network(widget.video); + _controllerVideo.initialize(); + super.initState(); + started(); + _controllerVideo.setVolume(0); + _controllerVideo.pause(); + } + + Future started() async { + var renderized = false; + double height = 0; + + await Future.delayed(const Duration(seconds: 2), () { + height = _controllerVideo.value.size.height; + if (height > 0) renderized = true; + }); + + return renderized; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: started(), + builder: ((context, AsyncSnapshot snapshot) { + if (snapshot.data ?? false) { + return Stack( + alignment: AlignmentDirectional.center, + children: [ + VideoPlayer(_controllerVideo), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: const [ + SizedBox(height: 30), + ], + ), + ], + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }), + ); + } + + @override + void dispose() { + super.dispose(); + var video = _controllerVideo; + video.dispose(); + } +} diff --git a/lib/pages/videos/gestor_videos_page.dart b/lib/pages/videos/gestor_videos_page.dart index 36cf511..6d95bcf 100644 --- a/lib/pages/videos/gestor_videos_page.dart +++ b/lib/pages/videos/gestor_videos_page.dart @@ -7,6 +7,7 @@ import 'package:nethive_neo/theme/theme.dart'; import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/widgets/premium_button.dart'; import 'package:nethive_neo/pages/videos/widgets/premium_upload_dialog.dart'; +import 'package:nethive_neo/pages/videos/widgets/video_player_dialog.dart'; import 'package:gap/gap.dart'; class GestorVideosPage extends StatefulWidget { @@ -582,10 +583,31 @@ class _GestorVideosPageState extends State { } void _playVideo(MediaFileModel video) { - // TODO: Implementar reproductor de video - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Reproduciendo: ${video.title ?? video.fileName}'), + // Verificar que el video tenga URL + if (video.fileUrl == null || video.fileUrl!.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error: El video no tiene URL válida'), + backgroundColor: Color(0xFFFF2D2D), + ), + ); + return; + } + + // Abrir diálogo con reproductor + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => VideoPlayerDialog( + video: video, + onPlaybackCompleted: () { + // Incrementar reproducciones cuando termine el video + final provider = Provider.of(context, listen: false); + provider.incrementReproduccion(video.mediaFileId); + }, + onClose: () { + // Opcional: realizar alguna acción al cerrar + }, ), ); } diff --git a/lib/pages/videos/widgets/video_player_dialog.dart b/lib/pages/videos/widgets/video_player_dialog.dart new file mode 100644 index 0000000..b2c7411 --- /dev/null +++ b/lib/pages/videos/widgets/video_player_dialog.dart @@ -0,0 +1,358 @@ +import 'package:flutter/material.dart'; +import 'package:chewie/chewie.dart'; +import 'package:video_player/video_player.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/models/media/media_models.dart'; +import 'package:gap/gap.dart'; + +class VideoPlayerDialog extends StatefulWidget { + final MediaFileModel video; + final VoidCallback? onPlaybackCompleted; + final VoidCallback? onClose; + + const VideoPlayerDialog({ + Key? key, + required this.video, + this.onPlaybackCompleted, + this.onClose, + }) : super(key: key); + + @override + State createState() => _VideoPlayerDialogState(); +} + +class _VideoPlayerDialogState extends State { + late VideoPlayerController _videoPlayerController; + ChewieController? _chewieController; + bool _isLoading = true; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _initializePlayer(); + } + + Future _initializePlayer() async { + try { + // Inicializar video player con URL del video + _videoPlayerController = VideoPlayerController.network( + widget.video.fileUrl!, + ); + + await _videoPlayerController.initialize(); + + // Configurar Chewie (controles de video) + _chewieController = ChewieController( + videoPlayerController: _videoPlayerController, + autoPlay: true, + looping: false, + showControls: true, + materialProgressColors: ChewieProgressColors( + playedColor: const Color(0xFF4EC9F5), + handleColor: const Color(0xFFFFB733), + backgroundColor: Colors.grey.shade800, + bufferedColor: Colors.grey.shade600, + ), + autoInitialize: true, + allowFullScreen: true, + allowMuting: true, + showControlsOnInitialize: true, + errorBuilder: (context, errorMessage) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Color(0xFFFF2D2D), + size: 60, + ), + const Gap(16), + Text( + 'Error al cargar el video', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Gap(8), + Text( + errorMessage, + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ); + + // Listener para detectar fin del video + _videoPlayerController.addListener(() { + if (_videoPlayerController.value.position == + _videoPlayerController.value.duration) { + widget.onPlaybackCompleted?.call(); + } + }); + + setState(() { + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _errorMessage = 'Error al cargar el video: $e'; + }); + print('Error inicializando video player: $e'); + } + } + + @override + void dispose() { + _videoPlayerController.dispose(); + _chewieController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final isMobile = screenSize.width <= 800; + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isMobile ? 8 : 40), + child: Container( + width: isMobile ? screenSize.width : screenSize.width * 0.8, + constraints: BoxConstraints( + maxWidth: 1200, + maxHeight: screenSize.height * 0.9, + ), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.5), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + _buildHeader(context, isMobile), + + // Video Player + Flexible( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: _buildVideoPlayer(context), + ), + ), + ], + ), + ), + ); + } + + Widget _buildHeader(BuildContext context, bool isMobile) { + return Container( + padding: EdgeInsets.all(isMobile ? 12 : 16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryBackground, + AppTheme.of(context).secondaryBackground, + ], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + border: Border( + bottom: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF4EC9F5), + Color(0xFFFFB733), + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.play_circle_filled, + color: Color(0xFF0B0B0D), + size: 20, + ), + ), + const Gap(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.video.title ?? widget.video.fileName, + style: AppTheme.of(context).bodyText1.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: isMobile ? 14 : 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (widget.video.fileDescription != null && + widget.video.fileDescription!.isNotEmpty && + !isMobile) + Text( + widget.video.fileDescription!, + style: AppTheme.of(context).bodyText2.override( + fontFamily: 'Poppins', + color: AppTheme.of(context).tertiaryText, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + icon: Icon( + Icons.close, + color: AppTheme.of(context).primaryText, + ), + onPressed: () { + widget.onClose?.call(); + Navigator.of(context).pop(); + }, + tooltip: 'Cerrar', + ), + ], + ), + ); + } + + Widget _buildVideoPlayer(BuildContext context) { + if (_isLoading) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: AppTheme.of(context).primaryColor, + ), + const Gap(16), + Text( + 'Cargando video...', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ); + } + + if (_errorMessage != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Color(0xFFFF2D2D), + size: 60, + ), + const Gap(16), + Text( + 'Error al cargar el video', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Gap(8), + Text( + _errorMessage!, + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const Gap(24), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF2D2D), + foregroundColor: Colors.white, + ), + child: const Text('Cerrar'), + ), + ], + ), + ), + ); + } + + if (_chewieController == null) { + return Center( + child: Text( + 'No se pudo inicializar el reproductor', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ); + } + + return ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + child: Chewie( + controller: _chewieController!, + ), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b030075..49284f0 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,11 +10,13 @@ import file_picker import file_selector_macos import flutter_secure_storage_macos import flutter_tts +import package_info_plus import path_provider_foundation import shared_preferences_foundation import sign_in_with_apple import url_launcher_macos -import wakelock_macos +import video_player_avfoundation +import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) @@ -22,9 +24,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index b37b739..c87e7be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.5.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -101,10 +109,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "3bc4fbae99a9f39dc9c04ba70ad2a2fb902dbd3d3e36ad8ab3a53f0dcbf311be" + sha256: "4d9554a8f87cc2dc6575dfd5ad20a4375015a29edd567fd6733febe6365e2566" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.11.3" chip_list: dependency: "direct main" description: @@ -201,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.17.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" dio: dependency: "direct main" description: @@ -853,6 +869,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.1" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: "direct main" description: @@ -917,6 +949,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.5" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" platform: dependency: transitive description: @@ -1350,26 +1390,26 @@ packages: dependency: "direct main" description: name: video_player - sha256: "868a139229acb5018d22aded3eb9cb4767ff43a8216573c086b6c535a4957481" + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.10.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: e7de6fabe5d96048cd8f4d710f25c3df84bb3cab8b22da6c082bd8f39e316984 + sha256: a8dc4324f67705de057678372bedb66cd08572fe7c495605ac68c5f503324a39 url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.8.15" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "90468226c8687adf7b567d9bb42c25588783c4d30509af1fbd663b2dd049f700" + sha256: f9a780aac57802b2892f93787e5ea53b5f43cc57dc107bee9436458365be71cd url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.8.4" video_player_platform_interface: dependency: transitive description: @@ -1394,38 +1434,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" - wakelock: + wakelock_plus: dependency: transitive description: - name: wakelock - sha256: "78bad4822be81d37e7bc34b6990da7dface2a445255cd37c6f053b51a4ccdb3b" + name: wakelock_plus + sha256: "775c50f226ab43ff859b479acc73f11c0744bf345a782e83355c4d25df758803" url: "https://pub.dev" source: hosted - version: "0.4.0" - wakelock_macos: + version: "1.3.0" + wakelock_plus_platform_interface: dependency: transitive description: - name: wakelock_macos - sha256: "73581e5d9ed2dd1ba951375c30e63f0eb8c58d7d6286ae9ddf927b88f2aea8d9" + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" url: "https://pub.dev" source: hosted - version: "0.1.0+3" - wakelock_platform_interface: - dependency: transitive - description: - name: wakelock_platform_interface - sha256: d0a8a1c02af68077db5df1e0f5e2b745f7b1f2cdcc48e3e0b6f8f4dcc349050e - url: "https://pub.dev" - source: hosted - version: "0.2.1+3" - wakelock_web: - dependency: transitive - description: - name: wakelock_web - sha256: "06b0033d5421712138e7fa482ff5c6280fe12e0a41c40c3fe8fda2c007eb4348" - url: "https://pub.dev" - source: hosted - version: "0.2.0+3" + version: "1.3.0" web: dependency: transitive description: @@ -1498,6 +1522,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yet_another_json_isolate: dependency: transitive description: @@ -1507,5 +1539,5 @@ packages: source: hosted version: "1.1.1" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1b08ecf..cbf501a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,7 +74,7 @@ dependencies: flutter_animate: ^4.2.0 flutter_flow_chart: 3.2.3 video_player: ^2.6.0 - chewie: ^1.0.0 + chewie: ^1.8.0 shimmer: ^3.0.0 fl_chart: 0.69.0