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
|
|
|
|
}
|