merge main

This commit is contained in:
guozhigq
2023-08-12 22:18:44 +08:00
100 changed files with 2684 additions and 944 deletions

View File

@ -13,36 +13,47 @@ import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
int tabInitialIndex = 0;
TabController? tabCtr;
// tabs
RxList<String> tabs = <String>['简介', '评论'].obs;
// 视频aid
/// 路由传参
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
// 视频类型 默认投稿视频
SearchType videoType = SearchType.video;
late PlayUrlModel data;
// 当前画质
late VideoQuality currentVideoQa;
// 当前音质
late AudioQuality currentAudioQa;
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
String heroTag = Get.arguments['heroTag'];
// 视频详情
Map videoItem = {};
// 视频类型 默认投稿视频
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
/// tabs相关配置
int tabInitialIndex = 0;
late TabController tabCtr;
RxList<String> tabs = <String>['简介', '评论'].obs;
// 请求返回的视频信息
late PlayUrlModel data;
// 请求状态
RxBool isLoading = false.obs;
String heroTag = '';
/// 播放器配置 画质 音质 解码格式
late VideoQuality currentVideoQa;
late AudioQuality currentAudioQa;
late VideoDecodeFormats currentDecodeFormats;
// PlPlayerController plPlayerController = PlPlayerController();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
// 封面图的展示
RxBool isShowCover = true.obs;
// 硬解
RxBool enableHA = true.obs;
/// 本地存储
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
int oid = 0;
// 评论id 请求楼中楼评论使用
@ -52,15 +63,7 @@ class VideoDetailController extends GetxController
final scaffoldKey = GlobalKey<ScaffoldState>();
Timer? timer;
RxString bgCover = ''.obs;
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
PlPlayerController plPlayerController = PlPlayerController.getInstance();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
// 封面图的展示
RxBool isShowCover = true.obs;
late VideoItem firstVideo;
late String videoUrl;
@ -70,24 +73,23 @@ class VideoDetailController extends GetxController
@override
void onInit() {
super.onInit();
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
var args = Get.arguments['videoItem'];
Map argMap = Get.arguments;
var keys = argMap.keys.toList();
if (keys.isNotEmpty) {
if (keys.contains('videoItem')) {
var args = argMap['videoItem'];
if (args.pic != null && args.pic != '') {
videoItem['pic'] = args.pic;
bgCover.value = args.pic;
}
}
if (Get.arguments.containsKey('pic')) {
videoItem['pic'] = Get.arguments['pic'];
bgCover.value = Get.arguments['pic'];
if (keys.contains('pic')) {
videoItem['pic'] = argMap['pic'];
}
heroTag = Get.arguments['heroTag'];
videoType = Get.arguments['videoType'] ?? SearchType.video;
}
tabCtr = TabController(length: 2, vsync: this);
// queryVideoUrl();
autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
}
showReplyReplyPanel() {
@ -120,8 +122,15 @@ class VideoDetailController extends GetxController
/// 暂不匹配解码规则
/// 根据currentVideoQa 重新设置videoUrl
firstVideo =
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
// firstVideo =
// data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
// videoUrl = firstVideo.baseUrl!;
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
List<VideoItem> videoList =
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
@ -133,6 +142,7 @@ class VideoDetailController extends GetxController
}
Future playerInit({video, audio, seekToTime, duration}) async {
print('data.timeLength:${data.timeLength}');
await plPlayerController.setDataSource(
DataSource(
videoSource: video ?? videoUrl,
@ -145,7 +155,7 @@ class VideoDetailController extends GetxController
},
),
// 硬解
enableHA: true,
enableHA: enableHA.value,
autoplay: autoPlay.value,
seekTo: seekToTime ?? defaultST,
duration: duration ?? Duration(milliseconds: data.timeLength ?? 0),
@ -170,14 +180,73 @@ class VideoDetailController extends GetxController
data = result['data'];
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
firstVideo = data.dash!.video!.first;
videoUrl = firstVideo.baseUrl!;
//
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// firstVideo = data.dash!.video!.first;
// videoUrl = firstVideo.baseUrl!;
// //
// currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
// AudioItem firstAudio =
// data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
// audioUrl = firstAudio.baseUrl ?? '';
List<VideoItem> allVideosList = data.dash!.video!;
try {
// 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code;
//
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: currentHighVideoQa);
int resVideoQa = currentHighVideoQa;
if (cacheVideoQa <= currentHighVideoQa) {
List<int> numbers = data.acceptQuality!
.where((e) => e <= currentHighVideoQa)
.toList();
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
}
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
/// 取出符合当前画质的videoList
List<VideoItem> videosList =
allVideosList.where((e) => e.quality!.code == resVideoQa).toList();
/// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
List<FormatItem> supportFormats = data.supportFormats!;
// 根据画质选编码格式
List supportDecodeFormats =
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
try {
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: supportDecodeFormats.first))!;
} catch (_) {}
/// 取出符合当前解码格式的videoItem
firstVideo = videosList
.firstWhere((e) => e.codecs!.startsWith(currentDecodeFormats.code));
videoUrl = firstVideo.baseUrl!;
} catch (_) {}
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
AudioItem firstAudio =
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
late AudioItem firstAudio;
List audiosList = data.dash!.audio!;
try {
if (audiosList.isNotEmpty) {
firstAudio = audiosList.first;
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: firstAudio.id);
// 选择最接近的那个音轨
firstAudio = audiosList.firstWhere(
(e) => e.id == resultAudioQa,
orElse: () => AudioItem(),
);
} else {
firstAudio = AudioItem();
}
} catch (_) {}
audioUrl = firstAudio.baseUrl ?? '';
//
if (firstAudio.id != null) {
@ -185,6 +254,13 @@ class VideoDetailController extends GetxController
}
defaultST = Duration(milliseconds: data.lastPlayTime!);
await playerInit();
// await playerInit(
// firstVideo,
// audioUrl,
// defaultST: Duration(milliseconds: data.lastPlayTime!),
// duration: data.timeLength ?? 0,
// );
} else {
SmartDialog.showToast(result['msg'].toString());
}

View File

@ -51,6 +51,8 @@ class VideoIntroController extends GetxController {
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
RxInt lastPlayCid = 0.obs;
@override
void onInit() {
super.onInit();
@ -76,6 +78,7 @@ class VideoIntroController extends GetxController {
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
lastPlayCid.value = int.parse(Get.parameters['cid']!);
}
// 获取视频简介&分p
@ -83,6 +86,9 @@ class VideoIntroController extends GetxController {
var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) {
videoDetail.value = result['data']!;
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
}
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.tabs
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];

View File

@ -1,4 +1,3 @@
import 'package:flutter/gestures.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@ -20,6 +19,7 @@ import 'widgets/action_item.dart';
import 'widgets/action_row_item.dart';
import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
import 'widgets/page.dart';
import 'widgets/season.dart';
class VideoIntroPanel extends StatefulWidget {
@ -62,7 +62,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
// return _buildView(context, false, videoDetail);
return Obx(
() => VideoInfo(
loadingStatus: false,
@ -95,22 +94,35 @@ class VideoInfo extends StatefulWidget {
}
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Map videoItem = Get.put(VideoIntroController()).videoItem!;
final VideoIntroController videoIntroController =
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
bool isExpand = false;
final String heroTag = Get.arguments['heroTag'];
late final VideoIntroController videoIntroController;
late final VideoDetailController videoDetailCtr;
late final Map<dynamic, dynamic> videoItem;
late VideoDetailController? videoDetailCtr;
Box localCache = GStrorage.localCache;
late double sheetHeight;
late final bool loadingStatus; // 加载状态
late final dynamic owner;
late final dynamic follower;
late final dynamic followStatus;
@override
void initState() {
super.initState();
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoItem = videoIntroController.videoItem!;
sheetHeight = localCache.get('sheetHeight');
loadingStatus = widget.loadingStatus;
owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner;
follower = loadingStatus
? '-'
: Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus;
}
// 收藏
@ -141,24 +153,39 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
);
}
// 用户主页
onPushMember() {
feedBack();
int mid = !loadingStatus
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
String face = !loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid',
arguments: {'face': face, 'heroTag': (mid + 99).toString()});
}
@override
Widget build(BuildContext context) {
ThemeData t = Theme.of(context);
Color outline = t.colorScheme.outline;
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter(
child: !widget.loadingStatus || videoItem.isNotEmpty
child: !loadingStatus || videoItem.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
Expanded(
child: Text(
!widget.loadingStatus
!loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle(
@ -182,14 +209,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
return t.highlightColor.withOpacity(0.2);
}),
),
onPressed: () => showIntroDetail(),
icon: const Icon(Icons.more_horiz),
onPressed: showIntroDetail,
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
),
InkWell(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
@ -237,7 +268,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 点赞收藏转发 布局样式2
// actionGrid(context, videoIntroController),
// 合集
if (!widget.loadingStatus &&
if (!loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[
SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!,
@ -247,97 +278,86 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
.changeSeasonOrbangu(bvid, cid, aid),
)
],
if (!loadingStatus &&
widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel(
pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value,
sheetHeight: sheetHeight,
changeFuc: (cid) =>
videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid, cid, null),
))
],
GestureDetector(
onTap: () {
feedBack();
int mid = !widget.loadingStatus
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
String face = !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid', arguments: {
'face': face,
'heroTag': (mid + 99).toString()
});
},
child: Padding(
padding: const EdgeInsets.only(
top: 12, bottom: 12, left: 4, right: 4),
onTap: onPushMember,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 4),
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face,
src: loadingStatus
? owner.face
: widget.videoDetail!.owner!.face,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Text(
!widget.loadingStatus
? widget.videoDetail!.owner!.name
: videoItem['owner'].name,
style: const TextStyle(fontSize: 13),
),
Text(owner.name,
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
widget.loadingStatus
? '-'
: Utils.numFormat(
videoIntroController.userStat['follower']),
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: t.colorScheme.outline),
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(),
AnimatedOpacity(
opacity: widget.loadingStatus ? 0 : 1,
opacity: loadingStatus ? 0 : 1,
duration: const Duration(milliseconds: 150),
child: SizedBox(
height: 32,
child: Obx(
() => videoIntroController
.followStatus.isNotEmpty
? TextButton(
onPressed: () => videoIntroController
.actionRelationMod(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
videoIntroController.followStatus[
'attribute'] !=
0
? t.colorScheme.outline
: t.colorScheme.onPrimary,
backgroundColor: videoIntroController
.followStatus[
'attribute'] !=
0
? t.colorScheme.onInverseSurface
: t.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
videoIntroController.followStatus[
'attribute'] !=
0
? '已关注'
: '关注',
style: TextStyle(
fontSize: t.textTheme.labelMedium!
.fontSize),
),
)
: ElevatedButton(
onPressed: () => videoIntroController
.actionRelationMod(),
child: const Text('关注'),
),
() =>
videoIntroController.followStatus.isNotEmpty
? TextButton(
onPressed: videoIntroController
.actionRelationMod,
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
followStatus['attribute'] != 0
? outline
: t.colorScheme.onPrimary,
backgroundColor:
followStatus['attribute'] != 0
? t.colorScheme
.onInverseSurface
: t.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
followStatus['attribute'] != 0
? '已关注'
: '关注',
style: TextStyle(
fontSize: t.textTheme
.labelMedium!.fontSize),
),
)
: ElevatedButton(
onPressed: videoIntroController
.actionRelationMod,
child: const Text('关注'),
),
),
),
),
@ -359,66 +379,64 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionGrid(BuildContext context, videoIntroController) {
return LayoutBuilder(builder: (context, constraints) {
return Padding(
return Container(
padding: const EdgeInsets.only(top: 6, bottom: 10),
child: SizedBox(
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.share!.toString()
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
],
),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.share!.toString()
: '-'),
],
),
);
});
@ -431,10 +449,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
),
),
const SizedBox(width: 8),
@ -443,10 +460,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
),
),
const SizedBox(width: 8),
@ -455,8 +471,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
@ -468,57 +484,20 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.reply!.toString()
: '-',
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: widget.loadingStatus,
// text: !widget.loadingStatus
loadingStatus: loadingStatus,
// text: !loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
InlineSpan buildContent(BuildContext context, content) {
String desc = content.desc;
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
List<InlineSpan> spanChilds = [];
if (descV2.isNotEmpty) {
for (var i = 0; i < descV2.length; i++) {
if (descV2[i].type == 1) {
spanChilds.add(TextSpan(text: descV2[i].rawText));
} else if (descV2[i].type == 2) {
spanChilds.add(
TextSpan(
text: '@${descV2[i].rawText}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String heroTag = Utils.makeHeroTag(descV2[i].bizId);
Get.toNamed(
'/member?mid=${descV2[i].bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
),
);
}
}
} else {
spanChilds.add(TextSpan(text: desc));
}
return TextSpan(children: spanChilds);
}
}

View File

@ -33,24 +33,13 @@ class _FavPanelState extends State<FavPanel> {
child: Column(
children: [
AppBar(
toolbarHeight: 50,
automaticallyImplyLeading: false,
centerTitle: false,
elevation: 1,
title: Text(
'选择文件夹',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
TextButton(
onPressed: () async {
feedBack();
await widget.ctr!.actionFavVideo();
},
child: const Text('完成'),
),
const SizedBox(width: 6),
],
elevation: 0,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title:
Text('添加到收藏夹', style: Theme.of(context).textTheme.titleMedium),
),
Expanded(
child: Material(
@ -63,45 +52,33 @@ class _FavPanelState extends State<FavPanel> {
return Obx(
() => ListView.builder(
itemCount:
widget.ctr!.favFolderData.value.list!.length + 1,
widget.ctr!.favFolderData.value.list!.length,
itemBuilder: (context, index) {
if (index == 0) {
return const SizedBox(height: 10);
} else {
return ListTile(
onTap: () => widget.ctr!.onChoose(
widget.ctr!.favFolderData.value
.list![index - 1].favState !=
1,
index - 1),
dense: true,
leading:
const Icon(Icons.folder_special_outlined),
minLeadingWidth: 0,
title: Text(widget.ctr!.favFolderData.value
.list![index - 1].title!),
subtitle: Text(
'${widget.ctr!.favFolderData.value.list![index - 1].mediaCount}个内容',
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
return ListTile(
onTap: () => widget.ctr!.onChoose(
widget.ctr!.favFolderData.value.list![index]
.favState !=
1,
index),
dense: true,
leading: const Icon(Icons.folder_outlined),
minLeadingWidth: 0,
title: Text(widget.ctr!.favFolderData.value
.list![index].title!),
subtitle: Text(
'${widget.ctr!.favFolderData.value.list![index].mediaCount}个内容',
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: widget.ctr!.favFolderData.value
.list![index].favState ==
1,
onChanged: (bool? checkValue) =>
widget.ctr!.onChoose(checkValue!, index),
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: widget.ctr!.favFolderData.value
.list![index - 1].favState ==
1,
onChanged: (bool? checkValue) => widget.ctr!
.onChoose(checkValue!, index - 1),
),
),
);
}
),
);
},
),
);
@ -119,6 +96,46 @@ class _FavPanelState extends State<FavPanel> {
),
),
),
Divider(
height: 1,
color: Theme.of(context).disabledColor.withOpacity(0.08),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 12,
bottom: MediaQuery.of(context).padding.bottom),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
backgroundColor: Theme.of(context)
.colorScheme
.onInverseSurface, // 设置按钮背景色
),
child: const Text('取消'),
),
const SizedBox(width: 10),
TextButton(
onPressed: () async {
feedBack();
await widget.ctr!.actionFavVideo();
},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
child: const Text('完成'),
),
],
),
),
],
),
);

View File

@ -27,19 +27,20 @@ class IntroDetail extends StatelessWidget {
height: sheetHeight,
child: Column(
children: [
Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.5),
borderRadius: const BorderRadius.all(Radius.circular(3))),
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
@ -125,33 +126,29 @@ class IntroDetail extends StatelessWidget {
// type
// 1 普通文本
// 2 @用户
List<InlineSpan> spanChilds = [];
if (descV2.isNotEmpty) {
for (var i = 0; i < descV2.length; i++) {
if (descV2[i].type == 1) {
spanChilds.add(TextSpan(text: descV2[i].rawText));
} else if (descV2[i].type == 2) {
spanChilds.add(
TextSpan(
text: '@${descV2[i].rawText}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String heroTag = Utils.makeHeroTag(descV2[i].bizId);
Get.toNamed(
'/member?mid=${descV2[i].bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
),
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
final currentDesc = descV2[index];
switch (currentDesc.type) {
case 1:
return TextSpan(text: currentDesc.rawText);
case 2:
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
return TextSpan(
text: '@${currentDesc.rawText}',
style: TextStyle(color: colorSchemePrimary),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/member?mid=${currentDesc.bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
);
}
default:
return TextSpan();
}
} else {
spanChilds.add(TextSpan(text: desc));
}
});
return TextSpan(children: spanChilds);
}
}

View File

@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
class PagesPanel extends StatefulWidget {
final List<Part> pages;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
const PagesPanel({
super.key,
required this.pages,
this.cid,
this.sheetHeight,
this.changeFuc,
});
@override
State<PagesPanel> createState() => _PagesPanelState();
}
class _PagesPanelState extends State<PagesPanel> {
late List<Part> episodes;
late int currentIndex;
@override
void initState() {
super.initState();
episodes = widget.pages;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
}
void changeFucCall(item, i) async {
await widget.changeFuc!(
item.cid,
);
currentIndex = i;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 10, bottom: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('视频选集 '),
Expanded(
child: Text(
' 正在播放:${widget.pages[currentIndex].pagePart}',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
const SizedBox(width: 10),
SizedBox(
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showBottomSheet(
context: context,
builder: (_) => Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Container(
height: 45,
padding:
const EdgeInsets.only(left: 14, right: 14),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${episodes.length}',
style:
Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
Divider(
height: 1,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
Expanded(
child: Material(
child: ListView.builder(
itemCount: episodes.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
changeFucCall(episodes[index], index);
Get.back();
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
left: 15,
right: 15),
child: Text(
episodes[index].pagePart!,
style: TextStyle(
color: index == currentIndex
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface),
),
),
);
},
),
),
),
],
),
),
);
},
child: Text(
'${widget.pages.length}',
style: const TextStyle(fontSize: 13),
),
),
),
],
),
),
Container(
height: 35,
margin: const EdgeInsets.only(bottom: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: ((context, i) {
return Container(
width: 150,
margin: const EdgeInsets.only(right: 10),
child: Material(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => changeFucCall(widget.pages[i], i),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 8),
child: Row(
children: [
if (i == currentIndex) ...[
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 6)
],
Expanded(
child: Text(
widget.pages[i].pagePart!,
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,
))
],
),
),
),
),
);
}),
),
)
],
);
}
}

View File

@ -391,6 +391,7 @@ class ReplyItemRow extends StatelessWidget {
),
if (replies![i].isUp)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: UpTag(),
),
buildContent(

View File

@ -118,7 +118,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override
Widget build(BuildContext context) {
return Container(
height: 400,
height: 500,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(

View File

@ -1,11 +1,13 @@
import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
@ -163,8 +165,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled,
expandedHeight: videoHeight,
// backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
backgroundColor:
MediaQuery.of(Get.context!).platformBrightness ==
Brightness.dark
? Colors.black
: Theme.of(context).colorScheme.background,
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(top: statusBarHeight),
@ -233,10 +238,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
backgroundColor:
Colors.transparent,
actions: [
/// TODO
IconButton(
tooltip: '稍后再看',
onPressed: () {},
onPressed: () async {
var res = await UserHttp
.toViewLater(
bvid:
videoDetailController
.bvid);
SmartDialog.showToast(
res['msg']);
},
icon: const Icon(Icons
.history_outlined))
],
@ -291,39 +303,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Opacity(
opacity: 0,
child: Container(
child: SizedBox(
width: double.infinity,
height: 0,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
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(),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: 280,
margin: const EdgeInsets.only(left: 20),
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(

View File

@ -28,6 +28,7 @@ class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
@ -81,7 +82,7 @@ class _HeaderControlState extends State<HeaderControl> {
enabled: false,
leading:
const Icon(Icons.network_cell_outlined, size: 20),
title: const Text('省流模式'),
title: Text('省流模式', style: titleStyle),
subtitle: Text('低画质 减少视频缓存', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.75,
@ -99,22 +100,22 @@ class _HeaderControlState extends State<HeaderControl> {
),
),
),
Obx(
() => ListTile(
onTap: () => {Get.back(), showSetSpeedSheet()},
dense: true,
leading: const Icon(Icons.speed_outlined, size: 20),
title: const Text('播放速度'),
subtitle: Text(
'当前倍速 x${widget.controller!.playbackSpeed}',
style: subTitleStyle),
),
),
// Obx(
// () => ListTile(
// onTap: () => {Get.back(), showSetSpeedSheet()},
// dense: true,
// leading: const Icon(Icons.speed_outlined, size: 20),
// title: Text('播放速度', style: titleStyle),
// subtitle: Text(
// '当前倍速 x${widget.controller!.playbackSpeed}',
// style: subTitleStyle),
// ),
// ),
ListTile(
onTap: () => {Get.back(), showSetVideoQa()},
dense: true,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('选择画质'),
title: Text('选择画质', style: titleStyle),
subtitle: Text(
'当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}',
style: subTitleStyle),
@ -123,24 +124,33 @@ class _HeaderControlState extends State<HeaderControl> {
onTap: () => {Get.back(), showSetAudioQa()},
dense: true,
leading: const Icon(Icons.album_outlined, size: 20),
title: const Text('选择音质'),
title: Text('选择音质', style: titleStyle),
subtitle: Text(
'当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}',
style: subTitleStyle),
),
ListTile(
onTap: () {},
onTap: () => {Get.back(), showSetDecodeFormats()},
dense: true,
enabled: false,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('播放设置'),
leading: const Icon(Icons.av_timer_outlined, size: 20),
title: Text('解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle),
),
// ListTile(
// onTap: () {},
// dense: true,
// enabled: false,
// leading: const Icon(Icons.play_circle_outline, size: 20),
// title: Text('播放设置', style: titleStyle),
// ),
ListTile(
onTap: () {},
dense: true,
enabled: false,
leading: const Icon(Icons.subtitles_outlined, size: 20),
title: const Text('弹幕设置'),
title: Text('弹幕设置', style: titleStyle),
),
],
),
@ -250,7 +260,7 @@ class _HeaderControlState extends State<HeaderControl> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('选择画质'),
Text('选择画质', style: titleStyle),
const SizedBox(width: 4),
Icon(
Icons.info_outline,
@ -329,7 +339,9 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12),
child: Column(
children: [
const SizedBox(height: 45, child: Center(child: Text('选择音质'))),
SizedBox(
height: 45,
child: Center(child: Text('选择音质', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
@ -370,6 +382,74 @@ class _HeaderControlState extends State<HeaderControl> {
);
}
// 选择解码格式
void showSetDecodeFormats() {
// 当前选中的解码格式
VideoDecodeFormats currentDecodeFormats =
widget.videoDetailCtr!.currentDecodeFormats;
// 当前视频可用的解码格式
List<FormatItem> videoFormat = videoInfo.supportFormats!;
List list = videoFormat.first.codecs!;
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择解码格式', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in list) ...[
ListTile(
onTap: () {
widget.videoDetailCtr!.currentDecodeFormats =
VideoDecodeFormatsCode.fromString(i)!;
widget.videoDetailCtr!.updatePlayer();
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(VideoDecodeFormatsCode.fromString(i)!
.description!),
subtitle: Text(
i!,
style: subTitleStyle,
),
trailing: i.startsWith(currentDecodeFormats.code)
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
),
]
],
),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final _ = widget.controller!;