Merge branch 'feature-watchLater'
This commit is contained in:
@ -575,4 +575,7 @@ class Api {
|
|||||||
/// 我的关注 - 正在直播
|
/// 我的关注 - 正在直播
|
||||||
static const String getFollowingLive =
|
static const String getFollowingLive =
|
||||||
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
|
'${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following';
|
||||||
|
|
||||||
|
/// 稍后再看&收藏夹视频列表
|
||||||
|
static const String mediaList = '/x/v2/medialist/resource/list';
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
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 '../common/constants.dart';
|
||||||
import '../models/model_hot_video_item.dart';
|
import '../models/model_hot_video_item.dart';
|
||||||
import '../models/user/fav_detail.dart';
|
import '../models/user/fav_detail.dart';
|
||||||
@ -430,4 +435,106 @@ class UserHttp {
|
|||||||
return {'status': false, 'msg': res.data['message']};
|
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);
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': jsonData['resourceList']
|
||||||
|
.map<MediaVideoItemModel>((e) => MediaVideoItemModel.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
270
lib/models/video/later.dart
Normal file
270
lib/models/video/later.dart
Normal 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"],
|
||||||
|
);
|
||||||
|
}
|
@ -6,17 +6,17 @@ import 'package:pilipala/http/video.dart';
|
|||||||
import 'package:pilipala/models/user/fav_detail.dart';
|
import 'package:pilipala/models/user/fav_detail.dart';
|
||||||
import 'package:pilipala/models/user/fav_folder.dart';
|
import 'package:pilipala/models/user/fav_folder.dart';
|
||||||
import 'package:pilipala/pages/fav/index.dart';
|
import 'package:pilipala/pages/fav/index.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
class FavDetailController extends GetxController {
|
class FavDetailController extends GetxController {
|
||||||
FavFolderItemData? item;
|
FavFolderItemData? item;
|
||||||
Rx<FavDetailData> favDetailData = FavDetailData().obs;
|
|
||||||
|
|
||||||
int? mediaId;
|
int? mediaId;
|
||||||
late String heroTag;
|
late String heroTag;
|
||||||
int currentPage = 1;
|
int currentPage = 1;
|
||||||
bool isLoadingMore = false;
|
bool isLoadingMore = false;
|
||||||
RxMap favInfo = {}.obs;
|
RxMap favInfo = {}.obs;
|
||||||
RxList favList = [].obs;
|
RxList<FavDetailItemData> favList = <FavDetailItemData>[].obs;
|
||||||
RxString loadingText = '加载中...'.obs;
|
RxString loadingText = '加载中...'.obs;
|
||||||
RxInt mediaCount = 0.obs;
|
RxInt mediaCount = 0.obs;
|
||||||
late String isOwner;
|
late String isOwner;
|
||||||
@ -128,4 +128,22 @@ class FavDetailController extends GetxController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future toViewPlayAll() async {
|
||||||
|
final FavDetailItemData firstItem = favList.first;
|
||||||
|
final String heroTag = Utils.makeHeroTag(firstItem.bvid);
|
||||||
|
Get.toNamed(
|
||||||
|
'/video?bvid=${firstItem.bvid}&cid=${firstItem.cid}',
|
||||||
|
arguments: {
|
||||||
|
'videoItem': firstItem,
|
||||||
|
'heroTag': heroTag,
|
||||||
|
'sourceType': 'fav',
|
||||||
|
'mediaId': favInfo['id'],
|
||||||
|
'oid': firstItem.id,
|
||||||
|
'favTitle': favInfo['title'],
|
||||||
|
'favInfo': favInfo,
|
||||||
|
'count': favInfo['media_count'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import 'package:pilipala/http/user.dart';
|
|||||||
import 'package:pilipala/models/model_hot_video_item.dart';
|
import 'package:pilipala/models/model_hot_video_item.dart';
|
||||||
import 'package:pilipala/models/user/info.dart';
|
import 'package:pilipala/models/user/info.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
class LaterController extends GetxController {
|
class LaterController extends GetxController {
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
@ -48,7 +49,7 @@ class LaterController extends GetxController {
|
|||||||
aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'),
|
aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => SmartDialog.dismiss(),
|
onPressed: SmartDialog.dismiss,
|
||||||
child: Text(
|
child: Text(
|
||||||
'取消',
|
'取消',
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||||
@ -87,7 +88,7 @@ class LaterController extends GetxController {
|
|||||||
content: const Text('确定要清空你的稍后再看列表吗?'),
|
content: const Text('确定要清空你的稍后再看列表吗?'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => SmartDialog.dismiss(),
|
onPressed: SmartDialog.dismiss,
|
||||||
child: Text(
|
child: Text(
|
||||||
'取消',
|
'取消',
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,11 @@ import 'package:get/get.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||||
import 'package:pilipala/http/constants.dart';
|
import 'package:pilipala/http/constants.dart';
|
||||||
|
import 'package:pilipala/http/user.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/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/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';
|
||||||
@ -24,7 +26,10 @@ import '../../../models/video/subTitile/content.dart';
|
|||||||
import '../../../http/danmaku.dart';
|
import '../../../http/danmaku.dart';
|
||||||
import '../../../plugin/pl_player/models/bottom_control_type.dart';
|
import '../../../plugin/pl_player/models/bottom_control_type.dart';
|
||||||
import '../../../utils/id_utils.dart';
|
import '../../../utils/id_utils.dart';
|
||||||
|
import 'introduction/controller.dart';
|
||||||
|
import 'reply/controller.dart';
|
||||||
import 'widgets/header_control.dart';
|
import 'widgets/header_control.dart';
|
||||||
|
import 'widgets/watch_later_list.dart';
|
||||||
|
|
||||||
class VideoDetailController extends GetxController
|
class VideoDetailController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
@ -37,9 +42,10 @@ class VideoDetailController extends GetxController
|
|||||||
Map videoItem = {};
|
Map videoItem = {};
|
||||||
// 视频类型 默认投稿视频
|
// 视频类型 默认投稿视频
|
||||||
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
|
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
|
||||||
|
// 页面来源 稍后再看 收藏夹
|
||||||
|
RxString sourceType = 'normal'.obs;
|
||||||
|
|
||||||
/// tabs相关配置
|
/// tabs相关配置
|
||||||
int tabInitialIndex = 0;
|
|
||||||
late TabController tabCtr;
|
late TabController tabCtr;
|
||||||
RxList<String> tabs = <String>['简介', '评论'].obs;
|
RxList<String> tabs = <String>['简介', '评论'].obs;
|
||||||
|
|
||||||
@ -110,6 +116,9 @@ class VideoDetailController extends GetxController
|
|||||||
RxDouble sheetHeight = 0.0.obs;
|
RxDouble sheetHeight = 0.0.obs;
|
||||||
RxString archiveSourceType = 'dash'.obs;
|
RxString archiveSourceType = 'dash'.obs;
|
||||||
ScrollController? replyScrillController;
|
ScrollController? replyScrillController;
|
||||||
|
List<MediaVideoItemModel> mediaList = <MediaVideoItemModel>[];
|
||||||
|
RxBool isWatchLaterVisible = false.obs;
|
||||||
|
RxString watchLaterTitle = ''.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -119,9 +128,7 @@ class VideoDetailController extends GetxController
|
|||||||
if (argMap.containsKey('videoItem')) {
|
if (argMap.containsKey('videoItem')) {
|
||||||
var args = argMap['videoItem'];
|
var args = argMap['videoItem'];
|
||||||
updateCover(args.pic);
|
updateCover(args.pic);
|
||||||
}
|
} else if (argMap.containsKey('pic')) {
|
||||||
|
|
||||||
if (argMap.containsKey('pic')) {
|
|
||||||
updateCover(argMap['pic']);
|
updateCover(argMap['pic']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +167,21 @@ class VideoDetailController extends GetxController
|
|||||||
bvid: bvid,
|
bvid: bvid,
|
||||||
videoType: videoType,
|
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) {
|
showReplyReplyPanel(oid, fRpid, firstFloor, currentReply, loadMore) {
|
||||||
@ -561,4 +583,101 @@ class VideoDetailController extends GetxController
|
|||||||
duration: const Duration(milliseconds: 300), curve: Curves.ease);
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ class VideoIntroController extends GetxController {
|
|||||||
// 视频详情 请求返回
|
// 视频详情 请求返回
|
||||||
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
|
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
|
||||||
// up主粉丝数
|
// up主粉丝数
|
||||||
Map userStat = {'follower': '-'};
|
RxInt follower = 0.obs;
|
||||||
// 是否点赞
|
// 是否点赞
|
||||||
RxBool hasLike = false.obs;
|
RxBool hasLike = false.obs;
|
||||||
// 是否投币
|
// 是否投币
|
||||||
@ -115,7 +115,7 @@ class VideoIntroController extends GetxController {
|
|||||||
Future queryUserStat() async {
|
Future queryUserStat() async {
|
||||||
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
|
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
|
||||||
if (result['status']) {
|
if (result['status']) {
|
||||||
userStat = result['data'];
|
follower.value = result['data']['follower'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
final Box<dynamic> setting = GStrorage.setting;
|
final Box<dynamic> setting = GStrorage.setting;
|
||||||
late double sheetHeight;
|
late double sheetHeight;
|
||||||
late final dynamic owner;
|
late final dynamic owner;
|
||||||
late final dynamic follower;
|
|
||||||
late int mid;
|
late int mid;
|
||||||
late String memberHeroTag;
|
late String memberHeroTag;
|
||||||
late bool enableAi;
|
late bool enableAi;
|
||||||
@ -177,7 +176,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
sheetHeight = localCache.get('sheetHeight');
|
sheetHeight = localCache.get('sheetHeight');
|
||||||
|
|
||||||
owner = widget.videoDetail!.owner;
|
owner = widget.videoDetail!.owner;
|
||||||
follower = Utils.numFormat(videoIntroController.userStat['follower']);
|
|
||||||
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
|
enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true);
|
||||||
_expandableCtr = ExpandableController(initialExpanded: false);
|
_expandableCtr = ExpandableController(initialExpanded: false);
|
||||||
|
|
||||||
@ -470,13 +468,16 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|||||||
fadeOutDuration: Duration.zero,
|
fadeOutDuration: Duration.zero,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
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),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Obx(
|
||||||
follower,
|
() => Text(
|
||||||
style: TextStyle(
|
Utils.numFormat(videoIntroController.follower.value),
|
||||||
fontSize: t.textTheme.labelSmall!.fontSize,
|
style: TextStyle(
|
||||||
color: outline,
|
fontSize: t.textTheme.labelSmall!.fontSize,
|
||||||
|
color: outline,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
@ -68,6 +68,10 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
late final AppLifecycleListener _lifecycleListener;
|
late final AppLifecycleListener _lifecycleListener;
|
||||||
late double statusHeight;
|
late double statusHeight;
|
||||||
|
|
||||||
|
// 稍后再看控制器
|
||||||
|
// late AnimationController _laterCtr;
|
||||||
|
// late Animation<Offset> _laterOffsetAni;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -104,6 +108,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
}
|
}
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
lifecycleListener();
|
lifecycleListener();
|
||||||
|
// watchLaterControllerInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取视频资源,初始化播放器
|
// 获取视频资源,初始化播放器
|
||||||
@ -211,6 +216,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
vdCtr.bottomList.removeAt(3);
|
vdCtr.bottomList.removeAt(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
vdCtr.toggeleWatchLaterVisible(!isFullScreen);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,6 +242,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
appbarStream.close();
|
appbarStream.close();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_lifecycleListener.dispose();
|
_lifecycleListener.dispose();
|
||||||
|
// _laterCtr.dispose();
|
||||||
|
// _laterOffsetAni.removeListener(() {});
|
||||||
super.dispose();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sizeContext = MediaQuery.sizeOf(context);
|
final sizeContext = MediaQuery.sizeOf(context);
|
||||||
@ -595,6 +618,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
child: AppBar(
|
child: AppBar(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: ExtendedNestedScrollView(
|
body: ExtendedNestedScrollView(
|
||||||
@ -757,6 +781,62 @@ class _VideoDetailPageState extends State<VideoDetailPage>
|
|||||||
null,
|
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),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
229
lib/pages/video/detail/widgets/watch_later_list.dart
Normal file
229
lib/pages/video/detail/widgets/watch_later_list.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user