diff --git a/lib/http/api.dart b/lib/http/api.dart index 5cd2c4a8d..d12b1f982 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -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'; diff --git a/lib/http/search.dart b/lib/http/search.dart index 945f9e344..65c2219fa 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -137,7 +137,7 @@ class SearchHttp { } } - static Future ab2c({int? aid, String? bvid}) async { + static Future ab2c({dynamic aid, dynamic bvid}) async { Map data = {}; if (aid != null) { data['aid'] = aid; diff --git a/lib/http/user.dart b/lib/http/user.dart index 22421766c..17abef7d4 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -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, diff --git a/lib/http/video.dart b/lib/http/video.dart index 7f888013a..928a638c4 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -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: { diff --git a/lib/pages/member/new/content/member_contribute/content/video/member_video.dart b/lib/pages/member/new/content/member_contribute/content/video/member_video.dart index e46370677..be76e0410 100644 --- a/lib/pages/member/new/content/member_contribute/content/video/member_video.dart +++ b/lib/pages/member/new/content/member_contribute/content/video/member_video.dart @@ -99,16 +99,13 @@ class _MemberVideoState extends State 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}'), + ), ), ), ], diff --git a/lib/pages/member/new/content/member_contribute/content/video/member_video_ctr.dart b/lib/pages/member/new/content/member_contribute/content/video/member_video_ctr.dart index bb79af5e0..f328622e1 100644 --- a/lib/pages/member/new/content/member_contribute/content/video/member_video_ctr.dart +++ b/lib/pages/member/new/content/member_contribute/content/video/member_video_ctr.dart @@ -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 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; diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 81b93dc17..025fa421b 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -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(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'], + ); + } + } } diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 6d8a3ea27..7b1e2cdad 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -556,6 +556,7 @@ class VideoIntroController extends GetxController // 重新获取视频资源 final VideoDetailController videoDetailCtr = Get.find(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(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 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 sections = ugcSeason.sections!; diff --git a/lib/pages/video/detail/widgets/watch_later_list.dart b/lib/pages/video/detail/widgets/watch_later_list.dart index 596542f13..67e39cde1 100644 --- a/lib/pages/video/detail/widgets/watch_later_list.dart +++ b/lib/pages/video/detail/widgets/watch_later_list.dart @@ -24,6 +24,7 @@ class MediaListPanel extends StatefulWidget { required this.count, required this.desc, required this.onReverse, + required this.loadPrevious, }); final List mediaList; @@ -34,6 +35,7 @@ class MediaListPanel extends StatefulWidget { final int? count; final bool? desc; final VoidCallback onReverse; + final Function? loadPrevious; @override State createState() => _MediaListPanelState(); @@ -89,152 +91,152 @@ class _MediaListPanelState extends State { 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: [ - 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: [ + 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, + ), + ], + ), + ], + ), + ), + ) + ], + ), + ); + }, + ), + ), + ); + }), + ), + ); }