opt: video bottom control

Closes #244

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-02-13 15:04:38 +08:00
parent 675932aa69
commit ba74cb8c01
3 changed files with 114 additions and 108 deletions

View File

@@ -3,13 +3,11 @@ enum BottomControlType {
playOrPause, playOrPause,
next, next,
time, time,
space,
episode, episode,
fit, fit,
subtitle, subtitle,
speed, speed,
fullscreen, fullscreen,
custom,
viewPoints, viewPoints,
superResolution, superResolution,
dmChart, dmChart,

View File

@@ -49,7 +49,6 @@ class PLVideoPlayer extends StatefulWidget {
this.headerControl, this.headerControl,
this.bottomControl, this.bottomControl,
this.danmuWidget, this.danmuWidget,
this.bottomList,
this.customWidget, this.customWidget,
this.customWidgets, this.customWidgets,
this.showEpisodes, this.showEpisodes,
@@ -63,7 +62,6 @@ class PLVideoPlayer extends StatefulWidget {
final PreferredSizeWidget? headerControl; final PreferredSizeWidget? headerControl;
final PreferredSizeWidget? bottomControl; final PreferredSizeWidget? bottomControl;
final Widget? danmuWidget; final Widget? danmuWidget;
final List<BottomControlType>? bottomList;
// List<Widget> or Widget // List<Widget> or Widget
final Widget? customWidget; final Widget? customWidget;
@@ -251,7 +249,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
} }
// 动态构建底部控制条 // 动态构建底部控制条
List<Widget> buildBottomControl() { Widget buildBottomControl() {
bool isSeason = videoIntroController?.videoDetail.value.ugcSeason != null; bool isSeason = videoIntroController?.videoDetail.value.ugcSeason != null;
bool isPage = videoIntroController?.videoDetail.value.pages != null && bool isPage = videoIntroController?.videoDetail.value.pages != null &&
videoIntroController!.videoDetail.value.pages!.length > 1; videoIntroController!.videoDetail.value.pages!.length > 1;
@@ -321,7 +319,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 时间进度 /// 时间进度
BottomControlType.time: Column( BottomControlType.time: Column(
crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 播放时间 // 播放时间
Obx(() { Obx(() {
@@ -354,9 +352,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
], ],
), ),
/// 空白占位
BottomControlType.space: const Spacer(),
/// 高能进度条 /// 高能进度条
BottomControlType.dmChart: Obx(() => plPlayerController.dmTrend.isEmpty BottomControlType.dmChart: Obx(() => plPlayerController.dmTrend.isEmpty
? const SizedBox.shrink() ? const SizedBox.shrink()
@@ -622,48 +617,61 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
BottomControlType.fullscreen: SizedBox( BottomControlType.fullscreen: SizedBox(
width: widgetWidth, width: widgetWidth,
height: 30, height: 30,
child: Obx(() => ComBtn( child: Obx(
icon: Icon( () => ComBtn(
isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, icon: Icon(
semanticLabel: isFullScreen ? '退出全屏' : '全屏', isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
size: 24, semanticLabel: isFullScreen ? '退出全屏' : '全屏',
color: Colors.white, size: 24,
), color: Colors.white,
fuc: () => ),
plPlayerController.triggerFullScreen(status: !isFullScreen), fuc: () =>
)), plPlayerController.triggerFullScreen(status: !isFullScreen),
),
),
), ),
}; };
final List<Widget> list = [];
List<BottomControlType> userSpecifyItem = widget.bottomList ?? List<BottomControlType> userSpecifyItemLeft = [
[ BottomControlType.playOrPause,
BottomControlType.playOrPause, BottomControlType.time,
BottomControlType.time, if (anySeason) BottomControlType.pre,
if (anySeason) BottomControlType.pre, if (anySeason) BottomControlType.next,
if (anySeason) BottomControlType.next, ];
BottomControlType.space,
BottomControlType.dmChart, List<BottomControlType> userSpecifyItemRight = [
BottomControlType.superResolution, BottomControlType.dmChart,
BottomControlType.viewPoints, BottomControlType.superResolution,
if (anySeason) BottomControlType.episode, BottomControlType.viewPoints,
if (isFullScreen) BottomControlType.fit, if (anySeason) BottomControlType.episode,
BottomControlType.subtitle, if (isFullScreen) BottomControlType.fit,
BottomControlType.speed, BottomControlType.subtitle,
BottomControlType.fullscreen, BottomControlType.speed,
]; BottomControlType.fullscreen,
for (var i = 0; i < userSpecifyItem.length; i++) { ];
if (userSpecifyItem[i] == BottomControlType.custom) {
if (widget.customWidget != null && widget.customWidget is Widget) { return Row(
list.add(widget.customWidget!); children: [
} ...userSpecifyItemLeft.map((item) => videoProgressWidgets[item]!),
if (widget.customWidgets != null && widget.customWidgets!.isNotEmpty) { Expanded(
list.addAll(widget.customWidgets!); child: LayoutBuilder(
} builder: (context, constraints) => FittedBox(
} else { child: ConstrainedBox(
list.add(videoProgressWidgets[userSpecifyItem[i]]!); constraints: BoxConstraints(
} minWidth: constraints.maxWidth,
} ),
return list; child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: userSpecifyItemRight
.map((item) => videoProgressWidgets[item]!)
.toList(),
),
),
),
),
),
],
);
} }
PlPlayerController get plPlayerController => widget.plPlayerController; PlPlayerController get plPlayerController => widget.plPlayerController;
@@ -1101,34 +1109,36 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
// 头部、底部控制条 // 头部、底部控制条
Obx( Obx(
() => Column( () => Positioned.fill(
children: [ child: Column(
if (widget.headerControl != null || children: [
plPlayerController.headerControl != null) if (widget.headerControl != null ||
ClipRect( plPlayerController.headerControl != null)
child: AppBarAni( ClipRect(
child: AppBarAni(
controller: animationController,
visible: !plPlayerController.controlsLock.value &&
plPlayerController.showControls.value,
position: 'top',
child: widget.headerControl ??
plPlayerController.headerControl!,
),
),
const Spacer(),
if (plPlayerController.showControls.value)
AppBarAni(
controller: animationController, controller: animationController,
visible: !plPlayerController.controlsLock.value && visible: !plPlayerController.controlsLock.value &&
plPlayerController.showControls.value, plPlayerController.showControls.value,
position: 'top', position: 'bottom',
child: widget.headerControl ?? child: widget.bottomControl ??
plPlayerController.headerControl!, BottomControl(
controller: plPlayerController,
buildBottomControl: buildBottomControl,
),
), ),
), ],
const Spacer(), ),
if (plPlayerController.showControls.value)
AppBarAni(
controller: animationController,
visible: !plPlayerController.controlsLock.value &&
plPlayerController.showControls.value,
position: 'bottom',
child: widget.bottomControl ??
BottomControl(
controller: plPlayerController,
buildBottomControl: buildBottomControl(),
),
),
],
), ),
), ),

View File

@@ -17,11 +17,11 @@ import 'package:PiliPlus/utils/feed_back.dart';
import '../../../common/widgets/audio_video_progress_bar.dart'; import '../../../common/widgets/audio_video_progress_bar.dart';
class BottomControl extends StatelessWidget implements PreferredSizeWidget { class BottomControl extends StatelessWidget implements PreferredSizeWidget {
final PlPlayerController? controller; final PlPlayerController controller;
final List<Widget>? buildBottomControl; final Function buildBottomControl;
const BottomControl({ const BottomControl({
this.controller, required this.controller,
this.buildBottomControl, required this.buildBottomControl,
super.key, super.key,
}); });
@@ -37,13 +37,13 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min,
children: [ children: [
Obx( Obx(
() { () {
final int value = controller!.sliderPositionSeconds.value; final int value = controller.sliderPositionSeconds.value;
final int max = controller!.durationSeconds.value.inSeconds; final int max = controller.durationSeconds.value.inSeconds;
final int buffer = controller!.bufferedSeconds.value; final int buffer = controller.bufferedSeconds.value;
if (value > max || max <= 0) { if (value > max || max <= 0) {
return nil; return nil;
} }
@@ -57,12 +57,12 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
if (controller?.dmTrend.isNotEmpty == true && if (controller.dmTrend.isNotEmpty &&
controller?.showDmChart.value == true) controller.showDmChart.value)
buildDmChart(context, controller!, 4.5), buildDmChart(context, controller, 4.5),
if (controller?.viewPointList.isNotEmpty == true && if (controller.viewPointList.isNotEmpty &&
controller?.showVP.value == true) controller.showVP.value)
buildViewPointWidget(controller!, 8.75), buildViewPointWidget(controller, 8.75),
ProgressBar( ProgressBar(
progress: Duration(seconds: value), progress: Duration(seconds: value),
buffered: Duration(seconds: buffer), buffered: Duration(seconds: buffer),
@@ -76,16 +76,16 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
thumbRadius: 7, thumbRadius: 7,
onDragStart: (duration) { onDragStart: (duration) {
feedBack(); feedBack();
controller!.onChangedSliderStart(); controller.onChangedSliderStart();
}, },
onDragUpdate: (duration) { onDragUpdate: (duration) {
double newProgress = double newProgress =
duration.timeStamp.inSeconds / max; duration.timeStamp.inSeconds / max;
if (controller!.showSeekPreview) { if (controller.showSeekPreview) {
if (controller!.showPreview.value.not) { if (controller.showPreview.value.not) {
controller!.showPreview.value = true; controller.showPreview.value = true;
} }
controller!.previewDx.value = controller.previewDx.value =
duration.localPosition.dx; duration.localPosition.dx;
} }
if ((newProgress - lastAnnouncedValue).abs() > 0.02) { if ((newProgress - lastAnnouncedValue).abs() > 0.02) {
@@ -98,17 +98,17 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
lastAnnouncedValue = newProgress; lastAnnouncedValue = newProgress;
}); });
} }
controller! controller
.onUpdatedSliderProgress(duration.timeStamp); .onUpdatedSliderProgress(duration.timeStamp);
}, },
onSeek: (duration) { onSeek: (duration) {
if (controller!.showSeekPreview) { if (controller.showSeekPreview) {
controller!.showPreview.value = false; controller.showPreview.value = false;
} }
controller!.onChangedSliderEnd(); controller.onChangedSliderEnd();
controller! controller
.onChangedSlider(duration.inSeconds.toDouble()); .onChangedSlider(duration.inSeconds.toDouble());
controller!.seekTo( controller.seekTo(
Duration(seconds: duration.inSeconds), Duration(seconds: duration.inSeconds),
type: 'slider'); type: 'slider');
SemanticsService.announce( SemanticsService.announce(
@@ -116,7 +116,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
TextDirection.ltr); TextDirection.ltr);
}, },
), ),
if (controller?.segmentList.isNotEmpty == true) if (controller.segmentList.isNotEmpty)
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -125,13 +125,13 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
child: CustomPaint( child: CustomPaint(
size: Size(double.infinity, 3.5), size: Size(double.infinity, 3.5),
painter: SegmentProgressBar( painter: SegmentProgressBar(
segmentColors: controller!.segmentList, segmentColors: controller.segmentList,
), ),
), ),
), ),
), ),
if (controller?.viewPointList.isNotEmpty == true && if (controller.viewPointList.isNotEmpty &&
controller?.showVP.value == true) controller.showVP.value)
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -140,17 +140,17 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
child: CustomPaint( child: CustomPaint(
size: Size(double.infinity, 3.5), size: Size(double.infinity, 3.5),
painter: SegmentProgressBar( painter: SegmentProgressBar(
segmentColors: controller!.viewPointList, segmentColors: controller.viewPointList,
), ),
), ),
), ),
), ),
if (controller?.showSeekPreview == true) if (controller.showSeekPreview)
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
bottom: 18, bottom: 18,
child: buildSeekPreviewWidget(controller!), child: buildSeekPreviewWidget(controller),
), ),
], ],
), ),
@@ -158,9 +158,7 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget {
); );
}, },
), ),
Row( buildBottomControl(),
children: [...buildBottomControl!],
),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
), ),