Compare commits

..

1 Commits

Author SHA1 Message Date
bc9ea43cd2 feat: 视频番剧详情页代码整理 2024-03-12 23:45:22 +08:00
18 changed files with 864 additions and 1150 deletions

View File

@ -7,5 +7,5 @@ enum DynamicsType {
extension BusinessTypeExtension on DynamicsType { extension BusinessTypeExtension on DynamicsType {
String get values => ['all', 'video', 'pgc', 'article'][index]; String get values => ['all', 'video', 'pgc', 'article'][index];
String get labels => ['全部', '投稿', '', '专栏'][index]; String get labels => ['全部', '视频', '', '专栏'][index];
} }

View File

@ -25,13 +25,6 @@ class BangumiIntroController extends GetxController {
? int.tryParse(Get.parameters['epId']!) ? int.tryParse(Get.parameters['epId']!)
: null; : null;
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
Map? videoItem = {};
BangumiInfoModel? bangumiItem;
// 请求状态 // 请求状态
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
@ -63,27 +56,6 @@ class BangumiIntroController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (Get.arguments.isNotEmpty as bool) {
if (Get.arguments.containsKey('bangumiItem') as bool) {
preRender = true;
bangumiItem = Get.arguments['bangumiItem'];
// bangumiItem!['pic'] = args.pic;
// if (args.title is String) {
// videoItem!['title'] = args.title;
// } else {
// String str = '';
// for (Map map in args.title) {
// str += map['text'];
// }
// videoItem!['title'] = str;
// }
// if (args.stat != null) {
// videoItem!['stat'] = args.stat;
// }
// videoItem!['pubdate'] = args.pubdate;
// videoItem!['owner'] = args.owner;
}
}
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
userLogin = userInfo != null; userLogin = userInfo != null;
} }
@ -196,7 +168,8 @@ class BangumiIntroController extends GetxController {
} }
Get.back(); Get.back();
}, },
child: const Text('确定')) child: const Text('确定'),
)
], ],
); );
}); });

View File

@ -12,11 +12,10 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/bangumi/widgets/bangumi_panel.dart'; import 'package:pilipala/pages/bangumi/widgets/bangumi_panel.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/action_item.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/action_item.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/action_row_item.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/fav_panel.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/fav_panel.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../../common/widgets/http_error.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
@ -51,9 +50,6 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
cid = widget.cid!; cid = widget.cid!;
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag); videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiIntroController.bangumiDetail.listen((BangumiInfoModel value) {
bangumiDetail = value;
});
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); _futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
videoDetailCtr.cid.listen((int p0) { videoDetailCtr.cid.listen((int p0) {
cid = p0; cid = p0;
@ -68,27 +64,32 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
future: _futureBuilderFuture, future: _futureBuilderFuture,
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
if (snapshot.data['status']) { if (snapshot.data['status']) {
// 请求成功 // 请求成功
return Obx(
return BangumiInfo( () => BangumiInfo(
loadingStatus: false, bangumiDetail: bangumiIntroController.bangumiDetail.value,
bangumiDetail: bangumiDetail,
cid: cid, cid: cid,
),
); );
} else { } else {
// 请求错误 // 请求错误
// return HttpError( return HttpError(
// errMsg: snapshot.data['msg'], errMsg: snapshot.data['msg'],
// fn: () => Get.back(), fn: () => Get.back(),
// ); );
return const SizedBox();
} }
} else { } else {
return BangumiInfo( return const SliverToBoxAdapter(
loadingStatus: true, child: SizedBox(
bangumiDetail: bangumiDetail, height: 100,
cid: cid, child: Center(
child: CircularProgressIndicator(),
),
),
); );
} }
}, },
@ -99,12 +100,10 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
class BangumiInfo extends StatefulWidget { class BangumiInfo extends StatefulWidget {
const BangumiInfo({ const BangumiInfo({
super.key, super.key,
this.loadingStatus = false,
this.bangumiDetail, this.bangumiDetail,
this.cid, this.cid,
}); });
final bool loadingStatus;
final BangumiInfoModel? bangumiDetail; final BangumiInfoModel? bangumiDetail;
final int? cid; final int? cid;
@ -117,7 +116,6 @@ class _BangumiInfoState extends State<BangumiInfo> {
late final BangumiIntroController bangumiIntroController; late final BangumiIntroController bangumiIntroController;
late final VideoDetailController videoDetailCtr; late final VideoDetailController videoDetailCtr;
Box localCache = GStrorage.localCache; Box localCache = GStrorage.localCache;
late final BangumiInfoModel? bangumiItem;
late double sheetHeight; late double sheetHeight;
int? cid; int? cid;
bool isProcessing = false; bool isProcessing = false;
@ -136,13 +134,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
super.initState(); super.initState();
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag); bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag); videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight'); sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!; cid = widget.cid!;
print('cid: $cid');
videoDetailCtr.cid.listen((p0) { videoDetailCtr.cid.listen((p0) {
cid = p0; cid = p0;
print('cid: $cid');
setState(() {}); setState(() {});
}); });
} }
@ -182,8 +177,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 20), left: StyleString.safeSpace, right: StyleString.safeSpace, top: 20),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: !widget.loadingStatus || bangumiItem != null child: Column(
? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
@ -194,15 +188,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
NetworkImgLayer( NetworkImgLayer(
width: 105, width: 105,
height: 160, height: 160,
src: !widget.loadingStatus src: widget.bangumiDetail!.cover!,
? widget.bangumiDetail!.cover!
: bangumiItem!.cover!,
), ),
if (bangumiItem != null &&
bangumiItem!.rating != null)
PBadge( PBadge(
text: text: '评分 ${widget.bangumiDetail!.rating!['score']!}',
'评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem!.rating!['score']!}',
top: null, top: null,
right: 6, right: 6,
bottom: 6, bottom: 6,
@ -224,9 +213,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
!widget.loadingStatus widget.bangumiDetail!.title!,
? widget.bangumiDetail!.title!
: bangumiItem!.title!,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -246,8 +233,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
backgroundColor: backgroundColor:
MaterialStateProperty.resolveWith( MaterialStateProperty.resolveWith(
(Set<MaterialState> states) { (Set<MaterialState> states) {
return t return t.colorScheme.primaryContainer
.colorScheme.primaryContainer
.withOpacity(0.7); .withOpacity(0.7);
}), }),
), ),
@ -266,18 +252,13 @@ class _BangumiInfoState extends State<BangumiInfo> {
children: [ children: [
StatView( StatView(
theme: 'gray', theme: 'gray',
view: !widget.loadingStatus view: widget.bangumiDetail!.stat!['views'],
? widget.bangumiDetail!.stat!['views']
: bangumiItem!.stat!['views'],
size: 'medium', size: 'medium',
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
StatDanMu( StatDanMu(
theme: 'gray', theme: 'gray',
danmu: !widget.loadingStatus danmu: widget.bangumiDetail!.stat!['danmakus'],
? widget
.bangumiDetail!.stat!['danmakus']
: bangumiItem!.stat!['danmakus'],
size: 'medium', size: 'medium',
), ),
], ],
@ -286,15 +267,8 @@ class _BangumiInfoState extends State<BangumiInfo> {
Row( Row(
children: [ children: [
Text( Text(
!widget.loadingStatus (widget.bangumiDetail!.areas!.isNotEmpty
? (widget.bangumiDetail!.areas! ? widget.bangumiDetail!.areas!.first['name']
.isNotEmpty
? widget.bangumiDetail!.areas!
.first['name']
: '')
: (bangumiItem!.areas!.isNotEmpty
? bangumiItem!
.areas!.first['name']
: ''), : ''),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
@ -303,11 +277,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
!widget.loadingStatus widget.bangumiDetail!.publish!['pub_time_show'],
? widget.bangumiDetail!
.publish!['pub_time_show']
: bangumiItem!
.publish!['pub_time_show'],
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: t.colorScheme.outline, color: t.colorScheme.outline,
@ -315,20 +285,16 @@ class _BangumiInfoState extends State<BangumiInfo> {
), ),
], ],
), ),
// const SizedBox(height: 4),
Text( Text(
!widget.loadingStatus widget.bangumiDetail!.newEp!['desc'],
? widget.bangumiDetail!.newEp!['desc']
: bangumiItem!.newEp!['desc'],
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: t.colorScheme.outline, color: t.colorScheme.outline,
), ),
), ),
// const SizedBox(height: 10),
const Spacer(), const Spacer(),
Text( Text(
'简介:${!widget.loadingStatus ? widget.bangumiDetail!.evaluate! : bangumiItem!.evaluate!}', '简介:${widget.bangumiDetail!.evaluate!}',
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
@ -344,45 +310,21 @@ class _BangumiInfoState extends State<BangumiInfo> {
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// 点赞收藏转发 布局样式1
// SingleChildScrollView( /// 点赞收藏转发
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: actionRow(
// context,
// bangumiIntroController,
// videoDetailCtr,
// ),
// ),
// 点赞收藏转发 布局样式2
actionGrid(context, bangumiIntroController), actionGrid(context, bangumiIntroController),
// 番剧分p // 番剧分p
if ((!widget.loadingStatus && if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[
widget.bangumiDetail!.episodes!.isNotEmpty) ||
bangumiItem != null &&
bangumiItem!.episodes!.isNotEmpty) ...[
BangumiPanel( BangumiPanel(
pages: bangumiItem != null pages: widget.bangumiDetail!.episodes!,
? bangumiItem!.episodes! cid: cid ?? widget.bangumiDetail!.episodes!.first.cid,
: widget.bangumiDetail!.episodes!,
cid: cid ??
(bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid),
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => bangumiIntroController changeFuc: (bvid, cid, aid) =>
.changeSeasonOrbangu(bvid, cid, aid), bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid),
) )
], ],
], ],
) )),
: const SizedBox(
height: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
); );
} }
@ -404,25 +346,19 @@ class _BangumiInfoState extends State<BangumiInfo> {
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: onTap: handleState(bangumiIntroController.actionLikeVideo),
handleState(bangumiIntroController.actionLikeVideo),
selectStatus: bangumiIntroController.hasLike.value, selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false, text: widget.bangumiDetail!.stat!['likes']!.toString(),
text: !widget.loadingStatus ),
? widget.bangumiDetail!.stat!['likes']!.toString()
: bangumiItem!.stat!['likes']!.toString()),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b), selectIcon: const Icon(FontAwesomeIcons.b),
onTap: onTap: handleState(bangumiIntroController.actionCoinVideo),
handleState(bangumiIntroController.actionCoinVideo),
selectStatus: bangumiIntroController.hasCoin.value, selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: false, text: widget.bangumiDetail!.stat!['coins']!.toString(),
text: !widget.loadingStatus ),
? widget.bangumiDetail!.stat!['coins']!.toString()
: bangumiItem!.stat!['coins']!.toString()),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
@ -430,29 +366,22 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.solidStar), selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(), onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value, selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: false, text: widget.bangumiDetail!.stat!['favorite']!.toString(),
text: !widget.loadingStatus ),
? widget.bangumiDetail!.stat!['favorite']!.toString()
: bangumiItem!.stat!['favorite']!.toString()),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.comment), icon: const Icon(FontAwesomeIcons.comment),
selectIcon: const Icon(FontAwesomeIcons.reply), selectIcon: const Icon(FontAwesomeIcons.reply),
onTap: () => videoDetailCtr.tabCtr.animateTo(1), onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false, selectStatus: false,
loadingStatus: false, text: widget.bangumiDetail!.stat!['reply']!.toString(),
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString()
: bangumiItem!.stat!['reply']!.toString(),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(), onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false, selectStatus: false,
loadingStatus: false, text: widget.bangumiDetail!.stat!['share']!.toString(),
text: !widget.loadingStatus ),
? widget.bangumiDetail!.stat!['share']!.toString()
: bangumiItem!.stat!['share']!.toString()),
], ],
), ),
), ),
@ -460,63 +389,4 @@ class _BangumiInfoState extends State<BangumiInfo> {
); );
}); });
} }
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: [
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString()
: '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString()
: '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString()
: '-',
),
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.comment),
onTap: () {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString()
: '-',
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: '转发'),
]);
}
} }

View File

@ -16,7 +16,7 @@ class FavDetailController extends GetxController {
RxMap favInfo = {}.obs; RxMap favInfo = {}.obs;
RxList favList = [].obs; RxList favList = [].obs;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs; int mediaCount = 0;
@override @override
void onInit() { void onInit() {
@ -29,7 +29,7 @@ class FavDetailController extends GetxController {
} }
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async { Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
if (type == 'onLoad' && favList.length >= mediaCount.value) { if (type == 'onLoad' && favList.length >= mediaCount) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
return; return;
} }
@ -43,11 +43,11 @@ class FavDetailController extends GetxController {
favInfo.value = res['data'].info; favInfo.value = res['data'].info;
if (currentPage == 1 && type == 'init') { if (currentPage == 1 && type == 'init') {
favList.value = res['data'].medias; favList.value = res['data'].medias;
mediaCount.value = res['data'].info['media_count']; mediaCount = res['data'].info['media_count'];
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
favList.addAll(res['data'].medias); favList.addAll(res['data'].medias);
} }
if (favList.length >= mediaCount.value) { if (favList.length >= mediaCount) {
loadingText.value = '没有更多了'; loadingText.value = '没有更多了';
} }
} }

View File

@ -84,7 +84,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
Text( Text(
'${_favDetailController.mediaCount}条视频', '${_favDetailController.item!.mediaCount!}条视频',
style: Theme.of(context).textTheme.labelMedium, style: Theme.of(context).textTheme.labelMedium,
) )
], ],
@ -175,7 +175,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
child: Obx( child: Obx(
() => Text( () => Text(
'${_favDetailController.mediaCount}条视频', '${_favDetailController.favList.length}条视频',
style: TextStyle( style: TextStyle(
fontSize: fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize, Theme.of(context).textTheme.labelMedium!.fontSize,

View File

@ -29,6 +29,7 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget {
class _BottomControlState extends State<BottomControl> { class _BottomControlState extends State<BottomControl> {
late PlayUrlModel videoInfo; late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
TextStyle subTitleStyle = const TextStyle(fontSize: 12); TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14); TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight); Size get preferredSize => const Size(double.infinity, kToolbarHeight);

View File

@ -115,7 +115,7 @@ class SSearchController extends GetxController {
onLongSelect(word) { onLongSelect(word) {
int index = historyList.indexOf(word); int index = historyList.indexOf(word);
historyList.removeAt(index); historyList.value = historyList.removeAt(index);
historyList.refresh(); historyList.refresh();
histiryWord.put('cacheList', historyList); histiryWord.put('cacheList', historyList);
} }

View File

@ -173,12 +173,6 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.enableAi, setKey: SettingBoxKey.enableAi,
defaultVal: true, defaultVal: true,
), ),
const SetSwitchItem(
title: '相关视频推荐',
subTitle: '视频详情页推荐相关视频',
setKey: SettingBoxKey.enableRelatedVideo,
defaultVal: true,
),
ListTile( ListTile(
dense: false, dense: false,
title: Text('评论展示', style: titleStyle), title: Text('评论展示', style: titleStyle),

View File

@ -17,7 +17,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
Box videoStorage = GStrorage.video; Box videoStorage = GStrorage.video;
Box settingStorage = GStrorage.setting; Box settingStorage = GStrorage.setting;
late double playSpeedDefault; late double playSpeedDefault;
late List<double> playSpeedSystem;
late double longPressSpeedDefault; late double longPressSpeedDefault;
late List customSpeedsList; late List customSpeedsList;
late bool enableAutoLongPressSpeed; late bool enableAutoLongPressSpeed;
@ -54,9 +53,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// 系统预设倍速
playSpeedSystem =
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
// 默认倍速 // 默认倍速
playSpeedDefault = playSpeedDefault =
videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0); videoStorage.get(VideoBoxKey.playSpeedDefault, defaultValue: 1.0);
@ -68,7 +64,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []); videoStorage.get(VideoBoxKey.customSpeedsList, defaultValue: []);
enableAutoLongPressSpeed = settingStorage enableAutoLongPressSpeed = settingStorage
.get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false); .get(SettingBoxKey.enableAutoLongPressSpeed, defaultValue: false);
// 开启动态长按倍速时不展示
if (enableAutoLongPressSpeed) { if (enableAutoLongPressSpeed) {
Map newItem = sheetMenu[1]; Map newItem = sheetMenu[1];
newItem['show'] = false; newItem['show'] = false;
@ -128,7 +123,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
} }
// 设定倍速弹窗 // 设定倍速弹窗
void showBottomSheet(String type, int i) { void showBottomSheet(type, i) {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -164,11 +159,18 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
} }
// //
void menuAction(type, int index, id) async { void menuAction(type, index, id) async {
double chooseSpeed = 1.0; double chooseSpeed = 1.0;
if (type == 'system' && id == -1) {
SmartDialog.showToast('系统预设倍速不支持删除');
return;
}
// 获取当前选中的倍速值 // 获取当前选中的倍速值
chooseSpeed = if (type == 'system') {
type == 'system' ? playSpeedSystem[index] : customSpeedsList[index]; chooseSpeed = PlaySpeed.values[index].value;
} else {
chooseSpeed = customSpeedsList[index];
}
// 设置 // 设置
if (id == 1) { if (id == 1) {
// 设置默认倍速 // 设置默认倍速
@ -180,22 +182,17 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
videoStorage.put( videoStorage.put(
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault); VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
} else if (id == -1) { } else if (id == -1) {
late List speedsList = if (customSpeedsList[index] == playSpeedDefault) {
type == 'system' ? playSpeedSystem : customSpeedsList; playSpeedDefault = 1.0;
if (speedsList[index] == playSpeedDefault) { videoStorage.put(VideoBoxKey.playSpeedDefault, playSpeedDefault);
SmartDialog.showToast('默认倍速不可删除');
} }
if (speedsList[index] == longPressSpeedDefault) { if (customSpeedsList[index] == longPressSpeedDefault) {
longPressSpeedDefault = 2.0; longPressSpeedDefault = 2.0;
videoStorage.put( videoStorage.put(
VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault); VideoBoxKey.longPressSpeedDefault, longPressSpeedDefault);
} }
speedsList.removeAt(index); customSpeedsList.removeAt(index);
await videoStorage.put( await videoStorage.put(VideoBoxKey.customSpeedsList, customSpeedsList);
type == 'system'
? VideoBoxKey.playSpeedSystem
: VideoBoxKey.customSpeedsList,
speedsList);
} }
setState(() {}); setState(() {});
SmartDialog.showToast('操作成功'); SmartDialog.showToast('操作成功');
@ -252,7 +249,6 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
subtitle: Text(longPressSpeedDefault.toString()), subtitle: Text(longPressSpeedDefault.toString()),
) )
: const SizedBox(), : const SizedBox(),
if (playSpeedSystem.isNotEmpty) ...[
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 14, left: 14,
@ -276,16 +272,15 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
spacing: 8, spacing: 8,
runSpacing: 2, runSpacing: 2,
children: [ children: [
for (int i = 0; i < playSpeedSystem.length; i++) ...[ for (var i in PlaySpeed.values) ...[
FilledButton.tonal( FilledButton.tonal(
onPressed: () => showBottomSheet('system', i), onPressed: () => showBottomSheet('system', i.index),
child: Text(playSpeedSystem[i].toString()), child: Text(i.description),
), ),
] ]
], ],
), ),
) ),
],
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 14, left: 14,

View File

@ -5,7 +5,6 @@ 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';
@ -20,7 +19,6 @@ 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';
@ -93,7 +91,6 @@ class VideoDetailController extends GetxController
late int cacheAudioQa; late int cacheAudioQa;
PersistentBottomSheetController? replyReplyBottomSheetCtr; PersistentBottomSheetController? replyReplyBottomSheetCtr;
late bool enableRelatedVideo;
@override @override
void onInit() { void onInit() {
@ -116,8 +113,7 @@ class VideoDetailController extends GetxController
autoPlay.value = autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true); enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
enableRelatedVideo =
setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true);
if (userInfo == null || if (userInfo == null ||
localCache.get(LocalCacheKey.historyPause) == true) { localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false; enableHeart = false;
@ -387,86 +383,4 @@ 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,13 +25,6 @@ class VideoIntroController extends GetxController {
VideoIntroController({required this.bvid}); VideoIntroController({required this.bvid});
// 视频bvid // 视频bvid
String bvid; String bvid;
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
Map? videoItem = {};
// 请求状态 // 请求状态
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
@ -74,26 +67,6 @@ class VideoIntroController extends GetxController {
try { try {
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
} catch (_) {} } catch (_) {}
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
var args = Get.arguments['videoItem'];
var keys = Get.arguments.keys.toList();
videoItem!['pic'] = args.pic;
if (args.title is String) {
videoItem!['title'] = args.title;
} else {
String str = '';
for (Map map in args.title) {
str += map['text'];
}
videoItem!['title'] = str;
}
videoItem!['stat'] = keys.contains('stat') && args.stat;
videoItem!['pubdate'] = keys.contains('pubdate') && args.pubdate;
videoItem!['owner'] = keys.contains('owner') && args.owner;
}
}
userLogin = userInfo != null; userLogin = userInfo != null;
lastPlayCid.value = int.parse(Get.parameters['cid']!); lastPlayCid.value = int.parse(Get.parameters['cid']!);
isShowOnlineTotal = isShowOnlineTotal =

View File

@ -15,9 +15,7 @@ import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'widgets/action_item.dart'; import 'widgets/action_item.dart';
import 'widgets/action_row_item.dart';
import 'widgets/fav_panel.dart'; import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart'; import 'widgets/intro_detail.dart';
import 'widgets/page.dart'; import 'widgets/page.dart';
@ -78,7 +76,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
// 请求成功 // 请求成功
return Obx( return Obx(
() => VideoInfo( () => VideoInfo(
loadingStatus: false,
videoDetail: videoIntroController.videoDetail.value, videoDetail: videoIntroController.videoDetail.value,
heroTag: heroTag, heroTag: heroTag,
bvid: widget.bvid, bvid: widget.bvid,
@ -96,11 +93,13 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
); );
} }
} else { } else {
return VideoInfo( return const SliverToBoxAdapter(
loadingStatus: true, child: SizedBox(
videoDetail: videoDetail, height: 100,
heroTag: heroTag, child: Center(
bvid: widget.bvid, child: CircularProgressIndicator(),
),
),
); );
} }
}, },
@ -109,14 +108,12 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
} }
class VideoInfo extends StatefulWidget { class VideoInfo extends StatefulWidget {
final bool loadingStatus;
final VideoDetailData? videoDetail; final VideoDetailData? videoDetail;
final String? heroTag; final String? heroTag;
final String bvid; final String bvid;
const VideoInfo({ const VideoInfo({
Key? key, Key? key,
this.loadingStatus = false,
this.videoDetail, this.videoDetail,
this.heroTag, this.heroTag,
required this.bvid, required this.bvid,
@ -127,18 +124,12 @@ class VideoInfo extends StatefulWidget {
} }
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin { class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// final String heroTag = Get.arguments['heroTag'];
late String heroTag; late String heroTag;
late final VideoIntroController videoIntroController; late final VideoIntroController videoIntroController;
late final VideoDetailController videoDetailCtr; late final VideoDetailController videoDetailCtr;
late final Map<dynamic, dynamic> videoItem;
final Box<dynamic> localCache = GStrorage.localCache; final Box<dynamic> localCache = GStrorage.localCache;
final Box<dynamic> setting = GStrorage.setting; final Box<dynamic> setting = GStrorage.setting;
late double sheetHeight; late double sheetHeight;
late final bool loadingStatus; // 加载状态
late final dynamic owner; late final dynamic owner;
late final dynamic follower; late final dynamic follower;
late final dynamic followStatus; late final dynamic followStatus;
@ -163,14 +154,10 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
videoIntroController = videoIntroController =
Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag); Get.put(VideoIntroController(bvid: widget.bvid), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag); videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoItem = videoIntroController.videoItem!;
sheetHeight = localCache.get('sheetHeight'); sheetHeight = localCache.get('sheetHeight');
loadingStatus = widget.loadingStatus; owner = widget.videoDetail!.owner;
owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner; follower = Utils.numFormat(videoIntroController.userStat['follower']);
follower = loadingStatus
? '-'
: Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus; followStatus = videoIntroController.followStatus;
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
} }
@ -224,9 +211,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 视频介绍 // 视频介绍
showIntroDetail() { showIntroDetail() {
if (loadingStatus) {
return;
}
feedBack(); feedBack();
showBottomSheet( showBottomSheet(
context: context, context: context,
@ -240,13 +224,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 用户主页 // 用户主页
onPushMember() { onPushMember() {
feedBack(); feedBack();
mid = !loadingStatus mid = widget.videoDetail!.owner!.mid!;
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
memberHeroTag = Utils.makeHeroTag(mid); memberHeroTag = Utils.makeHeroTag(mid);
String face = !loadingStatus String face = widget.videoDetail!.owner!.face!;
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid', Get.toNamed('/member?mid=$mid',
arguments: {'face': face, 'heroTag': memberHeroTag}); arguments: {'face': face, 'heroTag': memberHeroTag});
} }
@ -268,22 +248,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
final Color outline = t.colorScheme.outline; final Color outline = t.colorScheme.outline;
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: StyleString.safeSpace, left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
right: StyleString.safeSpace,
top: 16,
),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: !loadingStatus child: Column(
? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(), onTap: () => showIntroDetail(),
child: Text( child: Text(
!loadingStatus widget.videoDetail!.title!,
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -303,25 +277,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
children: [ children: [
StatView( StatView(
theme: 'gray', theme: 'gray',
view: !loadingStatus view: widget.videoDetail!.stat!.view,
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium', size: 'medium',
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
StatDanMu( StatDanMu(
theme: 'gray', theme: 'gray',
danmu: !loadingStatus danmu: widget.videoDetail!.stat!.danmaku,
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium', size: 'medium',
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
Utils.dateFormat( Utils.dateFormat(widget.videoDetail!.pubdate,
!loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'), formatType: 'detail'),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
@ -349,33 +316,21 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
top: 6, top: 6,
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () async {
final res = final res = await videoIntroController.aiConclusion();
await videoIntroController.aiConclusion();
if (res['status']) { if (res['status']) {
showAiBottomSheet(); showAiBottomSheet();
} }
}, },
child: child: Image.asset('assets/images/ai.png', height: 22),
Image.asset('assets/images/ai.png', height: 22),
), ),
) )
], ],
), ),
// 点赞收藏转发 布局样式1
// SingleChildScrollView( /// 点赞收藏转发
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: actionRow(
// context,
// videoIntroController,
// videoDetailCtr,
// ),
// ),
// 点赞收藏转发 布局样式2
actionGrid(context, videoIntroController), actionGrid(context, videoIntroController),
// 合集 // 合集
if (!loadingStatus && if (widget.videoDetail!.ugcSeason != null) ...[
widget.videoDetail!.ugcSeason != null) ...[
Obx( Obx(
() => SeasonPanel( () => SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!, ugcSeason: widget.videoDetail!.ugcSeason!,
@ -383,43 +338,37 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
? videoIntroController.lastPlayCid.value ? videoIntroController.lastPlayCid.value
: widget.videoDetail!.pages!.first.cid, : widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => videoIntroController changeFuc: (bvid, cid, aid) =>
.changeSeasonOrbangu(bvid, cid, aid), videoIntroController.changeSeasonOrbangu(bvid, cid, aid),
), ),
) )
], ],
if (!loadingStatus && if (widget.videoDetail!.pages != null &&
widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[ widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel( Obx(() => PagesPanel(
pages: widget.videoDetail!.pages!, pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value, cid: videoIntroController.lastPlayCid.value,
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
changeFuc: (cid) => changeFuc: (cid) => videoIntroController.changeSeasonOrbangu(
videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid, cid, null), videoIntroController.bvid, cid, null),
)) ))
], ],
GestureDetector( GestureDetector(
onTap: onPushMember, onTap: onPushMember,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
vertical: 12, horizontal: 4),
child: Row( child: Row(
children: [ children: [
NetworkImgLayer( NetworkImgLayer(
type: 'avatar', type: 'avatar',
src: loadingStatus src: widget.videoDetail!.owner!.face,
? owner.face
: widget.videoDetail!.owner!.face,
width: 34, width: 34,
height: 34, height: 34,
fadeInDuration: Duration.zero, fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero, fadeOutDuration: Duration.zero,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text(owner.name, Text(owner.name, style: const TextStyle(fontSize: 13)),
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
follower, follower,
@ -430,20 +379,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
const Spacer(), const Spacer(),
Obx(() => AnimatedOpacity( Obx(() => AnimatedOpacity(
opacity: loadingStatus || opacity:
videoIntroController videoIntroController.followStatus.isEmpty ? 0 : 1,
.followStatus.isEmpty
? 0
: 1,
duration: const Duration(milliseconds: 50), duration: const Duration(milliseconds: 50),
child: SizedBox( child: SizedBox(
height: 32, height: 32,
child: Obx( child: Obx(
() => videoIntroController () => videoIntroController.followStatus.isNotEmpty
.followStatus.isNotEmpty
? TextButton( ? TextButton(
onPressed: videoIntroController onPressed:
.actionRelationMod, videoIntroController.actionRelationMod,
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8, right: 8), left: 8, right: 8),
@ -453,8 +398,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
: t.colorScheme.onPrimary, : t.colorScheme.onPrimary,
backgroundColor: backgroundColor:
followStatus['attribute'] != 0 followStatus['attribute'] != 0
? t.colorScheme ? t.colorScheme.onInverseSurface
.onInverseSurface
: t.colorScheme : t.colorScheme
.primary, // 设置按钮背景色 .primary, // 设置按钮背景色
), ),
@ -463,13 +407,13 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
? '已关注' ? '已关注'
: '关注', : '关注',
style: TextStyle( style: TextStyle(
fontSize: t.textTheme fontSize: t
.labelMedium!.fontSize), .textTheme.labelMedium!.fontSize),
), ),
) )
: ElevatedButton( : ElevatedButton(
onPressed: videoIntroController onPressed:
.actionRelationMod, videoIntroController.actionRelationMod,
child: const Text('关注'), child: const Text('关注'),
), ),
), ),
@ -480,14 +424,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
), ),
], ],
) )),
: const SizedBox(
height: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
); );
} }
@ -509,10 +446,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo), onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, text: widget.videoDetail!.stat!.like!.toString()),
text: !loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
), ),
// ActionItem( // ActionItem(
// icon: const Icon(FontAwesomeIcons.clock), // icon: const Icon(FontAwesomeIcons.clock),
@ -526,10 +460,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
selectIcon: const Icon(FontAwesomeIcons.b), selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo), onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value, selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus, text: widget.videoDetail!.stat!.coin!.toString(),
text: !loadingStatus ),
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
@ -538,88 +470,24 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
onTap: () => showFavBottomSheet(), onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'), onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus, text: widget.videoDetail!.stat!.favorite!.toString(),
text: !loadingStatus ),
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.comment), icon: const Icon(FontAwesomeIcons.comment),
onTap: () => videoDetailCtr.tabCtr.animateTo(1), onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false, selectStatus: false,
loadingStatus: loadingStatus, text: widget.videoDetail!.stat!.reply!.toString(),
text: !loadingStatus ),
? widget.videoDetail!.stat!.reply!.toString()
: '评论'),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(), onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false, selectStatus: false,
loadingStatus: loadingStatus, text: '分享',
text: '分享'), ),
], ],
), ),
); );
}); });
} }
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: <Widget>[
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.comment),
onTap: () {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-',
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
// text: !loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
} }

View File

@ -7,7 +7,6 @@ class ActionItem extends StatelessWidget {
final Icon? selectIcon; final Icon? selectIcon;
final Function? onTap; final Function? onTap;
final Function? onLongPress; final Function? onLongPress;
final bool? loadingStatus;
final String? text; final String? text;
final bool selectStatus; final bool selectStatus;
@ -17,7 +16,6 @@ class ActionItem extends StatelessWidget {
this.selectIcon, this.selectIcon,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.loadingStatus,
this.text, this.text,
this.selectStatus = false, this.selectStatus = false,
}) : super(key: key); }) : super(key: key);
@ -43,25 +41,15 @@ class ActionItem extends StatelessWidget {
: Icon(icon!.icon!, : Icon(icon!.icon!,
size: 18, color: Theme.of(context).colorScheme.outline), size: 18, color: Theme.of(context).colorScheme.outline),
const SizedBox(height: 6), const SizedBox(height: 6),
AnimatedOpacity( Text(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
text ?? '', text ?? '',
key: ValueKey<String>(text ?? ''),
style: TextStyle( style: TextStyle(
color: selectStatus color: selectStatus
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize), fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
),
),
), ),
)
], ],
), ),
); );

View File

@ -9,6 +9,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:nil/nil.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/user.dart'; import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
@ -24,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/app_bar.dart'; import 'widgets/header_control.dart';
class VideoDetailPage extends StatefulWidget { class VideoDetailPage extends StatefulWidget {
const VideoDetailPage({Key? key}) : super(key: key); const VideoDetailPage({Key? key}) : super(key: key);
@ -37,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 vdCtr; late VideoDetailController videoDetailController;
PlPlayerController? plPlayerController; PlPlayerController? plPlayerController;
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream; late StreamController<double> appbarStream;
@ -64,18 +65,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void initState() { void initState() {
super.initState(); super.initState();
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
vdCtr = Get.put(VideoDetailController(), tag: heroTag); videoDetailController = 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(value, vdCtr.cid.value); videoPlayerServiceHandler.onVideoDetailChange(
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(value, vdCtr.cid.value); videoPlayerServiceHandler.onVideoDetailChange(
value, videoDetailController.cid.value);
}); });
vdCtr.cid.listen((p0) { videoDetailController.cid.listen((p0) {
videoPlayerServiceHandler.onVideoDetailChange( videoPlayerServiceHandler.onVideoDetailChange(
bangumiIntroController.bangumiDetail.value, p0); bangumiIntroController.bangumiDetail.value, p0);
}); });
@ -90,16 +93,16 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStreamListen(); appbarStreamListen();
fullScreenStatusListener(); fullScreenStatusListener();
if (Platform.isAndroid) { if (Platform.isAndroid) {
floating = vdCtr.floating!; floating = videoDetailController.floating!;
autoEnterPip(); autoEnterPip();
} }
} }
// 获取视频资源,初始化播放器 // 获取视频资源,初始化播放器
Future<void> videoSourceInit() async { Future<void> videoSourceInit() async {
_futureBuilderFuture = vdCtr.queryVideoUrl(); _futureBuilderFuture = videoDetailController.queryVideoUrl();
if (vdCtr.autoPlay.value) { if (videoDetailController.autoPlay.value) {
plPlayerController = vdCtr.plPlayerController; plPlayerController = videoDetailController.plPlayerController;
plPlayerController!.addStatusLister(playerListener); plPlayerController!.addStatusLister(playerListener);
} }
} }
@ -128,10 +131,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 (vdCtr.videoType == SearchType.video) { if (videoDetailController.videoType == SearchType.video) {
videoIntroController.nextPlay(); videoIntroController.nextPlay();
} }
if (vdCtr.videoType == SearchType.media_bangumi) { if (videoDetailController.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay(); bangumiIntroController.nextPlay();
} }
} }
@ -143,7 +146,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
} }
// 播放完展示控制栏 // 播放完展示控制栏
try { try {
PiPStatus currentStatus = await vdCtr.floating!.pipStatus; PiPStatus currentStatus =
await videoDetailController.floating!.pipStatus;
if (currentStatus == PiPStatus.disabled) { if (currentStatus == PiPStatus.disabled) {
plPlayerController!.onLockControl(false); plPlayerController!.onLockControl(false);
} }
@ -164,17 +168,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 未开启自动播放时触发播放 /// 未开启自动播放时触发播放
Future<void> handlePlay() async { Future<void> handlePlay() async {
await vdCtr.playerInit(); await videoDetailController.playerInit();
plPlayerController = vdCtr.plPlayerController; plPlayerController = videoDetailController.plPlayerController;
plPlayerController!.addStatusLister(playerListener); plPlayerController!.addStatusLister(playerListener);
vdCtr.autoPlay.value = true; videoDetailController.autoPlay.value = true;
vdCtr.isShowCover.value = false; videoDetailController.isShowCover.value = false;
} }
void fullScreenStatusListener() { void fullScreenStatusListener() {
plPlayerController?.isFullScreen.listen((bool isFullScreen) { plPlayerController?.isFullScreen.listen((bool isFullScreen) {
if (isFullScreen) { if (isFullScreen) {
vdCtr.hiddenReplyReplyPanel(); videoDetailController.hiddenReplyReplyPanel();
} }
}); });
} }
@ -186,8 +190,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.dispose(); plPlayerController!.dispose();
} }
if (vdCtr.floating != null) { if (videoDetailController.floating != null) {
vdCtr.floating!.dispose(); videoDetailController.floating!.dispose();
} }
videoPlayerServiceHandler.onVideoDetailDispose(); videoPlayerServiceHandler.onVideoDetailDispose();
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -203,10 +207,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
/// 开启 /// 开启
if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false) if (setting.get(SettingBoxKey.enableAutoBrightness, defaultValue: false)
as bool) { as bool) {
vdCtr.brightness = plPlayerController!.brightness.value; videoDetailController.brightness = plPlayerController!.brightness.value;
} }
if (plPlayerController != null) { if (plPlayerController != null) {
vdCtr.defaultST = plPlayerController!.position.value; videoDetailController.defaultST = plPlayerController!.position.value;
videoIntroController.isPaused = true; videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
@ -222,16 +226,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.videoPlayerController != null) { plPlayerController!.videoPlayerController != null) {
setState(() => isShowing = true); setState(() => isShowing = true);
} }
vdCtr.isFirstTime = false; videoDetailController.isFirstTime = false;
final bool autoplay = autoPlayEnable; final bool autoplay = autoPlayEnable;
vdCtr.playerInit(autoplay: autoplay); videoDetailController.playerInit(autoplay: autoplay);
/// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回 /// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回
vdCtr.autoPlay.value = !vdCtr.isShowCover.value; videoDetailController.autoPlay.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(vdCtr.defaultST); plPlayerController?.seekTo(videoDetailController.defaultST);
plPlayerController?.play(); plPlayerController?.play();
} }
plPlayerController?.addStatusLister(playerListener); plPlayerController?.addStatusLister(playerListener);
@ -254,166 +259,9 @@ 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.value; statusBarHeight + kToolbarHeight + videoHeight;
// 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( 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,
@ -425,7 +273,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [ children: [
Scaffold( Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
key: vdCtr.scaffoldKey, key: videoDetailController.scaffoldKey,
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(0), preferredSize: const Size.fromHeight(0),
@ -451,19 +299,21 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return SliverAppBar( return SliverAppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
// 假装使用一个非空变量避免Obx检测不到而罢工 // 假装使用一个非空变量避免Obx检测不到而罢工
pinned: vdCtr.autoPlay.value, pinned: videoDetailController.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.value, : videoHeight,
backgroundColor: Colors.black, backgroundColor: Colors.black,
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: PopScope( background: PopScope(
@ -483,27 +333,108 @@ 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) videoPlayerPanel, 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 (!videoDetailController
.autoPlay.value) ...<Widget>[
Obx( Obx(
() => Visibility( () => Visibility(
visible: !vdCtr.autoPlay.value && visible: videoDetailController
vdCtr.isShowCover.value, .isShowCover.value,
child: Positioned( child: Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: handlePlayPanel(), 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,
)),
),
],
)),
),
]
], ],
); );
}, },
@ -514,15 +445,17 @@ 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
: playerStatus != PlayerStatus.playing
? kToolbarHeight
: pinnedHeaderHeight; : pinnedHeaderHeight;
}, },
onlyOneScrollInBody: true, onlyOneScrollInBody: true,
@ -531,23 +464,54 @@ class _VideoDetailPageState extends State<VideoDetailPage>
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
child: Column( child: Column(
children: [ children: [
tabbarBuild, 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(),
),
),
),
),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
controller: vdCtr.tabCtr, controller: videoDetailController.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 (vdCtr.videoType == SearchType.video) ...[ if (videoDetailController.videoType ==
VideoIntroPanel(bvid: vdCtr.bvid), SearchType.video) ...[
] else if (vdCtr.videoType == VideoIntroPanel(
bvid: videoDetailController.bvid),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[ SearchType.media_bangumi) ...[
Obx(() => BangumiIntroPanel( Obx(() => BangumiIntroPanel(
cid: vdCtr.cid.value)), cid: videoDetailController.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,
@ -557,8 +521,6 @@ class _VideoDetailPageState extends State<VideoDetailPage>
.withOpacity(0.06), .withOpacity(0.06),
), ),
), ),
if (vdCtr.videoType == SearchType.video &&
vdCtr.enableRelatedVideo)
const RelatedVideoPanel(), const RelatedVideoPanel(),
], ],
); );
@ -566,8 +528,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
), ),
Obx( Obx(
() => VideoReplyPanel( () => VideoReplyPanel(
bvid: vdCtr.bvid, bvid: videoDetailController.bvid,
oid: vdCtr.oid.value, oid: videoDetailController.oid.value,
), ),
) )
], ],
@ -581,26 +543,56 @@ 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: videoPlayerPanel, childWhenEnabled: childWhenEnabled,
floating: floating, floating: floating,
); );
} else { } else {
@ -641,7 +633,8 @@ 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(bvid: vdCtr.bvid); var res = await UserHttp.toViewLater(
bvid: videoDetailController.bvid);
SmartDialog.showToast(res['msg']); SmartDialog.showToast(res['msg']);
}, },
), ),

View File

@ -1,4 +1,5 @@
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';
@ -16,6 +17,7 @@ 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';
@ -45,6 +47,7 @@ class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
class _HeaderControlState extends State<HeaderControl> { class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo; late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
static const TextStyle subTitleStyle = TextStyle(fontSize: 12); static const TextStyle subTitleStyle = TextStyle(fontSize: 12);
static const TextStyle titleStyle = TextStyle(fontSize: 14); static const TextStyle titleStyle = TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight); Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@ -218,6 +221,88 @@ 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 = [
@ -1081,6 +1166,41 @@ 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(

View File

@ -292,19 +292,11 @@ class PlPlayerController {
_longPressSpeed.value = videoStorage _longPressSpeed.value = videoStorage
.get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0); .get(VideoBoxKey.longPressSpeedDefault, defaultValue: 2.0);
} }
// 自定义倍速集合
speedsList = List<double>.from(videoStorage speedsList = List<double>.from(videoStorage
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[])); .get(VideoBoxKey.customSpeedsList, defaultValue: <double>[]));
// 默认倍速 for (final PlaySpeed i in PlaySpeed.values) {
speedsList = List<double>.from(videoStorage speedsList.add(i.value);
.get(VideoBoxKey.customSpeedsList, defaultValue: <double>[])); }
//playSpeedSystem
final List<double> playSpeedSystem =
videoStorage.get(VideoBoxKey.playSpeedSystem, defaultValue: playSpeed);
// for (final PlaySpeed i in PlaySpeed.values) {
speedsList.addAll(playSpeedSystem);
// }
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) { // if (status == PlayerStatus.playing) {
@ -684,6 +676,18 @@ class PlPlayerController {
_playbackSpeed.value = speed; _playbackSpeed.value = speed;
} }
/// 设置倍速
// Future<void> togglePlaybackSpeed() async {
// List<double> allowedSpeeds =
// PlaySpeed.values.map<double>((e) => e.value).toList();
// int index = allowedSpeeds.indexOf(_playbackSpeed.value);
// if (index < allowedSpeeds.length - 1) {
// setPlaybackSpeed(allowedSpeeds[index + 1]);
// } else {
// setPlaybackSpeed(allowedSpeeds[0]);
// }
// }
/// 播放视频 /// 播放视频
/// TODO _duration.value丢失 /// TODO _duration.value丢失
Future<void> play( Future<void> play(

View File

@ -1,15 +1,39 @@
List<double> generatePlaySpeedList() { enum PlaySpeed {
List<double> playSpeed = []; pointTwoFive,
double startSpeed = 0.25; pointFive,
double endSpeed = 2.0; pointSevenFive,
double increment = 0.25;
for (double speed = startSpeed; speed <= endSpeed; speed += increment) { one,
playSpeed.add(speed); onePointTwoFive,
onePointFive,
onePointSevenFive,
two,
} }
return playSpeed; extension PlaySpeedExtension on PlaySpeed {
} static final List<String> _descList = [
'0.25',
'0.5',
'0.75',
'正常',
'1.25',
'1.5',
'1.75',
'2.0',
];
String get description => _descList[index];
// 导出 playSpeed 列表 static final List<double> _valueList = [
List<double> playSpeed = generatePlaySpeedList(); 0.25,
0.5,
0.75,
1.0,
1.25,
1.5,
1.75,
2.0,
];
double get value => _valueList[index];
double get defaultValue => _valueList[3];
}

View File

@ -131,8 +131,7 @@ class SettingBoxKey {
enableSearchWord = 'enableSearchWord', enableSearchWord = 'enableSearchWord',
enableSystemProxy = 'enableSystemProxy', enableSystemProxy = 'enableSystemProxy',
enableAi = 'enableAi', enableAi = 'enableAi',
defaultHomePage = 'defaultHomePage', defaultHomePage = 'defaultHomePage';
enableRelatedVideo = 'enableRelatedVideo';
/// 外观 /// 外观
static const String themeMode = 'themeMode', static const String themeMode = 'themeMode',
@ -182,8 +181,6 @@ class VideoBoxKey {
videoSpeed = 'videoSpeed', videoSpeed = 'videoSpeed',
// 播放顺序 // 播放顺序
playRepeat = 'playRepeat', playRepeat = 'playRepeat',
// 系统预设倍速
playSpeedSystem = 'playSpeedSystem',
// 默认倍速 // 默认倍速
playSpeedDefault = 'playSpeedDefault', playSpeedDefault = 'playSpeedDefault',
// 默认长按倍速 // 默认长按倍速