feat: pgc timeline

Closes #653

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-13 19:54:21 +08:00
parent d9c6c31a4d
commit 68df173558
19 changed files with 791 additions and 378 deletions

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PBadge extends StatelessWidget { class PBadge extends StatelessWidget {
@@ -17,7 +18,7 @@ class PBadge extends StatelessWidget {
const PBadge({ const PBadge({
super.key, super.key,
this.text, required this.text,
this.top, this.top,
this.right, this.right,
this.bottom, this.bottom,
@@ -34,15 +35,19 @@ class PBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (text.isNullOrEmpty) {
return const SizedBox.shrink();
}
ColorScheme t = Theme.of(context).colorScheme; ColorScheme t = Theme.of(context).colorScheme;
// 背景色 // 背景色
Color bgColor = t.primary; Color bgColor = t.primary;
// 前景色 // 前景色
Color color = t.onPrimary; Color color = t.onPrimary;
// 边框色 // 边框色
Color borderColor = Colors.transparent; Color? borderColor;
if (type == 'gray') { if (type == 'gray') {
bgColor = Colors.black54.withOpacity(0.45); bgColor = Colors.black45;
color = Colors.white; color = Colors.white;
} else if (type == 'color') { } else if (type == 'color') {
bgColor = t.secondaryContainer.withOpacity(0.5); bgColor = t.secondaryContainer.withOpacity(0.5);
@@ -72,10 +77,10 @@ class PBadge extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: br, borderRadius: br,
color: bgColor, color: bgColor,
border: Border.all(color: borderColor), border: borderColor != null ? Border.all(color: borderColor) : null,
), ),
child: Text( child: Text(
text ?? "", text!,
textScaler: textScaleFactor != null textScaler: textScaleFactor != null
? TextScaler.linear(textScaleFactor!) ? TextScaler.linear(textScaleFactor!)
: null, : null,

View File

@@ -141,11 +141,7 @@ class VideoCardH extends StatelessWidget {
width: maxWidth, width: maxWidth,
height: maxHeight, height: maxHeight,
), ),
if (videoItem is HotVideoItemModel && if (videoItem is HotVideoItemModel)
(videoItem as HotVideoItemModel)
.pgcLabel
?.isNotEmpty ==
true)
PBadge( PBadge(
text: text:
(videoItem as HotVideoItemModel).pgcLabel, (videoItem as HotVideoItemModel).pgcLabel,

View File

@@ -760,4 +760,6 @@ class Api {
static const String getLiveEmoticons = static const String getLiveEmoticons =
'${HttpString.liveBaseUrl}/xlive/web-ucenter/v2/emoticon/GetEmoticons'; '${HttpString.liveBaseUrl}/xlive/web-ucenter/v2/emoticon/GetEmoticons';
static const String pgcTimeline = '/pgc/web/timeline';
} }

View File

@@ -1,4 +1,6 @@
import 'package:PiliPlus/http/loading_state.dart'; 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/list.dart';
import '../models/bangumi/pgc_index/condition.dart'; import '../models/bangumi/pgc_index/condition.dart';
@@ -86,4 +88,24 @@ class BangumiHttp {
return LoadingState.error(res.data['message']); return LoadingState.error(res.data['message']);
} }
} }
static Future<LoadingState<List<Result>?>> pgcTimeline({
int types = 1, // 1`番剧`<br />3`电影`<br />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']);
}
}
} }

View File

@@ -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<String, dynamic> 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<String, dynamic>),
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<String, dynamic> 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,
};
}

View File

@@ -0,0 +1,16 @@
class IconFont {
String? name;
String? text;
IconFont({this.name, this.text});
factory IconFont.fromJson(Map<String, dynamic> json) => IconFont(
name: json['name'] as String?,
text: json['text'] as String?,
);
Map<String, dynamic> toJson() => {
'name': name,
'text': text,
};
}

View File

@@ -0,0 +1,23 @@
import 'result.dart';
class PgcTimeline {
int? code;
String? message;
List<Result>? result;
PgcTimeline({this.code, this.message, this.result});
factory PgcTimeline.fromJson(Map<String, dynamic> json) => PgcTimeline(
code: json['code'] as int?,
message: json['message'] as String?,
result: (json['result'] as List<dynamic>?)
?.map((e) => Result.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> toJson() => {
'code': code,
'message': message,
'result': result?.map((e) => e.toJson()).toList(),
};
}

View File

@@ -0,0 +1,35 @@
import 'episode.dart';
class Result {
String? date;
int? dateTs;
int? dayOfWeek;
List<Episode>? episodes;
int? isToday;
Result({
this.date,
this.dateTs,
this.dayOfWeek,
this.episodes,
this.isToday,
});
factory Result.fromJson(Map<String, dynamic> 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<dynamic>?)
?.map((e) => Episode.fromJson(e as Map<String, dynamic>))
.toList(),
isToday: json['is_today'] as int?,
);
Map<String, dynamic> toJson() => {
'date': date,
'date_ts': dateTs,
'day_of_week': dayOfWeek,
'episodes': episodes?.map((e) => e.toJson()).toList(),
'is_today': isToday,
};
}

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/list.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/models/common/tab_type.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
@@ -24,6 +25,9 @@ class BangumiController extends CommonListController<
queryData(); queryData();
queryBangumiFollow(); queryBangumiFollow();
if (tabType == TabType.bangumi) {
queryPgcTimeline();
}
if (isLogin.value) { if (isLogin.value) {
followController = ScrollController(); followController = ScrollController();
} }
@@ -36,16 +40,30 @@ class BangumiController extends CommonListController<
followEnd = false; followEnd = false;
} }
queryBangumiFollow(); queryBangumiFollow();
if (tabType == TabType.bangumi) {
queryPgcTimeline();
}
return super.onRefresh(); return super.onRefresh();
} }
// follow
late int followPage = 1; late int followPage = 1;
late RxInt followCount = (-1).obs; late RxInt followCount = (-1).obs;
late bool followLoading = false; late bool followLoading = false;
late bool followEnd = false; late bool followEnd = false;
late Rx<LoadingState> followState = LoadingState.loading().obs; late Rx<LoadingState<List<BangumiListItemModel>?>> followState =
LoadingState<List<BangumiListItemModel>?>.loading().obs;
ScrollController? followController; ScrollController? followController;
// timeline
late Rx<LoadingState<List<Result>?>> timelineState =
LoadingState<List<Result>?>.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 { Future queryBangumiFollow([bool isRefresh = true]) async {
if (isLogin.value.not || followLoading || (isRefresh.not && followEnd)) { if (isLogin.value.not || followLoading || (isRefresh.not && followEnd)) {

View File

@@ -1,12 +1,15 @@
import 'dart:async'; import 'dart:math';
import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/bangumi/list.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/models/common/tab_type.dart';
import 'package:PiliPlus/pages/bangumi/pgc_index/pgc_index_page.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/pages/common/common_page.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/constants.dart';
@@ -45,199 +48,229 @@ class _BangumiPageState extends CommonPageState<BangumiPage, BangumiController>
super.build(context); super.build(context);
return refreshIndicator( return refreshIndicator(
onRefresh: () async { onRefresh: () async {
await Future.wait([ await controller.onRefresh();
controller.onRefresh(),
controller.queryBangumiFollow(),
]);
}, },
child: CustomScrollView( child: CustomScrollView(
controller: controller.scrollController, controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverToBoxAdapter( _buildFollow,
child: Obx( if (widget.tabType == TabType.bangumi)
() => controller.isLogin.value SliverToBoxAdapter(
? Column( child: Container(
children: [ margin: const EdgeInsets.only(top: 10),
Padding( height: Grid.smallCardWidth / 2 / 0.75 +
padding: const EdgeInsets.only(left: 16), MediaQuery.textScalerOf(context).scale(110),
child: Row( child:
children: [ Obx(() => _buildTimeline(controller.timelineState.value)),
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<String> titles = const [
'全部',
'电影',
'电视剧',
'纪录片',
'综艺',
];
List<int> 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,
),
],
),
),
),
],
), ),
), ),
), ..._buildRcmd,
SliverPadding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: Obx(
() => _buildBody(controller.loadingState.value),
),
),
], ],
), ),
); );
} }
Widget _buildBody(LoadingState<List<BangumiListItemModel>?> loadingState) { late final List<String> weekList = [
'',
'',
'',
'',
'',
'',
'',
];
Widget _buildTimeline(LoadingState<List<Result>?> 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<Widget> 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<String> titles = const [
'全部',
'电影',
'电视剧',
'纪录片',
'综艺',
];
List<int> 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<List<BangumiListItemModel>?> loadingState) {
return switch (loadingState) { return switch (loadingState) {
Loading() => const SliverToBoxAdapter(), Loading() => const SliverToBoxAdapter(),
Success() => loadingState.response?.isNotEmpty == true Success() => loadingState.response?.isNotEmpty == true
@@ -274,36 +307,122 @@ class _BangumiPageState extends CommonPageState<BangumiPage, BangumiController>
}; };
} }
Widget _buildFollowList(Success loadingState) { Widget get _buildFollow => SliverToBoxAdapter(
return ListView.builder( child: Obx(
controller: controller.followController, () => controller.isLogin.value
scrollDirection: Axis.horizontal, ? Column(
itemCount: loadingState.response.length, children: [
itemBuilder: (context, index) { _buildFollowTitle,
if (index == loadingState.response.length - 1) { SizedBox(
controller.queryBangumiFollow(false); height: Grid.smallCardWidth / 2 / 0.75 +
} MediaQuery.textScalerOf(context).scale(50),
return Container( child: Obx(
width: Grid.smallCardWidth / 2, () => _buildFollowBody(controller.followState.value),
margin: EdgeInsets.only( ),
left: StyleString.safeSpace, ),
right: index == loadingState.response.length - 1 ],
? StyleString.safeSpace )
: 0, : const SizedBox.shrink(),
), ),
child: BangumiCardV( );
bangumiItem: loadingState.response[index],
),
);
},
);
}
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<List<BangumiListItemModel>?> loadingState) {
return switch (loadingState) { return switch (loadingState) {
Loading() => loadingWidget, Loading() => loadingWidget,
Success() => (loadingState.response as List?)?.isNotEmpty == true Success() => loadingState.response?.isNotEmpty == true
? _buildFollowList(loadingState) ? 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( : Center(
child: Text( child: Text(
'还没有${widget.tabType == TabType.bangumi ? '追番' : '追剧'}')), '还没有${widget.tabType == TabType.bangumi ? '追番' : '追剧'}')),

View File

@@ -31,6 +31,7 @@ class BangumiCardV extends StatelessWidget {
Utils.viewBangumi(seasonId: seasonId); Utils.viewBangumi(seasonId: seasonId);
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: StyleString.mdRadius, borderRadius: StyleString.mdRadius,
@@ -49,14 +50,13 @@ class BangumiCardV extends StatelessWidget {
height: maxHeight, height: maxHeight,
), ),
), ),
if (bangumiItem.badge != null) PBadge(
PBadge( text: bangumiItem.badge,
text: bangumiItem.badge, top: 6,
top: 6, right: 6,
right: 6, bottom: null,
bottom: null, left: null,
left: null, ),
),
if (bangumiItem.isFinish == 0 && if (bangumiItem.isFinish == 0 &&
bangumiItem.renewalTime?.isNotEmpty == true) bangumiItem.renewalTime?.isNotEmpty == true)
PBadge( PBadge(
@@ -97,19 +97,14 @@ class BangumiCardV extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Text(
children: [ bangumiItem.title,
Expanded( textAlign: TextAlign.start,
child: Text( style: const TextStyle(
bangumiItem.title, letterSpacing: 0.3,
textAlign: TextAlign.start, ),
style: const TextStyle( maxLines: 1,
letterSpacing: 0.3, overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
],
), ),
const SizedBox(height: 1), const SizedBox(height: 1),
if (bangumiItem.indexShow != null) if (bangumiItem.indexShow != null)

View File

@@ -31,6 +31,7 @@ class BangumiCardVMemberHome extends StatelessWidget {
cover: bangumiItem.cover, cover: bangumiItem.cover,
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -94,19 +95,14 @@ Widget bangumiContent(Item bangumiItem) {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Text(
children: [ bangumiItem.title,
Expanded( textAlign: TextAlign.start,
child: Text( style: const TextStyle(
bangumiItem.title, letterSpacing: 0.3,
textAlign: TextAlign.start, ),
style: const TextStyle( maxLines: 2,
letterSpacing: 0.3, overflow: TextOverflow.ellipsis,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)),
],
), ),
const SizedBox(height: 1), const SizedBox(height: 1),
// if (bangumiItem.indexShow != null) // if (bangumiItem.indexShow != null)

View File

@@ -29,6 +29,7 @@ class BangumiCardVPgcIndex extends StatelessWidget {
Utils.viewBangumi(seasonId: bangumiItem['season_id']); Utils.viewBangumi(seasonId: bangumiItem['season_id']);
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: StyleString.mdRadius, borderRadius: StyleString.mdRadius,
@@ -44,25 +45,21 @@ class BangumiCardVPgcIndex extends StatelessWidget {
width: maxWidth, width: maxWidth,
height: maxHeight, height: maxHeight,
), ),
if (bangumiItem['badge'] != null && PBadge(
bangumiItem['badge'] != '') text: bangumiItem['badge'],
PBadge( top: 6,
text: bangumiItem['badge'], right: 6,
top: 6, bottom: null,
right: 6, left: null,
bottom: null, ),
left: null, PBadge(
), text: bangumiItem['order'],
if (bangumiItem['order'] != null && top: null,
bangumiItem['order'] != '') right: null,
PBadge( bottom: 6,
text: bangumiItem['order'], left: 6,
top: null, type: 'gray',
right: null, ),
bottom: 6,
left: 6,
type: 'gray',
),
], ],
); );
}), }),
@@ -86,19 +83,14 @@ class BangumiCardVPgcIndex extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row( Text(
children: [ bangumiItem['title'],
Expanded( textAlign: TextAlign.start,
child: Text( style: const TextStyle(
bangumiItem['title'], letterSpacing: 0.3,
textAlign: TextAlign.start, ),
style: const TextStyle( maxLines: 1,
letterSpacing: 0.3, overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
],
), ),
const SizedBox(height: 1), const SizedBox(height: 1),
if (bangumiItem['index_show'] != null) if (bangumiItem['index_show'] != null)

View File

@@ -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,
),
),
],
),
),
);
}
}

View File

@@ -516,15 +516,12 @@ Widget forWard(bool isSave, item, BuildContext context, source, callback,
src: item.modules.moduleDynamic.major.medialist['cover'], src: item.modules.moduleDynamic.major.medialist['cover'],
), ),
), ),
if (item.modules.moduleDynamic.major.medialist['badge'] PBadge(
?['text'] != right: 6,
null) top: 6,
PBadge( text: item.modules.moduleDynamic.major.medialist['badge']
right: 6, ?['text'],
top: 6, )
text: item.modules.moduleDynamic.major.medialist['badge']
['text'],
)
], ],
), ),
const SizedBox(width: 14), const SizedBox(width: 14),

View File

@@ -65,17 +65,16 @@ class FavPgcItem extends StatelessWidget {
width: boxConstraints.maxWidth, width: boxConstraints.maxWidth,
height: boxConstraints.maxHeight, height: boxConstraints.maxHeight,
), ),
if (item.badge?.isNotEmpty == true) PBadge(
PBadge( right: 4,
right: 4, top: 4,
top: 4, text: item.badge,
text: item.badge, fs: 10,
fs: 10, padding: const EdgeInsets.symmetric(
padding: const EdgeInsets.symmetric( horizontal: 2,
horizontal: 2, vertical: 1,
vertical: 1,
),
), ),
),
Positioned.fill( Positioned.fill(
child: IgnorePointer( child: IgnorePointer(
child: LayoutBuilder( child: LayoutBuilder(

View File

@@ -123,14 +123,13 @@ class FavVideoCardH extends StatelessWidget {
bottom: 6.0, bottom: 6.0,
type: 'gray', type: 'gray',
), ),
if (videoItem.ogv != null) PBadge(
PBadge( text: videoItem.ogv?['type_name'],
text: videoItem.ogv!['type_name'], top: 6.0,
top: 6.0, right: 6.0,
right: 6.0, bottom: null,
bottom: null, left: null,
left: null, ),
),
], ],
); );
}, },

View File

@@ -186,67 +186,71 @@ class _LivePageState extends CommonPageState<LivePage, LiveController>
child: loadingWidget, child: loadingWidget,
), ),
Success() => (loadingState.response as List?)?.isNotEmpty == true Success() => (loadingState.response as List?)?.isNotEmpty == true
? SelfSizedHorizontalList( ? MediaQuery.removePadding(
gapSize: 5, context: context,
childBuilder: (index) { removeLeft: context.orientation == Orientation.landscape,
if (index == loadingState.response.length - 1) { child: SelfSizedHorizontalList(
controller.fetchLiveFollowing(false); gapSize: 5,
} childBuilder: (index) {
return SizedBox( if (index == loadingState.response.length - 1) {
width: 65, controller.fetchLiveFollowing(false);
child: GestureDetector( }
onTap: () { return SizedBox(
Get.toNamed( width: 65,
'/liveRoom?roomid=${loadingState.response[index].roomId}', child: GestureDetector(
); onTap: () {
}, Get.toNamed(
onLongPress: () { '/liveRoom?roomid=${loadingState.response[index].roomId}',
Feedback.forLongPress(context); );
Get.toNamed( },
'/member?mid=${loadingState.response[index].uid}', onLongPress: () {
arguments: { Feedback.forLongPress(context);
'face': loadingState.response[index].face, Get.toNamed(
'heroTag': Utils.makeHeroTag( '/member?mid=${loadingState.response[index].uid}',
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), child: Column(
Container( mainAxisSize: MainAxisSize.min,
margin: const EdgeInsets.all(2), children: [
padding: const EdgeInsets.all(2), const SizedBox(height: 8),
decoration: BoxDecoration( Container(
border: Border.all( margin: const EdgeInsets.all(2),
width: 1.5, padding: const EdgeInsets.all(2),
color: Theme.of(context).colorScheme.primary, decoration: BoxDecoration(
strokeAlign: BorderSide.strokeAlignOutside, 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( const SizedBox(height: 4),
type: 'avatar', Text(
width: 45, loadingState.response[index].uname,
height: 45, maxLines: 1,
src: loadingState.response[index].face, 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(), : const SizedBox.shrink(),
Error() => GestureDetector( Error() => GestureDetector(

View File

@@ -60,14 +60,13 @@ Widget searchBangumiPanel(
height: 148, height: 148,
src: i.cover, src: i.cover,
), ),
if (i.seasonTypeName?.isNotEmpty == true) PBadge(
PBadge( text: i.seasonTypeName,
text: i.seasonTypeName, top: 6.0,
top: 6.0, right: 4.0,
right: 4.0, bottom: null,
bottom: null, left: null,
left: null, )
)
], ],
), ),
const SizedBox(width: 10), const SizedBox(width: 10),