feat: 视频新增双指缩放手势,底部控制条新增上下集快捷切换按钮,视频尺寸选项仅在全屏下显示

This commit is contained in:
orz12
2024-07-11 17:58:07 +08:00
parent 712542e0a1
commit d5adf07c13
3 changed files with 275 additions and 197 deletions

View File

@@ -315,6 +315,31 @@ class BangumiIntroController extends GetxController {
return result; return result;
} }
bool prevPlay() {
late List episodes;
if (bangumiDetail.value.episodes != null) {
episodes = bangumiDetail.value.episodes!;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
int currentIndex =
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
int prevIndex = currentIndex - 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
if (prevIndex < 0) {
if (platRepeat == PlayRepeat.listCycle) {
prevIndex = episodes.length - 1;
} else {
return false;
}
}
int cid = episodes[prevIndex].cid!;
String bvid = episodes[prevIndex].bvid!;
int aid = episodes[prevIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
return true;
}
/// 列表循环或者顺序播放时,自动播放下一个 /// 列表循环或者顺序播放时,自动播放下一个
bool nextPlay() { bool nextPlay() {
late List episodes; late List episodes;
@@ -328,16 +353,13 @@ class BangumiIntroController extends GetxController {
int nextIndex = currentIndex + 1; int nextIndex = currentIndex + 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat; PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环 // 列表循环
if (platRepeat == PlayRepeat.listCycle) {
if (nextIndex == episodes.length - 1) { if (nextIndex == episodes.length - 1) {
if (platRepeat == PlayRepeat.listCycle) {
nextIndex = 0; nextIndex = 0;
} } else {
}
if (nextIndex == episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {
return false; return false;
} }
}
int cid = episodes[nextIndex].cid!; int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!; String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!; int aid = episodes[nextIndex].aid!;

View File

@@ -486,6 +486,45 @@ class VideoIntroController extends GetxController {
super.onClose(); super.onClose();
} }
/// 播放上一个
bool prevPlay() {
final List episodes = [];
bool isPages = false;
if (videoDetail.value.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!;
for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList);
}
} else if (videoDetail.value.pages != null) {
isPages = true;
final List<Part> pages = videoDetail.value.pages!;
episodes.addAll(pages);
}
final int currentIndex =
episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int prevIndex = currentIndex - 1;
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (prevIndex < 0) {
if (platRepeat == PlayRepeat.listCycle) {
prevIndex = episodes.length - 1;
} else {
return false;
}
}
final int cid = episodes[prevIndex].cid!;
final String rBvid = isPages ? bvid : episodes[prevIndex].bvid;
final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[prevIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid);
return true;
}
/// 列表循环或者顺序播放时,自动播放下一个 /// 列表循环或者顺序播放时,自动播放下一个
bool nextPlay() { bool nextPlay() {
final List episodes = []; final List episodes = [];
@@ -514,8 +553,7 @@ class VideoIntroController extends GetxController {
if (nextIndex >= episodes.length) { if (nextIndex >= episodes.length) {
if (platRepeat == PlayRepeat.listCycle) { if (platRepeat == PlayRepeat.listCycle) {
nextIndex = 0; nextIndex = 0;
} } else {
if (platRepeat == PlayRepeat.listOrder) {
return false; return false;
} }
} }

View File

@@ -89,9 +89,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
late int defaultBtmProgressBehavior; late int defaultBtmProgressBehavior;
late bool enableQuickDouble; late bool enableQuickDouble;
late bool fullScreenGestureReverse; late bool fullScreenGestureReverse;
//播放器放缩
bool interacting = false;
// 用于记录上一次全屏切换手势触发时间,避免误触
DateTime? lastFullScreenToggleTime;
// 是否在调整固定进度条 // 是否在调整固定进度条
RxBool draggingFixedProgressBar = false.obs; RxBool draggingFixedProgressBar = false.obs;
// 阅读器限制 // 阅读器限制
@@ -214,17 +214,38 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 动态构建底部控制条 // 动态构建底部控制条
List<Widget> buildBottomControl() { List<Widget> buildBottomControl() {
final PlPlayerController _ = widget.controller; final PlPlayerController _ = widget.controller;
bool isSeason = videoIntroController?.videoDetail.value.ugcSeason != null;
bool isPage = videoIntroController?.videoDetail.value.pages != null &&
videoIntroController!.videoDetail.value.pages!.length > 1;
bool isBangumi = bangumiIntroController?.bangumiDetail.value != null;
bool anySeason = isSeason || isPage || isBangumi;
Map<BottomControlType, Widget> videoProgressWidgets = { Map<BottomControlType, Widget> videoProgressWidgets = {
/// 上一集 /// 上一集
BottomControlType.pre: ComBtn( BottomControlType.pre: Container(
width: 42,
height: 30,
alignment: Alignment.center,
child: ComBtn(
icon: const Icon( icon: const Icon(
Icons.skip_previous, Icons.skip_previous,
size: 15, size: 22,
color: Colors.white, color: Colors.white,
), ),
fuc: () {}, fuc: () {
bool? res;
if (videoIntroController != null) {
res = videoIntroController!.prevPlay();
}
if (bangumiIntroController != null) {
res = bangumiIntroController!.prevPlay();
}
if (res == false) {
SmartDialog.showToast('已经是第一集了');
}
},
tooltip: '上一集', tooltip: '上一集',
), ),
),
/// 播放暂停 /// 播放暂停
BottomControlType.playOrPause: PlayOrPauseButton( BottomControlType.playOrPause: PlayOrPauseButton(
@@ -232,15 +253,31 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
/// 下一集 /// 下一集
BottomControlType.next: ComBtn( BottomControlType.next: Container(
width: 42,
height: 30,
alignment: Alignment.center,
child: ComBtn(
icon: const Icon( icon: const Icon(
Icons.skip_next, Icons.skip_next,
size: 15, size: 22,
color: Colors.white, color: Colors.white,
), ),
fuc: () {}, fuc: () {
bool? res;
if (videoIntroController != null) {
res = videoIntroController!.nextPlay();
}
if (bangumiIntroController != null) {
res = bangumiIntroController!.nextPlay();
}
if (res == false) {
SmartDialog.showToast('已经是最后一集了');
}
},
tooltip: '下一集', tooltip: '下一集',
), ),
),
/// 时间进度 /// 时间进度
BottomControlType.time: Column( BottomControlType.time: Column(
@@ -260,14 +297,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
'已播放${Utils.durationReadFormat(Utils.timeFormat(_.positionSeconds.value))}', '已播放${Utils.durationReadFormat(Utils.timeFormat(_.positionSeconds.value))}',
); );
}), }),
// const SizedBox(width: 2),
// const ExcludeSemantics(
// child: Text(
// '/',
// style: textStyle,
// ),
// ),
// const SizedBox(width: 2),
Obx( Obx(
() => Text( () => Text(
Utils.timeFormat(_.durationSeconds.value), Utils.timeFormat(_.durationSeconds.value),
@@ -288,19 +317,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
BottomControlType.space: const Spacer(), BottomControlType.space: const Spacer(),
/// 选集 /// 选集
BottomControlType.episode: Obx(() { BottomControlType.episode: Container(
bool isSeason =
videoIntroController?.videoDetail.value.ugcSeason != null;
bool isPage = videoIntroController?.videoDetail.value.pages != null &&
videoIntroController!.videoDetail.value.pages!.length > 1;
bool isBangumi = bangumiIntroController?.bangumiDetail.value != null;
if (!isSeason && !isPage && !isBangumi) {
return const SizedBox.shrink();
}
return SizedBox(
width: 42,
height: 30,
child: Container(
width: 42, width: 42,
height: 30, height: 30,
alignment: Alignment.center, alignment: Alignment.center,
@@ -317,11 +334,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
final List episodes = []; final List episodes = [];
late Function changeFucCall; late Function changeFucCall;
if (isSeason) { if (isSeason) {
final List<SectionItem> sections = videoIntroController! final List<SectionItem> sections =
.videoDetail.value.ugcSeason!.sections!; videoIntroController!.videoDetail.value.ugcSeason!.sections!;
for (int i = 0; i < sections.length; i++) { for (int i = 0; i < sections.length; i++) {
final List<EpisodeItem> episodesList = final List<EpisodeItem> episodesList = sections[i].episodes!;
sections[i].episodes!;
episodes.addAll(episodesList); episodes.addAll(episodesList);
} }
changeFucCall = videoIntroController!.changeSeasonOrbangu; changeFucCall = videoIntroController!.changeSeasonOrbangu;
@@ -331,10 +347,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
episodes.addAll(pages); episodes.addAll(pages);
changeFucCall = videoIntroController!.changeSeasonOrbangu; changeFucCall = videoIntroController!.changeSeasonOrbangu;
} else if (isBangumi) { } else if (isBangumi) {
episodes.addAll(bangumiIntroController! episodes.addAll(
.bangumiDetail.value.episodes!); bangumiIntroController!.bangumiDetail.value.episodes!);
changeFucCall = changeFucCall = bangumiIntroController!.changeSeasonOrbangu;
bangumiIntroController!.changeSeasonOrbangu;
} }
ListSheet( ListSheet(
episodes: episodes, episodes: episodes,
@@ -345,8 +360,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
context: context, context: context,
).buildShowBottomSheet(); ).buildShowBottomSheet();
}, },
))); ),
}), ),
/// 画面比例 /// 画面比例
BottomControlType.fit: SizedBox( BottomControlType.fit: SizedBox(
@@ -463,11 +478,11 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
[ [
BottomControlType.playOrPause, BottomControlType.playOrPause,
BottomControlType.time, BottomControlType.time,
// BottomControlType.pre, if (anySeason) BottomControlType.pre,
// BottomControlType.next, if (anySeason) BottomControlType.next,
BottomControlType.space, BottomControlType.space,
BottomControlType.episode, if (anySeason) BottomControlType.episode,
BottomControlType.fit, if (_.isFullScreen.value) BottomControlType.fit,
BottomControlType.subtitle, BottomControlType.subtitle,
BottomControlType.speed, BottomControlType.speed,
BottomControlType.fullscreen, BottomControlType.fullscreen,
@@ -500,11 +515,102 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
key: _playerKey, key: _playerKey,
children: <Widget>[ children: <Widget>[
Obx( Obx(
() => Video( () => InteractiveViewer(
panEnabled: false, // 启用平移 //单指平移会与横竖手势冲突
scaleEnabled: true, // 启用缩放
minScale: 1.0,
maxScale: 2.0,
onInteractionStart: (ScaleStartDetails details) {
if (details.pointerCount == 2) {
interacting = true;
}
},
onInteractionUpdate: (ScaleUpdateDetails details) {
if (interacting) return;
if (details.pointerCount == 2) {
interacting = true;
return;
}
/// 锁定时禁用
if (_.controlsLock.value) return;
Offset delta = details.focalPointDelta;
if (delta.distance < 2) return;
RenderBox renderBox =
_playerKey.currentContext!.findRenderObject() as RenderBox;
// if onHorizontalDragUpdate
if (delta.dx.abs() > 5 * delta.dy.abs()) {
// live模式下禁用
if (_.videoType.value == 'live') return;
final int curSliderPosition =
_.sliderPosition.value.inMilliseconds;
final double scale = 90000 / renderBox.size.width;
final Duration pos = Duration(
milliseconds:
curSliderPosition + (delta.dx * scale).round());
final Duration result =
pos.clamp(Duration.zero, _.duration.value);
_.onUpdatedSliderProgress(result);
_.onChangedSliderStart();
} else if (delta.dx.abs() * 5 < delta.dy.abs()) {
// 垂直方向 音量/亮度调节
final double totalWidth = renderBox.size.width;
final double tapPosition = details.localFocalPoint.dx;
final double sectionWidth = totalWidth / 4;
if (tapPosition < sectionWidth) {
// 左边区域 👈
final double level = renderBox.size.height * 3;
final double brightness =
_brightnessValue.value - delta.dy / level;
final double result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else if (tapPosition < sectionWidth * 3) {
// 全屏
const double threshold = 7.0; // 滑动阈值
void fullScreenTrigger(bool status) async {
EasyThrottle.throttle(
'fullScreen', const Duration(milliseconds: 500), () {
// widget.controller.triggerFullScreen(status: status);
_.triggerFullScreen(status: status);
});
}
if (delta.dy > threshold) {
// 下滑
if (_.isFullScreen.value ^ fullScreenGestureReverse) {
fullScreenTrigger(fullScreenGestureReverse);
}
} else if (delta.dy < -threshold) {
// 上划
if (!_.isFullScreen.value ^ fullScreenGestureReverse) {
fullScreenTrigger(!fullScreenGestureReverse);
}
}
} else {
// 右边区域
final double level = renderBox.size.height * 0.5;
EasyThrottle.throttle(
'setVolume', const Duration(milliseconds: 20), () {
final double volume = _volumeValue.value - delta.dy / level;
final double result = volume.clamp(0.0, 1.0);
setVolume(result);
});
}
}
},
onInteractionEnd: (details) {
if (_.isSliderMoving.value) {
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value, type: 'slider');
}
interacting = false;
},
child: Video(
key: ValueKey('${_.videoFit.value}'), key: ValueKey('${_.videoFit.value}'),
controller: videoController, controller: videoController,
controls: NoVideoControls, controls: NoVideoControls,
pauseUponEnteringBackgroundMode: !_.continuePlayInBackground.value, pauseUponEnteringBackgroundMode:
!_.continuePlayInBackground.value,
resumeUponEnteringForegroundMode: true, resumeUponEnteringForegroundMode: true,
// 字幕尺寸调节 // 字幕尺寸调节
subtitleViewConfiguration: SubtitleViewConfiguration( subtitleViewConfiguration: SubtitleViewConfiguration(
@@ -514,6 +620,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
fit: _.videoFit.value, fit: _.videoFit.value,
), ),
), ),
),
/// 长按倍速 toast /// 长按倍速 toast
Obx( Obx(
@@ -758,95 +865,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
onLongPressEnd: (LongPressEndDetails details) { onLongPressEnd: (LongPressEndDetails details) {
_.setDoubleSpeedStatus(false); _.setDoubleSpeedStatus(false);
}, },
/// 水平位置 快进 live模式下禁用
onHorizontalDragUpdate: (DragUpdateDetails details) {
// live模式下禁用 锁定时🔒禁用
if (_.videoType.value == 'live' || _.controlsLock.value) {
return;
}
// final double tapPosition = details.localPosition.dx;
final int curSliderPosition =
_.sliderPosition.value.inMilliseconds;
RenderBox renderBox =
_playerKey.currentContext!.findRenderObject() as RenderBox;
final double scale = 90000 / renderBox.size.width;
final Duration pos = Duration(
milliseconds:
curSliderPosition + (details.delta.dx * scale).round());
final Duration result =
pos.clamp(Duration.zero, _.duration.value);
_.onUpdatedSliderProgress(result);
_.onChangedSliderStart();
// _initTapPositoin = tapPosition;
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_.videoType.value == 'live' || _.controlsLock.value) {
return;
}
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value, type: 'slider');
},
// 垂直方向 音量/亮度调节
onVerticalDragUpdate: (DragUpdateDetails details) async {
RenderBox renderBox =
_playerKey.currentContext!.findRenderObject() as RenderBox;
/// 锁定时禁用
if (_.controlsLock.value) {
return;
}
final double totalWidth = renderBox.size.width;
final double tapPosition = details.localPosition.dx;
final double sectionWidth = totalWidth / 4;
final double delta = details.delta.dy;
if (lastFullScreenToggleTime != null &&
DateTime.now().difference(lastFullScreenToggleTime!) <
const Duration(milliseconds: 500)) {
return;
}
if (tapPosition < sectionWidth) {
// 左边区域 👈
final double level = renderBox.size.height * 3;
final double brightness =
_brightnessValue.value - delta / level;
final double result = brightness.clamp(0.0, 1.0);
setBrightness(result);
} else if (tapPosition < sectionWidth * 3) {
// 全屏
final double dy = details.delta.dy;
const double threshold = 7.0; // 滑动阈值
void fullScreenTrigger(bool status) async {
lastFullScreenToggleTime = DateTime.now();
await widget.controller.triggerFullScreen(status: status);
}
if (dy > _distance.value && dy > threshold) {
// 下滑退出全屏/进入全屏
if (_.isFullScreen.value ^ fullScreenGestureReverse) {
fullScreenTrigger(fullScreenGestureReverse);
}
_distance.value = 0.0;
} else if (dy < _distance.value && dy < -threshold) {
// 上划进入全屏/退出全屏
if (!_.isFullScreen.value ^ fullScreenGestureReverse) {
fullScreenTrigger(!fullScreenGestureReverse);
}
_distance.value = 0.0;
}
_distance.value = dy;
} else {
// 右边区域 👈
final double level = renderBox.size.height * 0.5;
EasyThrottle.throttle(
'setVolume', const Duration(milliseconds: 20), () {
final double volume = _volumeValue.value - delta / level;
final double result = volume.clamp(0.0, 1.0);
setVolume(result);
});
}
},
onVerticalDragEnd: (DragEndDetails details) {},
), ),
), ),
), ),