From d62d0eddc2e12c7be4208639bc3b429cde5d306d Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Fri, 19 Sep 2025 18:09:10 +0800 Subject: [PATCH] feat: popular series/precious Signed-off-by: bggRGjQaUbCoE --- lib/http/api.dart | 7 + lib/http/video.dart | 59 +++++ .../popular/popular_precious/data.dart | 20 ++ .../popular/popular_series_list/list.dart | 16 ++ .../popular/popular_series_one/config.dart | 54 +++++ .../popular/popular_series_one/data.dart | 23 ++ lib/pages/history/view.dart | 1 + lib/pages/hot/view.dart | 20 +- lib/pages/popular_precious/controller.dart | 26 ++ lib/pages/popular_precious/view.dart | 80 +++++++ lib/pages/popular_series/controller.dart | 59 +++++ lib/pages/popular_series/view.dart | 226 ++++++++++++++++++ lib/router/app_pages.dart | 10 + 13 files changed, 585 insertions(+), 16 deletions(-) create mode 100644 lib/models_new/popular/popular_precious/data.dart create mode 100644 lib/models_new/popular/popular_series_list/list.dart create mode 100644 lib/models_new/popular/popular_series_one/config.dart create mode 100644 lib/models_new/popular/popular_series_one/data.dart create mode 100644 lib/pages/popular_precious/controller.dart create mode 100644 lib/pages/popular_precious/view.dart create mode 100644 lib/pages/popular_series/controller.dart create mode 100644 lib/pages/popular_series/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 73b1b64fe..47bd9dc1a 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -965,4 +965,11 @@ class Api { static const String superChatMsg = '${HttpString.liveBaseUrl}/av/v1/SuperChat/getMessageList'; + + static const String popularSeriesOne = '/x/web-interface/popular/series/one'; + + static const String popularSeriesList = + '/x/web-interface/popular/series/list'; + + static const String popularPrecious = '/x/web-interface/popular/precious'; } diff --git a/lib/http/video.dart b/lib/http/video.dart index db7876db0..022e374e0 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -14,6 +14,9 @@ import 'package:PiliPlus/models/model_rec_video_item.dart'; import 'package:PiliPlus/models/pgc_lcf.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/pgc/pgc_rank/pgc_rank_item_model.dart'; +import 'package:PiliPlus/models_new/popular/popular_precious/data.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_list/list.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_one/data.dart'; import 'package:PiliPlus/models_new/triple/pgc_triple.dart'; import 'package:PiliPlus/models_new/triple/ugc_triple.dart'; import 'package:PiliPlus/models_new/video/video_ai_conclusion/data.dart'; @@ -971,4 +974,60 @@ class VideoHttp { return Error(res.data['message']); } } + + static Future?>> + popularSeriesList() async { + var res = await Request().get( + Api.popularSeriesList, + queryParameters: await WbiSign.makSign({ + 'web_location': 333.934, + }), + ); + if (res.data['code'] == 0) { + return Success( + (res.data['data']?['list'] as List?) + ?.map( + (e) => PopularSeriesListItem.fromJson(e as Map), + ) + .toList(), + ); + } else { + return Error(res.data['message']); + } + } + + static Future> popularSeriesOne({ + required int number, + }) async { + var res = await Request().get( + Api.popularSeriesOne, + queryParameters: await WbiSign.makSign({ + 'number': number, + 'web_location': 333.934, + }), + ); + if (res.data['code'] == 0) { + return Success(PopularSeriesOneData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } + + static Future> popularPrecious({ + required int page, + }) async { + var res = await Request().get( + Api.popularPrecious, + queryParameters: await WbiSign.makSign({ + 'page_size': 100, + 'page': page, + 'web_location': 333.934, + }), + ); + if (res.data['code'] == 0) { + return Success(PopularPreciousData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models_new/popular/popular_precious/data.dart b/lib/models_new/popular/popular_precious/data.dart new file mode 100644 index 000000000..1be3efe06 --- /dev/null +++ b/lib/models_new/popular/popular_precious/data.dart @@ -0,0 +1,20 @@ +import 'package:PiliPlus/models/model_hot_video_item.dart'; + +class PopularPreciousData { + String? title; + int? mediaId; + String? explain; + List? list; + + PopularPreciousData({this.title, this.mediaId, this.explain, this.list}); + + factory PopularPreciousData.fromJson(Map json) => + PopularPreciousData( + title: json['title'] as String?, + mediaId: json['media_id'] as int?, + explain: json['explain'] as String?, + list: (json['list'] as List?) + ?.map((e) => HotVideoItemModel.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/models_new/popular/popular_series_list/list.dart b/lib/models_new/popular/popular_series_list/list.dart new file mode 100644 index 000000000..e9e5ec4fd --- /dev/null +++ b/lib/models_new/popular/popular_series_list/list.dart @@ -0,0 +1,16 @@ +class PopularSeriesListItem { + int? number; + String? subject; + int? status; + String? name; + + PopularSeriesListItem({this.number, this.subject, this.status, this.name}); + + factory PopularSeriesListItem.fromJson(Map json) => + PopularSeriesListItem( + number: json['number'] as int?, + subject: json['subject'] as String?, + status: json['status'] as int?, + name: json['name'] as String?, + ); +} diff --git a/lib/models_new/popular/popular_series_one/config.dart b/lib/models_new/popular/popular_series_one/config.dart new file mode 100644 index 000000000..ec0d7b72c --- /dev/null +++ b/lib/models_new/popular/popular_series_one/config.dart @@ -0,0 +1,54 @@ +class PopularSeriesConfig { + int? id; + String? type; + int? number; + String? subject; + int? stime; + int? etime; + int? status; + String? name; + String? label; + String? hint; + int? color; + String? cover; + String? shareTitle; + String? shareSubtitle; + int? mediaId; + + PopularSeriesConfig({ + this.id, + this.type, + this.number, + this.subject, + this.stime, + this.etime, + this.status, + this.name, + this.label, + this.hint, + this.color, + this.cover, + this.shareTitle, + this.shareSubtitle, + this.mediaId, + }); + + factory PopularSeriesConfig.fromJson(Map json) => + PopularSeriesConfig( + id: json['id'] as int?, + type: json['type'] as String?, + number: json['number'] as int?, + subject: json['subject'] as String?, + stime: json['stime'] as int?, + etime: json['etime'] as int?, + status: json['status'] as int?, + name: json['name'] as String?, + label: json['label'] as String?, + hint: json['hint'] as String?, + color: json['color'] as int?, + cover: json['cover'] as String?, + shareTitle: json['share_title'] as String?, + shareSubtitle: json['share_subtitle'] as String?, + mediaId: json['media_id'] as int?, + ); +} diff --git a/lib/models_new/popular/popular_series_one/data.dart b/lib/models_new/popular/popular_series_one/data.dart new file mode 100644 index 000000000..a3ed36029 --- /dev/null +++ b/lib/models_new/popular/popular_series_one/data.dart @@ -0,0 +1,23 @@ +import 'package:PiliPlus/models/model_hot_video_item.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_one/config.dart'; + +class PopularSeriesOneData { + PopularSeriesConfig? config; + String? reminder; + List? list; + + PopularSeriesOneData({this.config, this.reminder, this.list}); + + factory PopularSeriesOneData.fromJson(Map json) => + PopularSeriesOneData( + config: json['config'] == null + ? null + : PopularSeriesConfig.fromJson( + json['config'] as Map, + ), + reminder: json['reminder'] as String?, + list: (json['list'] as List?) + ?.map((e) => HotVideoItemModel.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 4f2fdf10a..4f0ff183b 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -264,6 +264,7 @@ class _HistoryPageState extends State TextSpan( children: [ WidgetSpan( + alignment: PlaceholderAlignment.middle, child: Icon( Icons.info_outline, size: 18, diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 7b1d94f17..f1faa618b 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -106,27 +106,15 @@ class _HotPageState extends CommonPageState ), _buildEntranceItem( iconUrl: - 'http://i0.hdslb.com/bfs/archive/552ebe8c4794aeef30ebd1568b59ad35f15e21ad.png', + 'https://i0.hdslb.com/bfs/archive/552ebe8c4794aeef30ebd1568b59ad35f15e21ad.png', title: '每周必看', - onTap: () => Get.toNamed( - '/webview', - parameters: { - 'url': - 'https://www.bilibili.com/h5/weekly-recommend', - }, - ), + onTap: () => Get.toNamed('/popularSeries'), ), _buildEntranceItem( iconUrl: - 'http://i0.hdslb.com/bfs/archive/3693ec9335b78ca57353ac0734f36a46f3d179a9.png', + 'https://i0.hdslb.com/bfs/archive/3693ec9335b78ca57353ac0734f36a46f3d179a9.png', title: '入站必刷', - onTap: () => Get.toNamed( - '/webview', - parameters: { - 'url': - 'https://www.bilibili.com/h5/good-history', - }, - ), + onTap: () => Get.toNamed('/popularPrecious'), ), ], ), diff --git a/lib/pages/popular_precious/controller.dart b/lib/pages/popular_precious/controller.dart new file mode 100644 index 000000000..bc3884218 --- /dev/null +++ b/lib/pages/popular_precious/controller.dart @@ -0,0 +1,26 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/video.dart'; +import 'package:PiliPlus/models/model_hot_video_item.dart'; +import 'package:PiliPlus/models_new/popular/popular_precious/data.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; + +class PopularPreciousController + extends CommonListController { + @override + void onInit() { + super.onInit(); + queryData(); + } + + int? mediaId; + + @override + List? getDataList(PopularPreciousData response) { + mediaId = response.mediaId; + return response.list; + } + + @override + Future> customGetData() => + VideoHttp.popularPrecious(page: page); +} diff --git a/lib/pages/popular_precious/view.dart b/lib/pages/popular_precious/view.dart new file mode 100644 index 000000000..f0069b865 --- /dev/null +++ b/lib/pages/popular_precious/view.dart @@ -0,0 +1,80 @@ +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; +import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/video/source_type.dart'; +import 'package:PiliPlus/models/model_hot_video_item.dart'; +import 'package:PiliPlus/pages/popular_precious/controller.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class PopularPreciousPage extends StatefulWidget { + const PopularPreciousPage({super.key}); + + @override + State createState() => _PopularPreciousPageState(); +} + +class _PopularPreciousPageState extends State + with GridMixin { + final _controller = Get.put(PopularPreciousController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar(title: const Text('入站必刷')), + body: refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + ViewSliverSafeArea( + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ), + ); + } + + Widget _buildBody(LoadingState?> value) { + switch (value) { + case Loading(): + return gridSkeleton; + case Success?>(:var response): + return SliverGrid.builder( + gridDelegate: gridDelegate, + itemCount: response!.length, + itemBuilder: (context, index) { + final item = response[index]; + return VideoCardH( + videoItem: item, + onTap: () { + PageUtils.toVideoPage( + bvid: item.bvid, + cid: item.cid!, + extraArguments: { + 'sourceType': SourceType.playlist, + 'favTitle': '入站必刷', + 'mediaId': _controller.mediaId, + 'desc': true, + 'oid': item.aid, + 'isContinuePlaying': index != 0, + }, + ); + }, + ); + }, + ); + case Error(:var errMsg): + return HttpError( + errMsg: errMsg, + onReload: _controller.onReload, + ); + } + } +} diff --git a/lib/pages/popular_series/controller.dart b/lib/pages/popular_series/controller.dart new file mode 100644 index 000000000..265de8bd9 --- /dev/null +++ b/lib/pages/popular_series/controller.dart @@ -0,0 +1,59 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/video.dart'; +import 'package:PiliPlus/models/model_hot_video_item.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_list/list.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_one/config.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_one/data.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:get/get.dart'; + +class PopularSeriesController + extends CommonListController { + late int number; + + final Rx config = Rx(null); + String? reminder; + List? seriesList; + + @override + void onInit() { + super.onInit(); + _getSeriesList(); + } + + Future _getSeriesList() async { + final res = await VideoHttp.popularSeriesList(); + if (res.isSuccess) { + final list = res.data; + if (list != null && list.isNotEmpty) { + number = list.first.number!; + seriesList = list; + queryData(); + } else { + loadingState.value = const Success(null); + } + } else { + loadingState.value = res as Error; + } + } + + @override + List? getDataList(PopularSeriesOneData response) { + config.value = response.config; + reminder = response.reminder; + return response.list; + } + + @override + Future> customGetData() => + VideoHttp.popularSeriesOne(number: number); + + @override + Future onReload() { + if (seriesList.isNullOrEmpty) { + return _getSeriesList(); + } + return super.onReload(); + } +} diff --git a/lib/pages/popular_series/view.dart b/lib/pages/popular_series/view.dart new file mode 100644 index 000000000..ef8a9f53b --- /dev/null +++ b/lib/pages/popular_series/view.dart @@ -0,0 +1,226 @@ +import 'dart:math'; + +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; +import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/video/source_type.dart'; +import 'package:PiliPlus/models/model_hot_video_item.dart'; +import 'package:PiliPlus/models_new/popular/popular_series_one/config.dart'; +import 'package:PiliPlus/pages/popular_series/controller.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class PopularSeriesPage extends StatefulWidget { + const PopularSeriesPage({super.key}); + + @override + State createState() => _PopularSeriesPageState(); +} + +class _PopularSeriesPageState extends State with GridMixin { + final _controller = Get.put(PopularSeriesController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Obx(() { + final config = _controller.config.value; + if (config != null) { + return Text(config.name!); + } + return const Text('每周必看'); + }), + ), + body: refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + ViewSliverSafeArea( + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ), + ); + } + + Widget _buildBody(LoadingState?> value) { + switch (value) { + case Loading(): + return gridSkeleton; + case Success?>(:var response): + Widget sliver; + if (response?.isNotEmpty == true) { + sliver = SliverGrid.builder( + gridDelegate: gridDelegate, + itemCount: response!.length, + itemBuilder: (context, index) { + final item = response[index]; + return VideoCardH( + videoItem: item, + onTap: () { + final config = _controller.config.value; + PageUtils.toVideoPage( + bvid: item.bvid, + cid: item.cid!, + extraArguments: { + 'sourceType': SourceType.playlist, + 'favTitle': '每周必看 ${config?.label ?? ''}', + 'mediaId': config?.mediaId, + 'desc': true, + 'oid': item.aid, + 'isContinuePlaying': index != 0, + }, + ); + }, + ); + }, + ); + } else { + sliver = HttpError(onReload: _controller.onReload); + } + if (_controller.config.value case final config?) { + sliver = SliverMainAxisGroup( + slivers: [ + _buildSeriesList(config), + sliver, + ], + ); + } + return sliver; + case Error(:var errMsg): + return HttpError( + errMsg: errMsg, + onReload: _controller.onReload, + ); + } + } + + Widget _buildSeriesList(PopularSeriesConfig config) { + final colorScheme = ColorScheme.of(context); + Widget child = GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + final number = _controller.number; + final seriesList = _controller.seriesList!; + final size = MediaQuery.sizeOf(context); + final padding = MediaQuery.viewPaddingOf(context); + final width = min( + min( + size.width - padding.horizontal - 80, + size.height - padding.vertical - 48, + ), + 525.0, + ); + final currIndex = seriesList.indexWhere((e) => e.number == number); + final controller = ScrollController( + initialScrollOffset: max(0, currIndex * 44 + 34 - width / 2), + ); + showDialog( + context: context, + builder: (context) { + final theme = Theme.of(context); + return Dialog( + clipBehavior: Clip.hardEdge, + child: SizedBox( + width: width, + height: width, + child: ListView.builder( + controller: controller, + padding: const EdgeInsets.symmetric(vertical: 12), + itemCount: seriesList.length, + itemExtent: 44, + itemBuilder: (context, index) { + final item = seriesList[index]; + final isCurr = index == currIndex; + Widget child = Text( + item.name!, + style: const TextStyle(fontSize: 14), + ); + if (isCurr) { + child = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + const Icon(Icons.check, size: 18), + ], + ); + } + return Material( + color: isCurr ? theme.highlightColor : null, + child: InkWell( + onTap: () { + Get.back(); + if (!isCurr) { + _controller + ..number = item.number! + ..onReload(); + } + }, + child: Padding( + padding: const EdgeInsetsGeometry.symmetric( + horizontal: 16, + ), + child: Align( + alignment: Alignment.centerLeft, + child: child, + ), + ), + ), + ); + }, + ), + ), + ); + }, + ).whenComplete(controller.dispose); + }, + child: Text.rich( + style: TextStyle( + height: 1, + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), + strutStyle: const StrutStyle(height: 1, leading: 0, fontSize: 14), + TextSpan( + children: [ + TextSpan(text: config.label!), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + size: 18, + Icons.keyboard_arrow_down, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + if (_controller.reminder case final reminder?) { + child = Row( + spacing: 16, + children: [ + child, + Text( + reminder, + style: TextStyle(color: colorScheme.outline), + ), + ], + ); + } + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 14, bottom: 7), + child: child, + ), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index cdf0ba527..8bb9cf652 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -37,6 +37,8 @@ import 'package:PiliPlus/pages/msg_feed_top/like_me/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/reply_me/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/sys_msg/view.dart'; import 'package:PiliPlus/pages/music/view.dart'; +import 'package:PiliPlus/pages/popular_precious/view.dart'; +import 'package:PiliPlus/pages/popular_series/view.dart'; import 'package:PiliPlus/pages/search/view.dart'; import 'package:PiliPlus/pages/search_result/view.dart'; import 'package:PiliPlus/pages/search_trending/view.dart'; @@ -209,6 +211,14 @@ class Routes { ), CustomGetPage(name: '/createVote', page: () => const CreateVotePage()), CustomGetPage(name: '/musicDetail', page: () => const MusicDetailPage()), + CustomGetPage( + name: '/popularSeries', + page: () => const PopularSeriesPage(), + ), + CustomGetPage( + name: '/popularPrecious', + page: () => const PopularPreciousPage(), + ), ]; }