feat: 番剧播放

This commit is contained in:
guozhigq
2023-08-04 17:04:55 +08:00
parent 90c617a1a4
commit c961dc6cf5
17 changed files with 1155 additions and 49 deletions

View File

@ -1,6 +1,4 @@
PODS: PODS:
- auto_orientation (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift - ReachabilitySwift
@ -43,7 +41,6 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@ -68,8 +65,6 @@ SPEC REPOS:
- ReachabilitySwift - ReachabilitySwift
EXTERNAL SOURCES: EXTERNAL SOURCES:
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
@ -106,7 +101,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios" :path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854

View File

@ -95,7 +95,7 @@ class BangumiInfoModel {
jpTitle = json['jp_title']; jpTitle = json['jp_title'];
link = json['link']; link = json['link'];
mediaId = json['media_id']; mediaId = json['media_id'];
newEp = json['newEp']; newEp = json['new_ep'];
playStrategy = json['play_strategy']; playStrategy = json['play_strategy'];
positive = json['positive']; positive = json['positive'];
publish = json['publish']; publish = json['publish'];
@ -184,7 +184,7 @@ class EpisodeItem {
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
aid = json['aid']; aid = json['aid'];
badge = json['badge']; badge = json['badge'] != '' ? json['badge'] : null;
badgeInfo = json['badge_info']; badgeInfo = json['badge_info'];
badgeType = json['badge_type']; badgeType = json['badge_type'];
bvid = json['bvid']; bvid = json['bvid'];

View File

@ -277,10 +277,12 @@ class SearchMBangumiModel {
SearchMBangumiModel({this.list}); SearchMBangumiModel({this.list});
List<SearchMBangumiItemModel>? list; List<SearchMBangumiItemModel>? list;
SearchMBangumiModel.fromJson(Map<String, dynamic> json) { SearchMBangumiModel.fromJson(Map<String, dynamic> json) {
list = json['result'] list = json['result'] != null
.map<SearchMBangumiItemModel>( ? json['result']
(e) => SearchMBangumiItemModel.fromJson(e)) .map<SearchMBangumiItemModel>(
.toList(); (e) => SearchMBangumiItemModel.fromJson(e))
.toList()
: [];
} }
} }

View File

@ -57,7 +57,7 @@ class VideoDetailData {
bool? isChargeableSeason; bool? isChargeableSeason;
bool? isStory; bool? isStory;
bool? noCache; bool? noCache;
List<Page>? pages; List<Part>? pages;
Subtitle? subtitle; Subtitle? subtitle;
// Label? label; // Label? label;
UgcSeason? ugcSeason; UgcSeason? ugcSeason;
@ -136,7 +136,7 @@ class VideoDetailData {
noCache = json["no_cache"]; noCache = json["no_cache"];
pages = json["pages"] == null pages = json["pages"] == null
? [] ? []
: List<Page>.from(json["pages"]!.map((e) => Page.fromJson(e))); : List<Part>.from(json["pages"]!.map((e) => Part.fromJson(e)));
subtitle = subtitle =
json["subtitle"] == null ? null : Subtitle.fromJson(json["subtitle"]); json["subtitle"] == null ? null : Subtitle.fromJson(json["subtitle"]);
ugcSeason = json["ugc_season"] != null ugcSeason = json["ugc_season"] != null
@ -352,7 +352,7 @@ class Owner {
} }
} }
class Page { class Part {
int? cid; int? cid;
int? page; int? page;
String? from; String? from;
@ -363,7 +363,7 @@ class Page {
Dimension? dimension; Dimension? dimension;
String? firstFrame; String? firstFrame;
Page({ Part({
this.cid, this.cid,
this.page, this.page,
this.from, this.from,
@ -375,11 +375,11 @@ class Page {
this.firstFrame, this.firstFrame,
}); });
fromRawJson(String str) => Page.fromJson(json.decode(str)); fromRawJson(String str) => Part.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
Page.fromJson(Map<String, dynamic> json) { Part.fromJson(Map<String, dynamic> json) {
cid = json["cid"]; cid = json["cid"];
page = json["page"]; page = json["page"];
from = json["from"]; from = json["from"];
@ -620,7 +620,7 @@ class EpisodeItem {
int? cid; int? cid;
String? title; String? title;
int? attribute; int? attribute;
Page? page; Part? page;
String? bvid; String? bvid;
EpisodeItem.fromJson(Map<String, dynamic> json) { EpisodeItem.fromJson(Map<String, dynamic> json) {
@ -631,7 +631,7 @@ class EpisodeItem {
cid = json['cid']; cid = json['cid'];
title = json['title']; title = json['title'];
attribute = json['attribute']; attribute = json['attribute'];
page = Page.fromJson(json['page']); page = Part.fromJson(json['page']);
bvid = json['bvid']; bvid = json['bvid'];
} }
} }

View File

@ -0,0 +1,401 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart';
class BangumiIntroController extends GetxController {
// 视频bvid
String bvid = Get.parameters['bvid']!;
int seasonId = int.parse(Get.parameters['seasonId']!);
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
Map? videoItem = {};
BangumiInfoModel? bangumiItem;
// 请求状态
RxBool isLoading = false.obs;
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
Rx<BangumiInfoModel> bangumiDetail = BangumiInfoModel().obs;
// 请求返回的信息
String responseMsg = '请求异常';
// up主粉丝数
Map userStat = {'follower': '-'};
// 是否点赞
RxBool hasLike = false.obs;
// 是否投币
RxBool hasCoin = false.obs;
// 是否收藏
RxBool hasFav = false.obs;
Box user = GStrorage.user;
bool userLogin = false;
Rx<FavFolderData> favFolderData = FavFolderData().obs;
List addMediaIdsNew = [];
List delMediaIdsNew = [];
// 关注状态 默认未关注
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
@override
void onInit() {
super.onInit();
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('bangumiItem')) {
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;
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
}
// 获取番剧简介&选集
Future queryBangumiIntro() async {
print('🐶🐶: $seasonId');
var result = await SearchHttp.bangumiInfo(seasonId: seasonId);
print("🐶🐶:${result['data']}");
if (result['status']) {
bangumiDetail.value = result['data'];
}
if (userLogin) {
// 获取点赞状态
// queryHasLikeVideo();
// 获取投币状态
// queryHasCoinVideo();
// 获取收藏状态
// queryHasFavVideo();
//
// queryFollowStatus();
}
return result;
}
// 获取up主粉丝数
Future queryUserStat() async {
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
if (result['status']) {
userStat = result['data'];
}
}
// 获取点赞状态
Future queryHasLikeVideo() async {
var result = await VideoHttp.hasLikeVideo(bvid: bvid);
// data num 被点赞标志 0未点赞 1已点赞
hasLike.value = result["data"] == 1 ? true : false;
}
// 获取投币状态
Future queryHasCoinVideo() async {
var result = await VideoHttp.hasCoinVideo(bvid: bvid);
hasCoin.value = result["data"]['multiply'] == 0 ? false : true;
}
// 获取收藏状态
Future queryHasFavVideo() async {
var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid));
if (result['status']) {
hasFav.value = result["data"]['favoured'];
} else {
hasFav.value = false;
}
}
// 一键三连
Future actionOneThree() async {
if (user.get(UserBoxKey.userMid) == null) {
SmartDialog.showToast('账号未登录');
return;
}
if (hasLike.value && hasCoin.value && hasFav.value) {
// 已点赞、投币、收藏
SmartDialog.showToast('🙏 UP已经收到了');
return false;
}
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('一键三连 给UP送温暖'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: const Text('点错了')),
TextButton(
onPressed: () async {
var result = await VideoHttp.oneThree(bvid: bvid);
if (result['status']) {
hasLike.value = result["data"]["like"];
hasCoin.value = result["data"]["coin"];
hasFav.value = result["data"]["fav"];
SmartDialog.showToast('三连成功 🎉');
} else {
SmartDialog.showToast(result['msg']);
}
SmartDialog.dismiss();
},
child: const Text('确认'),
)
],
);
},
);
}
// (取消)点赞
Future actionLikeVideo() async {
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) {
// hasLike.value = result["data"] == 1 ? true : false;
if (!hasLike.value) {
SmartDialog.showToast('点赞成功 👍');
hasLike.value = true;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1;
} else if (hasLike.value) {
SmartDialog.showToast('取消赞');
hasLike.value = false;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1;
}
hasLike.refresh();
} else {
SmartDialog.showToast(result['msg']);
}
}
// 投币
Future actionCoinVideo() async {
if (user.get(UserBoxKey.userMid) == null) {
SmartDialog.showToast('账号未登录');
return;
}
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
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();
},
),
],
);
}),
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('确定'))
],
);
});
}
// (取消)收藏
Future actionFavVideo() async {
try {
for (var i in favFolderData.value.list!.toList()) {
if (i.favState == 1) {
addMediaIdsNew.add(i.id);
} else {
delMediaIdsNew.add(i.id);
}
}
} catch (e) {
// ignore: avoid_print
print(e);
}
var result = await VideoHttp.favVideo(
aid: IdUtils.bv2av(bvid),
addIds: addMediaIdsNew.join(','),
delIds: delMediaIdsNew.join(','));
if (result['status']) {
if (result['data']['prompt']) {
addMediaIdsNew = [];
delMediaIdsNew = [];
Get.back();
// 重新获取收藏状态
queryHasFavVideo();
SmartDialog.showToast('✅ 操作成功');
}
}
}
// 分享视频
Future actionShareVideo() async {
var result = await Share.share('${HttpString.baseUrl}/video/$bvid')
.whenComplete(() {});
return result;
}
Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder(
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
if (result['status']) {
favFolderData.value = result['data'];
}
return result;
}
// 选择文件夹
onChoose(bool checkValue, int index) {
feedBack();
List<FavFolderItemData> datalist = favFolderData.value.list!;
for (var i = 0; i < datalist.length; i++) {
if (i == index) {
datalist[i].favState = checkValue == true ? 1 : 0;
datalist[i].mediaCount = checkValue == true
? datalist[i].mediaCount! + 1
: datalist[i].mediaCount! - 1;
}
}
favFolderData.value.list = datalist;
favFolderData.refresh();
}
// 查询关注状态
Future queryFollowStatus() async {
var result = await VideoHttp.hasFollow(mid: videoDetail.value.owner!.mid!);
if (result['status']) {
followStatus.value = result['data'];
}
return result;
}
// 关注/取关up
Future actionRelationMod() async {
feedBack();
if (user.get(UserBoxKey.userMid) == null) {
SmartDialog.showToast('账号未登录');
return;
}
int currentStatus = followStatus['attribute'];
int actionStatus = 0;
switch (currentStatus) {
case 0:
actionStatus = 1;
break;
case 2:
actionStatus = 2;
break;
default:
actionStatus = 0;
break;
}
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(currentStatus == 0 ? '关注UP主?' : '取消关注UP主?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: const Text('点错了')),
TextButton(
onPressed: () async {
var result = await VideoHttp.relationMod(
mid: videoDetail.value.owner!.mid!,
act: actionStatus,
reSrc: 14,
);
if (result['status']) {
switch (currentStatus) {
case 0:
actionStatus = 2;
break;
case 2:
actionStatus = 0;
break;
default:
actionStatus = 0;
break;
}
followStatus['attribute'] = actionStatus;
followStatus.refresh();
}
SmartDialog.dismiss();
},
child: const Text('确认'),
)
],
);
},
);
}
// 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid) async {
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid;
videoDetailCtr.queryVideoUrl();
}
}

View File

@ -0,0 +1,4 @@
library bangumi_intro;
export './controller.dart';
export './view.dart';

View File

@ -0,0 +1,446 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/bangumi/widgets/bangumi_panel.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_row_item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'controller.dart';
class BangumiIntroPanel extends StatefulWidget {
const BangumiIntroPanel({super.key});
@override
State<BangumiIntroPanel> createState() => _BangumiIntroPanelState();
}
class _BangumiIntroPanelState extends State<BangumiIntroPanel>
with AutomaticKeepAliveClientMixin {
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
BangumiInfoModel? bangumiDetail;
// 添加页面缓存
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value;
});
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: bangumiIntroController.queryBangumiIntro(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
return BangumiInfo(
loadingStatus: false,
bangumiDetail: bangumiDetail,
);
} else {
// 请求错误
return HttpError(
errMsg: snapshot.data['msg'],
fn: () => Get.back(),
);
}
} else {
return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
}
},
);
}
}
class BangumiInfo extends StatefulWidget {
final bool loadingStatus;
final BangumiInfoModel? bangumiDetail;
const BangumiInfo({
Key? key,
this.loadingStatus = false,
this.bangumiDetail,
}) : super(key: key);
@override
State<BangumiInfo> createState() => _BangumiInfoState();
}
class _BangumiInfoState extends State<BangumiInfo> {
late BangumiInfoModel bangumiItem;
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
bool isExpand = false;
late VideoDetailController? videoDetailCtr;
Box localCache = GStrorage.localCache;
late double sheetHeight;
@override
void initState() {
super.initState();
bangumiItem = bangumiIntroController.bangumiItem!;
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
sheetHeight = localCache.get('sheetHeight');
}
// 收藏
showFavBottomSheet() {
if (bangumiIntroController.user.get(UserBoxKey.userMid) == null) {
SmartDialog.showToast('账号未登录');
return;
}
// showModalBottomSheet(
// context: context,
// useRootNavigator: true,
// isScrollControlled: true,
// builder: (context) {
// return FavPanel(ctr: videoIntroController);
// },
// );
}
// 视频介绍
showIntroDetail() {
feedBack();
// showBottomSheet(
// context: context,
// enableDrag: true,
// builder: (BuildContext context) {
// return IntroDetail(videoDetail: widget.videoDetail!);
// },
// );
}
@override
Widget build(BuildContext context) {
ThemeData t = Theme.of(context);
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 13),
sliver: SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NetworkImgLayer(
width: 105,
height: 160,
src: !widget.loadingStatus
? widget.bangumiDetail!.cover!
: bangumiItem.cover!,
),
const SizedBox(width: 10),
Expanded(
child: InkWell(
onTap: () => showIntroDetail(),
child: SizedBox(
height: 158,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Text(
!widget.loadingStatus
? widget.bangumiDetail!.title!
: bangumiItem.title!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 20),
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return t.colorScheme.primaryContainer
.withOpacity(0.7);
}),
),
onPressed: () {},
icon: Icon(
Icons.favorite_border_rounded,
color: t.colorScheme.primary,
size: 22,
),
),
),
],
),
Row(
children: [
// const SizedBox(width: 6),
StatView(
theme: 'gray',
view: !widget.loadingStatus
? widget.bangumiDetail!.stat!['views']
: bangumiItem.stat!['views'],
size: 'medium',
),
const SizedBox(width: 6),
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
? widget.bangumiDetail!.stat!['danmakus']
: bangumiItem.stat!['danmakus'],
size: 'medium',
),
],
),
const SizedBox(height: 2),
Row(
children: [
Text(
!widget.loadingStatus
? widget.bangumiDetail!.areas!.first['name']
: bangumiItem.areas!.first['name'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
const SizedBox(width: 6),
Text(
!widget.loadingStatus
? widget.bangumiDetail!
.publish!['pub_time_show']
: bangumiItem.publish!['pub_time_show'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
const SizedBox(width: 6),
Text(
!widget.loadingStatus
? widget.bangumiDetail!.newEp!['desc']
: bangumiItem.newEp!['desc'],
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
],
),
const SizedBox(height: 10),
Text(
'简介:${!widget.loadingStatus ? widget.bangumiDetail!.evaluate! : bangumiItem.evaluate!}',
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,
),
),
const Spacer(),
Text(
'评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem.rating!['score']!}',
style: TextStyle(
fontSize: 13,
color: t.colorScheme.primary,
),
),
],
),
),
),
),
],
),
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),
// 番剧分p
if (!widget.loadingStatus &&
widget.bangumiDetail!.episodes!.isNotEmpty) ...[
BangumiPanel(
pages: widget.bangumiDetail!.episodes!,
cid: widget.bangumiDetail!.episodes!.first.cid,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid) =>
bangumiIntroController.changeSeasonOrbangu(bvid, cid),
)
],
],
),
),
);
}
Widget actionGrid(BuildContext context, bangumiIntroController) {
return LayoutBuilder(builder: (context, constraints) {
return Material(
child: Padding(
padding: const EdgeInsets.only(top: 16, bottom: 8),
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: () => bangumiIntroController.actionLikeVideo(),
selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () => () {},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => bangumiIntroController.actionCoinVideo(),
selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['share']!.toString()
: '-'),
],
),
),
),
);
});
}
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: [
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => 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: () => 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: !widget.loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
}

View File

@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:pilipala/models/bangumi/info.dart';
class BangumiPanel extends StatefulWidget {
final List<EpisodeItem> pages;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
const BangumiPanel({
super.key,
required this.pages,
this.cid,
this.sheetHeight,
this.changeFuc,
});
@override
State<BangumiPanel> createState() => _BangumiPanelState();
}
class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex;
@override
void initState() {
super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
}
void showBangumiPanel() {
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(
'合集(${widget.pages.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: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () async {
if (widget.pages[index].badge != null) {
SmartDialog.showToast('需要大会员');
return;
}
await widget.changeFuc!(
widget.pages[index].bvid,
widget.pages[index].cid,
);
currentIndex = index;
setState(() {});
},
dense: false,
title: Text(
widget.pages[index].longTitle!,
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('合集 '),
Expanded(
child: Text(
' 正在播放:${widget.pages[currentIndex].longTitle}',
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: () => showBangumiPanel(),
child: Text(
'${widget.pages.length}',
style: const TextStyle(fontSize: 13),
),
),
),
],
),
),
SingleChildScrollView(
padding: const EdgeInsets.only(top: 7, bottom: 7),
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (int i = 0; i < widget.pages.length; i++) ...[
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: () async {
if (widget.pages[i].badge != null) {
SmartDialog.showToast('需要大会员');
return;
}
await widget.changeFuc!(
widget.pages[i].bvid,
widget.pages[i].cid,
);
currentIndex = i;
setState(() {});
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (i == currentIndex) ...[
Image.asset(
'assets/images/live.gif',
color:
Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 6)
],
Text(
'${i + 1}',
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface),
),
const SizedBox(width: 2),
if (widget.pages[i].badge != null) ...[
Image.asset(
'assets/images/big-vip.png',
height: 16,
),
],
],
),
const SizedBox(height: 3),
Text(
widget.pages[i].longTitle!,
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context)
.colorScheme
.onSurface),
overflow: TextOverflow.ellipsis,
)
],
),
),
),
),
),
]
],
),
)
],
);
}
}

View File

@ -27,6 +27,7 @@ class _SearchPanelState extends State<SearchPanel>
late SearchPanelController? _searchPanelController; late SearchPanelController? _searchPanelController;
bool _isLoadingMore = false; bool _isLoadingMore = false;
late Future _futureBuilderFuture;
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
@ -53,6 +54,7 @@ class _SearchPanelState extends State<SearchPanel>
} }
} }
}); });
_futureBuilderFuture = _searchPanelController!.onSearch();
} }
@override @override
@ -63,7 +65,7 @@ class _SearchPanelState extends State<SearchPanel>
await _searchPanelController!.onRefresh(); await _searchPanelController!.onRefresh();
}, },
child: FutureBuilder( child: FutureBuilder(
future: _searchPanelController!.onSearch(), future: _futureBuilderFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data; Map data = snapshot.data;

View File

@ -5,6 +5,7 @@ import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget searchMbangumiPanel(BuildContext context, ctr, list) { Widget searchMbangumiPanel(BuildContext context, ctr, list) {
@ -19,8 +20,12 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
var i = list![index]; var i = list![index];
return InkWell( return InkWell(
onTap: () { onTap: () {
Get.toNamed('/video?bvid=${i.bvid}&cid=${i.cid}', /// TODO 番剧详情页面
arguments: {'videoItem': i, 'heroTag': Utils.makeHeroTag(i.id)}); // Get.toNamed('/video?bvid=${i.bvid}&cid=${i.cid}', arguments: {
// 'videoItem': i,
// 'heroTag': Utils.makeHeroTag(i.id),
// 'videoType': SearchType.media_bangumi
// });
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@ -107,10 +112,12 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
String pic = episode.cover!; String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid); String heroTag = Utils.makeHeroTag(cid);
Get.toNamed( Get.toNamed(
'/video?bvid=$bvid&cid=$cid', '/video?bvid=$bvid&cid=$cid&seasonId=${i.seasonId}',
arguments: { arguments: {
'pic': pic, 'pic': pic,
'heroTag': heroTag, 'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
}, },
); );
} }

View File

@ -5,6 +5,7 @@ import 'package:hive/hive.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';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart'; import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart'; import 'package:pilipala/models/video/reply/item.dart';
@ -22,6 +23,9 @@ class VideoDetailController extends GetxController
// 视频aid // 视频aid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!); int cid = int.parse(Get.parameters['cid']!);
// 视频类型 默认投稿视频
SearchType videoType = SearchType.video;
late PlayUrlModel data; late PlayUrlModel data;
// 当前画质 // 当前画质
late VideoQuality currentVideoQa; late VideoQuality currentVideoQa;
@ -74,6 +78,7 @@ class VideoDetailController extends GetxController
bgCover.value = Get.arguments['pic']; bgCover.value = Get.arguments['pic'];
} }
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
videoType = Get.arguments['videoType'] ?? SearchType.video;
} }
tabCtr = TabController(length: 2, vsync: this); tabCtr = TabController(length: 2, vsync: this);
// queryVideoUrl(); // queryVideoUrl();

View File

@ -389,4 +389,13 @@ class VideoIntroController extends GetxController {
}, },
); );
} }
// 修改分P或番剧分集
Future changeSeasonOrbangu(bvid, cid) async {
var _videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
_videoDetailCtr.bvid = bvid;
_videoDetailCtr.cid = cid;
_videoDetailCtr.queryVideoUrl();
}
} }

View File

@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart'; import 'package:pilipala/common/widgets/sliver_header.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'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/controller.dart';
@ -324,16 +326,25 @@ class _VideoDetailPageState extends State<VideoDetailPage>
return CustomScrollView( return CustomScrollView(
key: const PageStorageKey<String>('简介'), key: const PageStorageKey<String>('简介'),
slivers: <Widget>[ slivers: <Widget>[
const VideoIntroPanel(), if (videoDetailController.videoType ==
SliverPersistentHeader( SearchType.video) ...[
floating: true, const VideoIntroPanel(),
pinned: true, ] else if (videoDetailController.videoType ==
delegate: SliverHeaderDelegate( SearchType.media_bangumi) ...[
height: 50, const BangumiIntroPanel()
child: ],
const MenuRow(loadingStatus: false), if (videoDetailController.videoType ==
SearchType.video) ...[
SliverPersistentHeader(
floating: true,
pinned: true,
delegate: SliverHeaderDelegate(
height: 50,
child:
const MenuRow(loadingStatus: false),
),
), ),
), ],
const RelatedVideoPanel(), const RelatedVideoPanel(),
], ],
); );

View File

@ -1,16 +1,16 @@
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:auto_orientation/auto_orientation.dart'; // import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
//横屏 //横屏
/// 低版本xcode不支持auto_orientation /// 低版本xcode不支持auto_orientation
Future<void> landScape() async { // Future<void> landScape() async {
if (Platform.isAndroid || Platform.isIOS) { // if (Platform.isAndroid || Platform.isIOS) {
await AutoOrientation.landscapeAutoMode(forceSensor: true); // await AutoOrientation.landscapeAutoMode(forceSensor: true);
} // }
} // }
//竖屏 //竖屏
Future<void> verticalScreen() async { Future<void> verticalScreen() async {

View File

@ -153,7 +153,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
/// 进入全屏 /// 进入全屏
await enterFullScreen(); await enterFullScreen();
// 横屏 // 横屏
await landScape(); // await landScape();
} else { } else {
// 竖屏 // 竖屏
await verticalScreen(); await verticalScreen();

View File

@ -49,14 +49,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
auto_orientation:
dependency: "direct main"
description:
name: auto_orientation
sha256: cd56bb59b36fa54cc28ee254bc600524f022a4862f31d5ab20abd7bb1c54e678
url: "https://pub.dev"
source: hosted
version: "2.3.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View File

@ -107,7 +107,7 @@ dependencies:
universal_platform: ^1.0.0+1 universal_platform: ^1.0.0+1
# 进度条 # 进度条
audio_video_progress_bar: ^1.0.1 audio_video_progress_bar: ^1.0.1
auto_orientation: ^2.3.1 # auto_orientation: ^2.3.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: