Merge branch 'main' of github.com:guozhigq/pilipala

This commit is contained in:
guozhigq
2024-03-14 00:08:52 +08:00
3 changed files with 308 additions and 345 deletions

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/constants.dart'; import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart'; import 'package:pilipala/models/common/reply_type.dart';
@ -19,6 +20,7 @@ import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/video_utils.dart'; import 'package:pilipala/utils/video_utils.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
import '../../../http/danmaku.dart';
import '../../../utils/id_utils.dart'; import '../../../utils/id_utils.dart';
import 'widgets/header_control.dart'; import 'widgets/header_control.dart';
@ -383,4 +385,86 @@ class VideoDetailController extends GetxController
? replyReplyBottomSheetCtr!.close() ? replyReplyBottomSheetCtr!.close()
: print('replyReplyBottomSheetCtr is null'); : print('replyReplyBottomSheetCtr is null');
} }
/// 发送弹幕
void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController();
bool isSending = false; // 追踪是否正在发送
showDialog(
context: Get.context!,
builder: (BuildContext context) {
// TODO: 支持更多类型和颜色的弹幕
return AlertDialog(
title: const Text('发送弹幕'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextField(
controller: textController,
);
}),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextButton(
onPressed: isSending
? null
: () async {
final String msg = textController.text;
if (msg.isEmpty) {
SmartDialog.showToast('弹幕内容不能为空');
return;
} else if (msg.length > 100) {
SmartDialog.showToast('弹幕内容不能超过100个字符');
return;
}
setState(() {
isSending = true; // 开始发送,更新状态
});
//修改按钮文字
// SmartDialog.showToast('弹幕发送中,\n$msg');
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: cid.value,
msg: textController.text,
bvid: bvid,
progress:
plPlayerController.position.value.inMilliseconds,
type: 1,
);
setState(() {
isSending = false; // 发送结束,更新状态
});
if (res['status']) {
SmartDialog.showToast('发送成功');
// 发送成功,自动预览该弹幕,避免重新请求
// TODO: 暂停状态下预览弹幕仍会移动与计时可考虑添加到dmSegList或其他方式实现
plPlayerController.danmakuController?.addItems([
DanmakuItem(
msg,
color: Colors.white,
time: plPlayerController
.position.value.inMilliseconds,
type: DanmakuItemType.scroll,
isSend: true,
)
]);
Get.back();
} else {
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
}
},
child: Text(isSending ? '发送中...' : '发送'),
);
})
],
);
},
);
}
} }

View File

@ -25,7 +25,7 @@ import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../../services/shutdown_timer_service.dart'; import '../../../services/shutdown_timer_service.dart';
import 'widgets/header_control.dart'; import 'widgets/app_bar.dart';
class VideoDetailPage extends StatefulWidget { class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key); const VideoDetailPage({Key? key}) : super(key: key);
@ -38,7 +38,7 @@ class VideoDetailPage extends StatefulWidget {
class _VideoDetailPageState extends State<VideoDetailPage> class _VideoDetailPageState extends State<VideoDetailPage>
with TickerProviderStateMixin, RouteAware { with TickerProviderStateMixin, RouteAware {
late VideoDetailController videoDetailController; late VideoDetailController vdCtr;
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream; late StreamController<double> appbarStream;
@ -65,20 +65,18 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void initState() { void initState() {
super.initState(); super.initState();
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag); vdCtr = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put( videoIntroController = Get.put(
VideoIntroController(bvid: Get.parameters['bvid']!), VideoIntroController(bvid: Get.parameters['bvid']!),
tag: heroTag); tag: heroTag);
videoIntroController.videoDetail.listen((value) { videoIntroController.videoDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange( videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
value, videoDetailController.cid.value);
}); });
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) { bangumiIntroController.bangumiDetail.listen((value) {
videoPlayerServiceHandler.onVideoDetailChange( videoPlayerServiceHandler.onVideoDetailChange(value, vdCtr.cid.value);
value, videoDetailController.cid.value);
}); });
videoDetailController.cid.listen((p0) { vdCtr.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange( videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0); bangumiIntroController.bangumiDetail.value, p0);
}); });
@ -93,16 +91,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStreamListen(); appbarStreamListen();
fullScreenStatusListener(); fullScreenStatusListener();
if (Platform.isAndroid) { if (Platform.isAndroid) {
floating = videoDetailController.floating!; floating = vdCtr.floating!;
autoEnterPip(); autoEnterPip();
} }
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
Future<void> videoSourceInit() async { Future<void> videoSourceInit() async {
_futureBuilderFuture = videoDetailController.queryVideoUrl(); _futureBuilderFuture = vdCtr.queryVideoUrl();
if (videoDetailController.autoPlay.value) { if (vdCtr.autoPlay.value) {
plPlayerController = videoDetailController.plPlayerController; plPlayerController = vdCtr.plPlayerController;
plPlayerController!.addStatusLister(playerListener); plPlayerController!.addStatusLister(playerListener);
} }
} }
@ -131,10 +129,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 顺序播放 列表循环 /// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause && if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) { plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (videoDetailController.videoType == SearchType.video) { if (vdCtr.videoType == SearchType.video) {
videoIntroController.nextPlay(); videoIntroController.nextPlay();
} }
if (videoDetailController.videoType == SearchType.media_bangumi) { if (vdCtr.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay(); bangumiIntroController.nextPlay();
} }
} }
@ -146,8 +144,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
// 播放完展示控制栏 // 播放完展示控制栏
try { try {
PiPStatus currentStatus = PiPStatus currentStatus = await vdCtr.floating!.pipStatus;
await videoDetailController.floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) { if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false); plPlayerController!.onLockControl(false);
} }
@ -168,17 +165,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 未开启自动播放时触发播放 /// 未开启自动播放时触发播放
Future<void> handlePlay() async { Future<void> handlePlay() async {
await videoDetailController.playerInit(); await vdCtr.playerInit();
plPlayerController = videoDetailController.plPlayerController; plPlayerController = vdCtr.plPlayerController;
plPlayerController!.addStatusLister(playerListener); plPlayerController!.addStatusLister(playerListener);
videoDetailController.autoPlay.value = true; vdCtr.autoPlay.value = true;
videoDetailController.isShowCover.value = false; vdCtr.isShowCover.value = false;
} }
void fullScreenStatusListener() { void fullScreenStatusListener() {
plPlayerController?.isFullScreen.listen((bool isFullScreen) { plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) { if (isFullScreen) {
videoDetailController.hiddenReplyReplyPanel(); vdCtr.hiddenReplyReplyPanel();
} }
}); });
} }
@ -190,8 +187,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose(); plPlayerController!.dispose();
} }
if (videoDetailController.floating != null) { if (vdCtr.floating != null) {
videoDetailController.floating!.dispose(); vdCtr.floating!.dispose();
} }
videoPlayerServiceHandler.onVideoDetailDispose(); videoPlayerServiceHandler.onVideoDetailDispose();
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -207,10 +204,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 开启 /// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false) if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
as bool) { as bool) {
videoDetailController.brightness = plPlayerController!.brightness.value; vdCtr.brightness = plPlayerController!.brightness.value;
} }
if (plPlayerController != null) { if (plPlayerController != null) {
videoDetailController.defaultST = plPlayerController!.position.value; vdCtr.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true; videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
@ -226,17 +223,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.videoPlayerController != null) { plPlayerController!.videoPlayerController != null) {
setState(() => isShowing = true); setState(() => isShowing = true);
} }
videoDetailController.isFirstTime = false; vdCtr.isFirstTime = false;
final bool autoplay = autoPlayEnable; final bool autoplay = autoPlayEnable;
videoDetailController.playerInit(autoplay: autoplay); vdCtr.playerInit(autoplay: autoplay);
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回 /// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
videoDetailController.autoPlay.value = vdCtr.autoPlay.value = !vdCtr.isShowCover.value;
!videoDetailController.isShowCover.value;
videoIntroController.isPaused = false; videoIntroController.isPaused = false;
if (_extendNestCtr.position.pixels == 0 && autoplay) { if (_extendNestCtr.position.pixels == 0 && autoplay) {
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
plPlayerController?.seekTo(videoDetailController.defaultST); plPlayerController?.seekTo(vdCtr.defaultST);
plPlayerController?.play(); plPlayerController?.play();
} }
plPlayerController?.addStatusLister(playerListener); plPlayerController?.addStatusLister(playerListener);
@ -259,9 +255,158 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override @override
Widget build(BuildContext context) { 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 = 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 = SizedBox(
width: double.infinity,
height: 45,
child: Row(
children: [
Expanded(
child: TabBar(
controller: vdCtr.tabCtr,
dividerColor: Colors.transparent,
tabs: vdCtr.tabs.map((String name) => Tab(text: name)).toList(),
),
),
SizedBox(
width: 200,
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(
'发弹幕',
),
),
),
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(
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( Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait && top: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
@ -273,7 +418,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [ children: [
Scaffold( Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
key: videoDetailController.scaffoldKey, key: vdCtr.scaffoldKey,
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(0), preferredSize: const Size.fromHeight(0),
@ -299,21 +444,19 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return SliverAppBar( return SliverAppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
// 假装使用一个非空变量避免Obx检测不到而罢工 // 假装使用一个非空变量避免Obx检测不到而罢工
pinned: videoDetailController.autoPlay.value ^ pinned: vdCtr.autoPlay.value,
false ^
videoDetailController.autoPlay.value,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled, forceElevated: innerBoxIsScrolled,
expandedHeight: MediaQuery.of(context).orientation == expandedHeight: MediaQuery.of(context).orientation ==
Orientation.landscape || Orientation.landscape ||
plPlayerController?.isFullScreen.value == true plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height - ? (MediaQuery.sizeOf(context).height -
(MediaQuery.of(context).orientation == (MediaQuery.of(context).orientation ==
Orientation.landscape Orientation.landscape
? 0 ? 0
: MediaQuery.of(context).padding.top) : MediaQuery.of(context).padding.top))
: videoHeight, : videoHeight.value,
backgroundColor: Colors.black, backgroundColor: Colors.black,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: PopScope( background: PopScope(
@ -333,108 +476,27 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, builder: (BuildContext context,
BoxConstraints boxConstraints) { BoxConstraints boxConstraints) {
final double maxWidth = // final double maxWidth =
boxConstraints.maxWidth; // boxConstraints.maxWidth;
final double maxHeight = // final double maxHeight =
boxConstraints.maxHeight; // boxConstraints.maxHeight;
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
if (isShowing) if (isShowing) videoPlayerPanel,
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 (!videoDetailController Obx(
.autoPlay.value) ...<Widget>[ () => Visibility(
Obx( visible: !vdCtr.autoPlay.value &&
() => Visibility( vdCtr.isShowCover.value,
visible: videoDetailController child: Positioned(
.isShowCover.value, top: 0,
child: Positioned( left: 0,
top: 0, right: 0,
left: 0, child: handlePlayPanel(),
right: 0,
child: GestureDetector(
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: videoDetailController
.videoItem['pic'],
width: maxWidth,
height: maxHeight,
),
),
),
), ),
), ),
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,
)),
),
],
)),
),
]
], ],
); );
}, },
@ -445,18 +507,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
), ),
]; ];
}, },
// pinnedHeaderSliverHeightBuilder: () {
// return playerStatus != PlayerStatus.playing
// ? statusBarHeight + kToolbarHeight
// : pinnedHeaderHeight;
// },
/// 不收回 /// 不收回
pinnedHeaderSliverHeightBuilder: () { pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).orientation == return MediaQuery.of(context).orientation ==
Orientation.landscape || Orientation.landscape ||
plPlayerController?.isFullScreen.value == true plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height ? MediaQuery.sizeOf(context).height
: pinnedHeaderHeight; : playerStatus != PlayerStatus.playing
? kToolbarHeight
: pinnedHeaderHeight;
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
body: ColoredBox( body: ColoredBox(
@ -464,54 +524,23 @@ class _VideoDetailPageState extends State<VideoDetailPage>
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: Column(
children: [ children: [
Opacity( tabbarBuild,
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(),
),
),
),
),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: videoDetailController.tabCtr, controller: vdCtr.tabCtr,
children: <Widget>[ children: <Widget>[
Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return CustomScrollView( return CustomScrollView(
key: const PageStorageKey<String>('简介'), key: const PageStorageKey<String>('简介'),
slivers: <Widget>[ slivers: <Widget>[
if (videoDetailController.videoType == if (vdCtr.videoType == SearchType.video) ...[
SearchType.video) ...[ VideoIntroPanel(bvid: vdCtr.bvid),
VideoIntroPanel( ] else if (vdCtr.videoType ==
bvid: videoDetailController.bvid),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[ SearchType.media_bangumi) ...[
Obx(() => BangumiIntroPanel( 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( SliverToBoxAdapter(
child: Divider( child: Divider(
indent: 12, indent: 12,
@ -528,8 +557,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
), ),
Obx( Obx(
() => VideoReplyPanel( () => VideoReplyPanel(
bvid: videoDetailController.bvid, bvid: vdCtr.bvid,
oid: videoDetailController.oid.value, oid: vdCtr.oid.value,
), ),
) )
], ],
@ -543,56 +572,26 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 重新进入会刷新 /// 重新进入会刷新
// 播放完成/暂停播放 // 播放完成/暂停播放
// StreamBuilder( StreamBuilder(
// stream: appbarStream.stream, stream: appbarStream.stream,
// initialData: 0, initialData: 0,
// builder: ((context, snapshot) { builder: ((context, snapshot) {
// return ScrollAppBar( return ScrollAppBar(
// snapshot.data!.toDouble(), snapshot.data!.toDouble(),
// () => continuePlay(), () => continuePlay(),
// playerStatus, playerStatus,
// null, 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) { if (Platform.isAndroid) {
return PiPSwitcher( return PiPSwitcher(
childWhenDisabled: childWhenDisabled, childWhenDisabled: childWhenDisabled,
childWhenEnabled: childWhenEnabled, childWhenEnabled: videoPlayerPanel,
floating: floating, floating: floating,
); );
} else { } else {
@ -633,8 +632,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
ComBtn( ComBtn(
icon: const Icon(Icons.history_outlined, size: 22), icon: const Icon(Icons.history_outlined, size: 22),
fuc: () async { fuc: () async {
var res = await UserHttp.toViewLater( var res = await UserHttp.toViewLater(bvid: vdCtr.bvid);
bvid: videoDetailController.bvid);
SmartDialog.showToast(res['msg']); SmartDialog.showToast(res['msg']);
}, },
), ),

View File

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -17,7 +16,6 @@ import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/http/danmaku.dart';
import 'package:pilipala/services/shutdown_timer_service.dart'; import 'package:pilipala/services/shutdown_timer_service.dart';
import '../../../../models/common/search_type.dart'; import '../../../../models/common/search_type.dart';
import '../../../../models/video_detail_res.dart'; import '../../../../models/video_detail_res.dart';
@ -221,88 +219,6 @@ class _HeaderControlState extends State<HeaderControl> {
); );
} }
/// 发送弹幕
void showShootDanmakuSheet() {
final TextEditingController textController = TextEditingController();
bool isSending = false; // 追踪是否正在发送
showDialog(
context: Get.context!,
builder: (BuildContext context) {
// TODO: 支持更多类型和颜色的弹幕
return AlertDialog(
title: const Text('发送弹幕(测试)'),
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextField(
controller: textController,
);
}),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return TextButton(
onPressed: isSending
? null
: () async {
final String msg = textController.text;
if (msg.isEmpty) {
SmartDialog.showToast('弹幕内容不能为空');
return;
} else if (msg.length > 100) {
SmartDialog.showToast('弹幕内容不能超过100个字符');
return;
}
setState(() {
isSending = true; // 开始发送,更新状态
});
//修改按钮文字
// SmartDialog.showToast('弹幕发送中,\n$msg');
final dynamic res = await DanmakaHttp.shootDanmaku(
oid: widget.videoDetailCtr!.cid.value,
msg: textController.text,
bvid: widget.videoDetailCtr!.bvid,
progress:
widget.controller!.position.value.inMilliseconds,
type: 1,
);
setState(() {
isSending = false; // 发送结束,更新状态
});
if (res['status']) {
SmartDialog.showToast('发送成功');
// 发送成功,自动预览该弹幕,避免重新请求
// TODO: 暂停状态下预览弹幕仍会移动与计时可考虑添加到dmSegList或其他方式实现
widget.controller!.danmakuController!.addItems([
DanmakuItem(
msg,
color: Colors.white,
time: widget
.controller!.position.value.inMilliseconds,
type: DanmakuItemType.scroll,
isSend: true,
)
]);
Get.back();
} else {
SmartDialog.showToast('发送失败,错误信息为${res['msg']}');
}
},
child: Text(isSending ? '发送中...' : '发送'),
);
})
],
);
},
);
}
/// 定时关闭 /// 定时关闭
void scheduleExit() async { void scheduleExit() async {
const List<int> scheduleTimeChoices = [ const List<int> scheduleTimeChoices = [
@ -1166,41 +1082,6 @@ class _HeaderControlState extends State<HeaderControl> {
// ), // ),
// fuc: () => _.screenshot(), // fuc: () => _.screenshot(),
// ), // ),
SizedBox(
width: 56,
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => showShootDanmakuSheet(),
child: const Text(
'发弹幕',
style: textStyle,
),
),
),
SizedBox(
width: 34,
height: 34,
child: Obx(
() => IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
_.isOpenDanmu.value = !_.isOpenDanmu.value;
},
icon: Icon(
_.isOpenDanmu.value
? Icons.subtitles_outlined
: Icons.subtitles_off_outlined,
size: 19,
color: Colors.white,
),
),
),
),
SizedBox(width: buttonSpace), SizedBox(width: buttonSpace),
if (Platform.isAndroid) ...<Widget>[ if (Platform.isAndroid) ...<Widget>[
SizedBox( SizedBox(