From 11a0f2faca85de3e320fc17e17359af2add50a2f Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Wed, 7 May 2025 17:27:07 +0800 Subject: [PATCH] feat: dyn topic Signed-off-by: bggRGjQaUbCoE --- lib/http/api.dart | 5 + lib/http/dynamics.dart | 47 ++++++ .../dynamics/dyn_topic_feed/all_sort_by.dart | 16 ++ lib/models/dynamics/dyn_topic_feed/item.dart | 16 ++ .../dyn_topic_feed/topic_card_list.dart | 28 ++++ .../dyn_topic_feed/topic_sort_by_conf.dart | 25 ++++ .../dynamics/dyn_topic_top/top_details.dart | 39 +++++ .../dynamics/dyn_topic_top/topic_creator.dart | 23 +++ .../dynamics/dyn_topic_top/topic_item.dart | 63 ++++++++ lib/pages/dynamics/widgets/article_panel.dart | 2 +- lib/pages/dynamics/widgets/content_panel.dart | 30 +++- lib/pages/dynamics/widgets/forward_panel.dart | 16 +- .../dynamics/widgets/live_rcmd_panel.dart | 23 ++- .../dynamics/widgets/rich_node_panel.dart | 102 ++++++------- lib/pages/dynamics/widgets/video_panel.dart | 42 +++--- lib/pages/dynamics_tab/view.dart | 72 ++++----- lib/pages/dynamics_topic/controller.dart | 72 +++++++++ lib/pages/dynamics_topic/view.dart | 140 ++++++++++++++++++ lib/pages/member_dynamics/view.dart | 39 +---- lib/pages/member_search/child/view.dart | 40 +---- lib/router/app_pages.dart | 2 + 21 files changed, 635 insertions(+), 207 deletions(-) create mode 100644 lib/models/dynamics/dyn_topic_feed/all_sort_by.dart create mode 100644 lib/models/dynamics/dyn_topic_feed/item.dart create mode 100644 lib/models/dynamics/dyn_topic_feed/topic_card_list.dart create mode 100644 lib/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart create mode 100644 lib/models/dynamics/dyn_topic_top/top_details.dart create mode 100644 lib/models/dynamics/dyn_topic_top/topic_creator.dart create mode 100644 lib/models/dynamics/dyn_topic_top/topic_item.dart create mode 100644 lib/pages/dynamics_topic/controller.dart create mode 100644 lib/pages/dynamics_topic/view.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 10821e57..88cd499e 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -822,4 +822,9 @@ class Api { static const String liveSearch = '${HttpString.liveBaseUrl}/xlive/app-interface/v2/search_live'; + + static const String topicTop = + '${HttpString.appBaseUrl}/x/topic/web/details/top'; + + static const String topicFeed = '/x/polymer/web-dynamic/v1/feed/topic'; } diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index eee64ae9..49f8295d 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -2,6 +2,8 @@ import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_card_list.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_top/top_details.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/up.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart'; @@ -256,4 +258,49 @@ class DynamicsHttp { ? LoadingState.success(VoteInfo.fromJson(res.data['data']['vote_info'])) : LoadingState.error(res.data['message']); } + + static Future> topicTop({required topicId}) async { + final res = await Request().get( + Api.topicTop, + queryParameters: { + 'topic_id': topicId, + 'source': 'Web', + }, + ); + if (res.data['code'] == 0) { + TopDetails? data = res.data['data']?['top_details'] == null + ? null + : TopDetails.fromJson(res.data['data']['top_details']); + return LoadingState.success(data); + } else { + return LoadingState.error(res.data['message']); + } + } + + static Future> topicFeed({ + required topicId, + required String offset, + required int sortBy, + }) async { + final res = await Request().get( + Api.topicFeed, + queryParameters: { + 'topic_id': topicId, + 'sort_by': sortBy, + 'offset': offset, + 'page_size': 20, + 'source': 'Web', + // itemOpusStyle,listOnlyfans,opusBigCover,onlyfansVote,decorationCard + 'features': 'itemOpusStyle,listOnlyfans', + }, + ); + if (res.data['code'] == 0) { + TopicCardList? data = res.data['data']?['topic_card_list'] == null + ? null + : TopicCardList.fromJson(res.data['data']['topic_card_list']); + return LoadingState.success(data); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/models/dynamics/dyn_topic_feed/all_sort_by.dart b/lib/models/dynamics/dyn_topic_feed/all_sort_by.dart new file mode 100644 index 00000000..f23046b7 --- /dev/null +++ b/lib/models/dynamics/dyn_topic_feed/all_sort_by.dart @@ -0,0 +1,16 @@ +class AllSortBy { + int? sortBy; + String? sortName; + + AllSortBy({this.sortBy, this.sortName}); + + factory AllSortBy.fromJson(Map json) => AllSortBy( + sortBy: json['sort_by'] as int?, + sortName: json['sort_name'] as String?, + ); + + Map toJson() => { + 'sort_by': sortBy, + 'sort_name': sortName, + }; +} diff --git a/lib/models/dynamics/dyn_topic_feed/item.dart b/lib/models/dynamics/dyn_topic_feed/item.dart new file mode 100644 index 00000000..9b496a06 --- /dev/null +++ b/lib/models/dynamics/dyn_topic_feed/item.dart @@ -0,0 +1,16 @@ +import 'package:PiliPlus/models/dynamics/result.dart'; + +class TopicCardItem { + DynamicItemModel? dynamicCardItem; + String? topicType; + + TopicCardItem({this.dynamicCardItem, this.topicType}); + + factory TopicCardItem.fromJson(Map json) => TopicCardItem( + dynamicCardItem: json['dynamic_card_item'] == null + ? null + : DynamicItemModel.fromJson( + json['dynamic_card_item'] as Map), + topicType: json['topic_type'] as String?, + ); +} diff --git a/lib/models/dynamics/dyn_topic_feed/topic_card_list.dart b/lib/models/dynamics/dyn_topic_feed/topic_card_list.dart new file mode 100644 index 00000000..1dc81df7 --- /dev/null +++ b/lib/models/dynamics/dyn_topic_feed/topic_card_list.dart @@ -0,0 +1,28 @@ +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/item.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart'; + +class TopicCardList { + bool? hasMore; + List? items; + String? offset; + TopicSortByConf? topicSortByConf; + + TopicCardList({ + this.hasMore, + this.items, + this.offset, + this.topicSortByConf, + }); + + factory TopicCardList.fromJson(Map json) => TopicCardList( + hasMore: json['has_more'] as bool?, + items: (json['items'] as List?) + ?.map((e) => TopicCardItem.fromJson(e as Map)) + .toList(), + offset: json['offset'] as String?, + topicSortByConf: json['topic_sort_by_conf'] == null + ? null + : TopicSortByConf.fromJson( + json['topic_sort_by_conf'] as Map), + ); +} diff --git a/lib/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart b/lib/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart new file mode 100644 index 00000000..baf84d59 --- /dev/null +++ b/lib/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart @@ -0,0 +1,25 @@ +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/all_sort_by.dart'; + +class TopicSortByConf { + List? allSortBy; + int? defaultSortBy; + int? showSortBy; + + TopicSortByConf({this.allSortBy, this.defaultSortBy, this.showSortBy}); + + factory TopicSortByConf.fromJson(Map json) { + return TopicSortByConf( + allSortBy: (json['all_sort_by'] as List?) + ?.map((e) => AllSortBy.fromJson(e as Map)) + .toList(), + defaultSortBy: json['default_sort_by'] as int?, + showSortBy: json['show_sort_by'] as int?, + ); + } + + Map toJson() => { + 'all_sort_by': allSortBy?.map((e) => e.toJson()).toList(), + 'default_sort_by': defaultSortBy, + 'show_sort_by': showSortBy, + }; +} diff --git a/lib/models/dynamics/dyn_topic_top/top_details.dart b/lib/models/dynamics/dyn_topic_top/top_details.dart new file mode 100644 index 00000000..05d06094 --- /dev/null +++ b/lib/models/dynamics/dyn_topic_top/top_details.dart @@ -0,0 +1,39 @@ +import 'package:PiliPlus/models/dynamics/dyn_topic_top/topic_creator.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_top/topic_item.dart'; + +class TopDetails { + TopicItem? topicItem; + TopicCreator? topicCreator; + bool? hasCreateJurisdiction; + int? wordColor; + bool? closePubLayerEntry; + + TopDetails({ + this.topicItem, + this.topicCreator, + this.hasCreateJurisdiction, + this.wordColor, + this.closePubLayerEntry, + }); + + factory TopDetails.fromJson(Map json) => TopDetails( + topicItem: json['topic_item'] == null + ? null + : TopicItem.fromJson(json['topic_item'] as Map), + topicCreator: json['topic_creator'] == null + ? null + : TopicCreator.fromJson( + json['topic_creator'] as Map), + hasCreateJurisdiction: json['has_create_jurisdiction'] as bool?, + wordColor: json['word_color'] as int?, + closePubLayerEntry: json['close_pub_layer_entry'] as bool?, + ); + + Map toJson() => { + 'topic_item': topicItem?.toJson(), + 'topic_creator': topicCreator?.toJson(), + 'has_create_jurisdiction': hasCreateJurisdiction, + 'word_color': wordColor, + 'close_pub_layer_entry': closePubLayerEntry, + }; +} diff --git a/lib/models/dynamics/dyn_topic_top/topic_creator.dart b/lib/models/dynamics/dyn_topic_top/topic_creator.dart new file mode 100644 index 00000000..681a51a1 --- /dev/null +++ b/lib/models/dynamics/dyn_topic_top/topic_creator.dart @@ -0,0 +1,23 @@ +class TopicCreator { + int? uid; + String? face; + String? name; + + TopicCreator({ + this.uid, + this.face, + this.name, + }); + + factory TopicCreator.fromJson(Map json) => TopicCreator( + uid: json['uid'] as int?, + face: json['face'] as String?, + name: json['name'] as String?, + ); + + Map toJson() => { + 'uid': uid, + 'face': face, + 'name': name, + }; +} diff --git a/lib/models/dynamics/dyn_topic_top/topic_item.dart b/lib/models/dynamics/dyn_topic_top/topic_item.dart new file mode 100644 index 00000000..bfc0651c --- /dev/null +++ b/lib/models/dynamics/dyn_topic_top/topic_item.dart @@ -0,0 +1,63 @@ +class TopicItem { + int? id; + String? name; + int? view; + int? discuss; + int? fav; + int? dynamics; + String? jumpUrl; + String? backColor; + String? description; + String? sharePic; + String? shareUrl; + int? ctime; + bool? showInteractData; + + TopicItem({ + this.id, + this.name, + this.view, + this.discuss, + this.fav, + this.dynamics, + this.jumpUrl, + this.backColor, + this.description, + this.sharePic, + this.shareUrl, + this.ctime, + this.showInteractData, + }); + + factory TopicItem.fromJson(Map json) => TopicItem( + id: json['id'] as int?, + name: json['name'] as String?, + view: json['view'] as int?, + discuss: json['discuss'] as int?, + fav: json['fav'] as int?, + dynamics: json['dynamics'] as int?, + jumpUrl: json['jump_url'] as String?, + backColor: json['back_color'] as String?, + description: json['description'] as String?, + sharePic: json['share_pic'] as String?, + shareUrl: json['share_url'] as String?, + ctime: json['ctime'] as int?, + showInteractData: json['show_interact_data'] as bool?, + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'view': view, + 'discuss': discuss, + 'fav': fav, + 'dynamics': dynamics, + 'jump_url': jumpUrl, + 'back_color': backColor, + 'description': description, + 'share_pic': sharePic, + 'share_url': shareUrl, + 'ctime': ctime, + 'show_interact_data': showInteractData, + }; +} diff --git a/lib/pages/dynamics/widgets/article_panel.dart b/lib/pages/dynamics/widgets/article_panel.dart index 9b6ec6be..9d1d2119 100644 --- a/lib/pages/dynamics/widgets/article_panel.dart +++ b/lib/pages/dynamics/widgets/article_panel.dart @@ -9,7 +9,7 @@ Widget articlePanel( String? source, DynamicItemModel item, BuildContext context, - callback, { + Function(List, int)? callback, { floor = 1, }) { return Padding( diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index 26c05e71..c6ef7b96 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/common/widgets/image/image_view.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; Widget content( ThemeData theme, @@ -10,8 +11,9 @@ Widget content( BuildContext context, DynamicItemModel item, String? source, - Function(List, int)? callback, -) { + Function(List, int)? callback, { + floor = 1, +}) { InlineSpan picsNodes() { return WidgetSpan( child: LayoutBuilder( @@ -35,16 +37,28 @@ Widget content( TextSpan? richNodes = richNode(theme, item, context); - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), + return Padding( + padding: floor == 1 + ? const EdgeInsets.fromLTRB(12, 0, 12, 6) + : const EdgeInsets.only(bottom: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (item.modules.moduleDynamic?.topic != null) ...[ - Text( - '#${item.modules.moduleDynamic!.topic!.name}', - style: TextStyle(color: theme.colorScheme.primary), + GestureDetector( + onTap: () { + Get.toNamed( + '/dynTopic', + parameters: { + 'id': item.modules.moduleDynamic!.topic!.id!.toString(), + 'name': item.modules.moduleDynamic!.topic!.name!, + }, + ); + }, + child: Text( + '#${item.modules.moduleDynamic!.topic!.name}', + style: TextStyle(color: theme.colorScheme.primary), + ), ), ], if (richNodes != null) diff --git a/lib/pages/dynamics/widgets/forward_panel.dart b/lib/pages/dynamics/widgets/forward_panel.dart index 23fa78b6..0737405a 100644 --- a/lib/pages/dynamics/widgets/forward_panel.dart +++ b/lib/pages/dynamics/widgets/forward_panel.dart @@ -59,8 +59,8 @@ Widget forWard( bool isSave, DynamicItemModel item, BuildContext context, - source, - callback, { + String? source, + Function(List, int)? callback, { floor = 1, }) { switch (item.type) { @@ -137,7 +137,8 @@ Widget forWard( ); // 视频 case 'DYNAMIC_TYPE_AV': - return videoSeasonWidget(theme, source, item, context, 'archive', + return videoSeasonWidget( + theme, isSave, source, item, context, 'archive', callback, floor: floor); // 文章 case 'DYNAMIC_TYPE_ARTICLE': @@ -204,7 +205,8 @@ Widget forWard( return livePanel(theme, source, item, context, floor: floor); // 合集 case 'DYNAMIC_TYPE_UGC_SEASON': - return videoSeasonWidget(theme, source, item, context, 'ugcSeason'); + return videoSeasonWidget( + theme, isSave, source, item, context, 'ugcSeason', callback); case 'DYNAMIC_TYPE_WORD': late TextSpan? richNodes = richNode(theme, item, context); return floor == 2 @@ -262,10 +264,12 @@ Widget forWard( theme, item.modules.moduleDynamic!.major!.blocked!) : const SizedBox.shrink(); case 'DYNAMIC_TYPE_PGC': - return videoSeasonWidget(theme, source, item, context, 'pgc', + return videoSeasonWidget( + theme, isSave, source, item, context, 'pgc', callback, floor: floor); case 'DYNAMIC_TYPE_PGC_UNION': - return videoSeasonWidget(theme, source, item, context, 'pgc', + return videoSeasonWidget( + theme, isSave, source, item, context, 'pgc', callback, floor: floor); // 直播结束 case 'DYNAMIC_TYPE_NONE': diff --git a/lib/pages/dynamics/widgets/live_rcmd_panel.dart b/lib/pages/dynamics/widgets/live_rcmd_panel.dart index d6468ba8..d19dd5e4 100644 --- a/lib/pages/dynamics/widgets/live_rcmd_panel.dart +++ b/lib/pages/dynamics/widgets/live_rcmd_panel.dart @@ -53,12 +53,23 @@ Widget liveRcmdPanel( ], const SizedBox(height: 4), if (item.modules.moduleDynamic?.topic != null) ...[ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), - child: Text( - '#${item.modules.moduleDynamic!.topic!.name}', - style: authorStyle, + GestureDetector( + onTap: () { + Get.toNamed( + '/dynTopic', + parameters: { + 'id': item.modules.moduleDynamic!.topic!.id!.toString(), + 'name': item.modules.moduleDynamic!.topic!.name!, + }, + ); + }, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: StyleString.safeSpace), + child: Text( + '#${item.modules.moduleDynamic!.topic!.name}', + style: authorStyle, + ), ), ), const SizedBox(height: 6), diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart index 4a4c58c2..0651fb2f 100644 --- a/lib/pages/dynamics/widgets/rich_node_panel.dart +++ b/lib/pages/dynamics/widgets/rich_node_panel.dart @@ -1,15 +1,15 @@ import 'package:PiliPlus/common/widgets/image/image_view.dart'; +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/dynamics/widgets/vote.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; -import 'package:PiliPlus/http/search.dart'; -import 'package:PiliPlus/utils/app_scheme.dart'; // 富文本 TextSpan? richNode( @@ -45,38 +45,41 @@ TextSpan? richNode( switch (i.type) { case 'RICH_TEXT_NODE_TYPE_TEXT': spanChildren.add( - TextSpan(text: i.origText, style: const TextStyle(height: 1.65)), + TextSpan( + text: i.origText, + style: const TextStyle(height: 1.65), + ), ); break; // @用户 case 'RICH_TEXT_NODE_TYPE_AT': spanChildren.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () => Get.toNamed('/member?mid=${i.rid}'), - child: Text( - ' ${i.text}', - style: authorStyle, - ), - ), - ], - ), + TextSpan( + text: ' ${i.text}', + style: authorStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.toNamed('/member?mid=${i.rid}'); + }, ), ); break; // 话题 case 'RICH_TEXT_NODE_TYPE_TOPIC': spanChildren.add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Text( - '${i.origText}', - style: authorStyle, - ), + TextSpan( + text: i.origText!, + style: authorStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.toNamed( + '/searchResult', + parameters: { + 'keyword': + i.origText!.substring(1, i.origText!.length - 1), + }, + ); + }, ), ); break; @@ -94,10 +97,11 @@ TextSpan? richNode( ), ) ..add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: GestureDetector( - onTap: () { + TextSpan( + text: i.text ?? '', + style: authorStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { String? url = i.origText; if (url == null) { SmartDialog.showToast('未获取到链接'); @@ -105,11 +109,6 @@ TextSpan? richNode( } PiliScheme.routePushFromUrl(url); }, - child: Text( - i.text ?? '', - style: authorStyle, - ), - ), ), ); break; @@ -158,10 +157,11 @@ TextSpan? richNode( ), ) ..add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: GestureDetector( - onTap: () { + TextSpan( + text: '${i.origText} ', + style: authorStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { Get.toNamed( '/webview', parameters: { @@ -170,11 +170,6 @@ TextSpan? richNode( }, ); }, - child: Text( - '${i.origText} ', - style: authorStyle, - ), - ), ), ); break; @@ -193,12 +188,9 @@ TextSpan? richNode( ), ) ..add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Text( - '${i.text} ', - style: authorStyle, - ), + TextSpan( + text: '${i.text} ', + style: authorStyle, ), ); break; @@ -216,10 +208,11 @@ TextSpan? richNode( ), ) ..add( - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: GestureDetector( - onTap: () async { + TextSpan( + text: '${i.text} ', + style: authorStyle, + recognizer: TapGestureRecognizer() + ..onTap = () async { try { int cid = await SearchHttp.ab2c(bvid: i.rid); PageUtils.toVideoPage( @@ -232,11 +225,6 @@ TextSpan? richNode( SmartDialog.showToast(err.toString()); } }, - child: Text( - '${i.text} ', - style: authorStyle, - ), - ), ), ); break; diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart index e36a986e..6caece8a 100644 --- a/lib/pages/dynamics/widgets/video_panel.dart +++ b/lib/pages/dynamics/widgets/video_panel.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; +import 'package:PiliPlus/pages/dynamics/widgets/content_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -11,10 +12,12 @@ import 'package:get/get.dart'; Widget videoSeasonWidget( ThemeData theme, + bool isSave, String? source, DynamicItemModel item, BuildContext context, - String type, { + String type, + Function(List, int)? callback, { floor = 1, }) { if (item.modules.moduleDynamic?.major?.type == 'MAJOR_TYPE_NONE') { @@ -46,14 +49,14 @@ Widget videoSeasonWidget( // 1 投稿视频 铺满 borderRadius 0 // 2 转发视频 铺满 borderRadius 6 - DynamicArchiveModel? content = switch (type) { + DynamicArchiveModel? itemContent = switch (type) { 'ugcSeason' => item.modules.moduleDynamic?.major?.ugcSeason, 'archive' => item.modules.moduleDynamic?.major?.archive, 'pgc' => item.modules.moduleDynamic?.major?.pgc, _ => null, }; - if (content == null) { + if (itemContent == null) { return const SizedBox.shrink(); } @@ -69,16 +72,16 @@ Widget videoSeasonWidget( NetworkImgLayer( width: width, height: width / StyleString.aspectRatio, - src: content.cover, + src: itemContent.cover, ), - if (content.badge?['text'] != null) + if (itemContent.badge?['text'] != null) PBadge( - text: content.badge!['text'], + text: itemContent.badge!['text'], top: 8.0, right: 10.0, bottom: null, left: null, - type: content.badge!['text'] == '充电专属' + type: itemContent.badge!['text'] == '充电专属' ? PBadgeType.error : PBadgeType.primary, ), @@ -113,17 +116,17 @@ Widget videoSeasonWidget( child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (content.durationText != null) ...[ + if (itemContent.durationText != null) ...[ Text( - content.durationText!, + itemContent.durationText!, semanticsLabel: - '时长${Utils.durationReadFormat(content.durationText!)}', + '时长${Utils.durationReadFormat(itemContent.durationText!)}', ), const SizedBox(width: 6), ], - Text('${content.stat?.play}次围观'), + Text('${itemContent.stat?.play}次围观'), const SizedBox(width: 6), - Text('${content.stat?.danmu}条弹幕'), + Text('${itemContent.stat?.danmu}条弹幕'), const Spacer(), Image.asset( 'assets/images/play.png', @@ -172,12 +175,13 @@ Widget videoSeasonWidget( ], ), const SizedBox(height: 6), + content(theme, isSave, context, item, source, null, floor: 2), + if (itemContent.desc != null && richNodes != null) ...[ + Text.rich(richNodes), + const SizedBox(height: 6), + ], ], - if (floor == 2 && content.desc != null && richNodes != null) ...[ - Text.rich(richNodes), - const SizedBox(height: 6), - ], - if (content.cover != null) + if (itemContent.cover != null) if (item.isForwarded == true) buildCover() else @@ -187,13 +191,13 @@ Widget videoSeasonWidget( child: buildCover(), ), const SizedBox(height: 6), - if (content.title != null) + if (itemContent.title != null) Padding( padding: floor == 1 ? const EdgeInsets.only(left: 12, right: 12) : EdgeInsets.zero, child: Text( - content.title!, + itemContent.title!, maxLines: source == 'detail' ? null : 1, style: const TextStyle(fontWeight: FontWeight.bold), overflow: source == 'detail' ? null : TextOverflow.ellipsis, diff --git a/lib/pages/dynamics_tab/view.dart b/lib/pages/dynamics_tab/view.dart index bc289a17..6a177a52 100644 --- a/lib/pages/dynamics_tab/view.dart +++ b/lib/pages/dynamics_tab/view.dart @@ -24,6 +24,41 @@ class DynamicsTabPage extends CommonPage { @override State createState() => _DynamicsTabPageState(); + + static Widget dynSkeleton(bool dynamicsWaterfallFlow) { + if (!dynamicsWaterfallFlow) { + return SliverCrossAxisGroup( + slivers: [ + const SliverFillRemaining(), + SliverConstrainedCrossAxis( + maxExtent: Grid.smallCardWidth * 2, + sliver: SliverList.builder( + itemBuilder: (context, index) { + return const DynamicCardSkeleton(); + }, + itemCount: 10, + ), + ), + const SliverFillRemaining() + ], + ); + } + return SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + crossAxisSpacing: StyleString.cardSpace / 2, + mainAxisSpacing: StyleString.cardSpace / 2, + maxCrossAxisExtent: Grid.smallCardWidth * 2, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: 50, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const DynamicCardSkeleton(); + }, + childCount: 10, + ), + ); + } } class _DynamicsTabPageState @@ -91,44 +126,9 @@ class _DynamicsTabPageState ); } - Widget skeleton() { - if (!dynamicsWaterfallFlow) { - return SliverCrossAxisGroup( - slivers: [ - const SliverFillRemaining(), - SliverConstrainedCrossAxis( - maxExtent: Grid.smallCardWidth * 2, - sliver: SliverList.builder( - itemBuilder: (context, index) { - return const DynamicCardSkeleton(); - }, - itemCount: 10, - ), - ), - const SliverFillRemaining() - ], - ); - } - return SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - crossAxisSpacing: StyleString.cardSpace / 2, - mainAxisSpacing: StyleString.cardSpace / 2, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: StyleString.aspectRatio, - mainAxisExtent: 50, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - return const DynamicCardSkeleton(); - }, - childCount: 10, - ), - ); - } - Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { - Loading() => skeleton(), + Loading() => DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow), Success() => loadingState.response?.isNotEmpty == true ? SliverPadding( padding: EdgeInsets.only( diff --git a/lib/pages/dynamics_topic/controller.dart b/lib/pages/dynamics_topic/controller.dart new file mode 100644 index 00000000..7933d7bc --- /dev/null +++ b/lib/pages/dynamics_topic/controller.dart @@ -0,0 +1,72 @@ +import 'package:PiliPlus/http/dynamics.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/item.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_card_list.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:get/get.dart'; + +class DynTopicController + extends CommonListController { + final topicId = Get.parameters['id']!; + final topicName = Get.parameters['name']!; + + int sortBy = 0; + String offset = ''; + Rx topicSortByConf = Rx(null); + + // top + // Rx> topState = + // LoadingState.loading().obs; + + @override + void onInit() { + super.onInit(); + // queryTop(); + queryData(); + } + + // Future queryTop() async { + // topState.value = await DynamicsHttp.topicTop(topicId: topicId); + // } + + @override + List? getDataList(TopicCardList? response) { + offset = response?.offset ?? ''; + topicSortByConf.value = response?.topicSortByConf; + sortBy = response?.topicSortByConf?.showSortBy ?? 0; + if (response?.hasMore == false) { + isEnd = true; + } + return response?.items; + } + + @override + Future onRefresh() { + offset = ''; + return super.onRefresh(); + } + + @override + Future onReload() { + // if (topState.value is! Success) { + // queryTop(); + // } + scrollController.jumpToTop(); + return super.onReload(); + } + + @override + Future> customGetData() => + DynamicsHttp.topicFeed( + topicId: topicId, + offset: offset, + sortBy: sortBy, + ); + + void onSort(int sortBy) { + this.sortBy = sortBy; + onReload(); + } +} diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart new file mode 100644 index 00000000..a51ef3e8 --- /dev/null +++ b/lib/pages/dynamics_topic/view.dart @@ -0,0 +1,140 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_feed/item.dart'; +import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; +import 'package:PiliPlus/pages/dynamics_tab/view.dart'; +import 'package:PiliPlus/pages/dynamics_topic/controller.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:waterfall_flow/waterfall_flow.dart'; + +class DynTopicPage extends StatefulWidget { + const DynTopicPage({super.key}); + + @override + State createState() => _DynTopicPageState(); +} + +class _DynTopicPageState extends State { + final DynTopicController _controller = + Get.put(DynTopicController(), tag: Utils.generateRandomString(8)); + final dynamicsWaterfallFlow = GStorage.setting + .get(SettingBoxKey.dynamicsWaterfallFlow, defaultValue: true); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_controller.topicName), + actions: [ + Obx(() { + if (_controller.topicSortByConf.value?.allSortBy?.isNotEmpty == + true) { + return Padding( + padding: const EdgeInsets.only(right: 16), + child: PopupMenuButton( + initialValue: _controller.sortBy, + itemBuilder: (context) { + return _controller.topicSortByConf.value!.allSortBy! + .map((e) { + return PopupMenuItem( + value: e.sortBy, + child: Text(e.sortName!), + onTap: () { + _controller.onSort(e.sortBy!); + }, + ); + }).toList(); + }, + ), + ); + } + return const SizedBox.shrink(); + }) + ], + ), + body: SafeArea( + top: false, + bottom: false, + child: refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + controller: _controller.scrollController, + slivers: [ + Obx(() => _buildBody(_controller.loadingState.value)), + ], + ), + ), + ), + ); + } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow), + Success() => loadingState.response?.isNotEmpty == true + ? SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: dynamicsWaterfallFlow + ? SliverWaterfallFlow.extent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + crossAxisSpacing: StyleString.cardSpace / 2, + lastChildLayoutTypeBuilder: (index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return index == loadingState.response!.length + ? LastChildLayoutType.foot + : LastChildLayoutType.none; + }, + children: [ + for (var item in loadingState.response!) + if (item.dynamicCardItem != null) + DynamicPanel(item: item.dynamicCardItem!) + else + Text(item.topicType ?? 'err'), + ], + ) + : SliverCrossAxisGroup( + slivers: [ + const SliverFillRemaining(), + SliverConstrainedCrossAxis( + maxExtent: Grid.smallCardWidth * 2, + sliver: SliverList.builder( + itemBuilder: (context, index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + final item = loadingState.response![index]; + if (item.topicType != null) { + return DynamicPanel( + item: item.dynamicCardItem!, + ); + } else { + return Text(item.topicType ?? 'err'); + } + }, + itemCount: loadingState.response!.length, + ), + ), + const SliverFillRemaining(), + ], + ), + ) + : HttpError( + onReload: _controller.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } +} diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart index 75676650..8af70a65 100644 --- a/lib/pages/member_dynamics/view.dart +++ b/lib/pages/member_dynamics/view.dart @@ -1,10 +1,10 @@ import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/skeleton/dynamic_card.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; +import 'package:PiliPlus/pages/dynamics_tab/view.dart'; import 'package:PiliPlus/pages/member_dynamics/controller.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/storage.dart'; @@ -65,44 +65,9 @@ class _MemberDynamicsPageState extends State ), ); - Widget skeleton() { - if (!dynamicsWaterfallFlow) { - return SliverCrossAxisGroup( - slivers: [ - const SliverFillRemaining(), - SliverConstrainedCrossAxis( - maxExtent: Grid.smallCardWidth * 2, - sliver: SliverList.builder( - itemBuilder: (context, index) { - return const DynamicCardSkeleton(); - }, - itemCount: 10, - ), - ), - const SliverFillRemaining() - ], - ); - } - return SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - crossAxisSpacing: StyleString.cardSpace / 2, - mainAxisSpacing: StyleString.cardSpace / 2, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: StyleString.aspectRatio, - mainAxisExtent: 50, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - return const DynamicCardSkeleton(); - }, - childCount: 10, - ), - ); - } - Widget _buildContent(LoadingState?> loadingState) { return switch (loadingState) { - Loading() => skeleton(), + Loading() => DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow), Success() => loadingState.response?.isNotEmpty == true ? SliverPadding( padding: EdgeInsets.only( diff --git a/lib/pages/member_search/child/view.dart b/lib/pages/member_search/child/view.dart index 8b0d3d23..620703e7 100644 --- a/lib/pages/member_search/child/view.dart +++ b/lib/pages/member_search/child/view.dart @@ -1,5 +1,4 @@ import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/common/skeleton/dynamic_card.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; @@ -7,6 +6,7 @@ import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/search_type.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; +import 'package:PiliPlus/pages/dynamics_tab/view.dart'; import 'package:PiliPlus/pages/member_search/child/controller.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/storage.dart'; @@ -64,45 +64,11 @@ class _MemberSearchChildPageState extends State childCount: 10, ), ), - MemberSearchType.dynamic => dynSkeleton(), + MemberSearchType.dynamic => + DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow), }; } - Widget dynSkeleton() { - if (!dynamicsWaterfallFlow) { - return SliverCrossAxisGroup( - slivers: [ - const SliverFillRemaining(), - SliverConstrainedCrossAxis( - maxExtent: Grid.smallCardWidth * 2, - sliver: SliverList.builder( - itemBuilder: (context, index) { - return const DynamicCardSkeleton(); - }, - itemCount: 10, - ), - ), - const SliverFillRemaining() - ], - ); - } - return SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - crossAxisSpacing: StyleString.cardSpace / 2, - mainAxisSpacing: StyleString.cardSpace / 2, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: StyleString.aspectRatio, - mainAxisExtent: 50, - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - return const DynamicCardSkeleton(); - }, - childCount: 10, - ), - ); - } - Widget _buildBody(LoadingState loadingState) { return switch (loadingState) { Loading() => _buildLoading, diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 5d70b331..47380f47 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/pages/blacklist/view.dart'; import 'package:PiliPlus/pages/danmaku_block/view.dart'; import 'package:PiliPlus/pages/dynamics/view.dart'; import 'package:PiliPlus/pages/dynamics_detail/view.dart'; +import 'package:PiliPlus/pages/dynamics_topic/view.dart'; import 'package:PiliPlus/pages/fan/view.dart'; import 'package:PiliPlus/pages/fav/view.dart'; import 'package:PiliPlus/pages/fav_create/view.dart'; @@ -172,6 +173,7 @@ class Routes { name: '/webdavSetting', page: () => const WebDavSettingPage()), CustomGetPage( name: '/searchTrending', page: () => const SearchTrendingPage()), + CustomGetPage(name: '/dynTopic', page: () => const DynTopicPage()), ]; }