opt video bar

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-08-12 14:14:57 +08:00
parent e251eaf811
commit a94493705d
3 changed files with 232 additions and 283 deletions

View File

@@ -335,7 +335,7 @@ class PlPlayerController {
late final bool enableHA = Pref.enableHA; late final bool enableHA = Pref.enableHA;
late final String hwdec = Pref.hardwareDecoding; late final String hwdec = Pref.hardwareDecoding;
late final defaultBtmProgressBehavior = late final progressType =
BtmProgressBehavior.values[Pref.btmProgressBehavior]; BtmProgressBehavior.values[Pref.btmProgressBehavior];
late final enableQuickDouble = Pref.enableQuickDouble; late final enableQuickDouble = Pref.enableQuickDouble;
late final fullScreenGestureReverse = Pref.fullScreenGestureReverse; late final fullScreenGestureReverse = Pref.fullScreenGestureReverse;

View File

@@ -160,11 +160,16 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
} }
StreamSubscription? _listener; StreamSubscription? _listener;
StreamSubscription? _controlsListener;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
plPlayerController.getPlayerKey = () => key; plPlayerController.getPlayerKey = () => key;
_controlsListener = plPlayerController.showControls.listen((bool val) {
final visible = val && !plPlayerController.controlsLock.value;
visible ? animationController.forward() : animationController.reverse();
});
animationController = AnimationController( animationController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
@@ -240,6 +245,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
@override @override
void dispose() { void dispose() {
_listener?.cancel(); _listener?.cancel();
_controlsListener?.cancel();
animationController.dispose(); animationController.dispose();
FlutterVolumeController.removeListener(); FlutterVolumeController.removeListener();
transformationController.dispose(); transformationController.dispose();
@@ -659,12 +665,14 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
children: [ children: [
...userSpecifyItemLeft.map(progressWidget), ...userSpecifyItemLeft.map(progressWidget),
Expanded( Expanded(
child: FittedBox( child: LayoutBuilder(
child: ConstrainedBox( builder: (context, constraints) => FittedBox(
constraints: BoxConstraints(minWidth: maxWidth), child: ConstrainedBox(
child: Row( constraints: BoxConstraints(minWidth: constraints.maxWidth),
mainAxisAlignment: MainAxisAlignment.end, child: Row(
children: userSpecifyItemRight.map(progressWidget).toList(), mainAxisAlignment: MainAxisAlignment.end,
children: userSpecifyItemRight.map(progressWidget).toList(),
),
), ),
), ),
), ),
@@ -1265,43 +1273,33 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
// 头部、底部控制条 // 头部、底部控制条
Obx( Positioned.fill(
() { child: ClipRect(
final visible = child: Column(
!plPlayerController.controlsLock.value && mainAxisAlignment: MainAxisAlignment.spaceBetween,
plPlayerController.showControls.value; children: [
visible AppBarAni(
? animationController.forward() isTop: true,
: animationController.reverse(); controller: animationController,
return Positioned.fill( child: widget.headerControl,
child: ClipRect(
child: Column(
children: [
AppBarAni(
isTop: true,
controller: animationController,
child: widget.headerControl,
),
const Spacer(),
AppBarAni(
isTop: false,
controller: animationController,
child:
widget.bottomControl ??
BottomControl(
controller: plPlayerController,
buildBottomControl: (bottomMaxWidth) =>
buildBottomControl(
maxWidth > maxHeight,
bottomMaxWidth,
),
),
),
],
), ),
), AppBarAni(
); isTop: false,
}, controller: animationController,
child:
widget.bottomControl ??
BottomControl(
controller: plPlayerController,
buildBottomControl: (bottomMaxWidth) =>
buildBottomControl(
maxWidth > maxHeight,
bottomMaxWidth,
),
),
),
],
),
),
), ),
// Positioned( // Positioned(
@@ -1372,99 +1370,58 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
/// 进度条 live模式下禁用 /// 进度条 live模式下禁用
Obx( if (!plPlayerController.isLive &&
() { plPlayerController.progressType != BtmProgressBehavior.alwaysHide)
final int value = plPlayerController.sliderPositionSeconds.value; Positioned(
final int max = plPlayerController.durationSeconds.value.inSeconds; bottom: -2.2,
final int buffer = plPlayerController.bufferedSeconds.value; left: 0,
if (plPlayerController.showControls.value) { right: 0,
return const SizedBox.shrink(); child: Obx(
} () {
if (plPlayerController.showControls.value) {
switch (plPlayerController.defaultBtmProgressBehavior) {
case BtmProgressBehavior.alwaysShow:
break;
case BtmProgressBehavior.alwaysHide:
return const SizedBox.shrink();
case BtmProgressBehavior.onlyShowFullScreen:
if (!isFullScreen) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
case BtmProgressBehavior.onlyHideFullScreen:
if (isFullScreen) { switch (plPlayerController.progressType) {
return const SizedBox.shrink(); case BtmProgressBehavior.onlyShowFullScreen:
if (!isFullScreen) {
return const SizedBox.shrink();
}
case BtmProgressBehavior.onlyHideFullScreen:
if (isFullScreen) {
return const SizedBox.shrink();
}
default:
} }
}
if (plPlayerController.isLive) { return Stack(
return const SizedBox.shrink();
}
if (value > max || max <= 0) {
return const SizedBox.shrink();
}
return Positioned(
bottom: -2.2,
left: 0,
right: 0,
child: Semantics(
// label: '${(value / max * 100).round()}%',
value: '${(value / max * 100).round()}%',
// enabled: false,
child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
if (plPlayerController.dmTrend.isNotEmpty &&
plPlayerController.showDmTreandChart.value)
buildDmChart(theme, plPlayerController),
if (plPlayerController.viewPointList.isNotEmpty &&
plPlayerController.showVP.value)
buildViewPointWidget(
plPlayerController,
4.25,
maxWidth,
),
IgnorePointer( IgnorePointer(
child: ProgressBar( child: Obx(() {
progress: Duration(seconds: value), final int value =
buffered: Duration(seconds: buffer), plPlayerController.sliderPositionSeconds.value;
total: Duration(seconds: max), final int max =
progressBarColor: primary, plPlayerController.durationSeconds.value.inSeconds;
baseBarColor: Colors.white.withValues(alpha: 0.2), final int buffer =
bufferedBarColor: primary.withValues(alpha: 0.4), plPlayerController.bufferedSeconds.value;
timeLabelLocation: TimeLabelLocation.none, if (value > max || max <= 0) {
thumbColor: primary, return const SizedBox.shrink();
barHeight: 3.5, }
thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5, return ProgressBar(
// onDragStart: (duration) { progress: Duration(seconds: value),
// feedBack(); buffered: Duration(seconds: buffer),
// plPlayerController.onChangedSliderStart(); total: Duration(seconds: max),
// }, progressBarColor: primary,
// onDragUpdate: (duration) { baseBarColor: Colors.white.withValues(alpha: 0.2),
// plPlayerController bufferedBarColor: primary.withValues(alpha: 0.4),
// .onUpdatedSliderProgress(duration.timeStamp); timeLabelLocation: TimeLabelLocation.none,
// if (plPlayerController.showSeekPreview) { thumbColor: primary,
// if (plPlayerController.showPreview.value.not) { barHeight: 3.5,
// plPlayerController.showPreview.value = true; thumbRadius: draggingFixedProgressBar.value ? 7 : 2.5,
// } );
// plPlayerController.previewDx.value = }),
// duration.localPosition.dx;
// }
// },
// onSeek: (duration) {
// if (plPlayerController.showSeekPreview) {
// plPlayerController.showPreview.value = false;
// }
// plPlayerController.onChangedSliderEnd();
// plPlayerController
// .onChangedSlider(duration.inSeconds.toDouble());
// plPlayerController.seekTo(
// Duration(seconds: duration.inSeconds),
// type: 'slider');
// },
),
), ),
if (plPlayerController.segmentList.isNotEmpty) if (plPlayerController.segmentList.isNotEmpty)
Positioned( Positioned(
@@ -1484,7 +1441,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
if (plPlayerController.viewPointList.isNotEmpty && if (plPlayerController.viewPointList.isNotEmpty &&
plPlayerController.showVP.value) plPlayerController.showVP.value) ...[
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -1501,6 +1458,15 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
), ),
buildViewPointWidget(
plPlayerController,
4.25,
maxWidth,
),
],
if (plPlayerController.dmTrend.isNotEmpty &&
plPlayerController.showDmTreandChart.value)
buildDmChart(theme, plPlayerController),
if (plPlayerController.showSeekPreview) if (plPlayerController.showSeekPreview)
Positioned( Positioned(
left: 0, left: 0,
@@ -1512,20 +1478,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
), ),
), ),
], ],
), );
// SlideTransition( },
// position: Tween<Offset>( ),
// begin: Offset.zero, ),
// end: const Offset(0, -1),
// ).animate(CurvedAnimation(
// parent: animationController,
// curve: Curves.easeInOut,
// )),
// child: ),
),
);
},
),
// 锁 // 锁
SafeArea( SafeArea(

View File

@@ -27,159 +27,152 @@ class BottomControl extends StatelessWidget {
Timer? accessibilityDebounce; Timer? accessibilityDebounce;
double lastAnnouncedValue = -1; double lastAnnouncedValue = -1;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.fromLTRB(10, 0, 10, 12),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final maxWidth = constraints.maxWidth; final maxWidth = constraints.maxWidth;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Obx( Padding(
() { padding: const EdgeInsets.fromLTRB(10, 0, 10, 7),
final int value = controller.sliderPositionSeconds.value; child: Obx(
final int max = controller.durationSeconds.value.inSeconds; () => Stack(
final int buffer = controller.bufferedSeconds.value; clipBehavior: Clip.none,
if (value > max || max <= 0) { alignment: Alignment.bottomCenter,
return const SizedBox.shrink(); children: [
} Obx(() {
return Padding( final int value =
padding: const EdgeInsets.only( controller.sliderPositionSeconds.value;
left: 10, final int max =
right: 10, controller.durationSeconds.value.inSeconds;
bottom: 7, final int buffer = controller.bufferedSeconds.value;
), if (value > max || max <= 0) {
child: Semantics( return const SizedBox.shrink();
value: '${(value / max * 100).round()}%', }
child: Stack( return ProgressBar(
clipBehavior: Clip.none, progress: Duration(seconds: value),
alignment: Alignment.bottomCenter, buffered: Duration(seconds: buffer),
children: [ total: Duration(seconds: max),
if (controller.dmTrend.isNotEmpty && progressBarColor: colorTheme,
controller.showDmTreandChart.value) baseBarColor: Colors.white.withValues(alpha: 0.2),
buildDmChart(theme, controller, 4.5), bufferedBarColor: colorTheme.withValues(alpha: 0.4),
if (controller.viewPointList.isNotEmpty && timeLabelLocation: TimeLabelLocation.none,
controller.showVP.value) thumbColor: colorTheme,
buildViewPointWidget( barHeight: 3.5,
controller, thumbRadius: 7,
8.75, onDragStart: (duration) {
maxWidth - 20, feedBack();
controller.onChangedSliderStart(
duration.timeStamp,
);
},
onDragUpdate: (duration) {
double newProgress =
duration.timeStamp.inSeconds / max;
if (controller.showSeekPreview) {
if (!controller.showPreview.value) {
controller.showPreview.value = true;
}
controller.previewDx.value =
duration.localPosition.dx;
}
if ((newProgress - lastAnnouncedValue).abs() >
0.02) {
accessibilityDebounce?.cancel();
accessibilityDebounce = Timer(
const Duration(milliseconds: 200),
() {
SemanticsService.announce(
"${(newProgress * 100).round()}%",
TextDirection.ltr,
);
lastAnnouncedValue = newProgress;
},
);
}
controller.onUpdatedSliderProgress(
duration.timeStamp,
);
},
onSeek: (duration) {
if (controller.showSeekPreview) {
controller.showPreview.value = false;
}
controller
..onChangedSliderEnd()
..onChangedSlider(duration.inSeconds.toDouble())
..seekTo(
Duration(seconds: duration.inSeconds),
isSeek: false,
);
SemanticsService.announce(
"${(duration.inSeconds / max * 100).round()}%",
TextDirection.ltr,
);
},
);
}),
if (controller.segmentList.isNotEmpty)
Positioned(
left: 0,
right: 0,
bottom: 5.25,
child: IgnorePointer(
child: RepaintBoundary(
child: CustomPaint(
key: const Key('segmentList'),
size: const Size(double.infinity, 3.5),
painter: SegmentProgressBar(
segmentColors: controller.segmentList,
),
),
), ),
ProgressBar(
progress: Duration(seconds: value),
buffered: Duration(seconds: buffer),
total: Duration(seconds: max),
progressBarColor: colorTheme,
baseBarColor: Colors.white.withValues(alpha: 0.2),
bufferedBarColor: colorTheme.withValues(alpha: 0.4),
timeLabelLocation: TimeLabelLocation.none,
thumbColor: colorTheme,
barHeight: 3.5,
thumbRadius: 7,
onDragStart: (duration) {
feedBack();
controller.onChangedSliderStart(
duration.timeStamp,
);
},
onDragUpdate: (duration) {
double newProgress =
duration.timeStamp.inSeconds / max;
if (controller.showSeekPreview) {
if (!controller.showPreview.value) {
controller.showPreview.value = true;
}
controller.previewDx.value =
duration.localPosition.dx;
}
if ((newProgress - lastAnnouncedValue).abs() >
0.02) {
accessibilityDebounce?.cancel();
accessibilityDebounce = Timer(
const Duration(milliseconds: 200),
() {
SemanticsService.announce(
"${(newProgress * 100).round()}%",
TextDirection.ltr,
);
lastAnnouncedValue = newProgress;
},
);
}
controller.onUpdatedSliderProgress(
duration.timeStamp,
);
},
onSeek: (duration) {
if (controller.showSeekPreview) {
controller.showPreview.value = false;
}
controller
..onChangedSliderEnd()
..onChangedSlider(duration.inSeconds.toDouble())
..seekTo(
Duration(seconds: duration.inSeconds),
isSeek: false,
);
SemanticsService.announce(
"${(duration.inSeconds / max * 100).round()}%",
TextDirection.ltr,
);
},
), ),
if (controller.segmentList.isNotEmpty) ),
Positioned( if (controller.viewPointList.isNotEmpty &&
left: 0, controller.showVP.value) ...[
right: 0, Positioned(
bottom: 5.25, left: 0,
child: IgnorePointer( right: 0,
child: RepaintBoundary( bottom: 5.25,
child: CustomPaint( child: IgnorePointer(
key: const Key('segmentList'), child: RepaintBoundary(
size: const Size(double.infinity, 3.5), child: CustomPaint(
painter: SegmentProgressBar( key: const Key('viewPointList'),
segmentColors: controller.segmentList, size: const Size(double.infinity, 3.5),
), painter: SegmentProgressBar(
), segmentColors: controller.viewPointList,
), ),
), ),
), ),
if (controller.viewPointList.isNotEmpty && ),
controller.showVP.value) ),
Positioned( buildViewPointWidget(
left: 0, controller,
right: 0, 8.75,
bottom: 5.25, maxWidth - 20,
child: IgnorePointer( ),
child: RepaintBoundary( ],
child: CustomPaint( if (controller.dmTrend.isNotEmpty &&
key: const Key('viewPointList'), controller.showDmTreandChart.value)
size: const Size(double.infinity, 3.5), buildDmChart(theme, controller, 4.5),
painter: SegmentProgressBar( if (controller.showSeekPreview &&
segmentColors: controller.viewPointList, controller.showControls.value)
), Positioned(
), left: 0,
), right: 0,
), bottom: 18,
), child: buildSeekPreviewWidget(
if (controller.showSeekPreview && controller,
controller.showControls.value) maxWidth - 20,
Positioned( ),
left: 0, ),
right: 0, ],
bottom: 18, ),
child: buildSeekPreviewWidget( ),
controller,
maxWidth - 20,
),
),
],
),
),
);
},
), ),
buildBottomControl(maxWidth), buildBottomControl(maxWidth),
const SizedBox(height: 12),
], ],
); );
}, },