mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: medialist: continue playing #70
Closes #70 Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -247,6 +247,8 @@ class Api {
|
||||
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md
|
||||
static const String heartBeat = '/x/click-interface/web/heartbeat';
|
||||
|
||||
static const String mediaListHistory = '/x/v1/medialist/history';
|
||||
|
||||
// 查询视频分P列表 (avid/bvid转cid)
|
||||
static const String ab2c = '/x/player/pagelist';
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class SearchHttp {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<int> ab2c({int? aid, String? bvid}) async {
|
||||
static Future<int> ab2c({dynamic aid, dynamic bvid}) async {
|
||||
Map<String, dynamic> data = {};
|
||||
if (aid != null) {
|
||||
data['aid'] = aid;
|
||||
|
||||
@@ -514,11 +514,12 @@ class UserHttp {
|
||||
required dynamic type,
|
||||
required int bizId,
|
||||
required int ps,
|
||||
int? oid,
|
||||
dynamic oid,
|
||||
int? otype,
|
||||
bool withCurrent = false,
|
||||
bool desc = true,
|
||||
int sortField = 1,
|
||||
dynamic sortField = 1,
|
||||
bool direction = false,
|
||||
}) async {
|
||||
var res = await Request().get(
|
||||
Api.mediaList,
|
||||
@@ -529,7 +530,7 @@ class UserHttp {
|
||||
if (oid != null) 'oid': oid,
|
||||
if (otype != null) 'otype': otype, // video:2 // bangumi: 24
|
||||
'ps': ps,
|
||||
'direction': false,
|
||||
'direction': direction,
|
||||
'desc': desc,
|
||||
'sort_field': sortField,
|
||||
'tid': 0,
|
||||
|
||||
@@ -755,6 +755,19 @@ class VideoHttp {
|
||||
});
|
||||
}
|
||||
|
||||
static Future medialistHistory({
|
||||
required int desc,
|
||||
required dynamic oid,
|
||||
required dynamic upperMid,
|
||||
}) async {
|
||||
await Request().post(Api.mediaListHistory, queryParameters: {
|
||||
'desc': desc,
|
||||
'oid': oid,
|
||||
'upper_mid': upperMid,
|
||||
'csrf': await Request.getCsrf(),
|
||||
});
|
||||
}
|
||||
|
||||
// 添加追番
|
||||
static Future bangumiAdd({int? seasonId}) async {
|
||||
var res = await Request().post(Api.bangumiAdd, queryParameters: {
|
||||
|
||||
@@ -99,16 +99,13 @@ class _MemberVideoState extends State<MemberVideo>
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
label: Text(
|
||||
'播放全部',
|
||||
_controller.episodicButton?.text ?? '播放全部',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
), // TODO: continue playing
|
||||
// label: Text(
|
||||
// '${_controller.episodicButton?.text}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:PiliPalaX/http/loading_state.dart';
|
||||
import 'package:PiliPalaX/http/member.dart';
|
||||
import 'package:PiliPalaX/http/search.dart';
|
||||
import 'package:PiliPalaX/models/space_archive/data.dart';
|
||||
import 'package:PiliPalaX/models/space_archive/episodic_button.dart';
|
||||
import 'package:PiliPalaX/models/space_archive/item.dart';
|
||||
@@ -93,9 +94,42 @@ class MemberVideoCtr extends CommonController {
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
void toViewPlayAll() {
|
||||
void toViewPlayAll() async {
|
||||
if (loadingState.value is Success) {
|
||||
List<Item> list = (loadingState.value as Success).response;
|
||||
|
||||
if (episodicButton?.text == '继续播放') {
|
||||
dynamic oid = RegExp(r'oid=([\d]+)')
|
||||
.firstMatch('${episodicButton?.uri}')
|
||||
?.group(1);
|
||||
dynamic bvid = IdUtils.av2bv(int.tryParse(oid) ?? 0);
|
||||
dynamic cid = await SearchHttp.ab2c(aid: oid, bvid: bvid);
|
||||
Get.toNamed(
|
||||
'/video?bvid=$bvid&cid=$cid',
|
||||
arguments: {
|
||||
'heroTag': Utils.makeHeroTag(oid),
|
||||
'sourceType': 'archive',
|
||||
'mediaId': seasonId ?? seriesId ?? mid,
|
||||
'oid': oid,
|
||||
'favTitle': '$username: ${title ?? episodicButton?.text ?? '播放全部'}',
|
||||
if (seriesId == null) 'count': count.value,
|
||||
if (seasonId != null || seriesId != null)
|
||||
'mediaType': RegExp(r'page_type=([\d]+)')
|
||||
.firstMatch('${episodicButton?.uri}')
|
||||
?.group(1),
|
||||
'desc': RegExp(r'desc=([\d]+)')
|
||||
.firstMatch('${episodicButton?.uri}')
|
||||
?.group(1) ==
|
||||
'1',
|
||||
'sortField': RegExp(r'sort_field=([\d]+)')
|
||||
.firstMatch('${episodicButton?.uri}')
|
||||
?.group(1),
|
||||
'isContinuePlaying': true,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (Item element in list) {
|
||||
if (element.firstCid == null) {
|
||||
continue;
|
||||
@@ -103,7 +137,6 @@ class MemberVideoCtr extends CommonController {
|
||||
if (element.bvid != list.first.bvid) {
|
||||
SmartDialog.showToast('已跳过不支持播放的视频');
|
||||
}
|
||||
final String heroTag = Utils.makeHeroTag(element.bvid);
|
||||
bool desc = seasonId != null ? false : true;
|
||||
desc = (seasonId != null || seriesId != null) &&
|
||||
(type == ContributeType.video
|
||||
@@ -115,10 +148,10 @@ class MemberVideoCtr extends CommonController {
|
||||
'/video?bvid=${element.bvid}&cid=${element.firstCid}',
|
||||
arguments: {
|
||||
'videoItem': element,
|
||||
'heroTag': heroTag,
|
||||
'heroTag': Utils.makeHeroTag(element.bvid),
|
||||
'sourceType': 'archive',
|
||||
'mediaId': seasonId ?? seriesId ?? mid,
|
||||
'oid': IdUtils.bv2av(element.bvid!), // TODO: continue playing
|
||||
'oid': IdUtils.bv2av(element.bvid!),
|
||||
'favTitle':
|
||||
'$username: ${title ?? episodicButton?.text ?? '播放全部'}',
|
||||
if (seriesId == null) 'count': count.value,
|
||||
@@ -127,10 +160,8 @@ class MemberVideoCtr extends CommonController {
|
||||
.firstMatch('${episodicButton?.uri}')
|
||||
?.group(1),
|
||||
'desc': desc,
|
||||
'sortField':
|
||||
type == ContributeType.video && order.value == 'click'
|
||||
? 2
|
||||
: 1,
|
||||
if (type == ContributeType.video)
|
||||
'sortField': order.value == 'click' ? 2 : 1,
|
||||
},
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -329,7 +329,10 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
void getMediaList([bool isReverse = false]) async {
|
||||
void getMediaList({
|
||||
bool isReverse = false,
|
||||
bool isLoadPrevious = false,
|
||||
}) async {
|
||||
if (isReverse.not &&
|
||||
Get.arguments['count'] != null &&
|
||||
mediaList.length >= Get.arguments['count']) {
|
||||
@@ -339,10 +342,31 @@ class VideoDetailController extends GetxController
|
||||
type: Get.arguments['mediaType'] ?? _mediaType,
|
||||
bizId: Get.arguments['mediaId'] ?? -1,
|
||||
ps: 20,
|
||||
oid: isReverse || mediaList.isEmpty ? null : mediaList.last.id,
|
||||
otype: isReverse || mediaList.isEmpty ? null : mediaList.last.type,
|
||||
direction: isLoadPrevious ? true : false,
|
||||
oid: isReverse
|
||||
? null
|
||||
: mediaList.isEmpty
|
||||
? _mediaType == 1 &&
|
||||
Get.arguments['mediaType'] == null // member archive
|
||||
? Get.arguments['oid']
|
||||
: null
|
||||
: isLoadPrevious
|
||||
? mediaList.first.id
|
||||
: mediaList.last.id,
|
||||
otype: isReverse
|
||||
? null
|
||||
: mediaList.isEmpty
|
||||
? null
|
||||
: isLoadPrevious
|
||||
? mediaList.first.type
|
||||
: mediaList.last.type,
|
||||
desc: _mediaDesc,
|
||||
sortField: Get.arguments['sortField'] ?? 1,
|
||||
withCurrent: mediaList.isEmpty &&
|
||||
_mediaType == 1 &&
|
||||
Get.arguments['mediaType'] == null
|
||||
? true // init && member archive
|
||||
: false,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (res['data'].isNotEmpty) {
|
||||
@@ -364,6 +388,8 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
} else if (isLoadPrevious) {
|
||||
mediaList.insertAll(0, res['data']);
|
||||
} else {
|
||||
mediaList.addAll(res['data']);
|
||||
}
|
||||
@@ -379,7 +405,12 @@ class VideoDetailController extends GetxController
|
||||
childKey.currentState?.showBottomSheet(
|
||||
(context) => MediaListPanel(
|
||||
mediaList: mediaList,
|
||||
changeMediaList: changeMediaList,
|
||||
changeMediaList: (bvid, cid, aid, cover) {
|
||||
try {
|
||||
Get.find<VideoIntroController>(tag: heroTag)
|
||||
.changeSeasonOrbangu(null, bvid, cid, aid, cover);
|
||||
} catch (_) {}
|
||||
},
|
||||
panelTitle: watchLaterTitle,
|
||||
getBvId: () => bvid,
|
||||
count: Get.arguments['count'],
|
||||
@@ -387,8 +418,13 @@ class VideoDetailController extends GetxController
|
||||
desc: _mediaDesc,
|
||||
onReverse: () {
|
||||
_mediaDesc = !_mediaDesc;
|
||||
getMediaList(true);
|
||||
getMediaList(isReverse: true);
|
||||
},
|
||||
loadPrevious: Get.arguments['isContinuePlaying'] == true
|
||||
? () {
|
||||
getMediaList(isLoadPrevious: true);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1853,4 +1889,14 @@ class VideoDetailController extends GetxController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateMediaListHistory(aid) {
|
||||
if (Get.arguments['sortField'] != null) {
|
||||
VideoHttp.medialistHistory(
|
||||
desc: _mediaDesc ? 1 : 0,
|
||||
oid: aid,
|
||||
upperMid: Get.arguments['mediaId'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,6 +556,7 @@ class VideoIntroController extends GetxController
|
||||
// 重新获取视频资源
|
||||
final VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: heroTag);
|
||||
videoDetailCtr.updateMediaListHistory(aid);
|
||||
videoDetailCtr.vttSubtitlesIndex = null;
|
||||
videoDetailCtr.bvid = bvid;
|
||||
videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid);
|
||||
@@ -673,12 +674,13 @@ class VideoIntroController extends GetxController
|
||||
bool isPages = false;
|
||||
final videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
|
||||
if (videoDetailCtr.isPlayAll) {
|
||||
episodes.addAll(videoDetailCtr.mediaList);
|
||||
} else if ((videoDetail.value.pages?.length ?? 0) > 1) {
|
||||
// part -> playall -> season
|
||||
if ((videoDetail.value.pages?.length ?? 0) > 1) {
|
||||
isPages = true;
|
||||
final List<Part> pages = videoDetail.value.pages!;
|
||||
episodes.addAll(pages);
|
||||
} else if (videoDetailCtr.isPlayAll) {
|
||||
episodes.addAll(videoDetailCtr.mediaList);
|
||||
} else if (videoDetail.value.ugcSeason != null) {
|
||||
final UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
|
||||
final List<SectionItem> sections = ugcSeason.sections!;
|
||||
|
||||
@@ -24,6 +24,7 @@ class MediaListPanel extends StatefulWidget {
|
||||
required this.count,
|
||||
required this.desc,
|
||||
required this.onReverse,
|
||||
required this.loadPrevious,
|
||||
});
|
||||
|
||||
final List<MediaVideoItemModel> mediaList;
|
||||
@@ -34,6 +35,7 @@ class MediaListPanel extends StatefulWidget {
|
||||
final int? count;
|
||||
final bool? desc;
|
||||
final VoidCallback onReverse;
|
||||
final Function? loadPrevious;
|
||||
|
||||
@override
|
||||
State<MediaListPanel> createState() => _MediaListPanelState();
|
||||
@@ -89,152 +91,152 @@ class _MediaListPanelState extends State<MediaListPanel> {
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Obx(
|
||||
() => ScrollablePositionedList.builder(
|
||||
itemScrollController: _scrollController,
|
||||
itemCount: widget.mediaList.length,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 25,
|
||||
),
|
||||
itemBuilder: ((context, index) {
|
||||
var item = widget.mediaList[index];
|
||||
if (index == widget.mediaList.length - 1 &&
|
||||
(widget.count == null ||
|
||||
widget.mediaList.length < widget.count!)) {
|
||||
widget.loadMoreMedia();
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (item.type != 2) {
|
||||
SmartDialog.showToast('不支持播放该类型视频');
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
String bvid = item.bvid!;
|
||||
int? aid = item.id;
|
||||
String cover = item.cover ?? '';
|
||||
final int cid = item.cid ??
|
||||
await SearchHttp.ab2c(aid: aid, bvid: bvid);
|
||||
widget.changeMediaList?.call(bvid, cid, aid, cover);
|
||||
child: widget.loadPrevious != null
|
||||
? RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await widget.loadPrevious!();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, 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:
|
||||
item.bvid == widget.getBvId()
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
color:
|
||||
item.bvid == widget.getBvId()
|
||||
? 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(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
view: item.cntInfo!['play']
|
||||
as int,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
statDanMu(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
danmu: item.cntInfo!['danmaku']
|
||||
as int,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
child: _buildList,
|
||||
)
|
||||
: _buildList,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _buildList => Obx(
|
||||
() => ScrollablePositionedList.builder(
|
||||
itemScrollController: _scrollController,
|
||||
itemCount: widget.mediaList.length,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
itemBuilder: ((context, index) {
|
||||
var item = widget.mediaList[index];
|
||||
if (index == widget.mediaList.length - 1 &&
|
||||
(widget.count == null ||
|
||||
widget.mediaList.length < widget.count!)) {
|
||||
widget.loadMoreMedia();
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (item.type != 2) {
|
||||
SmartDialog.showToast('不支持播放该类型视频');
|
||||
return;
|
||||
}
|
||||
Get.back();
|
||||
String bvid = item.bvid!;
|
||||
int? aid = item.id;
|
||||
String cover = item.cover ?? '';
|
||||
final int cid =
|
||||
item.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: (context, 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: item.bvid == widget.getBvId()
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
color: item.bvid == widget.getBvId()
|
||||
? 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(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
view: item.cntInfo!['play'] as int,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
statDanMu(
|
||||
context: context,
|
||||
theme: 'gray',
|
||||
danmu: item.cntInfo!['danmaku'] as int,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user