import 'package:flutter/material.dart'; import 'package:skywood/global/client/play_client.dart'; import 'package:skywood/main/lib_file.dart'; import 'package:skywood/pages/home/players/page/drama_esp_page.dart'; import 'package:skywood/utils/models/drama/skywood_detail.dart'; import 'package:skywood/utils/models/drama/skywood_drama.dart'; import 'package:skywood/utils/models/drama/skywood_episode.dart'; import 'package:skywood/utils/tools/widgets/sky_common_widget.dart'; import 'package:video_player/video_player.dart'; /// 视频播放器 class SkyPlayerView extends StatefulWidget { // final String videoUrl; final bool autoPlay; final SkywoodDetail? detailBean; final SkywoodDrama dramaBean; final int? currentEpisode; const SkyPlayerView({ super.key, // required this.videoUrl, this.autoPlay = true, this.detailBean, required this.dramaBean, this.currentEpisode, }); @override State createState() => _VideoPlayerWidgetState(); } class _VideoPlayerWidgetState extends State { late VideoPlayerController _controller; bool _controllerCreated = false; bool _isPlaying = false; bool _isInitialized = false; bool _isSeeking = false; String? _errorMessage; Duration _currentPosition = Duration.zero; Duration _totalDuration = Duration.zero; bool _isLiked = false; SkywoodEpisode? _currentEpisode; @override void initState() { super.initState(); _initializePlayer(); } // @override // void didUpdateWidget(SkyPlayerView oldWidget) { // super.didUpdateWidget(oldWidget); // if (oldWidget.videoUrl != widget.videoUrl) { // _disposeController(); // _initializePlayer(); // } // } Future _initializePlayer() async { setState(() { _isInitialized = false; _isPlaying = false; _errorMessage = null; _currentPosition = Duration.zero; _totalDuration = Duration.zero; _isLiked = widget.dramaBean.isCollect ?? false; }); int _esp = widget.currentEpisode ?? 0; String videoUrl = ''; if (widget.detailBean != null) { if (widget.detailBean!.episodeList.isNotEmpty) { videoUrl = widget.detailBean!.episodeList[_esp].videoUrl; _currentEpisode = widget.detailBean!.episodeList[_esp]; } } final controller = VideoPlayerController.networkUrl( Uri.parse(videoUrl), ); _controller = controller; _controllerCreated = true; controller.addListener(_handlePlayerValueChanged); try { await controller.initialize(); if (!mounted || controller != _controller || !_controllerCreated) return; if (widget.autoPlay) { await controller.play(); } _syncPlayerValue(); } catch (error) { if (!mounted || controller != _controller) return; setState(() { _errorMessage = error.toString(); _isInitialized = false; _isPlaying = false; }); } } void _handlePlayerValueChanged() { if (!mounted || !_controllerCreated) return; _syncPlayerValue(); } void _syncPlayerValue() { final value = _controller.value; final nextPosition = _isSeeking ? _currentPosition : value.position; final nextDuration = value.duration; final nextPlaying = value.isPlaying; setState(() { _isInitialized = value.isInitialized; _isPlaying = nextPlaying; _currentPosition = nextPosition; _totalDuration = nextDuration; }); } void _disposeController() { if (!_controllerCreated) return; _controller.removeListener(_handlePlayerValueChanged); _controller.dispose(); _controllerCreated = false; } @override void dispose() { _disposeController(); super.dispose(); } @override Widget build(BuildContext context) { if (_errorMessage != null) { return _buildErrorView(); } return _isInitialized && _controllerCreated ? _buildVideoContent() : _buildLoadingView(); } Widget _buildVideoContent() { return Stack( fit: StackFit.expand, children: [ FittedBox( fit: BoxFit.cover, child: SizedBox( width: _controller.value.size.width, height: _controller.value.size.height, child: VideoPlayer(_controller), ), ), Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: _togglePlay, child: Container(), ), ), if (widget.detailBean != null) Positioned(bottom: 0, left: 0, right: 0, child: _buildBottomView()), if (!_isPlaying) Center( child: GestureDetector( onTap: _togglePlay, child: Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.white.withAlpha(204), shape: BoxShape.circle, ), child: const Icon( Icons.play_arrow_rounded, color: Colors.black, size: 40, ), ), ), ), ], ); } Widget _buildLoadingView() { return Container( color: Colors.black, child: Center( child: HelpImg.asset(ConstImg.loading, width: 110), ), ); } Widget _buildErrorView() { return Container( color: Colors.black, alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 24), child: SkyText( text: 'Video failed to load', color: Colors.white.withAlpha(180), fontSize: 14, ), ); } void _togglePlay() { if (!_isInitialized || !_controllerCreated) return; if (_controller.value.isPlaying) { _controller.pause(); } else { _controller.play(); } } void _collectionDrama() { if (_currentEpisode == null) return; if (_isLiked) { PlayClient.cancelCollect(shortPlayId: _currentEpisode!.shortPlayId).then((value) { setState(() { _isLiked = value; }); }); } else { PlayClient.collect( shortPlayId: _currentEpisode!.shortPlayId, videoId: _currentEpisode!.shortPlayVideoId, ).then((value) { setState(() { _isLiked = value; }); }); } } Widget _buildBottomView() { SkywoodDetail detail = widget.detailBean!; final int currentEpisode = _getCurrentEpisode(detail); final int totalEpisode = _episodeTotal(detail); return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ InkWell( onTap: () => _collectionDrama(), child: HelpImg.asset( _isLiked ? ConstImg.liked : ConstImg.unlike, width: 40, height: 40 ), ), const SizedBox(height: 24), Container( height: 36, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Colors.white.withAlpha(30), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ HelpImg.asset(ConstImg.playLt, width: 17, height: 17), const SizedBox(width: 10), SkyText( text: 'EP.$currentEpisode', fontSize: 13, color: Colors.white, ), SkyText( text: ' / EP.${detail.episodeList.length}', fontSize: 13, color: Colors.white.withAlpha(126), ), ], ), Icon(Icons.arrow_forward_ios, color: Colors.white, size: 13), ], ), ).addInkWell( onTap: () { // HelpNav.showBottom( // DramaEspPage(detail: detail, currentEpisode: currentEpisode), // ); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) { return SizedBox( height: 520, child: DramaEspPage( detail: detail, currentEpisode: currentEpisode, onSelectedEpisode: (episode) { if (widget.detailBean != null) { if (widget.detailBean!.episodeList.isNotEmpty) { _currentEpisode = widget.detailBean!.episodeList[episode]; } } }, ), ); }, ); }, ), const SizedBox(height: 8), _buildProgressBar(), const SizedBox(height: 7), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ SkyText( text: _formatDuration(_currentPosition), fontSize: 13, color: Colors.white, ), SkyText( text: ' / ${_formatDuration(_totalDuration)}', fontSize: 13, color: Colors.white.withAlpha(126), ), ], ), SkyText(text: 'x1.0', fontSize: 13, color: Colors.white), ], ), const SizedBox(height: 20), ], ), ); } Widget _buildProgressBar() { final int totalMilliseconds = _totalDuration.inMilliseconds; final double progress = totalMilliseconds <= 0 ? 0 : (_currentPosition.inMilliseconds / totalMilliseconds).clamp(0.0, 1.0); return SizedBox( width: double.infinity, height: 3, child: SliderTheme( data: SliderThemeData( trackHeight: 3, // ✅ 进度条高度 3 thumbShape: _ImageThumbShape( imagePath: ConstImg.playTag ), overlayShape: const RoundSliderOverlayShape(overlayRadius: 3), activeTrackColor: Colors.white, // ✅ 已播放 = 白色 inactiveTrackColor: Colors.white.withAlpha(126), // ✅ 未播放 = 灰色 thumbColor: Colors.white, overlayColor: Colors.white.withAlpha(76), ), child: Slider( padding: EdgeInsets.zero, value: progress, min: 0, max: 1, onChangeStart: (_) { _isSeeking = true; }, onChanged: (value) { setState(() { _currentPosition = Duration( milliseconds: (totalMilliseconds * value).round(), ); }); }, onChangeEnd: (value) { final position = Duration( milliseconds: (totalMilliseconds * value).round(), ); _controller.seekTo(position); _isSeeking = false; }, ), ), ); } String _formatDuration(Duration duration) { final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); return '$minutes:$seconds'; } int _getCurrentEpisode(SkywoodDetail detail) { final int? episode = detail.videoInfo?.episode ?? detail.videoInfo?.currentEpisode ?? detail.shortPlayInfo?.episode ?? detail.shortPlayInfo?.currentEpisode; if (episode != null && episode > 0) return episode; return 1; } int _episodeTotal(SkywoodDetail detail) { return detail.episodeList.length; } } class _ImageThumbShape extends SliderComponentShape { final String imagePath; final double imageSize; const _ImageThumbShape({ required this.imagePath, this.imageSize = 16, }); @override Size getPreferredSize(bool isEnabled, bool isDiscrete) { return Size(imageSize, imageSize); } @override void paint( PaintingContext context, Offset center, { required Animation activationAnimation, required Animation enableAnimation, required bool isDiscrete, required TextPainter labelPainter, required RenderBox parentBox, required SliderThemeData sliderTheme, required TextDirection textDirection, required double value, required double textScaleFactor, required Size sizeWithOverflow, }) { // 🔥 使用 Image 直接渲染图片 final imageWidget = Image.asset( imagePath, width: imageSize, height: imageSize, ); // 使用 WidgetsBinding 确保图片已加载 final image = imageWidget.image; final stream = image.resolve(ImageConfiguration()); // 添加监听器,但只在图片加载完成后绘制 stream.addListener(ImageStreamListener((imageInfo, _) { final rect = Rect.fromCenter( center: center, width: imageSize, height: imageSize, ); context.canvas.drawImageRect( imageInfo.image, Rect.fromLTWH( 0, 0, imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble(), ), rect, Paint(), ); }, onError: (error, stackTrace) { // 🔥 如果图片加载失败,绘制一个默认圆点 final paint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; context.canvas.drawCircle(center, imageSize / 2, paint); })); } }