| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | import 'dart:async'; | 
					
						
							|  |  |  |  | import 'dart:math'; | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  | import 'dart:ui'; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | import 'package:flutter/material.dart'; | 
					
						
							|  |  |  |  | import 'package:flutter/services.dart'; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | import 'package:qhd_prevention/tools/tools.dart'; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | import 'package:video_player/video_player.dart'; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | class VideoPlayerWidget extends StatefulWidget { | 
					
						
							|  |  |  |  |   final VideoPlayerController? controller; | 
					
						
							|  |  |  |  |   final String coverUrl; | 
					
						
							|  |  |  |  |   final double aspectRatio; | 
					
						
							|  |  |  |  |   final bool allowSeek; | 
					
						
							|  |  |  |  |   final bool isFullScreen; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const VideoPlayerWidget({ | 
					
						
							|  |  |  |  |     Key? key, | 
					
						
							|  |  |  |  |     required this.controller, | 
					
						
							|  |  |  |  |     required this.coverUrl, | 
					
						
							|  |  |  |  |     required this.aspectRatio, | 
					
						
							|  |  |  |  |     this.allowSeek = true, | 
					
						
							|  |  |  |  |     this.isFullScreen = false, | 
					
						
							|  |  |  |  |   }) : super(key: key); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); | 
					
						
							|  |  |  |  | } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | class _VideoPlayerWidgetState extends State<VideoPlayerWidget> { | 
					
						
							|  |  |  |  |   bool _visibleControls = true; | 
					
						
							|  |  |  |  |   Timer? _hideTimer; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   Timer? _positionTimer; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   Duration _currentPosition = Duration.zero; | 
					
						
							|  |  |  |  |   Duration _totalDuration = Duration.zero; | 
					
						
							|  |  |  |  |   bool _isPlaying = false; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   final ValueNotifier<double> _sliderValue = ValueNotifier<double>(0.0); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 判断是否有controller,避免被销毁后访问
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   bool get _hasController => widget.controller != null; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   /// 安全判断 controller 是否初始化,避免抛异常
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   bool _controllerInitializedSafe() { | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       final c = widget.controller; | 
					
						
							|  |  |  |  |       if (c == null) return false; | 
					
						
							|  |  |  |  |       return c.value.isInitialized; | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							|  |  |  |  |       return false; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void initState() { | 
					
						
							|  |  |  |  |     super.initState(); | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 初始化时立即启动隐藏控制栏的定时器
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |     _startHideTimer(); | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 如果有controller并且初始化了,则启动进度定时器
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     _maybeStartPositionTimer(); | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 给controller添加监听
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     _addControllerListenerSafely(); | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 如果controller已经初始化,获取初始值
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     if (_controllerInitializedSafe()) { | 
					
						
							|  |  |  |  |       _updateControllerValuesSafe(); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void didUpdateWidget(covariant VideoPlayerWidget oldWidget) { | 
					
						
							|  |  |  |  |     super.didUpdateWidget(oldWidget); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 如果controller发生变化,需要移除旧的监听,添加新的监听
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     if (oldWidget.controller != widget.controller) { | 
					
						
							|  |  |  |  |       _removeControllerListenerSafely(oldWidget.controller); | 
					
						
							|  |  |  |  |       _addControllerListenerSafely(); | 
					
						
							|  |  |  |  |       _restartPositionTimer(); | 
					
						
							|  |  |  |  |       _updateControllerValuesSafe(); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   void _addControllerListenerSafely() { | 
					
						
							|  |  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |       widget.controller?.addListener(_controllerListener); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     } catch (_) { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |       // 忽略已经被销毁的情况
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   void _removeControllerListenerSafely([VideoPlayerController? ctrl]) { | 
					
						
							|  |  |  |  |     final c = ctrl ?? widget.controller; | 
					
						
							|  |  |  |  |     if (c == null) return; | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       c.removeListener(_controllerListener); | 
					
						
							|  |  |  |  |     } catch (_) { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |       // 忽略
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   void _controllerListener() { | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |     if (!mounted) return; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     _updateControllerValuesSafe(); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 更新播放状态、进度等信息
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   void _updateControllerValuesSafe() { | 
					
						
							|  |  |  |  |     final c = widget.controller; | 
					
						
							|  |  |  |  |     if (c == null) { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |       // 没有controller时清空数据
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |       if (mounted) { | 
					
						
							|  |  |  |  |         setState(() { | 
					
						
							|  |  |  |  |           _isPlaying = false; | 
					
						
							|  |  |  |  |           _totalDuration = Duration.zero; | 
					
						
							|  |  |  |  |           _currentPosition = Duration.zero; | 
					
						
							|  |  |  |  |           _sliderValue.value = 0; | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       final value = c.value; | 
					
						
							|  |  |  |  |       if (!value.isInitialized) { | 
					
						
							|  |  |  |  |         if (mounted) { | 
					
						
							|  |  |  |  |           setState(() { | 
					
						
							|  |  |  |  |             _isPlaying = false; | 
					
						
							|  |  |  |  |             _totalDuration = Duration.zero; | 
					
						
							|  |  |  |  |             _currentPosition = Duration.zero; | 
					
						
							|  |  |  |  |             _sliderValue.value = 0; | 
					
						
							|  |  |  |  |           }); | 
					
						
							|  |  |  |  |         } | 
					
						
							|  |  |  |  |         return; | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       final pos = value.position; | 
					
						
							|  |  |  |  |       final dur = value.duration; | 
					
						
							|  |  |  |  |       final playing = value.isPlaying; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |       if (mounted) { | 
					
						
							|  |  |  |  |         setState(() { | 
					
						
							|  |  |  |  |           _isPlaying = playing; | 
					
						
							|  |  |  |  |           _totalDuration = dur; | 
					
						
							|  |  |  |  |           _currentPosition = pos; | 
					
						
							|  |  |  |  |           _sliderValue.value = pos.inMilliseconds.toDouble(); | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |     } catch (e) { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |       // controller被销毁时忽略
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 启动隐藏控制栏的定时器(3秒后隐藏)
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   void _startHideTimer() { | 
					
						
							|  |  |  |  |     _hideTimer?.cancel(); | 
					
						
							|  |  |  |  |     _hideTimer = Timer(const Duration(seconds: 3), () { | 
					
						
							|  |  |  |  |       if (mounted) setState(() => _visibleControls = false); | 
					
						
							|  |  |  |  |     }); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 启动进度定时器(定时刷新播放进度)
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   void _maybeStartPositionTimer() { | 
					
						
							|  |  |  |  |     if (_positionTimer != null && _positionTimer!.isActive) return; | 
					
						
							|  |  |  |  |     if (!_hasController) return; | 
					
						
							|  |  |  |  |     _positionTimer = Timer.periodic(const Duration(milliseconds: 300), (_) { | 
					
						
							|  |  |  |  |       if (!mounted) return; | 
					
						
							|  |  |  |  |       if (!_controllerInitializedSafe()) return; | 
					
						
							|  |  |  |  |       try { | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |         final c = widget.controller!; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |         final pos = c.value.position; | 
					
						
							|  |  |  |  |         final dur = c.value.duration; | 
					
						
							|  |  |  |  |         final playing = c.value.isPlaying; | 
					
						
							|  |  |  |  |         setState(() { | 
					
						
							|  |  |  |  |           _currentPosition = pos; | 
					
						
							|  |  |  |  |           _totalDuration = dur; | 
					
						
							|  |  |  |  |           _isPlaying = playing; | 
					
						
							|  |  |  |  |           _sliderValue.value = pos.inMilliseconds.toDouble(); | 
					
						
							|  |  |  |  |         }); | 
					
						
							|  |  |  |  |       } catch (_) { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |         // controller销毁时忽略
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |     }); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   void _restartPositionTimer() { | 
					
						
							|  |  |  |  |     _positionTimer?.cancel(); | 
					
						
							|  |  |  |  |     _maybeStartPositionTimer(); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   void _stopPositionTimer() { | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       _positionTimer?.cancel(); | 
					
						
							|  |  |  |  |     } catch (_) {} | 
					
						
							|  |  |  |  |     _positionTimer = null; | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 点击切换控制栏显示/隐藏
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   void _toggleControls() { | 
					
						
							|  |  |  |  |     setState(() => _visibleControls = !_visibleControls); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     if (_visibleControls) | 
					
						
							|  |  |  |  |       _startHideTimer(); | 
					
						
							|  |  |  |  |     else | 
					
						
							|  |  |  |  |       _hideTimer?.cancel(); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 播放/暂停切换
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   void _togglePlayPause() { | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     final c = widget.controller; | 
					
						
							|  |  |  |  |     if (c == null) return; | 
					
						
							|  |  |  |  |     try { | 
					
						
							|  |  |  |  |       if (_isPlaying) { | 
					
						
							|  |  |  |  |         c.pause(); | 
					
						
							|  |  |  |  |         setState(() => _isPlaying = false); | 
					
						
							|  |  |  |  |       } else { | 
					
						
							|  |  |  |  |         c.play(); | 
					
						
							|  |  |  |  |         setState(() => _isPlaying = true); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       _startHideTimer(); | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     } catch (_) {} | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 进入全屏播放
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   Future<void> _enterFullScreen() async { | 
					
						
							|  |  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |       // 设置横屏、沉浸式UI
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |       await SystemChrome.setPreferredOrientations([ | 
					
						
							|  |  |  |  |         DeviceOrientation.landscapeLeft, | 
					
						
							|  |  |  |  |         DeviceOrientation.landscapeRight, | 
					
						
							|  |  |  |  |       ]); | 
					
						
							|  |  |  |  |       await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); | 
					
						
							|  |  |  |  |       await NativeOrientation.setLandscape(); | 
					
						
							|  |  |  |  |     } catch (_) {} | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 跳转到全屏页面
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     await Navigator.of(context).push( | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |       MaterialPageRoute( | 
					
						
							|  |  |  |  |         builder: (ctx) => Scaffold( | 
					
						
							|  |  |  |  |           backgroundColor: Colors.black, | 
					
						
							|  |  |  |  |           body: SafeArea( | 
					
						
							|  |  |  |  |             top: false, | 
					
						
							|  |  |  |  |             bottom: false, | 
					
						
							|  |  |  |  |             child: VideoPlayerWidget( | 
					
						
							|  |  |  |  |               controller: widget.controller, | 
					
						
							|  |  |  |  |               coverUrl: widget.coverUrl, | 
					
						
							|  |  |  |  |               aspectRatio: max( | 
					
						
							|  |  |  |  |                 widget.aspectRatio, | 
					
						
							|  |  |  |  |                 MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height, | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |               ), | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |               allowSeek: widget.allowSeek, | 
					
						
							|  |  |  |  |               isFullScreen: true, | 
					
						
							|  |  |  |  |             ), | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |           ), | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |       ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 返回后恢复竖屏和UI
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     try { | 
					
						
							|  |  |  |  |       await NativeOrientation.setPortrait(); | 
					
						
							|  |  |  |  |       await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); | 
					
						
							|  |  |  |  |       await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); | 
					
						
							|  |  |  |  |     } catch (_) {} | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (!mounted) return; | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     setState(() {}); // 强制刷新,修复退出全屏后UI问题
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   void dispose() { | 
					
						
							|  |  |  |  |     _hideTimer?.cancel(); | 
					
						
							|  |  |  |  |     _stopPositionTimer(); | 
					
						
							|  |  |  |  |     _sliderValue.dispose(); | 
					
						
							|  |  |  |  |     _removeControllerListenerSafely(); | 
					
						
							|  |  |  |  |     super.dispose(); | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   @override | 
					
						
							|  |  |  |  |   Widget build(BuildContext context) { | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     final media = MediaQuery.of(context); | 
					
						
							|  |  |  |  |     final screenW = media.size.width; | 
					
						
							|  |  |  |  |     final screenH = media.size.height; | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     // 非全屏高度:按照宽高比计算,但不超过屏幕一半高度
 | 
					
						
							|  |  |  |  |     final preferredNonFullHeight = min( | 
					
						
							|  |  |  |  |       screenW / (widget.aspectRatio <= 0 ? (16 / 9) : widget.aspectRatio), | 
					
						
							|  |  |  |  |       screenH * 0.5, | 
					
						
							|  |  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |     final containerW = screenW; | 
					
						
							|  |  |  |  |     final containerH = widget.isFullScreen ? screenH : preferredNonFullHeight; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |     return Center( | 
					
						
							|  |  |  |  |       child: SizedBox( | 
					
						
							|  |  |  |  |         width: containerW, | 
					
						
							|  |  |  |  |         height: containerH, | 
					
						
							|  |  |  |  |         child: GestureDetector( | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |           behavior: HitTestBehavior.translucent, | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |           onTap: _toggleControls, | 
					
						
							|  |  |  |  |           child: Stack( | 
					
						
							|  |  |  |  |             fit: StackFit.expand, | 
					
						
							|  |  |  |  |             children: [ | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |               // 视频画面或封面图
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               _buildVideoOrCover(containerW, containerH), | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |               // 全屏模式时左上角显示返回按钮
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               if (widget.isFullScreen) | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |                 Positioned( | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |                   top: MediaQuery.of(context).padding.top + 8, | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                   left: 8, | 
					
						
							|  |  |  |  |                   child: SafeArea( | 
					
						
							|  |  |  |  |                     top: true, | 
					
						
							|  |  |  |  |                     bottom: false, | 
					
						
							|  |  |  |  |                     child: ClipOval( | 
					
						
							|  |  |  |  |                       child: Material( | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |                         color: Colors.black38, | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                         child: InkWell( | 
					
						
							|  |  |  |  |                           onTap: () async { | 
					
						
							|  |  |  |  |                             try { | 
					
						
							|  |  |  |  |                               await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); | 
					
						
							|  |  |  |  |                               await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); | 
					
						
							|  |  |  |  |                             } catch (_) {} | 
					
						
							|  |  |  |  |                             Navigator.of(context).maybePop(); | 
					
						
							|  |  |  |  |                           }, | 
					
						
							|  |  |  |  |                           child: const Padding( | 
					
						
							|  |  |  |  |                             padding: EdgeInsets.all(8.0), | 
					
						
							|  |  |  |  |                             child: Icon( | 
					
						
							|  |  |  |  |                               Icons.arrow_back, | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |                               color: Colors.white, | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                               size: 22, | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |                             ), | 
					
						
							|  |  |  |  |                           ), | 
					
						
							|  |  |  |  |                         ), | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |                       ), | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |                     ), | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |                   ), | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |                 ), | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |               // 控制栏
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |               if (_visibleControls) _buildControls(), | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |             ], | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |         ), | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |       ), | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 构建视频画面或封面图
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   Widget _buildVideoOrCover(double containerW, double containerH) { | 
					
						
							|  |  |  |  |     final c = widget.controller; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (c != null) { | 
					
						
							|  |  |  |  |       try { | 
					
						
							|  |  |  |  |         if (c.value.isInitialized) { | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |           final vidAspect = | 
					
						
							|  |  |  |  |           (c.value.size.height > 0) ? (c.value.size.width / c.value.size.height) : widget.aspectRatio; | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |           return Center( | 
					
						
							|  |  |  |  |             child: AspectRatio( | 
					
						
							|  |  |  |  |               aspectRatio: vidAspect > 0 ? vidAspect : widget.aspectRatio, | 
					
						
							|  |  |  |  |               child: VideoPlayer(c), | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |           ); | 
					
						
							|  |  |  |  |         } | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |       } catch (_) {} | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     if (widget.coverUrl.isNotEmpty) { | 
					
						
							|  |  |  |  |       return Image.network( | 
					
						
							|  |  |  |  |         widget.coverUrl, | 
					
						
							|  |  |  |  |         width: containerW, | 
					
						
							|  |  |  |  |         height: containerH, | 
					
						
							|  |  |  |  |         fit: BoxFit.cover, | 
					
						
							|  |  |  |  |         errorBuilder: (_, __, ___) => Container(color: Colors.black), | 
					
						
							|  |  |  |  |       ); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return Container(color: Colors.black); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 构建底部控制栏
 | 
					
						
							| 
									
										
										
										
											2025-08-29 09:52:48 +08:00
										 |  |  |  |   Widget _buildControls() { | 
					
						
							|  |  |  |  |     final totalMs = _totalDuration.inMilliseconds.toDouble(); | 
					
						
							|  |  |  |  |     final sliderMax = totalMs > 0 ? totalMs : 1.0; | 
					
						
							|  |  |  |  |     final sliderValue = _sliderValue.value.clamp(0.0, sliderMax).toDouble(); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return Positioned( | 
					
						
							|  |  |  |  |       bottom: 0, | 
					
						
							|  |  |  |  |       left: 0, | 
					
						
							|  |  |  |  |       right: 0, | 
					
						
							|  |  |  |  |       child: Container( | 
					
						
							|  |  |  |  |         height: 50, | 
					
						
							|  |  |  |  |         decoration: BoxDecoration( | 
					
						
							|  |  |  |  |           gradient: LinearGradient( | 
					
						
							|  |  |  |  |             begin: Alignment.bottomCenter, | 
					
						
							|  |  |  |  |             end: Alignment.topCenter, | 
					
						
							|  |  |  |  |             colors: [ | 
					
						
							|  |  |  |  |               Colors.black.withOpacity(0.7), | 
					
						
							|  |  |  |  |               Colors.transparent, | 
					
						
							|  |  |  |  |             ], | 
					
						
							|  |  |  |  |           ), | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |         padding: const EdgeInsets.symmetric(horizontal: 12), | 
					
						
							|  |  |  |  |         child: Row( | 
					
						
							|  |  |  |  |           children: [ | 
					
						
							|  |  |  |  |             IconButton( | 
					
						
							|  |  |  |  |               padding: EdgeInsets.zero, | 
					
						
							|  |  |  |  |               icon: Icon( | 
					
						
							|  |  |  |  |                 _isPlaying ? Icons.pause : Icons.play_arrow, | 
					
						
							|  |  |  |  |                 size: 28, | 
					
						
							|  |  |  |  |                 color: Colors.white, | 
					
						
							|  |  |  |  |               ), | 
					
						
							|  |  |  |  |               onPressed: _togglePlayPause, | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |             Expanded( | 
					
						
							|  |  |  |  |               child: ValueListenableBuilder<double>( | 
					
						
							|  |  |  |  |                 valueListenable: _sliderValue, | 
					
						
							|  |  |  |  |                 builder: (_, value, __) => SliderTheme( | 
					
						
							|  |  |  |  |                   data: SliderTheme.of(context).copyWith( | 
					
						
							|  |  |  |  |                     activeTrackColor: Colors.white, | 
					
						
							|  |  |  |  |                     inactiveTrackColor: Colors.white54, | 
					
						
							|  |  |  |  |                     thumbColor: Colors.white, | 
					
						
							|  |  |  |  |                     overlayColor: Colors.white24, | 
					
						
							|  |  |  |  |                     trackHeight: 2, | 
					
						
							|  |  |  |  |                     thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8), | 
					
						
							|  |  |  |  |                   ), | 
					
						
							|  |  |  |  |                   child: Slider( | 
					
						
							|  |  |  |  |                     value: sliderValue, | 
					
						
							|  |  |  |  |                     min: 0, | 
					
						
							|  |  |  |  |                     max: sliderMax, | 
					
						
							|  |  |  |  |                     onChanged: (v) { | 
					
						
							|  |  |  |  |                       if (widget.allowSeek && widget.controller != null) { | 
					
						
							|  |  |  |  |                         try { | 
					
						
							|  |  |  |  |                           widget.controller!.seekTo(Duration(milliseconds: v.toInt())); | 
					
						
							|  |  |  |  |                           setState(() => _currentPosition = Duration(milliseconds: v.toInt())); | 
					
						
							|  |  |  |  |                           _sliderValue.value = v; | 
					
						
							|  |  |  |  |                           _startHideTimer(); | 
					
						
							|  |  |  |  |                         } catch (_) {} | 
					
						
							|  |  |  |  |                       } | 
					
						
							|  |  |  |  |                     }, | 
					
						
							|  |  |  |  |                   ), | 
					
						
							|  |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ), | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |             SizedBox( | 
					
						
							|  |  |  |  |               width: 110, | 
					
						
							|  |  |  |  |               child: Text( | 
					
						
							|  |  |  |  |                 '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', | 
					
						
							|  |  |  |  |                 style: TextStyle( | 
					
						
							|  |  |  |  |                   color: Colors.white, | 
					
						
							|  |  |  |  |                   fontSize: 12, | 
					
						
							|  |  |  |  |                   fontFeatures: [FontFeature.tabularFigures()], | 
					
						
							|  |  |  |  |                 ), | 
					
						
							|  |  |  |  |               ), | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |             IconButton( | 
					
						
							|  |  |  |  |               padding: EdgeInsets.zero, | 
					
						
							|  |  |  |  |               icon: Icon( | 
					
						
							|  |  |  |  |                 widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, | 
					
						
							|  |  |  |  |                 size: 28, | 
					
						
							|  |  |  |  |                 color: Colors.white, | 
					
						
							|  |  |  |  |               ), | 
					
						
							|  |  |  |  |               onPressed: () { | 
					
						
							|  |  |  |  |                 if (widget.isFullScreen) { | 
					
						
							|  |  |  |  |                   Navigator.of(context).maybePop(); | 
					
						
							|  |  |  |  |                 } else { | 
					
						
							|  |  |  |  |                   _enterFullScreen(); | 
					
						
							|  |  |  |  |                 } | 
					
						
							|  |  |  |  |               }, | 
					
						
							|  |  |  |  |             ), | 
					
						
							|  |  |  |  |           ], | 
					
						
							|  |  |  |  |         ), | 
					
						
							|  |  |  |  |       ), | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |   } | 
					
						
							|  |  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-29 20:33:23 +08:00
										 |  |  |  |   // 格式化时间
 | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   String _formatDuration(Duration d) { | 
					
						
							|  |  |  |  |     String twoDigits(int n) => n.toString().padLeft(2, '0'); | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  |     final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60); | 
					
						
							|  |  |  |  |     if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; | 
					
						
							|  |  |  |  |     return '${twoDigits(m)}:${twoDigits(s)}'; | 
					
						
							| 
									
										
										
										
											2025-07-18 17:13:38 +08:00
										 |  |  |  |   } | 
					
						
							| 
									
										
										
										
											2025-07-22 13:34:34 +08:00
										 |  |  |  | } |