feat: 稍后再看&收藏夹播放全部

This commit is contained in:
guozhigq
2024-09-21 15:14:38 +08:00
parent 3088fd599b
commit 3dcce42309
11 changed files with 859 additions and 18 deletions

View File

@ -1,4 +1,9 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:html/parser.dart';
import 'package:pilipala/models/video/later.dart';
import '../common/constants.dart';
import '../models/model_hot_video_item.dart';
import '../models/user/fav_detail.dart';
@ -430,4 +435,107 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// 稍后再看播放全部
// static Future toViewPlayAll({required int oid, required String bvid}) async {
// var res = await Request().get(
// Api.watchLaterHtml,
// data: {
// 'oid': oid,
// 'bvid': bvid,
// },
// );
// String scriptContent =
// extractScriptContents(parse(res.data).body!.outerHtml)[0];
// int startIndex = scriptContent.indexOf('{');
// int endIndex = scriptContent.lastIndexOf('};');
// String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// // 解析JSON字符串为Map
// Map<String, dynamic> jsonData = json.decode(jsonContent);
// // 输出解析后的数据
// return {
// 'status': true,
// 'data': jsonData['resourceList']
// .map((e) => MediaVideoItemModel.fromJson(e))
// .toList()
// };
// }
static List<String> extractScriptContents(String htmlContent) {
RegExp scriptRegExp = RegExp(r'<script>([\s\S]*?)<\/script>');
Iterable<Match> matches = scriptRegExp.allMatches(htmlContent);
List<String> scriptContents = [];
for (Match match in matches) {
String scriptContent = match.group(1)!;
scriptContents.add(scriptContent);
}
return scriptContents;
}
// 稍后再看列表
static Future getMediaList({
required int type,
required int bizId,
required int ps,
int? oid,
}) async {
var res = await Request().get(
Api.mediaList,
data: {
'mobi_app': 'web',
'type': type,
'biz_id': bizId,
'oid': oid ?? '',
'otype': 2,
'ps': ps,
'direction': false,
'desc': true,
'sort_field': 1,
'tid': 0,
'with_current': false,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['media_list'] != null
? res.data['data']['media_list']
.map<MediaVideoItemModel>(
(e) => MediaVideoItemModel.fromJson(e))
.toList()
: []
};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 解析收藏夹视频
static Future parseFavVideo({
required int mediaId,
required int oid,
required String bvid,
}) async {
var res = await Request().get(
'https://www.bilibili.com/list/ml$mediaId',
data: {
'oid': mediaId,
'bvid': bvid,
},
);
String scriptContent =
extractScriptContents(parse(res.data).body!.outerHtml)[0];
int startIndex = scriptContent.indexOf('{');
int endIndex = scriptContent.lastIndexOf('};');
String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
// 解析JSON字符串为Map
Map<String, dynamic> jsonData = json.decode(jsonContent);
log('jsonData;${jsonData["resourceList"].first}');
return {
'status': true,
'data': jsonData['resourceList']
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
.toList()
};
}
}

270
lib/models/video/later.dart Normal file
View File

@ -0,0 +1,270 @@
class MediaVideoItemModel {
MediaVideoItemModel({
this.id,
this.offset,
this.index,
this.intro,
this.attr,
this.tid,
this.copyRight,
this.cntInfo,
this.cover,
this.duration,
this.pubtime,
this.likeState,
this.favState,
this.page,
this.pages,
this.title,
this.type,
this.upper,
this.link,
this.bvId,
this.shortLink,
this.rights,
this.elecInfo,
this.coin,
this.progressPercent,
this.badge,
this.forbidFav,
this.moreType,
this.businessOid,
});
int? id;
int? offset;
int? index;
String? intro;
int? attr;
int? tid;
int? copyRight;
Map? cntInfo;
String? cover;
int? duration;
int? pubtime;
int? likeState;
int? favState;
int? page;
List<Page>? pages;
String? title;
int? type;
Upper? upper;
String? link;
String? bvId;
String? shortLink;
Rights? rights;
dynamic elecInfo;
Coin? coin;
double? progressPercent;
dynamic badge;
bool? forbidFav;
int? moreType;
int? businessOid;
factory MediaVideoItemModel.fromJson(Map<String, dynamic> json) =>
MediaVideoItemModel(
id: json["id"],
offset: json["offset"],
index: json["index"],
intro: json["intro"],
attr: json["attr"],
tid: json["tid"],
copyRight: json["copy_right"],
cntInfo: json["cnt_info"],
cover: json["cover"],
duration: json["duration"],
pubtime: json["pubtime"],
likeState: json["like_state"],
favState: json["fav_state"],
page: json["page"],
// json["pages"] 可能为null
pages: json["pages"] == null
? []
: List<Page>.from(json["pages"].map((x) => Page.fromJson(x))),
title: json["title"],
type: json["type"],
upper: Upper.fromJson(json["upper"]),
link: json["link"],
bvId: json["bv_id"],
shortLink: json["short_link"],
rights: Rights.fromJson(json["rights"]),
elecInfo: json["elec_info"],
coin: Coin.fromJson(json["coin"]),
progressPercent: json["progress_percent"].toDouble(),
badge: json["badge"],
forbidFav: json["forbid_fav"],
moreType: json["more_type"],
businessOid: json["business_oid"],
);
}
class Coin {
Coin({
this.maxNum,
this.coinNumber,
});
int? maxNum;
int? coinNumber;
factory Coin.fromJson(Map<String, dynamic> json) => Coin(
maxNum: json["max_num"],
coinNumber: json["coin_number"],
);
}
class Page {
Page({
this.id,
this.title,
this.intro,
this.duration,
this.link,
this.page,
this.metas,
this.from,
this.dimension,
});
int? id;
String? title;
String? intro;
int? duration;
String? link;
int? page;
List<Meta>? metas;
String? from;
Dimension? dimension;
factory Page.fromJson(Map<String, dynamic> json) => Page(
id: json["id"],
title: json["title"],
intro: json["intro"],
duration: json["duration"],
link: json["link"],
page: json["page"],
metas: List<Meta>.from(json["metas"].map((x) => Meta.fromJson(x))),
from: json["from"],
dimension: Dimension.fromJson(json["dimension"]),
);
}
class Dimension {
Dimension({
this.width,
this.height,
this.rotate,
});
int? width;
int? height;
int? rotate;
factory Dimension.fromJson(Map<String, dynamic> json) => Dimension(
width: json["width"],
height: json["height"],
rotate: json["rotate"],
);
}
class Meta {
Meta({
this.quality,
this.size,
});
int? quality;
int? size;
factory Meta.fromJson(Map<String, dynamic> json) => Meta(
quality: json["quality"],
size: json["size"],
);
}
class Rights {
Rights({
this.bp,
this.elec,
this.download,
this.movie,
this.pay,
this.ugcPay,
this.hd5,
this.noReprint,
this.autoplay,
this.noBackground,
});
int? bp;
int? elec;
int? download;
int? movie;
int? pay;
int? ugcPay;
int? hd5;
int? noReprint;
int? autoplay;
int? noBackground;
factory Rights.fromJson(Map<String, dynamic> json) => Rights(
bp: json["bp"],
elec: json["elec"],
download: json["download"],
movie: json["movie"],
pay: json["pay"],
ugcPay: json["ugc_pay"],
hd5: json["hd5"],
noReprint: json["no_reprint"],
autoplay: json["autoplay"],
noBackground: json["no_background"],
);
}
class Upper {
Upper({
this.mid,
this.name,
this.face,
this.followed,
this.fans,
this.vipType,
this.vipStatue,
this.vipDueDate,
this.vipPayType,
this.officialRole,
this.officialTitle,
this.officialDesc,
this.displayName,
});
int? mid;
String? name;
String? face;
int? followed;
int? fans;
int? vipType;
int? vipStatue;
int? vipDueDate;
int? vipPayType;
int? officialRole;
String? officialTitle;
String? officialDesc;
String? displayName;
factory Upper.fromJson(Map<String, dynamic> json) => Upper(
mid: json["mid"],
name: json["name"],
face: json["face"],
followed: json["followed"],
fans: json["fans"],
vipType: json["vip_type"],
vipStatue: json["vip_statue"],
vipDueDate: json["vip_due_date"],
vipPayType: json["vip_pay_type"],
officialRole: json["official_role"],
officialTitle: json["official_title"],
officialDesc: json["official_desc"],
displayName: json["display_name"],
);
}

View File

@ -6,17 +6,17 @@ import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/user/fav_detail.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/utils/utils.dart';
class FavDetailController extends GetxController {
FavFolderItemData? item;
Rx<FavDetailData> favDetailData = FavDetailData().obs;
int? mediaId;
late String heroTag;
int currentPage = 1;
bool isLoadingMore = false;
RxMap favInfo = {}.obs;
RxList favList = [].obs;
RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
RxString loadingText = '加载中...'.obs;
RxInt mediaCount = 0.obs;
late String isOwner;

View File

@ -260,6 +260,15 @@ class _FavDetailPageState extends State<FavDetailPage> {
)
],
),
floatingActionButton: Obx(
() => _favDetailController.mediaCount > 0
? FloatingActionButton.extended(
onPressed: _favDetailController.toViewPlayAll,
label: const Text('播放全部'),
icon: const Icon(Icons.playlist_play),
)
: const SizedBox(),
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class LaterController extends GetxController {
final ScrollController scrollController = ScrollController();
@ -48,7 +49,7 @@ class LaterController extends GetxController {
aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
onPressed: SmartDialog.dismiss,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
@ -87,7 +88,7 @@ class LaterController extends GetxController {
content: const Text('确定要清空你的稍后再看列表吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
onPressed: SmartDialog.dismiss,
child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
@ -109,4 +110,19 @@ class LaterController extends GetxController {
},
);
}
// 稍后再看播放全部
Future toViewPlayAll() async {
final HotVideoItemModel firstItem = laterList.first;
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
Get.toNamed(
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
arguments: {
'videoItem': firstItem,
'heroTag': heroTag,
'sourceType': 'watchLater',
'count': laterList.length,
},
);
}
}

View File

@ -128,6 +128,15 @@ class _LaterPageState extends State<LaterPage> {
)
],
),
floatingActionButton: Obx(
() => _laterController.laterList.isNotEmpty
? FloatingActionButton.extended(
onPressed: _laterController.toViewPlayAll,
label: const Text('播放全部'),
icon: const Icon(Icons.playlist_play),
)
: const SizedBox(),
),
);
}
}

View File

@ -7,9 +7,11 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/video/later.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video/reply/item.dart';
@ -24,7 +26,10 @@ import '../../../models/video/subTitile/content.dart';
import '../../../http/danmaku.dart';
import '../../../plugin/pl_player/models/bottom_control_type.dart';
import '../../../utils/id_utils.dart';
import 'introduction/controller.dart';
import 'reply/controller.dart';
import 'widgets/header_control.dart';
import 'widgets/watch_later_list.dart';
class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
@ -37,9 +42,10 @@ class VideoDetailController extends GetxController
Map videoItem = {};
// 视频类型 默认投稿视频
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
// 页面来源 稍后再看 收藏夹
RxString sourceType = 'normal'.obs;
/// tabs相关配置
int tabInitialIndex = 0;
late TabController tabCtr;
RxList<String> tabs = <String>['简介', '评论'].obs;
@ -110,6 +116,9 @@ class VideoDetailController extends GetxController
RxDouble sheetHeight = 0.0.obs;
RxString archiveSourceType = 'dash'.obs;
ScrollController? replyScrillController;
List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];
RxBool isWatchLaterVisible = false.obs;
RxString watchLaterTitle = ''.obs;
@override
void onInit() {
@ -119,9 +128,7 @@ class VideoDetailController extends GetxController
if (argMap.containsKey('videoItem')) {
var args = argMap['videoItem'];
updateCover(args.pic);
}
if (argMap.containsKey('pic')) {
} else if (argMap.containsKey('pic')) {
updateCover(argMap['pic']);
}
@ -160,6 +167,21 @@ class VideoDetailController extends GetxController
bvid: bvid,
videoType: videoType,
);
sourceType.value = argMap['sourceType'] ?? 'normal';
isWatchLaterVisible.value =
sourceType.value == 'watchLater' || sourceType.value == 'fav';
if (sourceType.value == 'watchLater') {
watchLaterTitle.value = '稍后再看';
fetchMediaList();
}
if (sourceType.value == 'fav') {
watchLaterTitle.value = argMap['favTitle'];
queryFavVideoList();
}
tabCtr.addListener(() {
onTabChanged();
});
}
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
@ -561,4 +583,101 @@ class VideoDetailController extends GetxController
duration: const Duration(milliseconds: 300), curve: Curves.ease);
}
}
void toggeleWatchLaterVisible(bool val) {
if (sourceType.value == 'watchLater' || sourceType.value == 'fav') {
isWatchLaterVisible.value = !isWatchLaterVisible.value;
}
}
// 获取稍后再看列表
Future fetchMediaList() async {
final Map argMap = Get.arguments;
var count = argMap['count'];
var res = await UserHttp.getMediaList(
type: 2,
bizId: userInfo.mid,
ps: count,
);
if (res['status']) {
mediaList = res['data'].reversed.toList();
} else {
SmartDialog.showToast(res['msg']);
}
}
// 稍后再看面板展开
showMediaListPanel() {
replyReplyBottomSheetCtr =
scaffoldKey.currentState?.showBottomSheet((BuildContext context) {
return MediaListPanel(
sheetHeight: sheetHeight.value,
mediaList: mediaList,
changeMediaList: changeMediaList,
panelTitle: watchLaterTitle.value,
bvid: bvid,
mediaId: Get.arguments['mediaId'],
hasMore: mediaList.length != Get.arguments['count'],
);
});
replyReplyBottomSheetCtr?.closed.then((value) {
isWatchLaterVisible.value = true;
});
}
// 切换稍后再看
Future changeMediaList(bvidVal, cidVal, aidVal, coverVal) async {
final VideoIntroController videoIntroCtr =
Get.find<VideoIntroController>(tag: heroTag);
bvid = bvidVal;
oid.value = aidVal ?? IdUtils.bv2av(bvid);
cid.value = cidVal;
danmakuCid.value = cidVal;
cover.value = coverVal;
queryVideoUrl();
clearSubtitleContent();
await getSubtitle();
setSubtitleContent();
// 重新请求评论
try {
/// 未渲染回复组件时可能异常
final VideoReplyController videoReplyCtr =
Get.find<VideoReplyController>(tag: heroTag);
videoReplyCtr.aid = aidVal;
videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {}
videoIntroCtr.lastPlayCid.value = cidVal;
videoIntroCtr.bvid = bvidVal;
replyReplyBottomSheetCtr!.close();
await videoIntroCtr.queryVideoIntro();
}
// 获取收藏夹视频列表
Future queryFavVideoList() async {
final Map argMap = Get.arguments;
var mediaId = argMap['mediaId'];
var oid = argMap['oid'];
var res = await UserHttp.parseFavVideo(
mediaId: mediaId,
oid: oid,
bvid: bvid,
);
if (res['status']) {
mediaList = res['data'];
}
}
// 监听tabBarView切换
void onTabChanged() {
isWatchLaterVisible.value = tabCtr.index == 0;
}
@override
void onClose() {
super.onClose();
plPlayerController.dispose();
tabCtr.removeListener(() {
onTabChanged();
});
}
}

View File

@ -33,7 +33,7 @@ class VideoIntroController extends GetxController {
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
// up主粉丝数
Map userStat = {'follower': '-'};
RxInt follower = 0.obs;
// 是否点赞
RxBool hasLike = false.obs;
// 是否投币
@ -115,7 +115,7 @@ class VideoIntroController extends GetxController {
Future queryUserStat() async {
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
if (result['status']) {
userStat = result['data'];
follower.value = result['data']['follower'];
}
}

View File

@ -144,7 +144,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
final Box<dynamic> setting = GStrorage.setting;
late double sheetHeight;
late final dynamic owner;
late final dynamic follower;
late int mid;
late String memberHeroTag;
late bool enableAi;
@ -177,7 +176,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
sheetHeight = localCache.get('sheetHeight');
owner = widget.videoDetail!.owner;
follower = Utils.numFormat(videoIntroController.userStat['follower']);
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
_expandableCtr = ExpandableController(initialExpanded: false);
@ -470,13 +468,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Text(owner.name, style: const TextStyle(fontSize: 13)),
Text(widget.videoDetail!.owner!.name!,
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
Obx(
() => Text(
Utils.numFormat(videoIntroController.follower.value),
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
),
const Spacer(),

View File

@ -68,6 +68,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late final AppLifecycleListener _lifecycleListener;
late double statusHeight;
// 稍后再看控制器
// late AnimationController _laterCtr;
// late Animation<Offset> _laterOffsetAni;
@override
void initState() {
super.initState();
@ -104,6 +108,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
}
WidgetsBinding.instance.addObserver(this);
lifecycleListener();
// watchLaterControllerInit();
}
// 获取视频资源,初始化播放器
@ -211,6 +216,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
vdCtr.bottomList.removeAt(3);
}
}
vdCtr.toggeleWatchLaterVisible(!isFullScreen);
});
}
@ -236,6 +242,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
appbarStream.close();
WidgetsBinding.instance.removeObserver(this);
_lifecycleListener.dispose();
// _laterCtr.dispose();
// _laterOffsetAni.removeListener(() {});
super.dispose();
}
@ -482,6 +490,21 @@ class _VideoDetailPageState extends State<VideoDetailPage>
);
}
/// 稍后再看控制器初始化
// void watchLaterControllerInit() {
// _laterCtr = AnimationController(
// duration: const Duration(milliseconds: 300),
// vsync: this,
// );
// _laterOffsetAni = Tween<Offset>(
// begin: const Offset(0.0, 1.0),
// end: Offset.zero,
// ).animate(CurvedAnimation(
// parent: _laterCtr,
// curve: Curves.easeInOut,
// ));
// }
@override
Widget build(BuildContext context) {
final sizeContext = MediaQuery.sizeOf(context);
@ -595,6 +618,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
child: AppBar(
backgroundColor: Colors.black,
elevation: 0,
scrolledUnderElevation: 0,
),
),
body: ExtendedNestedScrollView(
@ -757,6 +781,62 @@ class _VideoDetailPageState extends State<VideoDetailPage>
null,
);
}),
),
/// 稍后再看列表
Obx(
() => Visibility(
visible: vdCtr.sourceType.value == 'watchLater' ||
vdCtr.sourceType.value == 'fav',
child: AnimatedPositioned(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
left: 12,
bottom: vdCtr.isWatchLaterVisible.value
? MediaQuery.of(context).padding.bottom + 12
: -100,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
vdCtr.toggeleWatchLaterVisible(
!vdCtr.isWatchLaterVisible.value);
vdCtr.showMediaListPanel();
},
borderRadius: const BorderRadius.all(Radius.circular(14)),
child: Container(
width: Get.width - 24,
height: 54,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.95),
borderRadius:
const BorderRadius.all(Radius.circular(14)),
),
child: Row(children: [
const Icon(Icons.playlist_play, size: 24),
const SizedBox(width: 10),
Text(
vdCtr.watchLaterTitle.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer,
fontWeight: FontWeight.bold,
letterSpacing: 0.2,
),
),
const Spacer(),
const Icon(Icons.keyboard_arrow_up_rounded, size: 26),
]),
),
),
),
),
),
)
],
),

View File

@ -0,0 +1,229 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.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/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/video/later.dart';
import 'package:pilipala/utils/utils.dart';
class MediaListPanel extends StatefulWidget {
const MediaListPanel({
this.sheetHeight,
required this.mediaList,
this.changeMediaList,
this.panelTitle,
this.bvid,
this.mediaId,
this.hasMore = false,
super.key,
});
final double? sheetHeight;
final List<MediaVideoItemModel> mediaList;
final Function? changeMediaList;
final String? panelTitle;
final String? bvid;
final int? mediaId;
final bool hasMore;
@override
State<MediaListPanel> createState() => _MediaListPanelState();
}
class _MediaListPanelState extends State<MediaListPanel> {
RxList<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[].obs;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
mediaList.value = widget.mediaList;
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
if (widget.hasMore) {
EasyThrottle.throttle(
'queryFollowDynamic', const Duration(seconds: 1), () {
loadMore();
});
}
}
});
}
void loadMore() async {
var res = await UserHttp.getMediaList(
type: 3,
bizId: widget.mediaId!,
ps: 20,
oid: mediaList.last.id,
);
if (res['status']) {
if (res['data'].isNotEmpty) {
mediaList.addAll(res['data']);
}
} else {
SmartDialog.showToast(res['msg']);
}
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
centerTitle: false,
title: Text(
widget.panelTitle ?? '稍后再看',
style: Theme.of(context).textTheme.titleSmall,
),
actions: [
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () {
Navigator.pop(context);
},
),
const SizedBox(width: 14),
],
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surface,
child: Obx(
() => ListView.builder(
controller: _scrollController,
itemCount: mediaList.length,
itemBuilder: ((context, index) {
var item = mediaList[index];
return InkWell(
onTap: () async {
String bvid = item.bvId!;
int? aid = item.id;
String cover = item.cover ?? '';
final int cid =
await SearchHttp.ab2c(aid: aid, bvid: bvid);
widget.changeMediaList?.call(bvid, cid, aid, cover);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8),
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
const double width = 120;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (BuildContext context,
BoxConstraints boxConstraints) {
final double maxWidth =
boxConstraints.maxWidth;
final double maxHeight =
boxConstraints.maxHeight;
return Stack(
children: [
NetworkImgLayer(
src: item.cover ?? '',
width: maxWidth,
height: maxHeight,
),
PBadge(
text: Utils.timeFormat(
item.duration!),
right: 6.0,
bottom: 6.0,
type: 'gray',
),
],
);
},
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(
10, 0, 6, 0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
item.title as String,
textAlign: TextAlign.start,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w500,
color: item.bvId == widget.bvid
? Theme.of(context)
.colorScheme
.primary
: null,
),
),
const Spacer(),
Text(
item.upper?.name as String,
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
color: Theme.of(context)
.colorScheme
.outline,
),
),
const SizedBox(height: 2),
Row(
children: [
StatView(
view: item.cntInfo!['play']
as int),
const SizedBox(width: 8),
StatDanMu(
danmu:
item.cntInfo!['danmaku']
as int),
],
),
],
),
),
)
],
),
);
},
),
),
);
}),
),
),
),
),
],
),
);
}
}