merge main

This commit is contained in:
guozhigq
2024-03-17 22:42:46 +08:00
45 changed files with 2150 additions and 1426 deletions

View File

@ -9,7 +9,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
@ -25,7 +24,7 @@ import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart';
import '../../../services/shutdown_timer_service.dart';
import 'widgets/header_control.dart';
import 'widgets/app_bar.dart';
class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key);
@ -38,7 +37,7 @@ class VideoDetailPage extends StatefulWidget {
class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware {
late VideoDetailController videoDetailController;
late VideoDetailController vdCtr;
PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream;
@ -65,20 +64,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void initState() {
super.initState();
heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
vdCtr = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(
VideoIntroController(bvid: Get.parameters['bvid']!),
tag: heroTag);
videoIntroController.videoDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
});
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
});
videoDetailController.cid.listen((p0) {
vdCtr.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0);
});
@ -93,16 +90,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStreamListen();
fullScreenStatusListener();
if (Platform.isAndroid) {
floating = videoDetailController.floating!;
floating = vdCtr.floating!;
autoEnterPip();
}
}
// 获取视频资源,初始化播放器
Future<void> videoSourceInit() async {
_futureBuilderFuture = videoDetailController.queryVideoUrl();
if (videoDetailController.autoPlay.value) {
plPlayerController = videoDetailController.plPlayerController;
_futureBuilderFuture = vdCtr.queryVideoUrl();
if (vdCtr.autoPlay.value) {
plPlayerController = vdCtr.plPlayerController;
plPlayerController!.addStatusLister(playerListener);
}
}
@ -131,10 +128,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (videoDetailController.videoType == SearchType.video) {
if (vdCtr.videoType == SearchType.video) {
videoIntroController.nextPlay();
}
if (videoDetailController.videoType == SearchType.media_bangumi) {
if (vdCtr.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay();
}
}
@ -146,8 +143,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
// 播放完展示控制栏
try {
PiPStatus currentStatus =
await videoDetailController.floating!.pipStatus;
PiPStatus currentStatus = await vdCtr.floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false);
}
@ -168,17 +164,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 未开启自动播放时触发播放
Future<void> handlePlay() async {
await videoDetailController.playerInit();
plPlayerController = videoDetailController.plPlayerController;
await vdCtr.playerInit();
plPlayerController = vdCtr.plPlayerController;
plPlayerController!.addStatusLister(playerListener);
videoDetailController.autoPlay.value = true;
videoDetailController.isShowCover.value = false;
vdCtr.autoPlay.value = true;
vdCtr.isShowCover.value = false;
}
void fullScreenStatusListener() {
plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) {
videoDetailController.hiddenReplyReplyPanel();
vdCtr.hiddenReplyReplyPanel();
}
});
}
@ -190,14 +186,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose();
}
if (videoDetailController.floating != null) {
videoDetailController.floating!.dispose();
if (vdCtr.floating != null) {
vdCtr.floating!.dispose();
}
videoPlayerServiceHandler.onVideoDetailDispose();
if (Platform.isAndroid) {
floating.toggleAutoPip(autoEnter: false);
floating.dispose();
}
appbarStream.close();
super.dispose();
}
@ -207,10 +204,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
as bool) {
videoDetailController.brightness = plPlayerController!.brightness.value;
vdCtr.brightness = plPlayerController!.brightness.value;
}
if (plPlayerController != null) {
videoDetailController.defaultST = plPlayerController!.position.value;
vdCtr.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause();
@ -225,21 +222,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (plPlayerController != null &&
plPlayerController!.videoPlayerController != null) {
setState(() {
videoDetailController.setSubtitleContent();
vdCtr.setSubtitleContent();
isShowing = true;
});
}
videoDetailController.isFirstTime = false;
vdCtr.isFirstTime = false;
final bool autoplay = autoPlayEnable;
videoDetailController.playerInit(autoplay: autoplay);
vdCtr.playerInit(autoplay: autoplay);
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
videoDetailController.autoPlay.value =
!videoDetailController.isShowCover.value;
vdCtr.autoPlay.value = !vdCtr.isShowCover.value;
videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300));
plPlayerController?.seekTo(videoDetailController.defaultST);
plPlayerController?.seekTo(vdCtr.defaultST);
plPlayerController?.play();
}
plPlayerController?.addStatusLister(playerListener);
@ -262,9 +258,166 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
Widget build(BuildContext context) {
final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
// final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
final sizeContext = MediaQuery.sizeOf(context);
final _context = MediaQuery.of(context);
late double defaultVideoHeight = sizeContext.width * 9 / 16;
late RxDouble videoHeight = defaultVideoHeight.obs;
final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight;
statusBarHeight + kToolbarHeight + videoHeight.value;
// ignore: no_leading_underscores_for_local_identifiers
// 竖屏
final bool isPortrait = _context.orientation == Orientation.portrait;
// 横屏
final bool isLandscape = _context.orientation == Orientation.landscape;
final Rx<bool> isFullScreen = plPlayerController?.isFullScreen ?? false.obs;
// 全屏时高度撑满
if (isLandscape || isFullScreen.value == true) {
videoHeight.value = Get.size.height;
enterFullScreen();
} else {
videoHeight.value = defaultVideoHeight;
exitFullScreen();
}
/// 播放器面板
Widget videoPlayerPanel = FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return Obx(
() {
return !vdCtr.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: vdCtr.headerControl,
danmuWidget: Obx(
() => PlDanmaku(
key: Key(vdCtr.danmakuCid.value.toString()),
cid: vdCtr.danmakuCid.value,
playerController: plPlayerController!,
),
),
);
},
);
} else {
// 加载失败异常处理
return const SizedBox();
}
},
);
/// tabbar
Widget tabbarBuild = Container(
width: double.infinity,
height: 45,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
),
),
child: Row(
children: [
const SizedBox(width: 20),
Expanded(
child: TabBar(
controller: vdCtr.tabCtr,
dividerColor: Colors.transparent,
tabs: vdCtr.tabs.map((String name) => Tab(text: name)).toList(),
),
),
SizedBox(
width: 220,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
height: 32,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => vdCtr.showShootDanmakuSheet(),
child: const Text('发弹幕', style: TextStyle(fontSize: 12)),
),
),
const SizedBox(width: 4),
SizedBox(
width: 34,
height: 32,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
plPlayerController?.isOpenDanmu.value =
!(plPlayerController?.isOpenDanmu.value ?? false);
},
child: Obx(() => Text(
'',
style: TextStyle(
fontSize: 12,
color: (plPlayerController?.isOpenDanmu.value ??
false)
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
),
)),
),
),
const SizedBox(width: 14),
],
),
),
),
],
),
);
/// 手动播放
Widget handlePlayPanel() {
return Stack(
children: [
GestureDetector(
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: vdCtr.videoItem['pic'],
width: Get.width,
height: videoHeight.value,
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: buildCustomAppBar(),
),
Positioned(
right: 12,
bottom: 10,
child: IconButton(
tooltip: '播放',
onPressed: () => handlePlay(),
icon: Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
)),
),
],
);
}
Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true,
@ -276,7 +429,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Scaffold(
resizeToAvoidBottomInset: false,
key: videoDetailController.scaffoldKey,
key: vdCtr.scaffoldKey,
backgroundColor: Colors.black,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(0),
@ -302,21 +455,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return SliverAppBar(
automaticallyImplyLeading: false,
// 假装使用一个非空变量避免Obx检测不到而罢工
pinned: videoDetailController.autoPlay.value ^
false ^
videoDetailController.autoPlay.value,
pinned: vdCtr.autoPlay.value,
elevation: 0,
scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled,
expandedHeight: MediaQuery.of(context).orientation ==
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height -
? (MediaQuery.sizeOf(context).height -
(MediaQuery.of(context).orientation ==
Orientation.landscape
? 0
: MediaQuery.of(context).padding.top)
: videoHeight,
: MediaQuery.of(context).padding.top))
: videoHeight.value,
backgroundColor: Colors.black,
flexibleSpace: FlexibleSpaceBar(
background: PopScope(
@ -336,108 +487,27 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth =
boxConstraints.maxWidth;
final double maxHeight =
boxConstraints.maxHeight;
// final double maxWidth =
// boxConstraints.maxWidth;
// final double maxHeight =
// boxConstraints.maxHeight;
return Stack(
children: <Widget>[
if (isShowing)
FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context,
AsyncSnapshot snapshot) {
if (snapshot.hasData &&
snapshot.data['status']) {
return Obx(
() =>
!videoDetailController
.autoPlay.value
? nil
: PLVideoPlayer(
controller:
plPlayerController!,
headerControl:
videoDetailController
.headerControl,
danmuWidget: Obx(
() => PlDanmaku(
key: Key(videoDetailController
.danmakuCid
.value
.toString()),
cid: videoDetailController
.danmakuCid
.value,
playerController:
plPlayerController!,
),
),
),
);
} else {
return buildCustomAppBar();
}
},
),
if (isShowing) videoPlayerPanel,
/// 关闭自动播放时 手动播放
if (!videoDetailController
.autoPlay.value) ...<Widget>[
Obx(
() => Visibility(
visible: videoDetailController
.isShowCover.value,
child: Positioned(
top: 0,
left: 0,
right: 0,
child: GestureDetector(
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: videoDetailController
.videoItem['pic'],
width: maxWidth,
height: maxHeight,
),
),
),
Obx(
() => Visibility(
visible: !vdCtr.autoPlay.value &&
vdCtr.isShowCover.value,
child: Positioned(
top: 0,
left: 0,
right: 0,
child: handlePlayPanel(),
),
),
Obx(
() => Visibility(
visible: videoDetailController
.isShowCover.value &&
videoDetailController
.isEffective.value,
child: Stack(
children: [
Positioned(
top: 0,
left: 0,
right: 0,
child: buildCustomAppBar(),
),
Positioned(
right: 12,
bottom: 10,
child: IconButton(
tooltip: '播放',
onPressed: () =>
handlePlay(),
icon: Image.asset(
'assets/images/play.png',
width: 60,
height: 60,
)),
),
],
)),
),
]
),
],
);
},
@ -448,18 +518,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
),
];
},
// pinnedHeaderSliverHeightBuilder: () {
// return playerStatus != PlayerStatus.playing
// ? statusBarHeight + kToolbarHeight
// : pinnedHeaderHeight;
// },
/// 不收回
pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).orientation ==
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height
: pinnedHeaderHeight;
: playerStatus != PlayerStatus.playing
? kToolbarHeight
: pinnedHeaderHeight;
},
onlyOneScrollInBody: true,
body: ColoredBox(
@ -467,54 +535,23 @@ class _VideoDetailPageState extends State<VideoDetailPage>
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Opacity(
opacity: 0,
child: SizedBox(
width: double.infinity,
height: 0,
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
),
tabbarBuild,
Expanded(
child: TabBarView(
controller: videoDetailController.tabCtr,
controller: vdCtr.tabCtr,
children: <Widget>[
Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: const PageStorageKey<String>('简介'),
slivers: <Widget>[
if (videoDetailController.videoType ==
SearchType.video) ...[
VideoIntroPanel(
bvid: videoDetailController.bvid),
] else if (videoDetailController.videoType ==
if (vdCtr.videoType == SearchType.video) ...[
VideoIntroPanel(bvid: vdCtr.bvid),
] else if (vdCtr.videoType ==
SearchType.media_bangumi) ...[
Obx(() => BangumiIntroPanel(
cid: videoDetailController.cid.value)),
cid: vdCtr.cid.value)),
],
// if (videoDetailController.videoType ==
// SearchType.video) ...[
// SliverPersistentHeader(
// floating: true,
// pinned: true,
// delegate: SliverHeaderDelegate(
// height: 50,
// child:
// const MenuRow(loadingStatus: false),
// ),
// ),
// ],
SliverToBoxAdapter(
child: Divider(
indent: 12,
@ -524,15 +561,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.withOpacity(0.06),
),
),
const RelatedVideoPanel(),
if (vdCtr.videoType == SearchType.video &&
vdCtr.enableRelatedVideo)
const RelatedVideoPanel(),
],
);
},
),
Obx(
() => VideoReplyPanel(
bvid: videoDetailController.bvid,
oid: videoDetailController.oid.value,
bvid: vdCtr.bvid,
oid: vdCtr.oid.value,
),
)
],
@ -546,56 +585,26 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 重新进入会刷新
// 播放完成/暂停播放
// StreamBuilder(
// stream: appbarStream.stream,
// initialData: 0,
// builder: ((context, snapshot) {
// return ScrollAppBar(
// snapshot.data!.toDouble(),
// () => continuePlay(),
// playerStatus,
// null,
// );
// }),
// )
StreamBuilder(
stream: appbarStream.stream,
initialData: 0,
builder: ((context, snapshot) {
return ScrollAppBar(
snapshot.data!.toDouble(),
() => continuePlay(),
playerStatus,
null,
);
}),
)
],
),
);
Widget childWhenEnabled = FutureBuilder(
key: Key(heroTag),
future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData && snapshot.data['status']) {
return Obx(
() => !videoDetailController.autoPlay.value
? const SizedBox()
: PLVideoPlayer(
controller: plPlayerController!,
headerControl: HeaderControl(
controller: plPlayerController,
videoDetailCtr: videoDetailController,
bvid: videoDetailController.bvid,
videoType: videoDetailController.videoType,
),
danmuWidget: Obx(
() => PlDanmaku(
key: Key(
videoDetailController.danmakuCid.value.toString()),
cid: videoDetailController.danmakuCid.value,
playerController: plPlayerController!,
),
),
),
);
} else {
return nil;
}
},
);
if (Platform.isAndroid) {
return PiPSwitcher(
childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled,
childWhenEnabled: videoPlayerPanel,
floating: floating,
);
} else {
@ -636,8 +645,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
ComBtn(
icon: const Icon(Icons.history_outlined, size: 22),
fuc: () async {
var res = await UserHttp.toViewLater(
bvid: videoDetailController.bvid);
var res = await UserHttp.toViewLater(bvid: vdCtr.bvid);
SmartDialog.showToast(res['msg']);
},
),