feat: 快进、快退、拖动时时间戳展示、关闭自动播放时处理、单p播放完成处理
This commit is contained in:
@ -50,6 +50,10 @@ class VideoDetailController extends GetxController
|
|||||||
Box user = GStrorage.user;
|
Box user = GStrorage.user;
|
||||||
Box localCache = GStrorage.localCache;
|
Box localCache = GStrorage.localCache;
|
||||||
PlPlayerController plPlayerController = PlPlayerController();
|
PlPlayerController plPlayerController = PlPlayerController();
|
||||||
|
// 是否开始自动播放 存在多p的情况下,第二p需要为true
|
||||||
|
RxBool autoPlay = true.obs;
|
||||||
|
// 视频资源是否有效
|
||||||
|
RxBool isEffective = true.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -130,12 +134,17 @@ class VideoDetailController extends GetxController
|
|||||||
),
|
),
|
||||||
// 硬解
|
// 硬解
|
||||||
enableHA: true,
|
enableHA: true,
|
||||||
autoplay: true,
|
autoplay: autoPlay.value,
|
||||||
seekTo: defaultST,
|
seekTo: defaultST,
|
||||||
duration: Duration(milliseconds: duration),
|
duration: Duration(milliseconds: duration),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 手动点击播放
|
||||||
|
handlePlay() {
|
||||||
|
plPlayerController.togglePlay();
|
||||||
|
}
|
||||||
|
|
||||||
// 视频链接
|
// 视频链接
|
||||||
queryVideoUrl() async {
|
queryVideoUrl() async {
|
||||||
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid);
|
||||||
|
@ -55,7 +55,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
isPlay = true;
|
isPlay = true;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
// 播放完成停止 or 切换下一个
|
// 播放完成停止 or 切换下一个
|
||||||
if (status == PlayerStatus.completed) {}
|
if (status == PlayerStatus.completed) {
|
||||||
|
// 当只有1p或多p未打开自动播放时,播放完成还原进度条,展示控制栏
|
||||||
|
plPlayerController!.seekTo(Duration.zero);
|
||||||
|
plPlayerController!.onLockControl(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -180,6 +184,38 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/// 关闭自动播放时 手动播放
|
||||||
|
Obx(
|
||||||
|
() => Visibility(
|
||||||
|
visible: isShowCover &&
|
||||||
|
videoDetailController
|
||||||
|
.isEffective.value &&
|
||||||
|
!videoDetailController.autoPlay.value,
|
||||||
|
child: Positioned(
|
||||||
|
right: 12,
|
||||||
|
bottom: 6,
|
||||||
|
child: TextButton.icon(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty
|
||||||
|
.resolveWith((states) {
|
||||||
|
return Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.primaryContainer;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onPressed: () => videoDetailController
|
||||||
|
.handlePlay(),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.play_circle_outline,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
label: const Text('Play'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -39,6 +39,7 @@ class PlPlayerController {
|
|||||||
// 播放位置
|
// 播放位置
|
||||||
final Rx<Duration> _position = Rx(Duration.zero);
|
final Rx<Duration> _position = Rx(Duration.zero);
|
||||||
final Rx<Duration> _sliderPosition = Rx(Duration.zero);
|
final Rx<Duration> _sliderPosition = Rx(Duration.zero);
|
||||||
|
final Rx<Duration> _sliderTempPosition = Rx(Duration.zero);
|
||||||
final Rx<Duration> _duration = Rx(Duration.zero);
|
final Rx<Duration> _duration = Rx(Duration.zero);
|
||||||
final Rx<Duration> _buffered = Rx(Duration.zero);
|
final Rx<Duration> _buffered = Rx(Duration.zero);
|
||||||
|
|
||||||
@ -51,13 +52,13 @@ class PlPlayerController {
|
|||||||
final Rx<bool> _showVolumeStatus = false.obs;
|
final Rx<bool> _showVolumeStatus = false.obs;
|
||||||
final Rx<bool> _showBrightnessStatus = false.obs;
|
final Rx<bool> _showBrightnessStatus = false.obs;
|
||||||
final Rx<bool> _doubleSpeedStatus = false.obs;
|
final Rx<bool> _doubleSpeedStatus = false.obs;
|
||||||
final Rx<bool> _controlsClose = false.obs;
|
final Rx<bool> _controlsLock = false.obs;
|
||||||
|
|
||||||
Rx<bool> videoFitChanged = false.obs;
|
Rx<bool> videoFitChanged = false.obs;
|
||||||
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
|
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
|
||||||
|
|
||||||
///
|
///
|
||||||
bool _isSliderMoving = false;
|
Rx<bool> _isSliderMoving = false.obs;
|
||||||
PlaylistMode _looping = PlaylistMode.none;
|
PlaylistMode _looping = PlaylistMode.none;
|
||||||
bool _autoPlay = false;
|
bool _autoPlay = false;
|
||||||
final bool _listenersInitialized = false;
|
final bool _listenersInitialized = false;
|
||||||
@ -112,10 +113,15 @@ class PlPlayerController {
|
|||||||
/// [videoController] instace of Player
|
/// [videoController] instace of Player
|
||||||
VideoController? get videoController => _videoController;
|
VideoController? get videoController => _videoController;
|
||||||
|
|
||||||
|
Rx<bool> get isSliderMoving => _isSliderMoving;
|
||||||
|
|
||||||
/// 进度条位置及监听
|
/// 进度条位置及监听
|
||||||
Rx<Duration> get sliderPosition => _sliderPosition;
|
Rx<Duration> get sliderPosition => _sliderPosition;
|
||||||
Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;
|
Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;
|
||||||
|
|
||||||
|
Rx<Duration> get sliderTempPosition => _sliderTempPosition;
|
||||||
|
// Stream<Duration> get onSliderPositionChanged => _sliderPosition.stream;
|
||||||
|
|
||||||
/// 是否展示控制条及监听
|
/// 是否展示控制条及监听
|
||||||
Rx<bool> get showControls => _showControls;
|
Rx<bool> get showControls => _showControls;
|
||||||
Stream<bool> get onShowControlsChanged => _showControls.stream;
|
Stream<bool> get onShowControlsChanged => _showControls.stream;
|
||||||
@ -151,9 +157,11 @@ class PlPlayerController {
|
|||||||
|
|
||||||
Rx<bool> isBuffering = true.obs;
|
Rx<bool> isBuffering = true.obs;
|
||||||
|
|
||||||
Rx<bool> get controlsClose => _controlsClose;
|
/// 屏幕锁 为true时,关闭控制栏
|
||||||
|
Rx<bool> get controlsLock => _controlsLock;
|
||||||
|
|
||||||
PlPlayerController({
|
PlPlayerController({
|
||||||
|
// 直播间 传false 关闭控制栏
|
||||||
this.controlsEnabled = true,
|
this.controlsEnabled = true,
|
||||||
this.fits = const [
|
this.fits = const [
|
||||||
BoxFit.contain,
|
BoxFit.contain,
|
||||||
@ -349,7 +357,7 @@ class PlPlayerController {
|
|||||||
}),
|
}),
|
||||||
videoPlayerController!.stream.position.listen((event) {
|
videoPlayerController!.stream.position.listen((event) {
|
||||||
_position.value = event;
|
_position.value = event;
|
||||||
if (!_isSliderMoving) {
|
if (!isSliderMoving.value) {
|
||||||
_sliderPosition.value = event;
|
_sliderPosition.value = event;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@ -387,8 +395,6 @@ class PlPlayerController {
|
|||||||
position = Duration.zero;
|
position = Duration.zero;
|
||||||
}
|
}
|
||||||
_position.value = position;
|
_position.value = position;
|
||||||
print('seek 🌹duration : ${duration.value.inSeconds}');
|
|
||||||
|
|
||||||
if (duration.value.inSeconds != 0) {
|
if (duration.value.inSeconds != 0) {
|
||||||
// await _videoPlayerController!.stream.buffer.first;
|
// await _videoPlayerController!.stream.buffer.first;
|
||||||
await _videoPlayerController?.seek(position);
|
await _videoPlayerController?.seek(position);
|
||||||
@ -396,13 +402,11 @@ class PlPlayerController {
|
|||||||
// play();
|
// play();
|
||||||
// }
|
// }
|
||||||
} else {
|
} else {
|
||||||
print('🌹🌹');
|
|
||||||
_timerForSeek?.cancel();
|
_timerForSeek?.cancel();
|
||||||
_timerForSeek =
|
_timerForSeek =
|
||||||
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
Timer.periodic(const Duration(milliseconds: 200), (Timer t) async {
|
||||||
//_timerForSeek = null;
|
//_timerForSeek = null;
|
||||||
if (duration.value.inSeconds != 0) {
|
if (duration.value.inSeconds != 0) {
|
||||||
print('🌹🌹🌹');
|
|
||||||
await _videoPlayerController?.seek(position);
|
await _videoPlayerController?.seek(position);
|
||||||
// if (playerStatus.stopped) {
|
// if (playerStatus.stopped) {
|
||||||
// play();
|
// play();
|
||||||
@ -471,7 +475,7 @@ class PlPlayerController {
|
|||||||
/// 隐藏控制条
|
/// 隐藏控制条
|
||||||
void _hideTaskControls() {
|
void _hideTaskControls() {
|
||||||
_timer = Timer(const Duration(milliseconds: 3000), () {
|
_timer = Timer(const Duration(milliseconds: 3000), () {
|
||||||
if (!_isSliderMoving) {
|
if (!isSliderMoving.value) {
|
||||||
controls = false;
|
controls = false;
|
||||||
}
|
}
|
||||||
_timer = null;
|
_timer = null;
|
||||||
@ -485,11 +489,15 @@ class PlPlayerController {
|
|||||||
|
|
||||||
void onChangedSliderStart() {
|
void onChangedSliderStart() {
|
||||||
feedBack();
|
feedBack();
|
||||||
_isSliderMoving = true;
|
_isSliderMoving.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onUodatedSliderProgress(value) {
|
||||||
|
_sliderTempPosition.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangedSliderEnd() {
|
void onChangedSliderEnd() {
|
||||||
_isSliderMoving = false;
|
_isSliderMoving.value = false;
|
||||||
_hideTaskControls();
|
_hideTaskControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,9 +618,9 @@ class PlPlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 关闭控制栏
|
/// 关闭控制栏
|
||||||
void onCloseControl(bool val) {
|
void onLockControl(bool val) {
|
||||||
feedBack();
|
feedBack();
|
||||||
_controlsClose.value = val;
|
_controlsLock.value = val;
|
||||||
showControls.value = !val;
|
showControls.value = !val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,10 +650,12 @@ class PlPlayerController {
|
|||||||
_position.close();
|
_position.close();
|
||||||
_playerEventSubs?.cancel();
|
_playerEventSubs?.cancel();
|
||||||
_sliderPosition.close();
|
_sliderPosition.close();
|
||||||
|
_sliderTempPosition.close();
|
||||||
|
_isSliderMoving.close();
|
||||||
_duration.close();
|
_duration.close();
|
||||||
_buffered.close();
|
_buffered.close();
|
||||||
_showControls.close();
|
_showControls.close();
|
||||||
_controlsClose.close();
|
_controlsLock.close();
|
||||||
|
|
||||||
playerStatus.status.close();
|
playerStatus.status.close();
|
||||||
dataStatus.status.close();
|
dataStatus.status.close();
|
||||||
|
29
lib/plugin/pl_player/models/duration.dart
Normal file
29
lib/plugin/pl_player/models/duration.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
extension DurationExtension on Duration {
|
||||||
|
/// Returns clamp of [Duration] between [min] and [max].
|
||||||
|
Duration clamp(Duration min, Duration max) {
|
||||||
|
if (this < min) return min;
|
||||||
|
if (this > max) return max;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [String] representation of [Duration].
|
||||||
|
String label({Duration? reference}) {
|
||||||
|
reference ??= this;
|
||||||
|
if (reference > const Duration(days: 1)) {
|
||||||
|
final days = inDays.toString().padLeft(3, '0');
|
||||||
|
final hours = (inHours - (inDays * 24)).toString().padLeft(2, '0');
|
||||||
|
final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');
|
||||||
|
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
|
||||||
|
return '$days:$hours:$minutes:$seconds';
|
||||||
|
} else if (reference > const Duration(hours: 1)) {
|
||||||
|
final hours = inHours.toString().padLeft(2, '0');
|
||||||
|
final minutes = (inMinutes - (inHours * 60)).toString().padLeft(2, '0');
|
||||||
|
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
|
||||||
|
return '$hours:$minutes:$seconds';
|
||||||
|
} else {
|
||||||
|
final minutes = inMinutes.toString().padLeft(2, '0');
|
||||||
|
final seconds = (inSeconds - (inMinutes * 60)).toString().padLeft(2, '0');
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,20 @@ import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
import 'package:pilipala/common/widgets/app_bar_ani.dart';
|
import 'package:pilipala/common/widgets/app_bar_ani.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/controller.dart';
|
import 'package:pilipala/plugin/pl_player/controller.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/models/duration.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
|
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/utils.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
|
|
||||||
|
import 'widgets/backward_seek.dart';
|
||||||
import 'widgets/bottom_control.dart';
|
import 'widgets/bottom_control.dart';
|
||||||
import 'widgets/common_btn.dart';
|
import 'widgets/common_btn.dart';
|
||||||
|
import 'widgets/forward_seek.dart';
|
||||||
|
import 'widgets/play_pause_btn.dart';
|
||||||
|
|
||||||
class PLVideoPlayer extends StatefulWidget {
|
class PLVideoPlayer extends StatefulWidget {
|
||||||
final PlPlayerController controller;
|
final PlPlayerController controller;
|
||||||
@ -32,6 +38,23 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
late AnimationController animationController;
|
late AnimationController animationController;
|
||||||
late VideoController videoController;
|
late VideoController videoController;
|
||||||
|
|
||||||
|
bool _mountSeekBackwardButton = false;
|
||||||
|
bool _mountSeekForwardButton = false;
|
||||||
|
bool _hideSeekBackwardButton = false;
|
||||||
|
bool _hideSeekForwardButton = false;
|
||||||
|
|
||||||
|
void onDoubleTapSeekBackward() {
|
||||||
|
setState(() {
|
||||||
|
_mountSeekBackwardButton = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDoubleTapSeekForward() {
|
||||||
|
setState(() {
|
||||||
|
_mountSeekForwardButton = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -59,10 +82,33 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
backgroundColor: Color(0xaa000000),
|
backgroundColor: Color(0xaa000000),
|
||||||
);
|
);
|
||||||
|
const textStyle = TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
);
|
||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
fit: StackFit.passthrough,
|
fit: StackFit.passthrough,
|
||||||
children: [
|
children: [
|
||||||
|
// Wrap [Video] widget with [MaterialVideoControlsTheme].
|
||||||
|
// MaterialVideoControlsTheme(
|
||||||
|
// normal: MaterialVideoControlsThemeData(
|
||||||
|
// // Modify theme options:
|
||||||
|
// buttonBarButtonSize: 24.0,
|
||||||
|
// buttonBarButtonColor: Colors.white,
|
||||||
|
// ),
|
||||||
|
// fullscreen: const MaterialVideoControlsThemeData(
|
||||||
|
// // Modify theme options:
|
||||||
|
// displaySeekBar: false,
|
||||||
|
// automaticallyImplySkipNextButton: false,
|
||||||
|
// automaticallyImplySkipPreviousButton: false,
|
||||||
|
// ),
|
||||||
|
// child: Scaffold(
|
||||||
|
// body: Video(
|
||||||
|
// controller: videoController,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
Video(
|
Video(
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
controls: NoVideoControls,
|
controls: NoVideoControls,
|
||||||
@ -78,11 +124,29 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
_.controls = !_.showControls.value;
|
_.controls = !_.showControls.value;
|
||||||
},
|
},
|
||||||
onDoubleTap: () {
|
// onDoubleTap: () {
|
||||||
if (_.playerStatus.status.value == PlayerStatus.playing) {
|
// if (_.playerStatus.status.value == PlayerStatus.playing) {
|
||||||
_.togglePlay();
|
// _.togglePlay();
|
||||||
|
// } else {
|
||||||
|
// _.play();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
onDoubleTapDown: (details) {
|
||||||
|
final totalWidth = MediaQuery.of(context).size.width;
|
||||||
|
final tapPosition = details.localPosition.dx;
|
||||||
|
final sectionWidth = totalWidth / 3;
|
||||||
|
if (tapPosition < sectionWidth) {
|
||||||
|
// 双击左边区域 👈
|
||||||
|
onDoubleTapSeekBackward();
|
||||||
|
} else if (tapPosition < sectionWidth * 2) {
|
||||||
|
if (_.playerStatus.status.value == PlayerStatus.playing) {
|
||||||
|
_.togglePlay();
|
||||||
|
} else {
|
||||||
|
_.play();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_.play();
|
// 双击右边区域 👈
|
||||||
|
onDoubleTapSeekForward();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPressStart: (detail) {
|
onLongPressStart: (detail) {
|
||||||
@ -112,7 +176,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: AppBarAni(
|
child: AppBarAni(
|
||||||
controller: animationController,
|
controller: animationController,
|
||||||
visible: !_.controlsClose.value && _.showControls.value,
|
visible: !_.controlsLock.value && _.showControls.value,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
child: widget.headerControl!,
|
child: widget.headerControl!,
|
||||||
),
|
),
|
||||||
@ -122,7 +186,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: AppBarAni(
|
child: AppBarAni(
|
||||||
controller: animationController,
|
controller: animationController,
|
||||||
visible: !_.controlsClose.value && _.showControls.value,
|
visible: !_.controlsLock.value && _.showControls.value,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
child: BottomControl(controller: widget.controller),
|
child: BottomControl(controller: widget.controller),
|
||||||
),
|
),
|
||||||
@ -140,7 +204,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: -4,
|
bottom: -3,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
@ -161,22 +225,22 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
Theme.of(context).colorScheme.primary.withOpacity(0.4),
|
Theme.of(context).colorScheme.primary.withOpacity(0.4),
|
||||||
timeLabelLocation: TimeLabelLocation.none,
|
timeLabelLocation: TimeLabelLocation.none,
|
||||||
thumbColor: colorTheme,
|
thumbColor: colorTheme,
|
||||||
barHeight: 3,
|
barHeight: 2,
|
||||||
thumbRadius: 0.0,
|
thumbRadius: 0.0,
|
||||||
onDragStart: (duration) {
|
// onDragStart: (duration) {
|
||||||
_.onChangedSliderStart();
|
// _.onChangedSliderStart();
|
||||||
},
|
// },
|
||||||
onDragEnd: () {
|
// onDragEnd: () {
|
||||||
_.onChangedSliderEnd();
|
// _.onChangedSliderEnd();
|
||||||
},
|
// },
|
||||||
// onDragUpdate: (details) {
|
// onDragUpdate: (details) {
|
||||||
// print(details);
|
// print(details);
|
||||||
// },
|
// },
|
||||||
onSeek: (duration) {
|
// onSeek: (duration) {
|
||||||
feedBack();
|
// feedBack();
|
||||||
_.onChangedSlider(duration.inSeconds.toDouble());
|
// _.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
_.seekTo(duration);
|
// _.seekTo(duration);
|
||||||
},
|
// },
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -212,13 +276,13 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
visible: _.showControls.value,
|
visible: _.showControls.value,
|
||||||
child: ComBtn(
|
child: ComBtn(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_.controlsClose.value
|
_.controlsLock.value
|
||||||
? FontAwesomeIcons.lock
|
? FontAwesomeIcons.lock
|
||||||
: FontAwesomeIcons.lockOpen,
|
: FontAwesomeIcons.lockOpen,
|
||||||
size: 15,
|
size: 15,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
fuc: () => _.onCloseControl(!_.controlsClose.value),
|
fuc: () => _.onLockControl(!_.controlsLock.value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -237,6 +301,141 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
|||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
// 时间进度
|
||||||
|
/// TDDO 样式
|
||||||
|
Obx(
|
||||||
|
() => Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: FractionalTranslation(
|
||||||
|
translation: const Offset(0.0, 2.5), // 上下偏移量(负数向上偏移)
|
||||||
|
child: Visibility(
|
||||||
|
visible: _.isSliderMoving.value,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Obx(() {
|
||||||
|
return Text(
|
||||||
|
_.sliderTempPosition.value.inMinutes >= 60
|
||||||
|
? printDurationWithHours(_.sliderTempPosition.value)
|
||||||
|
: printDuration(_.sliderTempPosition.value),
|
||||||
|
style: textStyle,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
const Text('/', style: textStyle),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Obx(
|
||||||
|
() => Text(
|
||||||
|
_.duration.value.inMinutes >= 60
|
||||||
|
? printDurationWithHours(_.duration.value)
|
||||||
|
: printDuration(_.duration.value),
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 点击 快进/快退
|
||||||
|
if (_mountSeekBackwardButton || _mountSeekForwardButton)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _mountSeekBackwardButton
|
||||||
|
? TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: _hideSeekBackwardButton ? 0.0 : 1.0,
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
builder: (context, value, child) => Opacity(
|
||||||
|
opacity: value,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
onEnd: () {
|
||||||
|
if (_hideSeekBackwardButton) {
|
||||||
|
setState(() {
|
||||||
|
_hideSeekBackwardButton = false;
|
||||||
|
_mountSeekBackwardButton = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: BackwardSeekIndicator(
|
||||||
|
onChanged: (value) {
|
||||||
|
print(value);
|
||||||
|
// _seekBarDeltaValueNotifier.value = -value;
|
||||||
|
},
|
||||||
|
onSubmitted: (value) {
|
||||||
|
setState(() {
|
||||||
|
_hideSeekBackwardButton = true;
|
||||||
|
});
|
||||||
|
Player player =
|
||||||
|
widget.controller.videoPlayerController!;
|
||||||
|
var result = player.state.position - value;
|
||||||
|
result = result.clamp(
|
||||||
|
Duration.zero,
|
||||||
|
player.state.duration,
|
||||||
|
);
|
||||||
|
player.seek(result);
|
||||||
|
widget.controller.play();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width / 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _mountSeekForwardButton
|
||||||
|
? TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 0.0,
|
||||||
|
end: _hideSeekForwardButton ? 0.0 : 1.0,
|
||||||
|
),
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
builder: (context, value, child) => Opacity(
|
||||||
|
opacity: value,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
onEnd: () {
|
||||||
|
if (_hideSeekForwardButton) {
|
||||||
|
setState(() {
|
||||||
|
_hideSeekForwardButton = false;
|
||||||
|
_mountSeekForwardButton = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: ForwardSeekIndicator(
|
||||||
|
onChanged: (value) {
|
||||||
|
// _seekBarDeltaValueNotifier.value = value;
|
||||||
|
},
|
||||||
|
onSubmitted: (value) {
|
||||||
|
setState(() {
|
||||||
|
_hideSeekForwardButton = true;
|
||||||
|
});
|
||||||
|
Player player =
|
||||||
|
widget.controller.videoPlayerController!;
|
||||||
|
var result = player.state.position + value;
|
||||||
|
result = result.clamp(
|
||||||
|
Duration.zero,
|
||||||
|
player.state.duration,
|
||||||
|
);
|
||||||
|
player.seek(result);
|
||||||
|
widget.controller.play();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -256,5 +455,3 @@ class MSliderTrackShape extends RoundedRectSliderTrackShape {
|
|||||||
return Rect.fromLTWH(trackLeft, -1, trackWidth, 3);
|
return Rect.fromLTWH(trackLeft, -1, trackWidth, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PLPlayerCtr extends GetxController {}
|
|
||||||
|
84
lib/plugin/pl_player/widgets/backward_seek.dart
Normal file
84
lib/plugin/pl_player/widgets/backward_seek.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class BackwardSeekIndicator extends StatefulWidget {
|
||||||
|
final void Function(Duration) onChanged;
|
||||||
|
final void Function(Duration) onSubmitted;
|
||||||
|
const BackwardSeekIndicator({
|
||||||
|
Key? key,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.onSubmitted,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BackwardSeekIndicator> createState() => BackwardSeekIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackwardSeekIndicatorState extends State<BackwardSeekIndicator> {
|
||||||
|
Duration value = const Duration(seconds: 10);
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
timer = Timer(const Duration(milliseconds: 400), () {
|
||||||
|
widget.onSubmitted.call(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void increment() {
|
||||||
|
timer?.cancel();
|
||||||
|
timer = Timer(const Duration(milliseconds: 400), () {
|
||||||
|
widget.onSubmitted.call(value);
|
||||||
|
});
|
||||||
|
widget.onChanged.call(value);
|
||||||
|
// 重复点击 快退秒数累加10
|
||||||
|
setState(() {
|
||||||
|
value += const Duration(seconds: 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0x88767676),
|
||||||
|
Color(0x00767676),
|
||||||
|
],
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
splashColor: const Color(0x44767676),
|
||||||
|
onTap: increment,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.fast_rewind,
|
||||||
|
size: 24.0,
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0),
|
||||||
|
Text(
|
||||||
|
'快退${value.inSeconds}秒',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/widgets/play_pause_btn.dart';
|
||||||
|
|
||||||
import '../utils.dart';
|
import '../utils.dart';
|
||||||
|
|
||||||
@ -56,6 +57,9 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
onDragStart: (duration) {
|
onDragStart: (duration) {
|
||||||
_.onChangedSliderStart();
|
_.onChangedSliderStart();
|
||||||
},
|
},
|
||||||
|
onDragUpdate: (duration) {
|
||||||
|
_.onUodatedSliderProgress(duration.timeStamp);
|
||||||
|
},
|
||||||
onSeek: (duration) {
|
onSeek: (duration) {
|
||||||
_.onChangedSliderEnd();
|
_.onChangedSliderEnd();
|
||||||
_.onChangedSlider(duration.inSeconds.toDouble());
|
_.onChangedSlider(duration.inSeconds.toDouble());
|
||||||
@ -80,7 +84,10 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
// fuc: () => _.togglePlay(),
|
// fuc: () => _.togglePlay(),
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// const SizedBox(width: 6),
|
PlayOrPauseButton(
|
||||||
|
controller: _,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
// 播放时间
|
// 播放时间
|
||||||
Obx(() {
|
Obx(() {
|
||||||
return Text(
|
return Text(
|
||||||
|
84
lib/plugin/pl_player/widgets/forward_seek.dart
Normal file
84
lib/plugin/pl_player/widgets/forward_seek.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ForwardSeekIndicator extends StatefulWidget {
|
||||||
|
final void Function(Duration) onChanged;
|
||||||
|
final void Function(Duration) onSubmitted;
|
||||||
|
const ForwardSeekIndicator({
|
||||||
|
Key? key,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.onSubmitted,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ForwardSeekIndicator> createState() => ForwardSeekIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ForwardSeekIndicatorState extends State<ForwardSeekIndicator> {
|
||||||
|
Duration value = const Duration(seconds: 10);
|
||||||
|
|
||||||
|
Timer? timer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
timer = Timer(const Duration(milliseconds: 400), () {
|
||||||
|
widget.onSubmitted.call(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void increment() {
|
||||||
|
timer?.cancel();
|
||||||
|
timer = Timer(const Duration(milliseconds: 400), () {
|
||||||
|
widget.onSubmitted.call(value);
|
||||||
|
});
|
||||||
|
widget.onChanged.call(value);
|
||||||
|
// 重复点击 快进秒数累加10
|
||||||
|
setState(() {
|
||||||
|
value += const Duration(seconds: 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Color(0x00767676),
|
||||||
|
Color(0x88767676),
|
||||||
|
],
|
||||||
|
begin: Alignment.centerLeft,
|
||||||
|
end: Alignment.centerRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
splashColor: const Color(0x44767676),
|
||||||
|
onTap: increment,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.fast_forward,
|
||||||
|
size: 24.0,
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8.0),
|
||||||
|
Text(
|
||||||
|
'快进${value.inSeconds}秒',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.0,
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
89
lib/plugin/pl_player/widgets/play_pause_btn.dart
Normal file
89
lib/plugin/pl_player/widgets/play_pause_btn.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
|
||||||
|
class PlayOrPauseButton extends StatefulWidget {
|
||||||
|
final double? iconSize;
|
||||||
|
final Color? iconColor;
|
||||||
|
final PlPlayerController? controller;
|
||||||
|
|
||||||
|
const PlayOrPauseButton({
|
||||||
|
super.key,
|
||||||
|
this.iconSize,
|
||||||
|
this.iconColor,
|
||||||
|
this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
PlayOrPauseButtonState createState() => PlayOrPauseButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayOrPauseButtonState extends State<PlayOrPauseButton>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController animation;
|
||||||
|
|
||||||
|
StreamSubscription<bool>? subscription;
|
||||||
|
late Player player;
|
||||||
|
bool isOpacity = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
player = widget.controller!.videoPlayerController!;
|
||||||
|
animation = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
value: player.state.playing ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
subscription ??= player.stream.playing.listen((event) {
|
||||||
|
if (event) {
|
||||||
|
animation.forward().then((value) => {
|
||||||
|
isOpacity = true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
animation.reverse().then((value) => {isOpacity = false});
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
animation.dispose();
|
||||||
|
subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
child: IconButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||||
|
),
|
||||||
|
onPressed: player.playOrPause,
|
||||||
|
color: Colors.white,
|
||||||
|
iconSize: 20,
|
||||||
|
// iconSize: widget.iconSize ?? _theme(context).buttonBarButtonSize,
|
||||||
|
// color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
|
||||||
|
icon: AnimatedIcon(
|
||||||
|
progress: animation,
|
||||||
|
icon: AnimatedIcons.play_pause,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 20,
|
||||||
|
// size: widget.iconSize ?? _theme(context).buttonBarButtonSize,
|
||||||
|
// color: widget.iconColor ?? _theme(context).buttonBarButtonColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user