Merge branch 'main' into design

This commit is contained in:
guozhigq
2024-04-27 21:46:04 +08:00
21 changed files with 446 additions and 220 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../models/common/video_episode_type.dart';
@ -44,25 +45,38 @@ class EpisodeBottomSheet {
title = '${episode.title}${episode.longTitle!}';
break;
}
return ListTile(
return InkWell(
onTap: () {
SmartDialog.showToast('切换至「$title');
changeFucCall.call(episode, index);
},
dense: false,
leading: isCurrentIndex
? Image.asset(
'assets/images/live.gif',
color: primary,
height: 12,
)
: null,
title: Text(
title,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
child: Padding(
padding: const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8),
child: isFullScreen
? Text(
title,
maxLines: 1,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
)
: Row(
children: [
NetworkImgLayer(width: 130, height: 75, src: episode.cover),
const SizedBox(width: 10),
Expanded(
child: Text(
title,
maxLines: 2,
style: TextStyle(
fontSize: 14,
color: isCurrentIndex ? primary : onSurface,
),
),
),
],
),
),
);
}

View File

@ -192,22 +192,15 @@ class VideoHttp {
// 视频信息 标题、简介
static Future videoIntro({required String bvid}) async {
var res = await Request().get(Api.videoIntro, data: {'bvid': bvid});
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
if (result.code == 0) {
if (res.data['code'] == 0) {
VideoDetailResponse result = VideoDetailResponse.fromJson(res.data);
return {'status': true, 'data': result.data!};
} else {
Map errMap = {
-400: '请求错误',
-403: '权限不足',
-404: '视频资源失效',
62002: '稿件不可见',
62004: '稿件审核中',
};
return {
'status': false,
'data': null,
'code': result.code,
'msg': errMap[result.code] ?? '请求异常',
'code': res.data['code'],
'msg': res.data['message'],
};
}
}

View File

@ -23,6 +23,7 @@ class HotVideoItemModel {
this.dimension,
this.shortLinkV2,
this.firstFrame,
this.cover,
this.pubLocation,
this.seasontype,
this.isOgv,
@ -50,6 +51,7 @@ class HotVideoItemModel {
Dimension? dimension;
String? shortLinkV2;
String? firstFrame;
String? cover;
String? pubLocation;
int? seasontype;
bool? isOgv;
@ -77,6 +79,7 @@ class HotVideoItemModel {
dimension = Dimension.fromMap(json['dimension']);
shortLinkV2 = json["short_link_v2"];
firstFrame = json["first_frame"];
cover = json["first_frame"];
pubLocation = json["pub_location"];
seasontype = json["seasontype"];
isOgv = json["isOgv"];

View File

@ -39,6 +39,14 @@ extension VideoQualityCode on VideoQuality {
}
return null;
}
static int? toCode(VideoQuality quality) {
final index = VideoQuality.values.indexOf(quality);
if (index != -1 && index < _codeList.length) {
return _codeList[index];
}
return null;
}
}
extension VideoQualityDesc on VideoQuality {

View File

@ -67,6 +67,7 @@ class VideoDetailData {
String? likeIcon;
bool? needJumpBv;
String? epId;
List<Staff>? staff;
VideoDetailData({
this.bvid,
@ -103,6 +104,7 @@ class VideoDetailData {
this.likeIcon,
this.needJumpBv,
this.epId,
this.staff,
});
VideoDetailData.fromJson(Map<String, dynamic> json) {
@ -155,6 +157,9 @@ class VideoDetailData {
if (json['redirect_url'] != null) {
epId = resolveEpId(json['redirect_url']);
}
staff = json["staff"] != null
? List<Staff>.from(json["staff"]!.map((e) => Staff.fromJson(e)))
: null;
}
String resolveEpId(url) {
@ -377,6 +382,7 @@ class Part {
String? weblink;
Dimension? dimension;
String? firstFrame;
String? cover;
Part({
this.cid,
@ -388,6 +394,7 @@ class Part {
this.weblink,
this.dimension,
this.firstFrame,
this.cover,
});
fromRawJson(String str) => Part.fromJson(json.decode(str));
@ -406,6 +413,7 @@ class Part {
? null
: Dimension.fromJson(json["dimension"]);
firstFrame = json["first_frame"];
cover = json["first_frame"];
}
Map<String, dynamic> toJson() {
@ -629,6 +637,7 @@ class EpisodeItem {
this.attribute,
this.page,
this.bvid,
this.cover,
});
int? seasonId;
int? sectionId;
@ -639,6 +648,7 @@ class EpisodeItem {
int? attribute;
Part? page;
String? bvid;
String? cover;
EpisodeItem.fromJson(Map<String, dynamic> json) {
seasonId = json['season_id'];
@ -650,5 +660,46 @@ class EpisodeItem {
attribute = json['attribute'];
page = Part.fromJson(json['page']);
bvid = json['bvid'];
cover = json['arc']['pic'];
}
}
class Staff {
Staff({
this.mid,
this.title,
this.name,
this.face,
this.vip,
});
int? mid;
String? title;
String? name;
String? face;
int? status;
Vip? vip;
Staff.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
title = json['title'];
name = json['name'];
face = json['face'];
vip = Vip.fromJson(json['vip']);
}
}
class Vip {
Vip({
this.type,
this.status,
});
int? type;
int? status;
Vip.fromJson(Map<String, dynamic> json) {
type = json['type'];
status = json['status'];
}
}

View File

@ -131,51 +131,37 @@ class BangumiIntroController extends GetxController {
builder: (context) {
return AlertDialog(
title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile(
value: 1,
title: const Text('1枚'),
groupValue: _tempThemeValue,
onChanged: (value) {
_tempThemeValue = value!;
Get.appUpdate();
},
),
RadioListTile(
value: 2,
title: const Text('2枚'),
groupValue: _tempThemeValue,
onChanged: (value) {
_tempThemeValue = value!;
Get.appUpdate();
},
),
],
children: [1, 2]
.map(
(e) => RadioListTile(
value: e,
title: Text('$e枚'),
groupValue: _tempThemeValue,
onChanged: (value) async {
_tempThemeValue = value!;
setState(() {});
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] +
_tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
),
)
.toList(),
);
}),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () async {
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
child: const Text('确定'),
)
],
);
});
}
@ -229,13 +215,15 @@ class BangumiIntroController extends GetxController {
}
// 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid) async {
Future changeSeasonOrbangu(bvid, cid, aid, cover) async {
// 重新获取视频资源
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.oid.value = aid;
videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
try {
@ -294,7 +282,8 @@ class BangumiIntroController extends GetxController {
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
String cover = episodes[nextIndex].cover!;
changeSeasonOrbangu(bvid, cid, aid, cover);
}
// 播放器底栏 选集 回调
@ -315,7 +304,7 @@ class BangumiIntroController extends GetxController {
sheetHeight: Get.size.height,
isFullScreen: true,
changeFucCall: (item, index) {
changeSeasonOrbangu(item.bvid, item.cid, item.aid);
changeSeasonOrbangu(item.bvid, item.cid, item.aid, item.cover);
SmartDialog.dismiss();
},
).buildShowContent(Get.context!),

View File

@ -322,8 +322,8 @@ class _BangumiInfoState extends State<BangumiInfo> {
pages: widget.bangumiDetail!.episodes!,
cid: cid! ?? widget.bangumiDetail!.episodes!.first.cid!,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) =>
bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid),
changeFuc: (bvid, cid, aid, cover) => bangumiIntroController
.changeSeasonOrbangu(bvid, cid, aid, cover),
bangumiDetail: bangumiIntroController.bangumiDetail.value,
bangumiIntroController: bangumiIntroController,
)

View File

@ -84,9 +84,12 @@ class _BangumiPanelState extends State<BangumiPanel> {
item.bvid,
item.cid,
item.aid,
item.cover,
);
_bottomSheetController?.close();
currentIndex = i;
if (_bottomSheetController != null) {
_bottomSheetController?.close();
}
currentIndex.value = i;
scrollToIndex();
}

View File

@ -29,7 +29,7 @@ class _SelectDialogState<T> extends State<SelectDialog<T>> {
return AlertDialog(
title: Text(widget.title),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return SingleChildScrollView(
child: Column(

View File

@ -51,7 +51,7 @@ class VideoDetailController extends GetxController
/// 播放器配置 画质 音质 解码格式
late VideoQuality currentVideoQa;
AudioQuality? currentAudioQa;
late VideoDecodeFormats currentDecodeFormats;
VideoDecodeFormats? currentDecodeFormats;
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
@ -73,6 +73,7 @@ class VideoDetailController extends GetxController
ReplyItemModel? firstFloor;
final scaffoldKey = GlobalKey<ScaffoldState>();
RxString bgCover = ''.obs;
RxString cover = ''.obs;
PlPlayerController plPlayerController = PlPlayerController.getInstance();
late VideoItem firstVideo;
@ -107,6 +108,7 @@ class VideoDetailController extends GetxController
BottomControlType.fullscreen,
].obs;
RxDouble sheetHeight = 0.0.obs;
RxString archiveSourceType = 'dash'.obs;
@override
void onInit() {
@ -119,10 +121,12 @@ class VideoDetailController extends GetxController
var args = argMap['videoItem'];
if (args.pic != null && args.pic != '') {
videoItem['pic'] = args.pic;
cover.value = args.pic;
}
}
if (keys.contains('pic')) {
videoItem['pic'] = argMap['pic'];
cover.value = argMap['pic'];
}
}
tabCtr = TabController(length: 2, vsync: this);
@ -190,37 +194,43 @@ class VideoDetailController extends GetxController
plPlayerController.isBuffering.value = false;
plPlayerController.buffered.value = Duration.zero;
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
List<VideoItem> videoList =
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
try {
firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
} catch (_) {
if (currentVideoQa == VideoQuality.dolbyVision) {
firstVideo = videoList.first;
currentDecodeFormats =
VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!;
} else {
// 当前格式不可用
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
if (archiveSourceType.value == 'dash') {
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
List<VideoItem> videoList =
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
try {
firstVideo = videoList.firstWhere(
(i) => i.codecs!.startsWith(currentDecodeFormats?.code));
} catch (_) {
if (currentVideoQa == VideoQuality.dolbyVision) {
firstVideo = videoList.first;
currentDecodeFormats =
VideoDecodeFormatsCode.fromString(videoList.first.codecs!)!;
} else {
// 当前格式不可用
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code))!;
firstVideo = videoList.firstWhere(
(i) => i.codecs!.startsWith(currentDecodeFormats?.code));
}
}
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
if (currentAudioQa != null) {
final AudioItem firstAudio = data.dash!.audio!.firstWhere(
(AudioItem i) => i.id == currentAudioQa!.code,
orElse: () => data.dash!.audio!.first,
);
audioUrl = firstAudio.baseUrl ?? '';
}
}
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
if (currentAudioQa != null) {
final AudioItem firstAudio = data.dash!.audio!.firstWhere(
(AudioItem i) => i.id == currentAudioQa!.code,
orElse: () => data.dash!.audio!.first,
);
audioUrl = firstAudio.baseUrl ?? '';
if (archiveSourceType.value == 'durl') {
cacheVideoQa = VideoQualityCode.toCode(currentVideoQa);
queryVideoUrl();
}
playerInit();
}
@ -273,7 +283,8 @@ class VideoDetailController extends GetxController
// 视频链接
Future queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
var result =
await VideoHttp.videoUrl(cid: cid.value, bvid: bvid, qn: cacheVideoQa);
if (result['status']) {
data = result['data'];
if (data.acceptDesc!.isNotEmpty && data.acceptDesc!.contains('试看')) {
@ -291,8 +302,22 @@ class VideoDetailController extends GetxController
}
return result;
}
if (data.durl != null) {
archiveSourceType.value = 'durl';
videoUrl = data.durl!.first.url!;
audioUrl = '';
defaultST = Duration.zero;
firstVideo = VideoItem();
currentVideoQa = VideoQualityCode.fromCode(data.quality!)!;
if (autoPlay.value) {
await playerInit();
isShowCover.value = false;
}
return result;
}
final List<VideoItem> allVideosList = data.dash!.video!;
try {
archiveSourceType.value = 'dash';
// 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code;
// 预设的画质为null则当前可用的最高质量
@ -322,7 +347,7 @@ class VideoDetailController extends GetxController
// 当前视频没有对应格式返回第一个
bool flag = false;
for (var i in supportDecodeFormats) {
if (i.startsWith(currentDecodeFormats.code)) {
if (i.startsWith(currentDecodeFormats?.code)) {
flag = true;
}
}
@ -336,7 +361,7 @@ class VideoDetailController extends GetxController
/// 取出符合当前解码格式的videoItem
try {
firstVideo = videosList.firstWhere(
(e) => e.codecs!.startsWith(currentDecodeFormats.code));
(e) => e.codecs!.startsWith(currentDecodeFormats?.code));
} catch (_) {
firstVideo = videosList.first;
}

View File

@ -219,50 +219,36 @@ class VideoIntroController extends GetxController {
builder: (context) {
return AlertDialog(
title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile(
value: 1,
title: const Text('1枚'),
groupValue: _tempThemeValue,
onChanged: (value) {
_tempThemeValue = value!;
Get.appUpdate();
},
),
RadioListTile(
value: 2,
title: const Text('2枚'),
groupValue: _tempThemeValue,
onChanged: (value) {
_tempThemeValue = value!;
Get.appUpdate();
},
),
],
children: [1, 2]
.map(
(e) => RadioListTile(
value: e,
title: Text('$e枚'),
groupValue: _tempThemeValue,
onChanged: (value) async {
_tempThemeValue = value!;
setState(() {});
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
),
)
.toList(),
);
}),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () async {
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
child: const Text('确定'))
],
);
});
}
@ -446,7 +432,7 @@ class VideoIntroController extends GetxController {
}
// 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid, aid) async {
Future changeSeasonOrbangu(bvid, cid, aid, cover) async {
// 重新获取视频资源
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
@ -461,6 +447,7 @@ class VideoIntroController extends GetxController {
videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid);
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.cover.value = cover;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
try {
@ -508,6 +495,7 @@ class VideoIntroController extends GetxController {
void nextPlay() {
final List episodes = [];
bool isPages = false;
late String cover;
if (videoDetail.value.ugcSeason != null) {
final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
final List<SectionItem> sections = ugcSeason.sections!;
@ -524,6 +512,7 @@ class VideoIntroController extends GetxController {
final int currentIndex =
episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1;
cover = episodes[nextIndex].cover;
final VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
final PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
@ -540,7 +529,7 @@ class VideoIntroController extends GetxController {
final int cid = episodes[nextIndex].cid!;
final String rBvid = isPages ? bvid : episodes[nextIndex].bvid;
final int rAid = isPages ? IdUtils.bv2av(bvid) : episodes[nextIndex].aid!;
changeSeasonOrbangu(rBvid, cid, rAid);
changeSeasonOrbangu(rBvid, cid, rAid, cover);
}
// 设置关注分组
@ -605,10 +594,11 @@ class VideoIntroController extends GetxController {
isFullScreen: true,
changeFucCall: (item, index) {
if (dataType == VideoEpidoesType.videoEpisode) {
changeSeasonOrbangu(IdUtils.av2bv(item.aid), item.cid, item.aid);
changeSeasonOrbangu(
IdUtils.av2bv(item.aid), item.cid, item.aid, item.cover);
}
if (dataType == VideoEpidoesType.videoPart) {
changeSeasonOrbangu(bvid, item.cid, null);
changeSeasonOrbangu(bvid, item.cid, null, item.cover);
}
SmartDialog.dismiss();
},

View File

@ -22,6 +22,7 @@ import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
import 'widgets/page_panel.dart';
import 'widgets/season_panel.dart';
import 'widgets/staff_up_item.dart';
class VideoIntroPanel extends StatefulWidget {
final String bvid;
@ -382,11 +383,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
? videoIntroController.lastPlayCid.value
: widget.videoDetail!.pages!.first.cid,
sheetHeight: videoDetailCtr.sheetHeight.value,
changeFuc: (bvid, cid, aid) =>
changeFuc: (bvid, cid, aid, cover) =>
videoIntroController.changeSeasonOrbangu(
bvid,
cid,
aid,
cover,
),
videoIntroCtr: videoIntroController,
),
@ -400,41 +402,46 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value,
sheetHeight: videoDetailCtr.sheetHeight.value,
changeFuc: (cid) => videoIntroController.changeSeasonOrbangu(
changeFuc: (cid, cover) =>
videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid,
cid,
null,
cover,
),
videoIntroCtr: videoIntroController,
),
)
],
GestureDetector(
onTap: onPushMember,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: widget.videoDetail!.owner!.face,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Text(owner.name, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
if (widget.videoDetail!.staff == null)
GestureDetector(
onTap: onPushMember,
child: Container(
padding:
const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: widget.videoDetail!.owner!.face,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
),
const Spacer(),
Obx(() => AnimatedOpacity(
const SizedBox(width: 10),
Text(owner.name, style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(),
Obx(
() => AnimatedOpacity(
opacity:
videoIntroController.followStatus.isEmpty ? 0 : 1,
duration: const Duration(milliseconds: 50),
@ -474,11 +481,58 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
),
),
),
)),
],
),
),
],
),
),
),
),
if (widget.videoDetail!.staff != null) ...[
const SizedBox(height: 15),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
),
children: [
TextSpan(
text: '创作团队',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(fontWeight: FontWeight.bold),
),
const WidgetSpan(child: SizedBox(width: 6)),
TextSpan(
text: '${widget.videoDetail!.staff!.length}',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
)
],
),
),
SizedBox(
height: 120,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
for (int i = 0;
i < widget.videoDetail!.staff!.length;
i++) ...[
StaffUpItem(item: widget.videoDetail!.staff![i])
],
],
),
),
],
),
]
],
)),
);
@ -545,4 +599,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
);
});
}
// Widget StaffPanel(BuildContext context, videoIntroController) {
// return
// }
}

View File

@ -58,7 +58,7 @@ class _PagesPanelState extends State<PagesPanel> {
}
void changeFucCall(item, i) async {
widget.changeFuc?.call(item.cid);
widget.changeFuc?.call(item.cid, item.cover);
currentIndex.value = i;
_bottomSheetController?.close();
scrollToIndex();
@ -129,7 +129,7 @@ class _PagesPanelState extends State<PagesPanel> {
),
),
Container(
height: 35,
height: 55,
margin: const EdgeInsets.only(bottom: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
@ -163,7 +163,7 @@ class _PagesPanelState extends State<PagesPanel> {
Expanded(
child: Text(
widget.pages[i].pagePart!,
maxLines: 1,
maxLines: 2,
style: TextStyle(
fontSize: 13,
color: isCurrentIndex

View File

@ -67,6 +67,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
IdUtils.av2bv(item.aid),
item.cid,
item.aid,
item.cover,
);
currentIndex.value = i;
_bottomSheetController?.close();

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/utils.dart';
class StaffUpItem extends StatelessWidget {
final Staff item;
const StaffUpItem({
super.key,
required this.item,
});
@override
Widget build(BuildContext context) {
final String heroTag = Utils.makeHeroTag(item.mid);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 15),
GestureDetector(
onTap: () => Get.toNamed(
'/member?mid=${item.mid}',
arguments: {'face': item.face, 'heroTag': heroTag},
),
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 45,
height: 45,
src: item.face,
type: 'avatar',
),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: 85,
child: Text(
item.name!,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: item.vip!.status == 1
? const Color.fromARGB(255, 251, 100, 163)
: null,
),
),
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: 85,
child: Text(
item.title!,
overflow: TextOverflow.ellipsis,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 12,
),
),
),
),
],
);
}
}

View File

@ -458,11 +458,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
onTap: () {
handlePlay();
},
child: NetworkImgLayer(
type: 'emote',
src: vdCtr.videoItem['pic'],
width: Get.width,
height: videoHeight.value,
child: Obx(
() => AnimatedOpacity(
duration: const Duration(milliseconds: 100), // 渐变动画的持续时间
opacity: 1, // 设置不透明度
child: NetworkImgLayer(
type: 'emote',
src: vdCtr.cover.value,
width: Get.width,
height: videoHeight.value,
),
),
),
),
Positioned(

View File

@ -180,15 +180,16 @@ class _HeaderControlState extends State<HeaderControl> {
'当前音质 ${widget.videoDetailCtr!.currentAudioQa!.description}',
style: subTitleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetDecodeFormats()},
dense: true,
leading: const Icon(Icons.av_timer_outlined, size: 20),
title: const Text('解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle),
),
if (widget.videoDetailCtr!.currentDecodeFormats != null)
ListTile(
onTap: () => {Get.back(), showSetDecodeFormats()},
dense: true,
leading: const Icon(Icons.av_timer_outlined, size: 20),
title: const Text('解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats!.description}',
style: subTitleStyle),
),
ListTile(
onTap: () => {Get.back(), showSetRepeat()},
dense: true,
@ -541,16 +542,24 @@ class _HeaderControlState extends State<HeaderControl> {
/// 可用的质量分类
int userfulQaSam = 0;
final List<VideoItem> video = videoInfo.dash!.video!;
final Set<int> idSet = {};
for (final VideoItem item in video) {
final int id = item.id!;
if (!idSet.contains(id)) {
idSet.add(id);
userfulQaSam++;
if (videoInfo.dash != null) {
// dash格式视频一次请求会返回所有可播放的清晰度video
final List<VideoItem> video = videoInfo.dash!.video!;
final Set<int> idSet = {};
for (final VideoItem item in video) {
final int id = item.id!;
if (!idSet.contains(id)) {
idSet.add(id);
userfulQaSam++;
}
}
}
if (videoInfo.durl != null) {
// durl格式视频一次请求返回对应清晰度video
userfulQaSam = videoFormat.length - 1;
}
showModalBottomSheet(
context: context,
elevation: 0,
@ -707,7 +716,7 @@ class _HeaderControlState extends State<HeaderControl> {
void showSetDecodeFormats() {
// 当前选中的解码格式
final VideoDecodeFormats currentDecodeFormats =
widget.videoDetailCtr!.currentDecodeFormats;
widget.videoDetailCtr!.currentDecodeFormats!;
final VideoItem firstVideo = widget.videoDetailCtr!.firstVideo;
// 当前视频可用的解码格式
final List<FormatItem> videoFormat = videoInfo.supportFormats!;

View File

@ -101,7 +101,7 @@ class PlPlayerController {
bool _isFirstTime = true;
Timer? _timer;
late Timer? _timerForSeek;
Timer? _timerForSeek;
Timer? _timerForVolume;
Timer? _timerForShowingVolume;
Timer? _timerForGettingVolume;
@ -335,8 +335,10 @@ class PlPlayerController {
}) {
// 如果实例尚未创建,则创建一个新实例
_instance ??= PlPlayerController._();
_instance!._playerCount.value += 1;
_videoType.value = videoType;
if (videoType != 'none') {
_instance!._playerCount.value += 1;
_videoType.value = videoType;
}
return _instance!;
}
@ -1120,9 +1122,6 @@ class PlPlayerController {
}
Future<void> dispose({String type = 'single'}) async {
print('dispose');
print('dispose: ${playerCount.value}');
// 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) {
_playerCount.value -= 1;
@ -1132,7 +1131,6 @@ class PlPlayerController {
}
_playerCount.value = 0;
try {
print('dispose dispose ---------');
_timer?.cancel();
_timerForVolume?.cancel();
_timerForGettingVolume?.cancel();

View File

@ -26,7 +26,7 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
static final List<MediaItem> _item = [];
Box setting = GStrorage.setting;
bool enableBackgroundPlay = false;
PlPlayerController player = PlPlayerController.getInstance();
PlPlayerController player = PlPlayerController.getInstance(videoType: 'none');
VideoPlayerServiceHandler() {
revalidateSetting();

View File

@ -18,7 +18,7 @@ class AudioSessionHandler {
session.configure(const AudioSessionConfiguration.music());
session.interruptionEventStream.listen((event) {
final player = PlPlayerController.getInstance();
final player = PlPlayerController.getInstance(videoType: 'none');
if (event.begin) {
if (!player.playerStatus.playing) return;
switch (event.type) {
@ -51,7 +51,7 @@ class AudioSessionHandler {
// 耳机拔出暂停
session.becomingNoisyEventStream.listen((_) {
final player = PlPlayerController.getInstance();
final player = PlPlayerController.getInstance(videoType: 'none');
if (player.playerStatus.playing) {
player.pause();
}

View File

@ -29,8 +29,8 @@ class ShutdownTimerService {
return;
}
SmartDialog.showToast("设置 $scheduledExitInMinutes 分钟后定时关闭");
_shutdownTimer = Timer(Duration(minutes: scheduledExitInMinutes),
() => _shutdownDecider());
_shutdownTimer = Timer(
Duration(minutes: scheduledExitInMinutes), () => _shutdownDecider());
}
void _showTimeUpButPauseDialog() {
@ -59,7 +59,7 @@ class ShutdownTimerService {
// Start the 10-second timer to auto close the dialog
_autoCloseDialogTimer?.cancel();
_autoCloseDialogTimer = Timer(const Duration(seconds: 10), () {
SmartDialog.dismiss();// Close the dialog
SmartDialog.dismiss(); // Close the dialog
_executeShutdown();
});
return AlertDialog(
@ -88,7 +88,8 @@ class ShutdownTimerService {
_showShutdownDialog();
return;
}
PlPlayerController plPlayerController = PlPlayerController.getInstance();
PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'none');
if (!exitApp && !waitForPlayingCompleted) {
if (!plPlayerController.playerStatus.playing) {
//仅提示用户
@ -108,19 +109,22 @@ class ShutdownTimerService {
//该方法依赖耦合实现,不够优雅
isWaiting = true;
}
void handleWaitingFinished(){
if(isWaiting){
void handleWaitingFinished() {
if (isWaiting) {
_showShutdownDialog();
isWaiting = false;
}
}
void _executeShutdown() {
if (exitApp) {
//退出app
exit(0);
} else {
//暂停播放
PlPlayerController plPlayerController = PlPlayerController.getInstance();
PlPlayerController plPlayerController =
PlPlayerController.getInstance(videoType: 'none');
if (plPlayerController.playerStatus.playing) {
plPlayerController.pause();
waitForPlayingCompleted = true;