Drama 新增 tagCode,推荐页只看 NEW_ARRIVAL 这个码决定上新横滑, 不再依赖可变的标签文案;分类网格高度改为复用卡片的统一常量。 头部轮播改用虚拟无限页码,首尾相接且能反向滑,自动播放不再倒滑回第一张。
This commit is contained in:
1 parent
c311985601
commit
3590cd0c55
5 files changed
+235
-73
No files matched your search
@@ -138,8 +138,7 @@ class _RecommendDiscoverPageState extends State<RecommendDiscoverPage>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openDrama(Drama drama) =>
|
||||
playDramaFromCatalog(context, drama);
|
||||
Future<void> _openDrama(Drama drama) => playDramaFromCatalog(context, drama);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -163,9 +162,7 @@ class _RecommendDiscoverPageState extends State<RecommendDiscoverPage>
|
||||
Positioned.fill(
|
||||
child: categoryList.isEmpty
|
||||
? const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.brandRed,
|
||||
),
|
||||
child: CircularProgressIndicator(color: AppColors.brandRed),
|
||||
)
|
||||
: PageView.builder(
|
||||
key: const Key('recommend-channel-pager'),
|
||||
@@ -184,7 +181,8 @@ class _RecommendDiscoverPageState extends State<RecommendDiscoverPage>
|
||||
carouselActive: widget.active && index == selectedIndex,
|
||||
mediaTop: mediaTop,
|
||||
onRequest: _ensureCategory,
|
||||
onRefresh: () => _refreshCategory(category.id, index == 0),
|
||||
onRefresh: () =>
|
||||
_refreshCategory(category.id, index == 0),
|
||||
onOpenDrama: _openDrama,
|
||||
);
|
||||
},
|
||||
@@ -294,11 +292,7 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
List<Drama> newArrivals = const [];
|
||||
List<Drama> rest = dramas;
|
||||
if (widget.isFeatured && dramas.isNotEmpty) {
|
||||
final lang = Localizations.localeOf(context).languageCode;
|
||||
final badge = AppLocalizations.of(context)!.newArrivalBadge;
|
||||
newArrivals = dramas
|
||||
.where((drama) => _isNewArrivalTag(drama, lang, badge))
|
||||
.toList(growable: false);
|
||||
newArrivals = dramas.where(isNewArrivalTag).toList(growable: false);
|
||||
if (newArrivals.isNotEmpty) {
|
||||
final newIds = newArrivals.map((drama) => drama.id).toSet();
|
||||
rest = dramas
|
||||
@@ -326,7 +320,9 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
hasScrollBody: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: showCarousel ? 0 : widget.mediaTop + RecommendChannelNav.height,
|
||||
top: showCarousel
|
||||
? 0
|
||||
: widget.mediaTop + RecommendChannelNav.height,
|
||||
),
|
||||
child: Center(child: _placeholder(slot.loading)),
|
||||
),
|
||||
@@ -334,12 +330,11 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
);
|
||||
} else if (widget.isFeatured) {
|
||||
// 无轮播图时内容需避开顶部悬浮导航。
|
||||
final contentTop =
|
||||
showCarousel ? 24.0 : widget.mediaTop + RecommendChannelNav.height + 16;
|
||||
final contentTop = showCarousel
|
||||
? 24.0
|
||||
: widget.mediaTop + RecommendChannelNav.height + 16;
|
||||
if (newArrivals.isNotEmpty) {
|
||||
sliverList.add(
|
||||
SliverToBoxAdapter(child: SizedBox(height: contentTop)),
|
||||
);
|
||||
sliverList.add(SliverToBoxAdapter(child: SizedBox(height: contentTop)));
|
||||
sliverList.add(
|
||||
SliverToBoxAdapter(
|
||||
child: HomeNewArrivalRow(
|
||||
@@ -348,9 +343,7 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
),
|
||||
),
|
||||
);
|
||||
sliverList.add(
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 28)),
|
||||
);
|
||||
sliverList.add(const SliverToBoxAdapter(child: SizedBox(height: 28)));
|
||||
}
|
||||
sliverList.add(
|
||||
_gridSliver(
|
||||
@@ -363,7 +356,9 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
sliverList.add(
|
||||
_gridSliver(
|
||||
dramas,
|
||||
topPadding: showCarousel ? 20 : widget.mediaTop + RecommendChannelNav.height + 16,
|
||||
topPadding: showCarousel
|
||||
? 20
|
||||
: widget.mediaTop + RecommendChannelNav.height + 16,
|
||||
crossAxisCount: 3,
|
||||
),
|
||||
);
|
||||
@@ -427,7 +422,6 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
const pageSide = 14.0;
|
||||
const cardGap = 9.0;
|
||||
const rowGap = 16.0;
|
||||
const cardBodyExtent = 44.0; // 封面下间距 + 剧名行 + 集数行
|
||||
// 两列时卡更大,卡间距略放宽。
|
||||
final spacing = crossAxisCount == 2 ? 12.0 : cardGap;
|
||||
return SliverPadding(
|
||||
@@ -437,7 +431,7 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
final itemWidth =
|
||||
(constraints.crossAxisExtent - spacing * (crossAxisCount - 1)) /
|
||||
crossAxisCount;
|
||||
final itemExtent = itemWidth * 157 / 110 + cardBodyExtent;
|
||||
final itemExtent = itemWidth * 157 / 110 + homePosterCardBodyExtent;
|
||||
return SliverGrid(
|
||||
key: Key('recommend-category-grid-${widget.categoryId}'),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
@@ -446,17 +440,14 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
mainAxisSpacing: rowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final drama = dramas[index];
|
||||
return HomePosterCard(
|
||||
key: Key('recommend-category-card-${drama.id}'),
|
||||
drama: drama,
|
||||
onTap: () => widget.onOpenDrama(drama),
|
||||
);
|
||||
},
|
||||
childCount: dramas.length,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final drama = dramas[index];
|
||||
return HomePosterCard(
|
||||
key: Key('recommend-category-card-${drama.id}'),
|
||||
drama: drama,
|
||||
onTap: () => widget.onOpenDrama(drama),
|
||||
);
|
||||
}, childCount: dramas.length),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -464,14 +455,11 @@ class _ChannelFeedState extends State<_ChannelFeed>
|
||||
}
|
||||
}
|
||||
|
||||
/// 判定接口下发的短剧标签是否为「上新」。
|
||||
/// 「上新」标签语义码,对应后台 content_tag.code。
|
||||
const String newArrivalTagCode = 'NEW_ARRIVAL';
|
||||
|
||||
/// 判定短剧是否属于「上新」板块。
|
||||
///
|
||||
/// 完全以后端返回的 `tag` / `tagLocales`(经 [Drama.localizedTag] 本地化)
|
||||
/// 为准;兼容「上新 / NEW / BARU」三种文案,并兼容「本周上新」这类带前后缀的写法。
|
||||
bool _isNewArrivalTag(Drama drama, String lang, String localizedBadge) {
|
||||
final tag = drama.localizedTag(lang).trim();
|
||||
if (tag.isEmpty) return false;
|
||||
if (tag == localizedBadge) return true;
|
||||
final lower = tag.toLowerCase();
|
||||
return tag.contains('上新') || lower == 'new' || lower == 'baru';
|
||||
}
|
||||
/// 只看语义码:码由后台标签管理配置,文案怎么改、切什么语言都不影响。
|
||||
bool isNewArrivalTag(Drama drama) =>
|
||||
drama.tagCode.trim().toUpperCase() == newArrivalTagCode;
|
||||
@@ -46,15 +46,25 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
|
||||
static const double _dotGap = 9;
|
||||
static const double _ctaHeight = 38;
|
||||
|
||||
final PageController _controller = PageController();
|
||||
/// 无限轮播:PageView 用虚拟大页码,取模映射到真实 banner,首尾天然相接。
|
||||
static const int _basePage = 1000;
|
||||
|
||||
late final PageController _controller;
|
||||
Timer? _timer;
|
||||
int _page = 0;
|
||||
int _index = 0;
|
||||
bool _pressed = false;
|
||||
bool _tickerEnabled = true;
|
||||
|
||||
/// 起始页码取 banner 数量的大倍数,向左右滑都不会到头。
|
||||
static int _initialPageOf(int bannerCount) =>
|
||||
bannerCount > 1 ? bannerCount * _basePage : 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_page = _initialPageOf(widget.banners.length);
|
||||
_controller = PageController(initialPage: _page);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _syncTimer());
|
||||
}
|
||||
|
||||
@@ -72,8 +82,11 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
|
||||
void didUpdateWidget(covariant HomeHeroCarousel oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.banners.length != widget.banners.length) {
|
||||
if (_index >= widget.banners.length) _index = 0;
|
||||
_syncTimer();
|
||||
// 首屏 banner 是异步到的:列表从空变有之后要把页码挪到虚拟起点,
|
||||
// 否则左滑第一下就到头了。
|
||||
_page = _initialPageOf(widget.banners.length);
|
||||
_index = 0;
|
||||
_jumpToBasePage();
|
||||
}
|
||||
if (oldWidget.banners.length != widget.banners.length ||
|
||||
oldWidget.active != widget.active) {
|
||||
@@ -91,7 +104,8 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
|
||||
void _syncTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
final canAdvance = mounted &&
|
||||
final canAdvance =
|
||||
mounted &&
|
||||
widget.active &&
|
||||
_tickerEnabled &&
|
||||
!_pressed &&
|
||||
@@ -102,12 +116,16 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
|
||||
|
||||
void _advance() {
|
||||
if (!_controller.hasClients || widget.banners.length < 2) return;
|
||||
final next = (_index + 1) % widget.banners.length;
|
||||
_controller.animateToPage(
|
||||
next,
|
||||
duration: _pageDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
// 虚拟页码无限,直接翻下一页即可,不用在末尾跳回第 0 页。
|
||||
_controller.nextPage(duration: _pageDuration, curve: Curves.easeOutCubic);
|
||||
}
|
||||
|
||||
/// 把 PageView 静默挪到虚拟起点:视觉上是同一张图,不会闪。
|
||||
void _jumpToBasePage() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || !_controller.hasClients) return;
|
||||
if (_controller.page?.round() != _page) _controller.jumpToPage(_page);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -134,9 +152,17 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _controller,
|
||||
itemCount: widget.banners.length,
|
||||
onPageChanged: (value) => setState(() => _index = value),
|
||||
itemBuilder: (context, index) => _bannerPage(context, index),
|
||||
// 数量大于 1 时用无限页码做无缝循环,取模映射到真实 banner。
|
||||
itemCount: widget.banners.length > 1
|
||||
? null
|
||||
: widget.banners.length,
|
||||
onPageChanged: (value) => setState(() {
|
||||
_page = value;
|
||||
final count = widget.banners.length;
|
||||
_index = count == 0 ? 0 : value % count;
|
||||
}),
|
||||
itemBuilder: (context, page) =>
|
||||
_bannerPage(context, page % widget.banners.length),
|
||||
),
|
||||
Positioned(left: 0, right: 0, bottom: 14, child: _dots(context)),
|
||||
],
|
||||
@@ -288,16 +314,18 @@ class _HeroCopy extends StatelessWidget {
|
||||
color: AppColors.textPrimary.withValues(alpha: 0.95),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w400,
|
||||
shadows: const [
|
||||
Shadow(color: Color(0x80000000), blurRadius: 8),
|
||||
],
|
||||
shadows: const [Shadow(color: Color(0x80000000), blurRadius: 8)],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_WatchButton(height: ctaHeight, label: l10n.watchNowAction, onTap: onWatch),
|
||||
_WatchButton(
|
||||
height: ctaHeight,
|
||||
label: l10n.watchNowAction,
|
||||
onTap: onWatch,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_FavoriteCircle(size: ctaHeight, drama: drama),
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user