opt video header

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-06-30 18:40:02 +08:00
parent 0df637fb73
commit ed6353e6d5
8 changed files with 244 additions and 216 deletions

View File

@@ -0,0 +1,59 @@
import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart';
class VideoHeader extends SliverPinnedDynamicHeader {
const VideoHeader({
super.key,
required super.minExtent,
required super.maxExtent,
required this.minVideoHeight,
required this.onScrollRatioChanged,
required super.child,
});
final double minVideoHeight;
final ValueChanged<double> onScrollRatioChanged;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderVideoHeader(
minExtent: minExtent,
maxExtent: maxExtent,
minVideoHeight: minVideoHeight,
onScrollRatioChanged: onScrollRatioChanged,
);
}
}
class RenderVideoHeader extends RenderSliverPinnedDynamicHeader {
RenderVideoHeader({
required super.minExtent,
required super.maxExtent,
required this.minVideoHeight,
required this.onScrollRatioChanged,
});
double? _scrollRatio;
final double minVideoHeight;
final ValueChanged<double> onScrollRatioChanged;
@override
void performLayout() {
super.performLayout();
final scrollOffset = constraints.scrollOffset;
final offset = scrollOffset - (maxExtent - minVideoHeight);
final scrollRatio = clampDouble(
offset.toPrecision(2) / (minVideoHeight - kToolbarHeight).toPrecision(2),
0.0,
1.0,
);
if (_scrollRatio != scrollRatio) {
_scrollRatio = scrollRatio;
WidgetsBinding.instance.addPostFrameCallback((_) {
onScrollRatioChanged(scrollRatio);
});
}
}
}

View File

@@ -486,7 +486,7 @@ class LiveRoomController extends GetxController {
void addDm(dynamic msg, [DanmakuContentItem<DanmakuExtra>? item]) { void addDm(dynamic msg, [DanmakuContentItem<DanmakuExtra>? item]) {
if (plPlayerController.showDanmaku) { if (plPlayerController.showDanmaku) {
if (item != null) { if (item != null && plPlayerController.enableShowLiveDanmaku.value) {
danmakuController?.addDanmaku(item); danmakuController?.addDanmaku(item);
} }
if (autoScroll && !disableAutoScroll.value) { if (autoScroll && !disableAutoScroll.value) {

View File

@@ -151,7 +151,6 @@ class _LiveRoomPageState extends State<LiveRoomPage>
plPlayerController.removeStatusLister(playerListener); plPlayerController.removeStatusLister(playerListener);
_liveRoomController _liveRoomController
..danmakuController?.clear() ..danmakuController?.clear()
..danmakuController?.pause()
..cancelLiveTimer() ..cancelLiveTimer()
..closeLiveMsg() ..closeLiveMsg()
..isPlaying = plPlayerController.playerStatus.isPlaying; ..isPlaying = plPlayerController.playerStatus.isPlaying;
@@ -809,7 +808,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
Obx( Obx(
() { () {
final enableShowLiveDanmaku = final enableShowLiveDanmaku =
plPlayerController.enableShowDanmaku.value; plPlayerController.enableShowLiveDanmaku.value;
return SizedBox( return SizedBox(
width: 34, width: 34,
height: 34, height: 34,
@@ -817,7 +816,8 @@ class _LiveRoomPageState extends State<LiveRoomPage>
style: IconButton.styleFrom(padding: .zero), style: IconButton.styleFrom(padding: .zero),
onPressed: () { onPressed: () {
final newVal = !enableShowLiveDanmaku; final newVal = !enableShowLiveDanmaku;
plPlayerController.enableShowDanmaku.value = newVal; plPlayerController.enableShowLiveDanmaku.value =
newVal;
if (!plPlayerController.tempPlayerConf) { if (!plPlayerController.tempPlayerConf) {
GStorage.setting.put( GStorage.setting.put(
SettingBoxKey.enableShowLiveDanmaku, SettingBoxKey.enableShowLiveDanmaku,
@@ -1091,7 +1091,7 @@ class _LiveDanmakuState extends State<LiveDanmaku> {
final option = DanmakuOptions.get(notFullscreen: widget.notFullscreen); final option = DanmakuOptions.get(notFullscreen: widget.notFullscreen);
return Obx( return Obx(
() => AnimatedOpacity( () => AnimatedOpacity(
opacity: plPlayerController.enableShowDanmaku.value opacity: plPlayerController.enableShowLiveDanmaku.value
? plPlayerController.danmakuOpacity.value ? plPlayerController.danmakuOpacity.value
: 0, : 0,
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),

View File

@@ -88,7 +88,7 @@ class _BottomControlState extends State<BottomControl> with HeaderMixin {
Obx( Obx(
() { () {
final enableShowLiveDanmaku = final enableShowLiveDanmaku =
plPlayerController.enableShowDanmaku.value; plPlayerController.enableShowLiveDanmaku.value;
return ComBtn( return ComBtn(
height: 30, height: 30,
tooltip: "${enableShowLiveDanmaku ? '关闭' : '开启'}弹幕", tooltip: "${enableShowLiveDanmaku ? '关闭' : '开启'}弹幕",
@@ -105,7 +105,7 @@ class _BottomControlState extends State<BottomControl> with HeaderMixin {
), ),
onTap: () { onTap: () {
final newVal = !enableShowLiveDanmaku; final newVal = !enableShowLiveDanmaku;
plPlayerController.enableShowDanmaku.value = newVal; plPlayerController.enableShowLiveDanmaku.value = newVal;
if (!plPlayerController.tempPlayerConf) { if (!plPlayerController.tempPlayerConf) {
GStorage.setting.put( GStorage.setting.put(
SettingBoxKey.enableShowLiveDanmaku, SettingBoxKey.enableShowLiveDanmaku,

View File

@@ -65,7 +65,8 @@ import 'package:PiliPlus/utils/theme_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:PiliPlus/utils/video_utils.dart'; import 'package:PiliPlus/utils/video_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'
show ExtendedNestedScrollViewState;
import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -169,8 +170,7 @@ class VideoDetailController extends GetxController
late final RxDouble scrollRatio = 0.0.obs; late final RxDouble scrollRatio = 0.0.obs;
ScrollController? _scrollCtr; ScrollController? _scrollCtr;
ScrollController get scrollCtr => ScrollController get scrollCtr => _scrollCtr ??= ScrollController();
_scrollCtr ??= ScrollController()..addListener(scrollListener);
late bool isExpanding = false; late bool isExpanding = false;
late bool isCollapsing = false; late bool isCollapsing = false;
@@ -315,26 +315,6 @@ class VideoDetailController extends GetxController
} catch (_) {} } catch (_) {}
} }
void scrollListener() {
if (scrollCtr.hasClients) {
if (scrollCtr.offset == 0) {
scrollRatio.value = 0;
} else {
double offset = scrollCtr.offset - (videoHeight - minVideoHeight);
if (offset > 0) {
scrollRatio.value = clampDouble(
offset.toPrecision(2) /
(minVideoHeight - kToolbarHeight).toPrecision(2),
0.0,
1.0,
);
} else {
scrollRatio.value = 0;
}
}
}
}
final isLoginVideo = Accounts.get(AccountType.video).isLogin; final isLoginVideo = Accounts.get(AccountType.video).isLogin;
late final watchProgress = GStorage.watchProgress; late final watchProgress = GStorage.watchProgress;
@@ -1247,9 +1227,7 @@ class VideoDetailController extends GetxController
introScrollCtr?.dispose(); introScrollCtr?.dispose();
introScrollCtr = null; introScrollCtr = null;
tabCtr.dispose(); tabCtr.dispose();
_scrollCtr _scrollCtr?.dispose();
?..removeListener(scrollListener)
..dispose();
animController animController
?..removeListener(_animListener) ?..removeListener(_animListener)
..dispose(); ..dispose();
@@ -1268,10 +1246,6 @@ class VideoDetailController extends GetxController
videoUrl = null; videoUrl = null;
audioUrl = null; audioUrl = null;
if (scrollRatio.value != 0) {
scrollRatio.refresh();
}
// danmaku // danmaku
savedDanmaku = null; savedDanmaku = null;

View File

@@ -11,7 +11,7 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/route_aware_mixin.dart'; import 'package:PiliPlus/common/widgets/route_aware_mixin.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart'; import 'package:PiliPlus/common/widgets/sliver/video_header.dart';
import 'package:PiliPlus/common/widgets/svg/play_icon.dart'; import 'package:PiliPlus/common/widgets/svg/play_icon.dart';
import 'package:PiliPlus/models/common/episode_panel_type.dart'; import 'package:PiliPlus/models/common/episode_panel_type.dart';
import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart';
@@ -493,24 +493,21 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
() { () {
final scrollRatio = final scrollRatio =
videoDetailController.scrollRatio.value; videoDetailController.scrollRatio.value;
final flag =
isPortrait &&
videoDetailController.scrollCtr.offset != 0;
return AppBar( return AppBar(
backgroundColor: flag && scrollRatio > 0 toolbarHeight: 0,
backgroundColor: isPortrait && scrollRatio > 0
? Color.lerp( ? Color.lerp(
Colors.black, Colors.black,
themeData.colorScheme.surface, themeData.colorScheme.surface,
scrollRatio, scrollRatio,
) )
: Colors.black, : Colors.black,
toolbarHeight: 0,
systemOverlayStyle: Platform.isAndroid systemOverlayStyle: Platform.isAndroid
? SystemUiOverlayStyle( ? SystemUiOverlayStyle(
statusBarIconBrightness: statusBarIconBrightness:
flag && scrollRatio >= 0.5 isPortrait && scrollRatio >= 0.5
? themeData.brightness.reverse ? themeData.brightness.reverse
: Brightness.light, : .light,
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
themeData.brightness.reverse, themeData.brightness.reverse,
) )
@@ -557,179 +554,20 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
? videoDetailController.animHeight ? videoDetailController.animHeight
: videoDetailController.videoHeight; : videoDetailController.videoHeight;
return [ return [
SliverPinnedDynamicHeader( VideoHeader(
minExtent: kToolbarHeight, minExtent: kToolbarHeight,
maxExtent: height, maxExtent: height,
minVideoHeight: videoDetailController.minVideoHeight,
onScrollRatioChanged: videoDetailController.scrollRatio.call,
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: .none,
children: [ children: [
SizedBox( SizedBox(
width: maxWidth, width: maxWidth,
height: height, height: height,
child: videoPlayer( child: videoPlayer(width: maxWidth, height: height),
width: maxWidth,
height: height,
),
),
Obx(
() {
Widget toolbar() => Opacity(
opacity: videoDetailController.scrollRatio.value,
child: Container(
color: themeData.colorScheme.surface,
alignment: Alignment.topCenter,
child: SizedBox(
height: kToolbarHeight,
child: Stack(
clipBehavior: Clip.none,
children: [
Align(
alignment: Alignment.centerLeft,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: '返回',
icon: Icon(
FontAwesomeIcons.arrowLeft,
size: 15,
color: themeData
.colorScheme
.onSurface,
),
onPressed: Get.back,
),
),
SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: '返回主页',
icon: Icon(
FontAwesomeIcons.house,
size: 15,
color: themeData
.colorScheme
.onSurface,
),
onPressed: videoDetailController
.plPlayerController
.onCloseAll,
),
),
],
),
),
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_arrow_rounded,
color:
themeData.colorScheme.primary,
),
Text(
'${videoDetailController.playedTime == null
? '立即'
: plPlayerController!.isCompleted
? '重新'
: '继续'}播放',
style: TextStyle(
color:
themeData.colorScheme.primary,
),
),
],
),
),
Align(
alignment: Alignment.centerRight,
child:
videoDetailController.playedTime ==
null
? _moreBtn(
themeData.colorScheme.onSurface,
)
: SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: "更多设置",
style: const ButtonStyle(
padding:
WidgetStatePropertyAll(
EdgeInsets.zero,
),
),
onPressed: () =>
(videoDetailController
.headerCtrKey
.currentState
as HeaderControlState?)
?.showSettingSheet(),
icon: Icon(
Icons.more_vert_outlined,
size: 19,
color: themeData
.colorScheme
.onSurface,
),
),
),
),
],
),
),
),
);
return videoDetailController.scrollRatio.value == 0 ||
videoDetailController.scrollCtr.offset == 0 ||
!isPortrait
? const SizedBox.shrink()
: Positioned.fill(
bottom: -2,
child: GestureDetector(
onTap: () {
if (!videoDetailController.isFileSource) {
if (videoDetailController.isQuerying) {
if (kDebugMode) {
debugPrint('handlePlay: querying');
}
return;
}
if (videoDetailController.videoUrl ==
null ||
videoDetailController.audioUrl ==
null) {
if (kDebugMode) {
debugPrint(
'handlePlay: videoUrl/audioUrl not initialized',
);
}
videoDetailController.queryVideoUrl();
return;
}
}
videoDetailController.scrollRatio.value =
0;
if (plPlayerController == null ||
videoDetailController.playedTime ==
null) {
handlePlay();
} else {
plPlayerController!.onDoubleTapCenter();
}
},
behavior: HitTestBehavior.opaque,
child: toolbar(),
),
);
},
), ),
_buildHeaderOverlay(),
], ],
), ),
), ),
@@ -766,6 +604,163 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
); );
} }
Widget _buildOverlayToolBar(double scrollRatio) {
final Icon icon;
final double spacing;
final String playStat;
if (videoDetailController.playedTime == null) {
spacing = 2;
icon = Icon(
Icons.play_arrow_rounded,
color: themeData.colorScheme.primary,
);
playStat = '立即';
} else if (plPlayerController!.isCompleted) {
spacing = 4;
icon = Icon(
size: 18,
Icons.replay_rounded,
color: themeData.colorScheme.primary,
);
playStat = '重新';
} else {
spacing = 2;
icon = Icon(
Icons.play_arrow_rounded,
color: themeData.colorScheme.primary,
);
playStat = '继续';
}
final playBtn = Row(
spacing: spacing,
mainAxisSize: .min,
children: [
icon,
Text(
'$playStat播放',
style: TextStyle(color: themeData.colorScheme.primary),
),
],
);
return Opacity(
opacity: videoDetailController.scrollRatio.value,
child: Container(
color: themeData.colorScheme.surface,
alignment: .topCenter,
child: SizedBox(
height: kToolbarHeight,
child: Stack(
clipBehavior: .none,
children: [
Align(
alignment: .centerLeft,
child: Row(
mainAxisSize: .min,
children: [
SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: '返回',
icon: Icon(
FontAwesomeIcons.arrowLeft,
size: 15,
color: themeData.colorScheme.onSurface,
),
onPressed: Get.back,
),
),
SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: '返回主页',
icon: Icon(
FontAwesomeIcons.house,
size: 15,
color: themeData.colorScheme.onSurface,
),
onPressed:
videoDetailController.plPlayerController.onCloseAll,
),
),
],
),
),
Center(child: playBtn),
Align(
alignment: .centerRight,
child: videoDetailController.playedTime == null
? _moreBtn(themeData.colorScheme.onSurface)
: SizedBox(
width: 42,
height: 34,
child: IconButton(
tooltip: "更多设置",
style: const ButtonStyle(
padding: WidgetStatePropertyAll(EdgeInsets.zero),
),
onPressed: () =>
(videoDetailController.headerCtrKey.currentState
as HeaderControlState?)
?.showSettingSheet(),
icon: Icon(
Icons.more_vert_outlined,
size: 19,
color: themeData.colorScheme.onSurface,
),
),
),
),
],
),
),
),
);
}
Widget _buildHeaderOverlay() {
return Obx(
() {
final scrollRatio = videoDetailController.scrollRatio.value;
if (scrollRatio == 0) {
return const SizedBox.shrink();
}
return Positioned.fill(
bottom: -2,
child: GestureDetector(
onTap: () {
if (!videoDetailController.isFileSource) {
if (videoDetailController.isQuerying) {
if (kDebugMode) {
debugPrint('handlePlay: querying');
}
return;
}
if (videoDetailController.videoUrl == null ||
videoDetailController.audioUrl == null) {
if (kDebugMode) {
debugPrint('handlePlay: videoUrl/audioUrl not initialized');
}
videoDetailController.queryVideoUrl();
return;
}
}
if (plPlayerController == null ||
videoDetailController.playedTime == null) {
handlePlay();
} else {
plPlayerController!.onDoubleTapCenter();
}
},
behavior: .opaque,
child: _buildOverlayToolBar(scrollRatio),
),
);
},
);
}
Widget get childWhenDisabledLandscape => Obx( Widget get childWhenDisabledLandscape => Obx(
() { () {
final isFullScreen = this.isFullScreen; final isFullScreen = this.isFullScreen;

View File

@@ -180,8 +180,8 @@ class PlayerFocus extends StatelessWidget {
return true; return true;
case LogicalKeyboardKey.keyD: case LogicalKeyboardKey.keyD:
final newVal = !plPlayerController.enableShowDanmaku.value; final newVal = !plPlayerController.enableShowDanmakuAdaptive.value;
plPlayerController.enableShowDanmaku.value = newVal; plPlayerController.enableShowDanmakuAdaptive.value = newVal;
if (!plPlayerController.tempPlayerConf) { if (!plPlayerController.tempPlayerConf) {
GStorage.setting.put( GStorage.setting.put(
plPlayerController.isLive plPlayerController.isLive

View File

@@ -184,10 +184,10 @@ class PlPlayerController with BlockConfigMixin {
bool get isVertical => _isVertical; bool get isVertical => _isVertical;
/// 弹幕开关 /// 弹幕开关
late final RxBool _enableShowDanmaku = Pref.enableShowDanmaku.obs; late final RxBool enableShowDanmaku = Pref.enableShowDanmaku.obs;
late final RxBool _enableShowLiveDanmaku = Pref.enableShowLiveDanmaku.obs; late final RxBool enableShowLiveDanmaku = Pref.enableShowLiveDanmaku.obs;
RxBool get enableShowDanmaku => RxBool get enableShowDanmakuAdaptive =>
isLive ? _enableShowLiveDanmaku : _enableShowDanmaku; isLive ? enableShowLiveDanmaku : enableShowDanmaku;
late final bool autoPiP = Pref.autoPiP; late final bool autoPiP = Pref.autoPiP;
bool get isPipMode => bool get isPipMode =>