From 68df1735589d2e09014aefa0c1a2c283af9bd714 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sun, 13 Apr 2025 19:54:21 +0800 Subject: [PATCH] feat: pgc timeline Closes #653 Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/badge.dart | 15 +- lib/common/widgets/video_card_h.dart | 6 +- lib/http/api.dart | 2 + lib/http/bangumi.dart | 22 + lib/models/bangumi/pgc_timeline/episode.dart | 91 +++ .../bangumi/pgc_timeline/icon_font.dart | 16 + .../bangumi/pgc_timeline/pgc_timeline.dart | 23 + lib/models/bangumi/pgc_timeline/result.dart | 35 ++ lib/pages/bangumi/controller.dart | 20 +- lib/pages/bangumi/view.dart | 537 +++++++++++------- lib/pages/bangumi/widgets/bangumi_card_v.dart | 37 +- .../widgets/bangumi_card_v_member_home.dart | 22 +- .../widgets/bangumi_card_v_pgc_index.dart | 56 +- .../widgets/bangumi_card_v_timeline.dart | 105 ++++ lib/pages/dynamics/widgets/forward_panel.dart | 15 +- lib/pages/fav/pgc/widget/item.dart | 19 +- .../fav_detail/widget/fav_video_card.dart | 15 +- lib/pages/live/view.dart | 118 ++-- .../widgets/media_bangumi_panel.dart | 15 +- 19 files changed, 791 insertions(+), 378 deletions(-) create mode 100644 lib/models/bangumi/pgc_timeline/episode.dart create mode 100644 lib/models/bangumi/pgc_timeline/icon_font.dart create mode 100644 lib/models/bangumi/pgc_timeline/pgc_timeline.dart create mode 100644 lib/models/bangumi/pgc_timeline/result.dart create mode 100644 lib/pages/bangumi/widgets/bangumi_card_v_timeline.dart diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart index e999ab47..064114e9 100644 --- a/lib/common/widgets/badge.dart +++ b/lib/common/widgets/badge.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; class PBadge extends StatelessWidget { @@ -17,7 +18,7 @@ class PBadge extends StatelessWidget { const PBadge({ super.key, - this.text, + required this.text, this.top, this.right, this.bottom, @@ -34,15 +35,19 @@ class PBadge extends StatelessWidget { @override Widget build(BuildContext context) { + if (text.isNullOrEmpty) { + return const SizedBox.shrink(); + } + ColorScheme t = Theme.of(context).colorScheme; // 背景色 Color bgColor = t.primary; // 前景色 Color color = t.onPrimary; // 边框色 - Color borderColor = Colors.transparent; + Color? borderColor; if (type == 'gray') { - bgColor = Colors.black54.withOpacity(0.45); + bgColor = Colors.black45; color = Colors.white; } else if (type == 'color') { bgColor = t.secondaryContainer.withOpacity(0.5); @@ -72,10 +77,10 @@ class PBadge extends StatelessWidget { decoration: BoxDecoration( borderRadius: br, color: bgColor, - border: Border.all(color: borderColor), + border: borderColor != null ? Border.all(color: borderColor) : null, ), child: Text( - text ?? "", + text!, textScaler: textScaleFactor != null ? TextScaler.linear(textScaleFactor!) : null, diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index e3fbfc82..46058ded 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -141,11 +141,7 @@ class VideoCardH extends StatelessWidget { width: maxWidth, height: maxHeight, ), - if (videoItem is HotVideoItemModel && - (videoItem as HotVideoItemModel) - .pgcLabel - ?.isNotEmpty == - true) + if (videoItem is HotVideoItemModel) PBadge( text: (videoItem as HotVideoItemModel).pgcLabel, diff --git a/lib/http/api.dart b/lib/http/api.dart index 58c18d88..469b85db 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -760,4 +760,6 @@ class Api { static const String getLiveEmoticons = '${HttpString.liveBaseUrl}/xlive/web-ucenter/v2/emoticon/GetEmoticons'; + + static const String pgcTimeline = '/pgc/web/timeline'; } diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart index 71916e25..833ebc69 100644 --- a/lib/http/bangumi.dart +++ b/lib/http/bangumi.dart @@ -1,4 +1,6 @@ import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/bangumi/pgc_timeline/pgc_timeline.dart'; +import 'package:PiliPlus/models/bangumi/pgc_timeline/result.dart'; import '../models/bangumi/list.dart'; import '../models/bangumi/pgc_index/condition.dart'; @@ -86,4 +88,24 @@ class BangumiHttp { return LoadingState.error(res.data['message']); } } + + static Future?>> pgcTimeline({ + int types = 1, // 1:`番剧`
3:`电影`
4:`国创` | + required int before, + required int after, + }) async { + var res = await Request().get( + Api.pgcTimeline, + queryParameters: { + 'types': types, + 'before': before, + 'after': after, + }, + ); + if (res.data['code'] == 0) { + return LoadingState.success(PgcTimeline.fromJson(res.data).result); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/models/bangumi/pgc_timeline/episode.dart b/lib/models/bangumi/pgc_timeline/episode.dart new file mode 100644 index 00000000..c955fac5 --- /dev/null +++ b/lib/models/bangumi/pgc_timeline/episode.dart @@ -0,0 +1,91 @@ +import 'icon_font.dart'; + +class Episode { + String? cover; + int? delay; + int? delayId; + String? delayIndex; + String? delayReason; + bool? enableVt; + String? epCover; + int? episodeId; + int? follow; + String? follows; + IconFont? iconFont; + String? plays; + String? pubIndex; + String? pubTime; + int? pubTs; + int? published; + int? seasonId; + String? squareCover; + String? title; + + Episode({ + this.cover, + this.delay, + this.delayId, + this.delayIndex, + this.delayReason, + this.enableVt, + this.epCover, + this.episodeId, + this.follow, + this.follows, + this.iconFont, + this.plays, + this.pubIndex, + this.pubTime, + this.pubTs, + this.published, + this.seasonId, + this.squareCover, + this.title, + }); + + factory Episode.fromJson(Map json) => Episode( + cover: json['cover'] as String?, + delay: json['delay'] as int?, + delayId: json['delay_id'] as int?, + delayIndex: json['delay_index'] as String?, + delayReason: json['delay_reason'] as String?, + enableVt: json['enable_vt'] as bool?, + epCover: json['ep_cover'] as String?, + episodeId: json['episode_id'] as int?, + follow: json['follow'] as int?, + follows: json['follows'] as String?, + iconFont: json['icon_font'] == null + ? null + : IconFont.fromJson(json['icon_font'] as Map), + plays: json['plays'] as String?, + pubIndex: json['pub_index'] as String?, + pubTime: json['pub_time'] as String?, + pubTs: json['pub_ts'] as int?, + published: json['published'] as int?, + seasonId: json['season_id'] as int?, + squareCover: json['square_cover'] as String?, + title: json['title'] as String?, + ); + + Map toJson() => { + 'cover': cover, + 'delay': delay, + 'delay_id': delayId, + 'delay_index': delayIndex, + 'delay_reason': delayReason, + 'enable_vt': enableVt, + 'ep_cover': epCover, + 'episode_id': episodeId, + 'follow': follow, + 'follows': follows, + 'icon_font': iconFont?.toJson(), + 'plays': plays, + 'pub_index': pubIndex, + 'pub_time': pubTime, + 'pub_ts': pubTs, + 'published': published, + 'season_id': seasonId, + 'square_cover': squareCover, + 'title': title, + }; +} diff --git a/lib/models/bangumi/pgc_timeline/icon_font.dart b/lib/models/bangumi/pgc_timeline/icon_font.dart new file mode 100644 index 00000000..291523b8 --- /dev/null +++ b/lib/models/bangumi/pgc_timeline/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_timeline/pgc_timeline.dart b/lib/models/bangumi/pgc_timeline/pgc_timeline.dart new file mode 100644 index 00000000..9173cd80 --- /dev/null +++ b/lib/models/bangumi/pgc_timeline/pgc_timeline.dart @@ -0,0 +1,23 @@ +import 'result.dart'; + +class PgcTimeline { + int? code; + String? message; + List? result; + + PgcTimeline({this.code, this.message, this.result}); + + factory PgcTimeline.fromJson(Map json) => PgcTimeline( + code: json['code'] as int?, + message: json['message'] as String?, + result: (json['result'] as List?) + ?.map((e) => Result.fromJson(e as Map)) + .toList(), + ); + + Map toJson() => { + 'code': code, + 'message': message, + 'result': result?.map((e) => e.toJson()).toList(), + }; +} diff --git a/lib/models/bangumi/pgc_timeline/result.dart b/lib/models/bangumi/pgc_timeline/result.dart new file mode 100644 index 00000000..fa7d8a81 --- /dev/null +++ b/lib/models/bangumi/pgc_timeline/result.dart @@ -0,0 +1,35 @@ +import 'episode.dart'; + +class Result { + String? date; + int? dateTs; + int? dayOfWeek; + List? episodes; + int? isToday; + + Result({ + this.date, + this.dateTs, + this.dayOfWeek, + this.episodes, + this.isToday, + }); + + factory Result.fromJson(Map json) => Result( + date: json['date'] as String?, + dateTs: json['date_ts'] as int?, + dayOfWeek: json['day_of_week'] as int?, + episodes: (json['episodes'] as List?) + ?.map((e) => Episode.fromJson(e as Map)) + .toList(), + isToday: json['is_today'] as int?, + ); + + Map toJson() => { + 'date': date, + 'date_ts': dateTs, + 'day_of_week': dayOfWeek, + 'episodes': episodes?.map((e) => e.toJson()).toList(), + 'is_today': isToday, + }; +} diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index 664e748e..11579780 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/bangumi/list.dart'; +import 'package:PiliPlus/models/bangumi/pgc_timeline/result.dart'; import 'package:PiliPlus/models/common/tab_type.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/utils/extension.dart'; @@ -24,6 +25,9 @@ class BangumiController extends CommonListController< queryData(); queryBangumiFollow(); + if (tabType == TabType.bangumi) { + queryPgcTimeline(); + } if (isLogin.value) { followController = ScrollController(); } @@ -36,16 +40,30 @@ class BangumiController extends CommonListController< followEnd = false; } queryBangumiFollow(); + if (tabType == TabType.bangumi) { + queryPgcTimeline(); + } return super.onRefresh(); } + // follow late int followPage = 1; late RxInt followCount = (-1).obs; late bool followLoading = false; late bool followEnd = false; - late Rx followState = LoadingState.loading().obs; + late Rx?>> followState = + LoadingState?>.loading().obs; ScrollController? followController; + // timeline + late Rx?>> timelineState = + LoadingState?>.loading().obs; + + Future queryPgcTimeline() async { + final res = await BangumiHttp.pgcTimeline(types: 1, before: 6, after: 6); + timelineState.value = res; + } + // 我的订阅 Future queryBangumiFollow([bool isRefresh = true]) async { if (isLogin.value.not || followLoading || (isRefresh.not && followEnd)) { diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 1314a3fa..9530847c 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -1,12 +1,15 @@ -import 'dart:async'; +import 'dart:math'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/bangumi/list.dart'; +import 'package:PiliPlus/models/bangumi/pgc_timeline/result.dart'; import 'package:PiliPlus/models/common/tab_type.dart'; import 'package:PiliPlus/pages/bangumi/pgc_index/pgc_index_page.dart'; +import 'package:PiliPlus/pages/bangumi/widgets/bangumi_card_v_timeline.dart'; import 'package:PiliPlus/pages/common/common_page.dart'; +import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/constants.dart'; @@ -45,199 +48,229 @@ class _BangumiPageState extends CommonPageState super.build(context); return refreshIndicator( onRefresh: () async { - await Future.wait([ - controller.onRefresh(), - controller.queryBangumiFollow(), - ]); + await controller.onRefresh(); }, child: CustomScrollView( controller: controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - SliverToBoxAdapter( - child: Obx( - () => controller.isLogin.value - ? Column( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16), - child: Row( - children: [ - Obx( - () => Text( - '最近${widget.tabType == TabType.bangumi ? '追番' : '追剧'}${controller.followCount.value == -1 ? '' : ' ${controller.followCount.value}'}', - style: - Theme.of(context).textTheme.titleMedium, - ), - ), - const Spacer(), - IconButton( - tooltip: '刷新', - onPressed: () { - controller - ..followPage = 1 - ..followEnd = false - ..queryBangumiFollow(); - }, - icon: const Icon( - Icons.refresh, - size: 20, - ), - ), - Obx( - () => controller.isLogin.value - ? Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Get.toNamed( - '/fav', - arguments: widget.tabType == - TabType.bangumi - ? 1 - : 2, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '查看全部', - strutStyle: StrutStyle( - leading: 0, height: 1), - style: TextStyle( - height: 1, - color: Theme.of(context) - .colorScheme - .secondary, - ), - ), - Icon( - Icons.chevron_right, - color: Theme.of(context) - .colorScheme - .secondary, - ), - ], - ), - ), - ), - ) - : const SizedBox.shrink(), - ), - ], - ), - ), - SizedBox( - height: Grid.smallCardWidth / 2 / 0.75 + - MediaQuery.textScalerOf(context).scale(50), - child: Obx( - () => - _buildFollowBody(controller.followState.value), - ), - ), - ], - ) - : const SizedBox.shrink(), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - left: 16, - right: 10, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '推荐', - style: Theme.of(context).textTheme.titleMedium, - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (widget.tabType == TabType.bangumi) { - Get.to(PgcIndexPage()); - } else { - List titles = const [ - '全部', - '电影', - '电视剧', - '纪录片', - '综艺', - ]; - List types = const [102, 2, 5, 3, 7]; - Get.to( - Scaffold( - appBar: AppBar(title: const Text('索引')), - body: DefaultTabController( - length: types.length, - child: Column( - children: [ - TabBar( - tabs: titles - .map((title) => Tab(text: title)) - .toList()), - Expanded( - child: tabBarView( - children: types - .map((type) => - PgcIndexPage(indexType: type)) - .toList()), - ) - ], - ), - ), - ), - ); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '查看更多', - strutStyle: StrutStyle(leading: 0, height: 1), - style: TextStyle( - height: 1, - color: Theme.of(context).colorScheme.secondary, - ), - ), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.secondary, - ), - ], - ), - ), - ), - ], + _buildFollow, + if (widget.tabType == TabType.bangumi) + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.only(top: 10), + height: Grid.smallCardWidth / 2 / 0.75 + + MediaQuery.textScalerOf(context).scale(110), + child: + Obx(() => _buildTimeline(controller.timelineState.value)), ), ), - ), - SliverPadding( - padding: const EdgeInsets.fromLTRB( - StyleString.safeSpace, 0, StyleString.safeSpace, 0), - sliver: Obx( - () => _buildBody(controller.loadingState.value), - ), - ), + ..._buildRcmd, ], ), ); } - Widget _buildBody(LoadingState?> loadingState) { + late final List weekList = [ + '一', + '二', + '三', + '四', + '五', + '六', + '日', + ]; + + Widget _buildTimeline(LoadingState?> loadingState) => + switch (loadingState) { + Loading() => loadingWidget, + Success() => loadingState.response?.isNotEmpty == true + ? Builder(builder: (context) { + final initialIndex = max( + 0, + loadingState.response! + .indexWhere((item) => item.isToday == 1)); + return DefaultTabController( + initialIndex: initialIndex, + length: loadingState.response!.length, + child: Column( + children: [ + Row( + children: [ + const SizedBox(width: 16), + Text( + '追番时间表', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(width: 16), + Expanded( + child: Material( + color: Colors.transparent, + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + tabs: loadingState.response! + .map( + (item) => Tab( + text: + '${item.date} ${item.isToday == 1 ? '今天' : '周${weekList[item.dayOfWeek! - 1]}'}', + ), + ) + .toList(), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + Expanded( + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + children: loadingState.response!.map((item) { + if (item.episodes!.isNullOrEmpty) { + return const SizedBox.shrink(); + } + return MediaQuery.removePadding( + context: context, + removeLeft: context.orientation == + Orientation.landscape, + child: ListView.builder( + physics: + const AlwaysScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: item.episodes!.length, + itemBuilder: (context, index) { + return Container( + width: Grid.smallCardWidth / 2, + margin: EdgeInsets.only( + left: StyleString.safeSpace, + right: + index == item.episodes!.length - 1 + ? StyleString.safeSpace + : 0, + ), + child: BangumiCardVTimeline( + item: item.episodes![index], + ), + ); + }, + ), + ); + }).toList()), + ), + ], + ), + ); + }) + : const SizedBox.shrink(), + Error() => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: controller.queryPgcTimeline, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.center, + child: Text( + loadingState.errMsg, + textAlign: TextAlign.center, + ), + ), + ), + LoadingState() => throw UnimplementedError(), + }; + + List get _buildRcmd => [ + _buildRcmdTitle, + SliverPadding( + padding: const EdgeInsets.fromLTRB( + StyleString.safeSpace, 0, StyleString.safeSpace, 0), + sliver: Obx( + () => _buildRcmdBody(controller.loadingState.value), + ), + ), + ]; + + Widget get _buildRcmdTitle => SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + left: 16, + right: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '推荐', + style: Theme.of(context).textTheme.titleMedium, + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (widget.tabType == TabType.bangumi) { + Get.to(PgcIndexPage()); + } else { + List titles = const [ + '全部', + '电影', + '电视剧', + '纪录片', + '综艺', + ]; + List types = const [102, 2, 5, 3, 7]; + Get.to( + Scaffold( + appBar: AppBar(title: const Text('索引')), + body: DefaultTabController( + length: types.length, + child: Column( + children: [ + TabBar( + tabs: titles + .map((title) => Tab(text: title)) + .toList()), + Expanded( + child: tabBarView( + children: types + .map((type) => + PgcIndexPage(indexType: type)) + .toList()), + ) + ], + ), + ), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '查看更多', + strutStyle: StrutStyle(leading: 0, height: 1), + style: TextStyle( + height: 1, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + ], + ), + ), + ); + + Widget _buildRcmdBody( + LoadingState?> loadingState) { return switch (loadingState) { Loading() => const SliverToBoxAdapter(), Success() => loadingState.response?.isNotEmpty == true @@ -274,36 +307,122 @@ class _BangumiPageState extends CommonPageState }; } - Widget _buildFollowList(Success loadingState) { - return ListView.builder( - controller: controller.followController, - scrollDirection: Axis.horizontal, - itemCount: loadingState.response.length, - itemBuilder: (context, index) { - if (index == loadingState.response.length - 1) { - controller.queryBangumiFollow(false); - } - return Container( - width: Grid.smallCardWidth / 2, - margin: EdgeInsets.only( - left: StyleString.safeSpace, - right: index == loadingState.response.length - 1 - ? StyleString.safeSpace - : 0, - ), - child: BangumiCardV( - bangumiItem: loadingState.response[index], - ), - ); - }, - ); - } + Widget get _buildFollow => SliverToBoxAdapter( + child: Obx( + () => controller.isLogin.value + ? Column( + children: [ + _buildFollowTitle, + SizedBox( + height: Grid.smallCardWidth / 2 / 0.75 + + MediaQuery.textScalerOf(context).scale(50), + child: Obx( + () => _buildFollowBody(controller.followState.value), + ), + ), + ], + ) + : const SizedBox.shrink(), + ), + ); - Widget _buildFollowBody(LoadingState loadingState) { + Widget get _buildFollowTitle => Padding( + padding: const EdgeInsets.only(left: 16), + child: Row( + children: [ + Obx( + () => Text( + '最近${widget.tabType == TabType.bangumi ? '追番' : '追剧'}${controller.followCount.value == -1 ? '' : ' ${controller.followCount.value}'}', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + const Spacer(), + IconButton( + tooltip: '刷新', + onPressed: () { + controller + ..followPage = 1 + ..followEnd = false + ..queryBangumiFollow(); + }, + icon: const Icon( + Icons.refresh, + size: 20, + ), + ), + Obx( + () => controller.isLogin.value + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Get.toNamed( + '/fav', + arguments: + widget.tabType == TabType.bangumi ? 1 : 2, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '查看全部', + strutStyle: StrutStyle(leading: 0, height: 1), + style: TextStyle( + height: 1, + color: + Theme.of(context).colorScheme.secondary, + ), + ), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + ) + : const SizedBox.shrink(), + ), + ], + ), + ); + + Widget _buildFollowBody( + LoadingState?> loadingState) { return switch (loadingState) { Loading() => loadingWidget, - Success() => (loadingState.response as List?)?.isNotEmpty == true - ? _buildFollowList(loadingState) + Success() => loadingState.response?.isNotEmpty == true + ? MediaQuery.removePadding( + context: context, + removeLeft: context.orientation == Orientation.landscape, + child: ListView.builder( + controller: controller.followController, + scrollDirection: Axis.horizontal, + itemCount: loadingState.response!.length, + itemBuilder: (context, index) { + if (index == loadingState.response!.length - 1) { + controller.queryBangumiFollow(false); + } + return Container( + width: Grid.smallCardWidth / 2, + margin: EdgeInsets.only( + left: StyleString.safeSpace, + right: index == loadingState.response!.length - 1 + ? StyleString.safeSpace + : 0, + ), + child: BangumiCardV( + bangumiItem: loadingState.response![index], + ), + ); + }, + ), + ) : Center( child: Text( '还没有${widget.tabType == TabType.bangumi ? '追番' : '追剧'}')), diff --git a/lib/pages/bangumi/widgets/bangumi_card_v.dart b/lib/pages/bangumi/widgets/bangumi_card_v.dart index 3b772ab7..7b62271d 100644 --- a/lib/pages/bangumi/widgets/bangumi_card_v.dart +++ b/lib/pages/bangumi/widgets/bangumi_card_v.dart @@ -31,6 +31,7 @@ class BangumiCardV extends StatelessWidget { Utils.viewBangumi(seasonId: seasonId); }, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: StyleString.mdRadius, @@ -49,14 +50,13 @@ class BangumiCardV extends StatelessWidget { height: maxHeight, ), ), - if (bangumiItem.badge != null) - PBadge( - text: bangumiItem.badge, - top: 6, - right: 6, - bottom: null, - left: null, - ), + PBadge( + text: bangumiItem.badge, + top: 6, + right: 6, + bottom: null, + left: null, + ), if (bangumiItem.isFinish == 0 && bangumiItem.renewalTime?.isNotEmpty == true) PBadge( @@ -97,19 +97,14 @@ class BangumiCardV extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Expanded( - child: Text( - bangumiItem.title, - textAlign: TextAlign.start, - style: const TextStyle( - letterSpacing: 0.3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - ], + Text( + bangumiItem.title, + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), const SizedBox(height: 1), if (bangumiItem.indexShow != null) diff --git a/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart b/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart index 0309ee79..91391909 100644 --- a/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart +++ b/lib/pages/bangumi/widgets/bangumi_card_v_member_home.dart @@ -31,6 +31,7 @@ class BangumiCardVMemberHome extends StatelessWidget { cover: bangumiItem.cover, ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: const BorderRadius.only( @@ -94,19 +95,14 @@ Widget bangumiContent(Item bangumiItem) { crossAxisAlignment: CrossAxisAlignment.start, // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Expanded( - child: Text( - bangumiItem.title, - textAlign: TextAlign.start, - style: const TextStyle( - letterSpacing: 0.3, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - )), - ], + Text( + bangumiItem.title, + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), const SizedBox(height: 1), // if (bangumiItem.indexShow != null) diff --git a/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart b/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart index e32ac715..5a8f614a 100644 --- a/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart +++ b/lib/pages/bangumi/widgets/bangumi_card_v_pgc_index.dart @@ -29,6 +29,7 @@ class BangumiCardVPgcIndex extends StatelessWidget { Utils.viewBangumi(seasonId: bangumiItem['season_id']); }, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: StyleString.mdRadius, @@ -44,25 +45,21 @@ class BangumiCardVPgcIndex extends StatelessWidget { width: maxWidth, height: maxHeight, ), - if (bangumiItem['badge'] != null && - bangumiItem['badge'] != '') - PBadge( - text: bangumiItem['badge'], - top: 6, - right: 6, - bottom: null, - left: null, - ), - if (bangumiItem['order'] != null && - bangumiItem['order'] != '') - PBadge( - text: bangumiItem['order'], - top: null, - right: null, - bottom: 6, - left: 6, - type: 'gray', - ), + PBadge( + text: bangumiItem['badge'], + top: 6, + right: 6, + bottom: null, + left: null, + ), + PBadge( + text: bangumiItem['order'], + top: null, + right: null, + bottom: 6, + left: 6, + type: 'gray', + ), ], ); }), @@ -86,19 +83,14 @@ class BangumiCardVPgcIndex extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, // mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Expanded( - child: Text( - bangumiItem['title'], - textAlign: TextAlign.start, - style: const TextStyle( - letterSpacing: 0.3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - ], + Text( + bangumiItem['title'], + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), const SizedBox(height: 1), if (bangumiItem['index_show'] != null) diff --git a/lib/pages/bangumi/widgets/bangumi_card_v_timeline.dart b/lib/pages/bangumi/widgets/bangumi_card_v_timeline.dart new file mode 100644 index 00000000..339e70c8 --- /dev/null +++ b/lib/pages/bangumi/widgets/bangumi_card_v_timeline.dart @@ -0,0 +1,105 @@ +import 'package:PiliPlus/common/widgets/image_save.dart'; +import 'package:PiliPlus/models/bangumi/pgc_timeline/episode.dart'; +import 'package:flutter/material.dart'; +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; + +// 视频卡片 - 垂直布局 +class BangumiCardVTimeline extends StatelessWidget { + const BangumiCardVTimeline({ + super.key, + required this.item, + }); + + final Episode item; + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + child: InkWell( + onLongPress: () => imageSaveDialog( + context: context, + title: item.title, + cover: item.cover, + ), + onTap: () async { + Utils.viewBangumi( + seasonId: item.seasonId, + epId: item.episodeId, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: StyleString.mdRadius, + child: AspectRatio( + aspectRatio: 0.75, + child: LayoutBuilder(builder: (context, boxConstraints) { + final double maxWidth = boxConstraints.maxWidth; + final double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + NetworkImgLayer( + src: item.cover, + width: maxWidth, + height: maxHeight, + ), + if (item.follow == 1) + PBadge( + text: '已追番', + right: 6, + top: 6, + ), + PBadge( + text: '${item.pubTime}', + left: 6, + bottom: 6, + type: 'gray', + ), + ], + ); + }), + ), + ), + bagumiContent(context) + ], + ), + ), + ); + } + + Widget bagumiContent(context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(4, 5, 0, 3), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title ?? '', + textAlign: TextAlign.start, + style: const TextStyle( + letterSpacing: 0.3, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + item.pubIndex ?? '', + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/dynamics/widgets/forward_panel.dart b/lib/pages/dynamics/widgets/forward_panel.dart index 830b0359..9431a3c8 100644 --- a/lib/pages/dynamics/widgets/forward_panel.dart +++ b/lib/pages/dynamics/widgets/forward_panel.dart @@ -516,15 +516,12 @@ Widget forWard(bool isSave, item, BuildContext context, source, callback, src: item.modules.moduleDynamic.major.medialist['cover'], ), ), - if (item.modules.moduleDynamic.major.medialist['badge'] - ?['text'] != - null) - PBadge( - right: 6, - top: 6, - text: item.modules.moduleDynamic.major.medialist['badge'] - ['text'], - ) + PBadge( + right: 6, + top: 6, + text: item.modules.moduleDynamic.major.medialist['badge'] + ?['text'], + ) ], ), const SizedBox(width: 14), diff --git a/lib/pages/fav/pgc/widget/item.dart b/lib/pages/fav/pgc/widget/item.dart index 43d08dd7..0acbc1a6 100644 --- a/lib/pages/fav/pgc/widget/item.dart +++ b/lib/pages/fav/pgc/widget/item.dart @@ -65,17 +65,16 @@ class FavPgcItem extends StatelessWidget { width: boxConstraints.maxWidth, height: boxConstraints.maxHeight, ), - if (item.badge?.isNotEmpty == true) - PBadge( - right: 4, - top: 4, - text: item.badge, - fs: 10, - padding: const EdgeInsets.symmetric( - horizontal: 2, - vertical: 1, - ), + PBadge( + right: 4, + top: 4, + text: item.badge, + fs: 10, + padding: const EdgeInsets.symmetric( + horizontal: 2, + vertical: 1, ), + ), Positioned.fill( child: IgnorePointer( child: LayoutBuilder( diff --git a/lib/pages/fav_detail/widget/fav_video_card.dart b/lib/pages/fav_detail/widget/fav_video_card.dart index 855d57c5..e75ee63f 100644 --- a/lib/pages/fav_detail/widget/fav_video_card.dart +++ b/lib/pages/fav_detail/widget/fav_video_card.dart @@ -123,14 +123,13 @@ class FavVideoCardH extends StatelessWidget { bottom: 6.0, type: 'gray', ), - if (videoItem.ogv != null) - PBadge( - text: videoItem.ogv!['type_name'], - top: 6.0, - right: 6.0, - bottom: null, - left: null, - ), + PBadge( + text: videoItem.ogv?['type_name'], + top: 6.0, + right: 6.0, + bottom: null, + left: null, + ), ], ); }, diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index a2ac5657..23b1cb13 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -186,67 +186,71 @@ class _LivePageState extends CommonPageState child: loadingWidget, ), Success() => (loadingState.response as List?)?.isNotEmpty == true - ? SelfSizedHorizontalList( - gapSize: 5, - childBuilder: (index) { - if (index == loadingState.response.length - 1) { - controller.fetchLiveFollowing(false); - } - return SizedBox( - width: 65, - child: GestureDetector( - onTap: () { - Get.toNamed( - '/liveRoom?roomid=${loadingState.response[index].roomId}', - ); - }, - onLongPress: () { - Feedback.forLongPress(context); - Get.toNamed( - '/member?mid=${loadingState.response[index].uid}', - arguments: { - 'face': loadingState.response[index].face, - 'heroTag': Utils.makeHeroTag( - loadingState.response[index].uid) - }, - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - border: Border.all( - width: 1.5, - color: Theme.of(context).colorScheme.primary, - strokeAlign: BorderSide.strokeAlignOutside, + ? MediaQuery.removePadding( + context: context, + removeLeft: context.orientation == Orientation.landscape, + child: SelfSizedHorizontalList( + gapSize: 5, + childBuilder: (index) { + if (index == loadingState.response.length - 1) { + controller.fetchLiveFollowing(false); + } + return SizedBox( + width: 65, + child: GestureDetector( + onTap: () { + Get.toNamed( + '/liveRoom?roomid=${loadingState.response[index].roomId}', + ); + }, + onLongPress: () { + Feedback.forLongPress(context); + Get.toNamed( + '/member?mid=${loadingState.response[index].uid}', + arguments: { + 'face': loadingState.response[index].face, + 'heroTag': Utils.makeHeroTag( + loadingState.response[index].uid) + }, + ); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + border: Border.all( + width: 1.5, + color: Theme.of(context).colorScheme.primary, + strokeAlign: BorderSide.strokeAlignOutside, + ), + shape: BoxShape.circle, + ), + child: NetworkImgLayer( + type: 'avatar', + width: 45, + height: 45, + src: loadingState.response[index].face, ), - shape: BoxShape.circle, ), - child: NetworkImgLayer( - type: 'avatar', - width: 45, - height: 45, - src: loadingState.response[index].face, + const SizedBox(height: 4), + Text( + loadingState.response[index].uname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 12), + textAlign: TextAlign.center, ), - ), - const SizedBox(height: 2), - Text( - loadingState.response[index].uname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 11), - textAlign: TextAlign.center, - ), - ], + ], + ), ), - ), - ); - }, - itemCount: loadingState.response.length, + ); + }, + itemCount: loadingState.response.length, + ), ) : const SizedBox.shrink(), Error() => GestureDetector( diff --git a/lib/pages/search_panel/widgets/media_bangumi_panel.dart b/lib/pages/search_panel/widgets/media_bangumi_panel.dart index d9d56e07..98fb24b4 100644 --- a/lib/pages/search_panel/widgets/media_bangumi_panel.dart +++ b/lib/pages/search_panel/widgets/media_bangumi_panel.dart @@ -60,14 +60,13 @@ Widget searchBangumiPanel( height: 148, src: i.cover, ), - if (i.seasonTypeName?.isNotEmpty == true) - PBadge( - text: i.seasonTypeName, - top: 6.0, - right: 4.0, - bottom: null, - left: null, - ) + PBadge( + text: i.seasonTypeName, + top: 6.0, + right: 4.0, + bottom: null, + left: null, + ) ], ), const SizedBox(width: 10),