diff --git a/lib/common/widgets/stat/stat.dart b/lib/common/widgets/stat/stat.dart index 83ba3db2..006ae4e7 100644 --- a/lib/common/widgets/stat/stat.dart +++ b/lib/common/widgets/stat/stat.dart @@ -65,6 +65,7 @@ class StatView extends _StatItemBase { 'picture' => Icons.remove_red_eye_outlined, 'like' => Icons.thumb_up_outlined, 'reply' => Icons.comment_outlined, + 'follow' => Icons.favorite_border, _ => Icons.play_circle_outlined, }; diff --git a/lib/http/api.dart b/lib/http/api.dart index 04178453..6b38058d 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -691,6 +691,10 @@ class Api { /// 排行榜 static const String getRankApi = "/x/web-interface/ranking/v2"; + static const String pgcRank = "/pgc/web/rank/list"; + + static const String pgcSeasonRank = "/pgc/season/rank/web/list"; + /// 取消订阅-合集 static const String unfavSeason = '/x/v3/fav/season/unfav'; diff --git a/lib/http/video.dart b/lib/http/video.dart index c159b5ec..40808177 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:PiliPlus/grpc/app/card/v1/card.pb.dart' as card; import 'package:PiliPlus/grpc/grpc_repo.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/bangumi/pgc_rank/pgc_rank_item_model.dart'; import 'package:PiliPlus/models/member/article.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:dio/dio.dart'; @@ -966,8 +967,13 @@ class VideoHttp { // 视频排行 static Future>> getRankVideoList( int rid) async { - var rankApi = "${Api.getRankApi}?rid=$rid&type=all"; - var res = await Request().get(rankApi); + var res = await Request().get( + Api.getRankApi, + queryParameters: await WbiSign.makSign({ + 'rid': rid, + 'type': 'all', + }), + ); if (res.data['code'] == 0) { List list = []; Set blackMids = GStorage.blackMids; @@ -990,6 +996,44 @@ class VideoHttp { } } + // pgc 排行 + static Future pgcRankList( + {int day = 3, required int seasonType}) async { + var res = await Request().get( + Api.pgcRank, + queryParameters: await WbiSign.makSign({ + 'day': day, + 'season_type': seasonType, + }), + ); + if (res.data['code'] == 0) { + return LoadingState.success((res.data['result']?['list'] as List?) + ?.map((e) => PgcRankItemModel.fromJson(e)) + .toList()); + } else { + return LoadingState.error(res.data['message']); + } + } + + // pgc season 排行 + static Future pgcSeasonRankList( + {int day = 3, required int seasonType}) async { + var res = await Request().get( + Api.pgcSeasonRank, + queryParameters: await WbiSign.makSign({ + 'day': day, + 'season_type': seasonType, + }), + ); + if (res.data['code'] == 0) { + return LoadingState.success((res.data['data']?['list'] as List?) + ?.map((e) => PgcRankItemModel.fromJson(e)) + .toList()); + } else { + return LoadingState.error(res.data['message']); + } + } + static Future getVideoNoteList({ dynamic oid, dynamic uperMid, diff --git a/lib/models/bangumi/pgc_rank/badge_info.dart b/lib/models/bangumi/pgc_rank/badge_info.dart new file mode 100644 index 00000000..c760ba72 --- /dev/null +++ b/lib/models/bangumi/pgc_rank/badge_info.dart @@ -0,0 +1,19 @@ +class BadgeInfo { + String? bgColor; + String? bgColorNight; + String? text; + + BadgeInfo({this.bgColor, this.bgColorNight, this.text}); + + factory BadgeInfo.fromJson(Map json) => BadgeInfo( + bgColor: json['bg_color'] as String?, + bgColorNight: json['bg_color_night'] as String?, + text: json['text'] as String?, + ); + + Map toJson() => { + 'bg_color': bgColor, + 'bg_color_night': bgColorNight, + 'text': text, + }; +} diff --git a/lib/models/bangumi/pgc_rank/data.dart b/lib/models/bangumi/pgc_rank/data.dart new file mode 100644 index 00000000..9d7a0ef1 --- /dev/null +++ b/lib/models/bangumi/pgc_rank/data.dart @@ -0,0 +1,23 @@ +import 'pgc_rank_item_model.dart'; + +class Data { + List? list; + String? note; + int? seasonType; + + Data({this.list, this.note, this.seasonType}); + + factory Data.fromJson(Map json) => Data( + list: (json['list'] as List?) + ?.map((e) => PgcRankItemModel.fromJson(e as Map)) + .toList(), + note: json['note'] as String?, + seasonType: json['season_type'] as int?, + ); + + Map toJson() => { + 'list': list?.map((e) => e.toJson()).toList(), + 'note': note, + 'season_type': seasonType, + }; +} diff --git a/lib/models/bangumi/pgc_rank/icon_font.dart b/lib/models/bangumi/pgc_rank/icon_font.dart new file mode 100644 index 00000000..291523b8 --- /dev/null +++ b/lib/models/bangumi/pgc_rank/icon_font.dart @@ -0,0 +1,16 @@ +class IconFont { + String? name; + String? text; + + IconFont({this.name, this.text}); + + factory IconFont.fromJson(Map json) => IconFont( + name: json['name'] as String?, + text: json['text'] as String?, + ); + + Map toJson() => { + 'name': name, + 'text': text, + }; +} diff --git a/lib/models/bangumi/pgc_rank/new_ep.dart b/lib/models/bangumi/pgc_rank/new_ep.dart new file mode 100644 index 00000000..73562eed --- /dev/null +++ b/lib/models/bangumi/pgc_rank/new_ep.dart @@ -0,0 +1,16 @@ +class NewEp { + String? cover; + String? indexShow; + + NewEp({this.cover, this.indexShow}); + + factory NewEp.fromJson(Map json) => NewEp( + cover: json['cover'] as String?, + indexShow: json['index_show'] as String?, + ); + + Map toJson() => { + 'cover': cover, + 'index_show': indexShow, + }; +} diff --git a/lib/models/bangumi/pgc_rank/pgc_rank_item_model.dart b/lib/models/bangumi/pgc_rank/pgc_rank_item_model.dart new file mode 100644 index 00000000..88b364d7 --- /dev/null +++ b/lib/models/bangumi/pgc_rank/pgc_rank_item_model.dart @@ -0,0 +1,85 @@ +import 'badge_info.dart'; +import 'icon_font.dart'; +import 'new_ep.dart'; +import 'stat.dart'; + +class PgcRankItemModel { + String? badge; + BadgeInfo? badgeInfo; + int? badgeType; + String? cover; + String? desc; + bool? enableVt; + IconFont? iconFont; + NewEp? newEp; + int? rank; + String? rating; + int? seasonId; + String? ssHorizontalCover; + Stat? stat; + String? title; + String? url; + + PgcRankItemModel({ + this.badge, + this.badgeInfo, + this.badgeType, + this.cover, + this.desc, + this.enableVt, + this.iconFont, + this.newEp, + this.rank, + this.rating, + this.seasonId, + this.ssHorizontalCover, + this.stat, + this.title, + this.url, + }); + + factory PgcRankItemModel.fromJson(Map json) => + PgcRankItemModel( + badge: json['badge'] as String?, + badgeInfo: json['badge_info'] == null + ? null + : BadgeInfo.fromJson(json['badge_info'] as Map), + badgeType: json['badge_type'] as int?, + cover: json['cover'] as String?, + desc: json['desc'] as String?, + enableVt: json['enable_vt'] as bool?, + iconFont: json['icon_font'] == null + ? null + : IconFont.fromJson(json['icon_font'] as Map), + newEp: json['new_ep'] == null + ? null + : NewEp.fromJson(json['new_ep'] as Map), + rank: json['rank'] as int?, + rating: json['rating'] as String?, + seasonId: json['season_id'] as int?, + ssHorizontalCover: json['ss_horizontal_cover'] as String?, + stat: json['stat'] == null + ? null + : Stat.fromJson(json['stat'] as Map), + title: json['title'] as String?, + url: json['url'] as String?, + ); + + Map toJson() => { + 'badge': badge, + 'badge_info': badgeInfo?.toJson(), + 'badge_type': badgeType, + 'cover': cover, + 'desc': desc, + 'enable_vt': enableVt, + 'icon_font': iconFont?.toJson(), + 'new_ep': newEp?.toJson(), + 'rank': rank, + 'rating': rating, + 'season_id': seasonId, + 'ss_horizontal_cover': ssHorizontalCover, + 'stat': stat?.toJson(), + 'title': title, + 'url': url, + }; +} diff --git a/lib/models/bangumi/pgc_rank/stat.dart b/lib/models/bangumi/pgc_rank/stat.dart new file mode 100644 index 00000000..a572a0b6 --- /dev/null +++ b/lib/models/bangumi/pgc_rank/stat.dart @@ -0,0 +1,22 @@ +class Stat { + int? danmaku; + int? follow; + int? seriesFollow; + int? view; + + Stat({this.danmaku, this.follow, this.seriesFollow, this.view}); + + factory Stat.fromJson(Map json) => Stat( + danmaku: json['danmaku'] as int?, + follow: (json['follow'] as int?) ?? 0, + seriesFollow: json['series_follow'] as int?, + view: (json['view'] as int?) ?? 0, + ); + + Map toJson() => { + 'danmaku': danmaku, + 'follow': follow, + 'series_follow': seriesFollow, + 'view': view, + }; +} diff --git a/lib/models/common/rank_type.dart b/lib/models/common/rank_type.dart index 73ddb372..78477f43 100644 --- a/lib/models/common/rank_type.dart +++ b/lib/models/common/rank_type.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - enum RandType { all, + bangumi, creation, animation, music, @@ -20,12 +19,14 @@ enum RandType { film, documentary, movie, - teleplay + teleplay, + show, } extension RankTypeDesc on RandType { - String get description => [ + String get description => const [ '全站', + '番剧', '国创', '动画', '音乐', @@ -35,7 +36,6 @@ extension RankTypeDesc on RandType { '科技', '运动', '汽车', - '生活', '美食', '动物', '鬼畜', @@ -44,212 +44,120 @@ extension RankTypeDesc on RandType { '影视', '纪录', '电影', - '剧集' - ][index]; - - String get id => [ - 'all', - 'creation', - 'animation', - 'music', - 'dance', - 'game', - 'knowledge', - 'technology', - 'sport', - 'car', - 'life', - 'food', - 'animal', - 'madness', - 'fashion', - 'entertainment', - 'film', - 'documentary', - 'movie', - 'teleplay' + '剧集', + '综艺', ][index]; } -List tabsConfig = [ +const List tabsConfig = [ { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 0, 'label': '全站', 'type': RandType.all, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), - 'rid': 168, + 'season_type': 1, + 'label': '番剧', + 'type': RandType.bangumi, + }, + { + // 'rid': 168, + 'season_type': 4, 'label': '国创', 'type': RandType.creation, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1005, //1, 'label': '动画', 'type': RandType.animation, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1003, // 3, 'label': '音乐', 'type': RandType.music, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1004, // 129, 'label': '舞蹈', 'type': RandType.dance, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1008, // 4, 'label': '游戏', 'type': RandType.game, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1010, // 36, 'label': '知识', 'type': RandType.knowledge, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1012, // 188, 'label': '科技', 'type': RandType.technology, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1018, //234, 'label': '运动', 'type': RandType.sport, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1013, // 223, 'label': '汽车', 'type': RandType.car, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), - 'rid': 160, - 'label': '生活', - 'type': RandType.life, - }, - { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1020, // 211, 'label': '美食', 'type': RandType.food, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1024, //217, 'label': '动物', 'type': RandType.animal, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1007, // 119, 'label': '鬼畜', 'type': RandType.madness, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1014, // 155, 'label': '时尚', 'type': RandType.fashion, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1002, // 5, 'label': '娱乐', 'type': RandType.entertainment, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), 'rid': 1001, // 181, 'label': '影视', 'type': RandType.film, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), - 'rid': 177, + // 'rid': 177, + 'season_type': 3, 'label': '纪录', 'type': RandType.documentary, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), - 'rid': 23, + // 'rid': 23, + 'season_type': 2, 'label': '电影', 'type': RandType.movie, }, { - 'icon': const Icon( - Icons.live_tv_outlined, - size: 15, - ), - 'rid': 11, + // 'rid': 11, + 'season_type': 5, 'label': '剧集', 'type': RandType.teleplay, - } + }, + { + // 'rid': 11, + 'season_type': 7, + 'label': '综艺', + 'type': RandType.show, + }, ]; diff --git a/lib/pages/rank/view.dart b/lib/pages/rank/view.dart index 6784fb64..e4e1e047 100644 --- a/lib/pages/rank/view.dart +++ b/lib/pages/rank/view.dart @@ -89,8 +89,12 @@ class _RankPageState extends State child: TabBarView( physics: const NeverScrollableScrollPhysics(), controller: _rankController.tabController, - children: - tabsConfig.map((item) => ZonePage(rid: item['rid'])).toList(), + children: tabsConfig + .map((item) => ZonePage( + rid: item['rid'], + seasonType: item['season_type'], + )) + .toList(), ), ), ], diff --git a/lib/pages/rank/zone/controller.dart b/lib/pages/rank/zone/controller.dart index 1c85e257..1a5ceae0 100644 --- a/lib/pages/rank/zone/controller.dart +++ b/lib/pages/rank/zone/controller.dart @@ -1,12 +1,12 @@ 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/pages/common/common_list_controller.dart'; -class ZoneController - extends CommonListController, HotVideoItemModel> { - ZoneController({required this.zoneID}); - int zoneID; +class ZoneController extends CommonListController { + ZoneController({this.rid, this.seasonType}); + + int? rid; + int? seasonType; @override void onInit() { @@ -15,6 +15,13 @@ class ZoneController } @override - Future>> customGetData() => - VideoHttp.getRankVideoList(zoneID); + Future customGetData() { + if (rid != null) { + return VideoHttp.getRankVideoList(rid!); + } + if (seasonType == 1) { + return VideoHttp.pgcRankList(seasonType: seasonType!); + } + return VideoHttp.pgcSeasonRankList(seasonType: seasonType!); + } } diff --git a/lib/pages/rank/zone/view.dart b/lib/pages/rank/zone/view.dart index 44f7d0ed..3f3a0d45 100644 --- a/lib/pages/rank/zone/view.dart +++ b/lib/pages/rank/zone/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/model_hot_video_item.dart'; import 'package:PiliPlus/pages/common/common_page.dart'; +import 'package:PiliPlus/pages/rank/zone/widget/pgc_rank_item.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/constants.dart'; @@ -13,9 +14,10 @@ import 'package:PiliPlus/pages/rank/zone/index.dart'; import '../../../utils/grid.dart'; class ZonePage extends CommonPage { - const ZonePage({super.key, required this.rid}); + const ZonePage({super.key, this.rid, this.seasonType}); - final int rid; + final int? rid; + final int? seasonType; @override State createState() => _ZonePageState(); @@ -25,8 +27,8 @@ class _ZonePageState extends CommonPageState with AutomaticKeepAliveClientMixin { @override late ZoneController controller = Get.put( - ZoneController(zoneID: widget.rid), - tag: widget.rid.toString(), + ZoneController(rid: widget.rid, seasonType: widget.seasonType), + tag: '${widget.rid}${widget.seasonType}', ); @override @@ -66,7 +68,7 @@ class _ZonePageState extends CommonPageState ); } - Widget _buildBody(LoadingState?> loadingState) { + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => _buildSkeleton(), Success() => loadingState.response?.isNotEmpty == true @@ -74,10 +76,14 @@ class _ZonePageState extends CommonPageState gridDelegate: Grid.videoCardHDelegate(context), delegate: SliverChildBuilderDelegate( (context, index) { - return VideoCardH( - videoItem: loadingState.response![index], - showPubdate: true, - ); + final item = loadingState.response![index]; + if (item is HotVideoItemModel) { + return VideoCardH( + videoItem: item, + showPubdate: true, + ); + } + return PgcRankItem(item: item); }, childCount: loadingState.response!.length, ), diff --git a/lib/pages/rank/zone/widget/pgc_rank_item.dart b/lib/pages/rank/zone/widget/pgc_rank_item.dart new file mode 100644 index 00000000..799fb428 --- /dev/null +++ b/lib/pages/rank/zone/widget/pgc_rank_item.dart @@ -0,0 +1,98 @@ +import 'package:PiliPlus/common/constants.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/models/bangumi/pgc_rank/pgc_rank_item_model.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:flutter/material.dart'; + +class PgcRankItem extends StatelessWidget { + const PgcRankItem({super.key, required this.item}); + + final PgcRankItemModel item; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + if (item.url != null) { + PiliScheme.routePushFromUrl(item.url!); + } + }, + onLongPress: () { + imageSaveDialog( + context: context, + title: item.title, + cover: item.cover, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 3 / 4, + child: LayoutBuilder( + builder: (context, constraints) { + return NetworkImgLayer( + radius: 6, + width: constraints.maxWidth, + height: constraints.maxHeight, + src: item.cover, + ); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text(item.title!), + ), + if (item.newEp?.indexShow?.isNotEmpty == true) ...[ + Text( + item.newEp!.indexShow!, + maxLines: 1, + style: TextStyle( + fontSize: 12, + height: 1, + color: Theme.of(context).colorScheme.outline, + overflow: TextOverflow.clip, + ), + ), + const SizedBox(height: 4), + ], + Row( + children: [ + StatView( + context: context, + theme: 'gray', + value: item.stat!.view!, + ), + const SizedBox(width: 8), + StatView( + context: context, + theme: 'gray', + goto: 'follow', + value: item.stat!.follow!, + ), + ], + ) + ], + ), + ), + ], + ), + ), + ), + ); + } +}