From 9da3a538fb440eebbba482a0610a33c0e74a9ae5 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sun, 30 Mar 2025 16:20:40 +0800 Subject: [PATCH] refa: horizontal member page Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/pair.dart | 11 ++ lib/http/api.dart | 3 + lib/http/member.dart | 42 +++++ .../video/detail/introduction/controller.dart | 5 +- lib/pages/video/detail/member/controller.dart | 104 +++++++------ .../detail/member/horizontal_member_page.dart | 109 +++++-------- .../widget/video_card_h_member_video.dart | 145 ++++++++++++++++++ lib/pages/video/detail/view.dart | 6 + lib/pages/video/detail/view_v.dart | 3 + 9 files changed, 310 insertions(+), 118 deletions(-) create mode 100644 lib/pages/video/detail/member/widget/video_card_h_member_video.dart diff --git a/lib/common/widgets/pair.dart b/lib/common/widgets/pair.dart index 0abf2ad9..c7cc9985 100644 --- a/lib/common/widgets/pair.dart +++ b/lib/common/widgets/pair.dart @@ -6,3 +6,14 @@ class Pair { T first; R second; } + +class Triple { + Triple({ + required this.first, + required this.second, + required this.third, + }); + T first; + R second; + S third; +} diff --git a/lib/http/api.dart b/lib/http/api.dart index ad2a0b8a..18519540 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -329,6 +329,9 @@ class Api { static const String spaceArchive = '${HttpString.appBaseUrl}/x/v2/space/archive/cursor'; + static const String spaceStory = + '${HttpString.appBaseUrl}/x/v2/feed/index/space/story/cursor'; + static const String spaceChargingArchive = '${HttpString.appBaseUrl}/x/v2/space/archive/charging'; diff --git a/lib/http/member.dart b/lib/http/member.dart index 9b9fd9dc..60a26295 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -196,6 +196,48 @@ class MemberHttp { } } + static Future spaceStory({ + required mid, + required aid, + required beforeSize, + required afterSize, + required cid, + required contain, + required index, + }) async { + Map data = { + 'aid': aid.toString(), + 'before_size': beforeSize.toString(), + 'after_size': afterSize.toString(), + 'cid': cid.toString(), + 'contain': contain.toString(), + 'index': index.toString(), + 'build': '1462100', + 'c_locale': 'zh_CN', + 'channel': 'yingyongbao', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'vmid': mid.toString(), + }; + dynamic res = await Request().get( + Api.spaceStory, + queryParameters: data, + options: Options( + headers: { + 'bili-http-engine': 'cronet', + 'user-agent': Constants.userAgent, + }, + ), + ); + if (res.data['code'] == 0) { + return LoadingState.success(res.data['data']); + } else { + return LoadingState.error(res.data['message']); + } + } + static Future space({ int? mid, }) async { diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 5b1b5a25..2c97cce6 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -651,7 +651,7 @@ class VideoIntroController extends GetxController } // 修改分P或番剧分集 - Future changeSeasonOrbangu(epid, bvid, cid, aid, cover, [isStein]) async { + bool changeSeasonOrbangu(epid, bvid, cid, aid, cover, [isStein]) { // 重新获取视频资源 final videoDetailCtr = Get.find(tag: heroTag); @@ -665,7 +665,7 @@ class VideoIntroController extends GetxController 'heroTag': Utils.makeHeroTag(bvid), }, ); - return; + return false; } } @@ -710,6 +710,7 @@ class VideoIntroController extends GetxController lastPlayCid.value = cid; queryOnlineTotal(); + return true; } void startTimer() { diff --git a/lib/pages/video/detail/member/controller.dart b/lib/pages/video/detail/member/controller.dart index ed335ec8..83a5b519 100644 --- a/lib/pages/video/detail/member/controller.dart +++ b/lib/pages/video/detail/member/controller.dart @@ -1,16 +1,12 @@ +import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; -import 'package:PiliPlus/models/space_archive/data.dart'; -import 'package:PiliPlus/models/space_archive/item.dart'; import 'package:PiliPlus/pages/common/common_controller.dart'; -import 'package:PiliPlus/pages/member/new/content/member_contribute/member_contribute.dart' - show ContributeType; -import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:get/get.dart'; class HorizontalMemberPageController extends CommonController { - HorizontalMemberPageController({this.mid}); + HorizontalMemberPageController({this.mid, required this.firstParam}); dynamic mid; dynamic wwebid; @@ -21,8 +17,8 @@ class HorizontalMemberPageController extends CommonController { @override void onInit() { super.onInit(); - currentPage = 0; getUserInfo(); + queryData(); } Future getUserInfo() async { @@ -31,7 +27,6 @@ class HorizontalMemberPageController extends CommonController { if (res['status']) { userState.value = LoadingState.success(res['data']); getMemberStat(); - queryData(); } else { userState.value = LoadingState.error(res['msg']); } @@ -54,51 +49,68 @@ class HorizontalMemberPageController extends CommonController { @override bool customHandleResponse(Success response) { - Data data = response.response; - next = data.next; - aid = data.item?.lastOrNull?.param; - if (data.hasNext == false || data.item.isNullOrEmpty) { - isEnd = true; - } - count.value = data.count ?? -1; + final data = response.response; + hasPrev = data['page']['has_prev']; + hasNext = data['page']['has_next']; + if (currentPage != 0 && loadingState.value is Success) { - data.item ??= []; - data.item!.insertAll(0, (loadingState.value as Success).response); + data['items'] ??= []; + if (isLoadPrevious) { + data['items'].addAll((loadingState.value as Success).response); + } else { + data['items'].insertAll(0, (loadingState.value as Success).response); + } } - loadingState.value = LoadingState.success(data.item); + + if ((data['items'] as List?)?.isNotEmpty == true) { + final first = data['items'].first; + final last = data['items'].last; + + firstParam = Triple( + first: first['param'], + second: first['player_args']?['cid'], + third: first['index'], + ); + lastParam = Triple( + first: last['param'], + second: last['player_args']?['cid'], + third: last['index'], + ); + } + + loadingState.value = LoadingState.success(data['items']); + isLoadPrevious = false; return true; } - String? aid; - RxString order = 'pubdate'.obs; - RxString sort = 'desc'.obs; - RxInt count = (-1).obs; - int? next; + bool isLoadPrevious = false; + Triple firstParam; + Triple? lastParam; + bool hasPrev = true; + bool hasNext = true; @override - Future customGetData() => MemberHttp.spaceArchive( - type: ContributeType.video, + Future customGetData() => MemberHttp.spaceStory( mid: mid, - aid: aid, - order: order.value, - sort: sort.value, - pn: null, - next: next, - seasonId: null, - seriesId: null, + aid: lastParam == null || isLoadPrevious + ? firstParam.first + : lastParam?.first, + cid: lastParam == null || isLoadPrevious + ? firstParam.second + : lastParam?.second, + index: lastParam == null || isLoadPrevious + ? firstParam.third + : lastParam?.third, + beforeSize: lastParam == null + ? 0 + : isLoadPrevious + ? 20 + : 0, + afterSize: lastParam == null + ? 10 + : isLoadPrevious + ? 0 + : 20, + contain: lastParam == null ? 1 : 0, ); - - @override - Future onRefresh() async { - aid = null; - next = null; - currentPage = 0; - isEnd = false; - await queryData(); - } - - queryBySort() { - order.value = order.value == 'pubdate' ? 'click' : 'pubdate'; - onReload(); - } } diff --git a/lib/pages/video/detail/member/horizontal_member_page.dart b/lib/pages/video/detail/member/horizontal_member_page.dart index 95fb2881..27dd47fa 100644 --- a/lib/pages/video/detail/member/horizontal_member_page.dart +++ b/lib/pages/video/detail/member/horizontal_member_page.dart @@ -1,27 +1,24 @@ import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/icon_button.dart'; import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart' show SourceModel; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import 'package:PiliPlus/common/widgets/video_card_h_member_video.dart'; +import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/member/info.dart'; import 'package:PiliPlus/pages/video/detail/controller.dart'; import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; import 'package:PiliPlus/pages/video/detail/member/controller.dart'; +import 'package:PiliPlus/pages/video/detail/member/widget/video_card_h_member_video.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import '../../../../models/space_archive/item.dart'; - class HorizontalMemberPage extends StatefulWidget { const HorizontalMemberPage({ super.key, @@ -42,23 +39,40 @@ class _HorizontalMemberPageState extends State { late final HorizontalMemberPageController _controller; int? _ownerMid; dynamic _bvid; - late final String _tag; @override void initState() { super.initState(); - _tag = Utils.makeHeroTag(widget.mid); _controller = Get.put( - HorizontalMemberPageController(mid: widget.mid), - tag: _tag, + HorizontalMemberPageController( + mid: widget.mid, + firstParam: Triple( + first: widget.videoDetailController.oid.value, + second: widget.videoDetailController.cid.value, + third: 0, + ), + ), + tag: widget.videoDetailController.heroTag, ); _bvid = widget.videoDetailController.bvid; _ownerMid = GStorage.userInfo.get('userInfoCache')?.mid; + if (_controller.hasPrev) { + _controller.scrollController.addListener(listener); + } + } + + void listener() { + if (_controller.scrollController.position.pixels == 0) { + if (_controller.hasPrev) { + _controller.isLoadPrevious = true; + _controller.onLoadMore(); + } + } } @override void dispose() { - Get.delete(tag: _tag); + _controller.scrollController.removeListener(listener); super.dispose(); } @@ -106,64 +120,15 @@ class _HorizontalMemberPageState extends State { }; } - Widget get _buildSliverHeader { - return SliverPersistentHeader( - pinned: false, - floating: true, - delegate: CustomSliverPersistentHeaderDelegate( - extent: 40, - bgColor: Theme.of(context).colorScheme.surface, - child: Container( - height: 40, - padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Obx( - () => Text( - _controller.count.value != -1 - ? '共${_controller.count.value}视频' - : '', - style: const TextStyle(fontSize: 13), - ), - ), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, - color: Theme.of(context).colorScheme.secondary, - ), - label: Obx( - () => Text( - _controller.order.value == 'pubdate' ? '最新发布' : '最多播放', - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - ) - ], - ), - ), - ), - ); - } - Widget _buildVideoList(LoadingState loadingState) { return switch (loadingState) { Loading() => loadingWidget, Success() => Material( color: Colors.transparent, child: CustomScrollView( - controller: ScrollController(), + controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - _buildSliverHeader, SliverPadding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).padding.bottom + 80, @@ -176,23 +141,27 @@ class _HorizontalMemberPageState extends State { ), delegate: SliverChildBuilderDelegate( (context, index) { - if (index == loadingState.response.length - 1) { + if (index == loadingState.response.length - 1 && + _controller.hasNext) { _controller.onLoadMore(); } return VideoCardHMemberVideo( videoItem: loadingState.response[index], bvid: _bvid, onTap: () { - final Item videoItem = loadingState.response[index]; - widget.videoIntroController.changeSeasonOrbangu( + final videoItem = loadingState.response[index]; + final status = + widget.videoIntroController.changeSeasonOrbangu( null, - videoItem.bvid, - videoItem.cid, - IdUtils.bv2av(videoItem.bvid!), - videoItem.cover, + videoItem['bvid'], + videoItem['player_args']?['cid'], + int.parse(videoItem['param']), + videoItem['cover'], ); - _bvid = videoItem.bvid; - setState(() {}); + if (status) { + _bvid = videoItem['bvid']; + setState(() {}); + } }, ); }, @@ -203,7 +172,7 @@ class _HorizontalMemberPageState extends State { ], ), ), - Error() => errorWidget( + Error() => scrollErrorWidget( errMsg: loadingState.errMsg, callback: _controller.onReload, ), diff --git a/lib/pages/video/detail/member/widget/video_card_h_member_video.dart b/lib/pages/video/detail/member/widget/video_card_h_member_video.dart new file mode 100644 index 00000000..a0916ac3 --- /dev/null +++ b/lib/pages/video/detail/member/widget/video_card_h_member_video.dart @@ -0,0 +1,145 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/stat/stat.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; + +// 视频卡片 - 水平布局 +class VideoCardHMemberVideo extends StatelessWidget { + const VideoCardHMemberVideo({ + super.key, + required this.videoItem, + this.onTap, + this.bvid, + }); + final dynamic videoItem; + final VoidCallback? onTap; + final dynamic bvid; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + InkWell( + onLongPress: () => imageSaveDialog( + context: context, + title: videoItem['title'], + cover: videoItem['cover'], + ), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints boxConstraints) { + return 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: videoItem['cover'], + width: maxWidth, + height: maxHeight, + ), + if (videoItem['duration'] > 0) + PBadge( + text: Utils.timeFormat(videoItem['duration']), + right: 6.0, + bottom: 6.0, + type: 'gray', + ), + ], + ); + }, + ), + ), + const SizedBox(width: 10), + videoContent(context), + ], + ); + }, + ), + ), + ), + // Positioned( + // bottom: 0, + // right: 12, + // child: VideoPopupMenu( + // size: 29, + // iconSize: 17, + // videoItem: videoItem, + // ), + // ), + ], + ); + } + + Widget videoContent(context) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + videoItem['title'], + textAlign: TextAlign.start, + style: TextStyle( + fontWeight: + videoItem['bvid'] != null && videoItem['bvid'] == bvid + ? FontWeight.bold + : null, + fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize, + height: 1.42, + letterSpacing: 0.3, + color: videoItem['bvid'] != null && videoItem['bvid'] == bvid + ? Theme.of(context).colorScheme.primary + : null, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + Utils.dateFormat(videoItem['pubdate']), + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + height: 1, + color: Theme.of(context).colorScheme.outline, + overflow: TextOverflow.clip, + ), + ), + const SizedBox(height: 3), + Row( + children: [ + StatView( + context: context, + theme: 'gray', + value: videoItem['stat']['view'], + ), + const SizedBox(width: 8), + StatDanMu( + context: context, + theme: 'gray', + value: videoItem['stat']['danmaku'], + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index b23bfe13..fdd978c8 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -14,6 +14,7 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/intro_detail.da as video; import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; +import 'package:PiliPlus/pages/video/detail/member/controller.dart'; import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart'; @@ -351,6 +352,11 @@ class _VideoDetailPageState extends State videoDetailController.skipTimer?.cancel(); videoDetailController.skipTimer = null; + try { + Get.delete( + tag: videoDetailController.heroTag); + } catch (_) {} + WidgetsBinding.instance.removeObserver(this); if (!Get.previousRoute.startsWith('/video')) { ScreenBrightness().resetApplicationScreenBrightness(); diff --git a/lib/pages/video/detail/view_v.dart b/lib/pages/video/detail/view_v.dart index f0f69f62..34c0704b 100644 --- a/lib/pages/video/detail/view_v.dart +++ b/lib/pages/video/detail/view_v.dart @@ -15,6 +15,7 @@ import 'package:PiliPlus/pages/video/detail/introduction/widgets/intro_detail.da as video; import 'package:PiliPlus/pages/video/detail/introduction/widgets/page.dart'; import 'package:PiliPlus/pages/video/detail/introduction/widgets/season.dart'; +import 'package:PiliPlus/pages/video/detail/member/controller.dart'; import 'package:PiliPlus/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPlus/pages/video/detail/reply_reply/view.dart'; import 'package:PiliPlus/pages/video/detail/view_point/view_points_page.dart'; @@ -383,6 +384,8 @@ class _VideoDetailPageVState extends State videoDetailController.skipTimer = null; try { + Get.delete( + tag: videoDetailController.heroTag); videoDetailController.animationController.removeListener(animListener); if (videoDetailController.showReply) { videoDetailController.scrollKey.currentState?.innerController