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 {
return false;
} }
} }
if (nextIndex == episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {
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,16 +214,37 @@ 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(
icon: const Icon( width: 42,
Icons.skip_previous, height: 30,
size: 15, alignment: Alignment.center,
color: Colors.white, child: ComBtn(
icon: const Icon(
Icons.skip_previous,
size: 22,
color: Colors.white,
),
fuc: () {
bool? res;
if (videoIntroController != null) {
res = videoIntroController!.prevPlay();
}
if (bangumiIntroController != null) {
res = bangumiIntroController!.prevPlay();
}
if (res == false) {
SmartDialog.showToast('已经是第一集了');
}
},
tooltip: '上一集',
), ),
fuc: () {},
tooltip: '上一集',
), ),
/// 播放暂停 /// 播放暂停
@@ -232,14 +253,30 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
/// 下一集 /// 下一集
BottomControlType.next: ComBtn( BottomControlType.next: Container(
icon: const Icon( width: 42,
Icons.skip_next, height: 30,
size: 15, alignment: Alignment.center,
color: Colors.white, child: ComBtn(
icon: const Icon(
Icons.skip_next,
size: 22,
color: Colors.white,
),
fuc: () {
bool? res;
if (videoIntroController != null) {
res = videoIntroController!.nextPlay();
}
if (bangumiIntroController != null) {
res = bangumiIntroController!.nextPlay();
}
if (res == false) {
SmartDialog.showToast('已经是最后一集了');
}
},
tooltip: '下一集',
), ),
fuc: () {},
tooltip: '下一集',
), ),
/// 时间进度 /// 时间进度
@@ -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,65 +317,51 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
BottomControlType.space: const Spacer(), BottomControlType.space: const Spacer(),
/// 选集 /// 选集
BottomControlType.episode: Obx(() { BottomControlType.episode: Container(
bool isSeason = width: 42,
videoIntroController?.videoDetail.value.ugcSeason != null; height: 30,
bool isPage = videoIntroController?.videoDetail.value.pages != null && alignment: Alignment.center,
videoIntroController!.videoDetail.value.pages!.length > 1; child: ComBtn(
bool isBangumi = bangumiIntroController?.bangumiDetail.value != null; icon: const Icon(
if (!isSeason && !isPage && !isBangumi) { Icons.list,
return const SizedBox.shrink(); size: 22,
} color: Colors.white,
return SizedBox( ),
width: 42, tooltip: '选集',
height: 30, fuc: () {
child: Container( int currentCid = widget.controller.cid;
width: 42, String bvid = widget.controller.bvid;
height: 30, final List episodes = [];
alignment: Alignment.center, late Function changeFucCall;
child: ComBtn( if (isSeason) {
icon: const Icon( final List<SectionItem> sections =
Icons.list, videoIntroController!.videoDetail.value.ugcSeason!.sections!;
size: 22, for (int i = 0; i < sections.length; i++) {
color: Colors.white, final List<EpisodeItem> episodesList = sections[i].episodes!;
), episodes.addAll(episodesList);
tooltip: '选集', }
fuc: () { changeFucCall = videoIntroController!.changeSeasonOrbangu;
int currentCid = widget.controller.cid; } else if (isPage) {
String bvid = widget.controller.bvid; final List<Part> pages =
final List episodes = []; videoIntroController!.videoDetail.value.pages!;
late Function changeFucCall; episodes.addAll(pages);
if (isSeason) { changeFucCall = videoIntroController!.changeSeasonOrbangu;
final List<SectionItem> sections = videoIntroController! } else if (isBangumi) {
.videoDetail.value.ugcSeason!.sections!; episodes.addAll(
for (int i = 0; i < sections.length; i++) { bangumiIntroController!.bangumiDetail.value.episodes!);
final List<EpisodeItem> episodesList = changeFucCall = bangumiIntroController!.changeSeasonOrbangu;
sections[i].episodes!; }
episodes.addAll(episodesList); ListSheet(
} episodes: episodes,
changeFucCall = videoIntroController!.changeSeasonOrbangu; bvid: bvid,
} else if (isPage) { aid: IdUtils.bv2av(bvid),
final List<Part> pages = currentCid: currentCid,
videoIntroController!.videoDetail.value.pages!; changeFucCall: changeFucCall,
episodes.addAll(pages); context: context,
changeFucCall = videoIntroController!.changeSeasonOrbangu; ).buildShowBottomSheet();
} else if (isBangumi) { },
episodes.addAll(bangumiIntroController! ),
.bangumiDetail.value.episodes!); ),
changeFucCall =
bangumiIntroController!.changeSeasonOrbangu;
}
ListSheet(
episodes: episodes,
bvid: bvid,
aid: IdUtils.bv2av(bvid),
currentCid: currentCid,
changeFucCall: changeFucCall,
context: context,
).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,18 +515,110 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
key: _playerKey, key: _playerKey,
children: <Widget>[ children: <Widget>[
Obx( Obx(
() => Video( () => InteractiveViewer(
key: ValueKey('${_.videoFit.value}'), panEnabled: false, // 启用平移 //单指平移会与横竖手势冲突
controller: videoController, scaleEnabled: true, // 启用缩放
controls: NoVideoControls, minScale: 1.0,
pauseUponEnteringBackgroundMode: !_.continuePlayInBackground.value, maxScale: 2.0,
resumeUponEnteringForegroundMode: true, onInteractionStart: (ScaleStartDetails details) {
// 字幕尺寸调节 if (details.pointerCount == 2) {
subtitleViewConfiguration: SubtitleViewConfiguration( interacting = true;
style: subTitleStyle, }
padding: const EdgeInsets.all(24.0), },
textScaleFactor: MediaQuery.textScaleFactorOf(context)), onInteractionUpdate: (ScaleUpdateDetails details) {
fit: _.videoFit.value, 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}'),
controller: videoController,
controls: NoVideoControls,
pauseUponEnteringBackgroundMode:
!_.continuePlayInBackground.value,
resumeUponEnteringForegroundMode: true,
// 字幕尺寸调节
subtitleViewConfiguration: SubtitleViewConfiguration(
style: subTitleStyle,
padding: const EdgeInsets.all(24.0),
textScaleFactor: MediaQuery.textScaleFactorOf(context)),
fit: _.videoFit.value,
),
), ),
), ),
@@ -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) {},
), ),
), ),
), ),