fix(home): 调整首页分类与卡片标签样式
This commit is contained in:
1 parent
7446431511
commit
4f6116896f
14 files changed
+251
-117
No files matched your search
@@ -1111,7 +1111,7 @@ $ flutter test
|
|||||||
|
|
||||||
## 2026-10-22 首页布局对齐修正
|
## 2026-10-22 首页布局对齐修正
|
||||||
|
|
||||||
- 一级分类改为页内切换 6 个列表,选中 18/w700 白字、未选中 15/w400 灰字,无下划线。
|
- 一级分类改为页内切换 6 个列表,未选 16/w400 灰字、选中 20/w700 白字,无下划线;二级 chips 仅精选显示。
|
||||||
- 二级 chips 与一级行左对齐可滑动;未选项纯文字,选中项深灰胶囊。
|
- 四类短剧卡片移除底板/描边,封面统一 8 圆角,标题左对齐,题材移至封面右上角异形彩色丝带。
|
||||||
- 首页、剧单、全部短剧、追剧卡片移除底板/描边,标题与题材从封面左边起始,题材改为 `|` 分隔纯文字。
|
- 播放量统一 spark 图标与 `w` 格式,整数无小数;首页、剧单、全部短剧、追剧共用。
|
||||||
- 全部短剧的地区、字幕、配音筛选 UI 已占位;以下接口待补:地区、字幕/配音筛选数据。
|
- 全部短剧的地区、字幕、配音筛选 UI 已占位;以下接口待补:地区、字幕/配音筛选数据。
|
||||||
@@ -7,3 +7,11 @@ String formatInteractionCount(int count) {
|
|||||||
if (count < 10000) return '$count';
|
if (count < 10000) return '$count';
|
||||||
return '${(count / 10000).toStringAsFixed(1)}万';
|
return '${(count / 10000).toStringAsFixed(1)}万';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 播放量展示:以 w 为单位,整数不保留小数,非整数保留一位。
|
||||||
|
String formatPlayCount(int count) {
|
||||||
|
if (count < 10000) return '$count';
|
||||||
|
final value = count / 10000;
|
||||||
|
if (value == value.roundToDouble()) return '${value.toInt()}w';
|
||||||
|
return '${value.toStringAsFixed(1)}w';
|
||||||
|
}
|
||||||
@@ -81,11 +81,16 @@ abstract final class AppDimens {
|
|||||||
static const double homeGridGap = 6;
|
static const double homeGridGap = 6;
|
||||||
static const double homeGridRowGap = 18;
|
static const double homeGridRowGap = 18;
|
||||||
static const double homeCardRadius = 6;
|
static const double homeCardRadius = 6;
|
||||||
|
static const double cardCoverRadius = 8;
|
||||||
static const double homeCardPadding = 8;
|
static const double homeCardPadding = 8;
|
||||||
static const double cardTitleFont = 15;
|
static const double cardTitleFont = 15;
|
||||||
static const double cardTitleLineHeight = 1.3;
|
static const double cardTitleLineHeight = 1.3;
|
||||||
static const double cardViewsFont = 11;
|
static const double cardViewsFont = 11;
|
||||||
static const double cardViewsIcon = 11;
|
static const double cardViewsIcon = 11;
|
||||||
|
static const double cardGenreRibbonHeight = 20;
|
||||||
|
static const double cardGenreRibbonRadius = 10;
|
||||||
|
static const double cardGenreRibbonLeftInset = 8;
|
||||||
|
static const double cardGenreRibbonMaxWidth = 86;
|
||||||
static const double cardTagFont = 11;
|
static const double cardTagFont = 11;
|
||||||
static const double cardTagHeight = 18;
|
static const double cardTagHeight = 18;
|
||||||
static const double cardTagRadius = 9;
|
static const double cardTagRadius = 9;
|
||||||
@@ -98,8 +103,8 @@ abstract final class AppDimens {
|
|||||||
static const double cardTitleTagGap = 6;
|
static const double cardTitleTagGap = 6;
|
||||||
static const double cardCoverAspectRatio = 5 / 6;
|
static const double cardCoverAspectRatio = 5 / 6;
|
||||||
static const double homeCardBodyExtent = 84;
|
static const double homeCardBodyExtent = 84;
|
||||||
static const double homePrimaryTabFont = 15;
|
static const double homePrimaryTabFont = 16;
|
||||||
static const double homePrimaryTabSelectedFont = 18;
|
static const double homePrimaryTabSelectedFont = 20;
|
||||||
static const double homePrimaryRowHeight = 30;
|
static const double homePrimaryRowHeight = 30;
|
||||||
static const Duration homePrimaryTabAnimationDuration = Duration(
|
static const Duration homePrimaryTabAnimationDuration = Duration(
|
||||||
milliseconds: 180,
|
milliseconds: 180,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/app_colors.dart';
|
||||||
|
import '../theme/app_dimens.dart';
|
||||||
|
|
||||||
|
const List<Color> _ribbonColors = [
|
||||||
|
AppColors.badgeHot,
|
||||||
|
AppColors.badgeNew,
|
||||||
|
AppColors.badgeAnime,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 封面右上角的斜边题材丝带。
|
||||||
|
class DramaGenreRibbon extends StatelessWidget {
|
||||||
|
const DramaGenreRibbon({
|
||||||
|
super.key,
|
||||||
|
required this.tags,
|
||||||
|
required this.keyPrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<String> tags;
|
||||||
|
final String keyPrefix;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final visible = tags
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.where((tag) => tag.isNotEmpty)
|
||||||
|
.take(3)
|
||||||
|
.toList(growable: false);
|
||||||
|
if (visible.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final (index, tag) in visible.indexed) ...[
|
||||||
|
if (index > 0) const SizedBox(height: 2),
|
||||||
|
ClipPath(
|
||||||
|
key: Key('drama-genre-ribbon-$keyPrefix-$index'),
|
||||||
|
clipper: const _SlantedRibbonClipper(),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: AppDimens.cardGenreRibbonMaxWidth,
|
||||||
|
),
|
||||||
|
height: AppDimens.cardGenreRibbonHeight,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left:
|
||||||
|
AppDimens.cardTagHorizontalPadding +
|
||||||
|
AppDimens.cardGenreRibbonLeftInset,
|
||||||
|
right: AppDimens.cardTagHorizontalPadding,
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: _ribbonColors[index % _ribbonColors.length],
|
||||||
|
child: Text(
|
||||||
|
tag,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
fontSize: AppDimens.cardBadgeFont,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlantedRibbonClipper extends CustomClipper<Path> {
|
||||||
|
const _SlantedRibbonClipper();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Path getClip(Size size) {
|
||||||
|
return Path()
|
||||||
|
..moveTo(AppDimens.cardGenreRibbonLeftInset, 0)
|
||||||
|
..lineTo(size.width, 0)
|
||||||
|
..lineTo(size.width, size.height)
|
||||||
|
..lineTo(0, size.height)
|
||||||
|
..close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReclip(_SlantedRibbonClipper oldClipper) => false;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import '../../core/formatting/count_format.dart';
|
import '../../core/formatting/count_format.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../core/theme/app_dimens.dart';
|
import '../../core/theme/app_dimens.dart';
|
||||||
|
import '../../core/widgets/drama_genre_ribbon.dart';
|
||||||
import '../../core/widgets/remote_image.dart';
|
import '../../core/widgets/remote_image.dart';
|
||||||
|
|
||||||
class FollowingGridCard extends StatelessWidget {
|
class FollowingGridCard extends StatelessWidget {
|
||||||
@@ -37,7 +38,7 @@ class FollowingGridCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
key: Key('following-card-$dramaId'),
|
key: Key('following-card-$dramaId'),
|
||||||
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
|
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -52,7 +53,7 @@ class FollowingGridCard extends StatelessWidget {
|
|||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
AppDimens.homeCardRadius,
|
AppDimens.cardCoverRadius,
|
||||||
),
|
),
|
||||||
child: RemoteImage(
|
child: RemoteImage(
|
||||||
posterUrl,
|
posterUrl,
|
||||||
@@ -78,12 +79,12 @@ class FollowingGridCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.play_arrow_rounded,
|
Icons.auto_awesome_rounded,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
size: AppDimens.cardViewsIcon,
|
size: AppDimens.cardViewsIcon,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
formatInteractionCount(viewCount),
|
formatPlayCount(viewCount),
|
||||||
key: Key('following-views-$dramaId'),
|
key: Key('following-views-$dramaId'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
@@ -94,6 +95,12 @@ class FollowingGridCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (!editing)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: DramaGenreRibbon(tags: tags, keyPrefix: dramaId),
|
||||||
|
),
|
||||||
if (editing)
|
if (editing)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -6,
|
top: -6,
|
||||||
@@ -128,10 +135,6 @@ class FollowingGridCard extends StatelessWidget {
|
|||||||
height: AppDimens.cardTitleLineHeight,
|
height: AppDimens.cardTitleLineHeight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (tags.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
|
||||||
_tags(),
|
|
||||||
],
|
|
||||||
const SizedBox(height: AppDimens.spaceXs),
|
const SizedBox(height: AppDimens.spaceXs),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -161,19 +164,4 @@ class FollowingGridCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _tags() {
|
|
||||||
final visible = tags.take(3).toList(growable: false);
|
|
||||||
return Text(
|
|
||||||
visible.join(' | '),
|
|
||||||
key: Key('following-tags-$dramaId'),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
fontSize: AppDimens.cardTagFont,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -339,7 +339,7 @@ List<String> _dramaTags(Drama? drama) {
|
|||||||
.expand((value) => value.split(_tagSplitter))
|
.expand((value) => value.split(_tagSplitter))
|
||||||
.map((value) => value.trim())
|
.map((value) => value.trim())
|
||||||
.where((value) => value.isNotEmpty && seen.add(value))
|
.where((value) => value.isNotEmpty && seen.add(value))
|
||||||
.take(2)
|
.take(3)
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../../core/data/models/drama.dart';
|
|||||||
import '../../../core/formatting/count_format.dart';
|
import '../../../core/formatting/count_format.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_dimens.dart';
|
import '../../../core/theme/app_dimens.dart';
|
||||||
|
import '../../../core/widgets/drama_genre_ribbon.dart';
|
||||||
import '../../../core/widgets/remote_image.dart';
|
import '../../../core/widgets/remote_image.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import 'home_drama_classification.dart';
|
import 'home_drama_classification.dart';
|
||||||
@@ -33,7 +34,7 @@ class HomeFeedCard extends StatelessWidget {
|
|||||||
key: Key('home-feed-card-${drama.id}'),
|
key: Key('home-feed-card-${drama.id}'),
|
||||||
color: AppColors.transparent,
|
color: AppColors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
|
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -62,8 +63,6 @@ class HomeFeedCard extends StatelessWidget {
|
|||||||
height: AppDimens.cardTitleLineHeight,
|
height: AppDimens.cardTitleLineHeight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
|
||||||
_tags(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -76,30 +75,39 @@ class HomeFeedCard extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _cover(AppLocalizations l10n) {
|
Widget _cover(AppLocalizations l10n) {
|
||||||
final badge = _badge();
|
final badge = _badge();
|
||||||
return AspectRatio(
|
return ClipRRect(
|
||||||
aspectRatio: AppDimens.cardCoverAspectRatio,
|
key: Key('home-feed-cover-clip-${drama.id}'),
|
||||||
child: Stack(
|
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
fit: StackFit.expand,
|
child: AspectRatio(
|
||||||
children: [
|
aspectRatio: AppDimens.cardCoverAspectRatio,
|
||||||
RemoteImage(
|
child: Stack(
|
||||||
drama.posterUrl,
|
fit: StackFit.expand,
|
||||||
key: Key('home-feed-cover-${drama.id}'),
|
children: [
|
||||||
fit: BoxFit.cover,
|
RemoteImage(
|
||||||
showFallbackIcon: true,
|
drama.posterUrl,
|
||||||
fallbackColor: AppColors.contentSurface,
|
key: Key('home-feed-cover-${drama.id}'),
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
if (badge != null)
|
showFallbackIcon: true,
|
||||||
Positioned(
|
fallbackColor: AppColors.contentSurface,
|
||||||
top: AppDimens.spaceXs,
|
|
||||||
left: AppDimens.spaceXs,
|
|
||||||
child: _badgeChip(l10n, badge),
|
|
||||||
),
|
),
|
||||||
Positioned(
|
if (badge != null)
|
||||||
left: AppDimens.spaceXs,
|
Positioned(
|
||||||
bottom: AppDimens.spaceXs,
|
top: AppDimens.spaceXs,
|
||||||
child: _views(),
|
left: AppDimens.spaceXs,
|
||||||
),
|
child: _badgeChip(l10n, badge),
|
||||||
],
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: DramaGenreRibbon(tags: _cardTags(), keyPrefix: drama.id),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: AppDimens.spaceXs,
|
||||||
|
bottom: AppDimens.spaceXs,
|
||||||
|
child: _views(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -155,13 +163,13 @@ class HomeFeedCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.play_arrow_rounded,
|
Icons.auto_awesome_rounded,
|
||||||
size: AppDimens.cardViewsIcon,
|
size: AppDimens.cardViewsIcon,
|
||||||
color: AppColors.navSelected,
|
color: AppColors.navSelected,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 1),
|
const SizedBox(width: 1),
|
||||||
Text(
|
Text(
|
||||||
formatInteractionCount(drama.viewCount),
|
formatPlayCount(drama.viewCount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.navSelected,
|
color: AppColors.navSelected,
|
||||||
fontSize: AppDimens.cardViewsFont,
|
fontSize: AppDimens.cardViewsFont,
|
||||||
@@ -174,23 +182,6 @@ class HomeFeedCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _tags() {
|
|
||||||
final tags = _cardTags();
|
|
||||||
if (tags.isEmpty) return const SizedBox.shrink();
|
|
||||||
return Text(
|
|
||||||
tags.join(' | '),
|
|
||||||
key: Key('home-feed-tags-${drama.id}'),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
fontSize: AppDimens.cardTagFont,
|
|
||||||
letterSpacing: 0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _cardTags() {
|
List<String> _cardTags() {
|
||||||
final seen = <String>{};
|
final seen = <String>{};
|
||||||
return [drama.genre, drama.category]
|
return [drama.genre, drama.category]
|
||||||
|
|||||||
@@ -78,7 +78,11 @@ class HomeGridHeader extends StatelessWidget {
|
|||||||
key: const Key('home-grid-header'),
|
key: const Key('home-grid-header'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [_searchRow(l10n), _primaryRow(l10n), _tagRow(l10n)],
|
children: [
|
||||||
|
_searchRow(l10n),
|
||||||
|
_primaryRow(l10n),
|
||||||
|
if (selectedPrimary == HomePrimaryTab.featured) _tagRow(l10n),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../../core/data/models/drama.dart';
|
|||||||
import '../../../core/formatting/count_format.dart';
|
import '../../../core/formatting/count_format.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_dimens.dart';
|
import '../../../core/theme/app_dimens.dart';
|
||||||
|
import '../../../core/widgets/drama_genre_ribbon.dart';
|
||||||
import '../../../core/widgets/remote_image.dart';
|
import '../../../core/widgets/remote_image.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import '../../home/widgets/home_drama_classification.dart';
|
import '../../home/widgets/home_drama_classification.dart';
|
||||||
@@ -31,7 +32,7 @@ class RecommendSectionCard extends StatelessWidget {
|
|||||||
width: AppDimens.recommendCardWidth,
|
width: AppDimens.recommendCardWidth,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
key: Key('recommend-card-$sectionKey-${drama.id}'),
|
key: Key('recommend-card-$sectionKey-${drama.id}'),
|
||||||
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
|
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -39,7 +40,7 @@ class RecommendSectionCard extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
|
borderRadius: BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: AppDimens.cardCoverAspectRatio,
|
aspectRatio: AppDimens.cardCoverAspectRatio,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -77,6 +78,14 @@ class RecommendSectionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: DramaGenreRibbon(
|
||||||
|
tags: _cardTags(),
|
||||||
|
keyPrefix: '$sectionKey-${drama.id}',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -88,12 +97,12 @@ class RecommendSectionCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons.play_arrow_rounded,
|
Icons.auto_awesome_rounded,
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
size: AppDimens.cardViewsIcon,
|
size: AppDimens.cardViewsIcon,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
formatInteractionCount(drama.viewCount),
|
formatPlayCount(drama.viewCount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.textPrimary,
|
color: AppColors.textPrimary,
|
||||||
fontSize: AppDimens.cardViewsFont,
|
fontSize: AppDimens.cardViewsFont,
|
||||||
@@ -114,31 +123,12 @@ class RecommendSectionCard extends StatelessWidget {
|
|||||||
height: AppDimens.cardTitleLineHeight,
|
height: AppDimens.cardTitleLineHeight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_cardTags().isNotEmpty) ...[
|
|
||||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
|
||||||
_tags(),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _tags() {
|
|
||||||
final tags = _cardTags();
|
|
||||||
return Text(
|
|
||||||
tags.join(' | '),
|
|
||||||
key: Key('recommend-tags-$sectionKey-${drama.id}'),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.textTertiary,
|
|
||||||
fontSize: AppDimens.cardTagFont,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> _cardTags() {
|
List<String> _cardTags() {
|
||||||
final seen = <String>{};
|
final seen = <String>{};
|
||||||
return [drama.genre, drama.category]
|
return [drama.genre, drama.category]
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ void main() {
|
|||||||
expect(formatInteractionCount(9999), '9999');
|
expect(formatInteractionCount(9999), '9999');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should_format_play_count_with_w_and_trim_integer_decimals', () {
|
||||||
|
expect(formatPlayCount(8500), '8500');
|
||||||
|
expect(formatPlayCount(10000), '1w');
|
||||||
|
expect(formatPlayCount(500000), '50w');
|
||||||
|
expect(formatPlayCount(444500), '44.5w');
|
||||||
|
});
|
||||||
|
|
||||||
test('should_use_one_decimal_wan_from_ten_thousand', () {
|
test('should_use_one_decimal_wan_from_ten_thousand', () {
|
||||||
expect(formatInteractionCount(10000), '1.0万');
|
expect(formatInteractionCount(10000), '1.0万');
|
||||||
expect(formatInteractionCount(12400), '1.2万');
|
expect(formatInteractionCount(12400), '1.2万');
|
||||||
|
|||||||
@@ -68,6 +68,16 @@ void main() {
|
|||||||
find.byKey(const Key('following-cover-the-signal-demo')),
|
find.byKey(const Key('following-cover-the-signal-demo')),
|
||||||
);
|
);
|
||||||
expect(cover.aspectRatio, AppDimens.cardCoverAspectRatio);
|
expect(cover.aspectRatio, AppDimens.cardCoverAspectRatio);
|
||||||
|
final coverClip = tester.widget<ClipRRect>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('following-cover-the-signal-demo')),
|
||||||
|
matching: find.byType(ClipRRect),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coverClip.borderRadius,
|
||||||
|
BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
|
);
|
||||||
final title = tester.widget<Text>(
|
final title = tester.widget<Text>(
|
||||||
find.descendant(of: card, matching: find.text('The Signal')),
|
find.descendant(of: card, matching: find.text('The Signal')),
|
||||||
);
|
);
|
||||||
@@ -79,14 +89,24 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(views.style?.fontSize, AppDimens.cardViewsFont);
|
expect(views.style?.fontSize, AppDimens.cardViewsFont);
|
||||||
expect(views.style?.color, AppColors.textPrimary);
|
expect(views.style?.color, AppColors.textPrimary);
|
||||||
final tags = tester.widget<Text>(
|
expect(
|
||||||
find.byKey(const Key('following-tags-the-signal-demo')),
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('following-views-the-signal-demo')),
|
||||||
|
matching: find.byIcon(Icons.auto_awesome_rounded),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(tags.data, 'Suspense | Mystery');
|
final ribbon = find.byKey(
|
||||||
expect(tags.style?.fontSize, AppDimens.cardTagFont);
|
const Key('drama-genre-ribbon-the-signal-demo-0'),
|
||||||
expect(tags.style?.color, AppColors.textTertiary);
|
);
|
||||||
|
expect(ribbon, findsOneWidget);
|
||||||
|
final ribbonText = tester.widget<Text>(
|
||||||
|
find.descendant(of: ribbon, matching: find.byType(Text)),
|
||||||
|
);
|
||||||
|
expect(ribbonText.data, 'Suspense');
|
||||||
|
expect(ribbonText.style?.fontSize, AppDimens.cardBadgeFont);
|
||||||
|
expect(ribbonText.style?.color, AppColors.textPrimary);
|
||||||
expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx);
|
expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx);
|
||||||
expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx);
|
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('following-edit')));
|
await tester.tap(find.byKey(const Key('following-edit')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ void main() {
|
|||||||
final cover = find.byKey(const Key('home-feed-cover-anime-card'));
|
final cover = find.byKey(const Key('home-feed-cover-anime-card'));
|
||||||
final coverSize = tester.getSize(cover);
|
final coverSize = tester.getSize(cover);
|
||||||
expect(coverSize.height / coverSize.width, closeTo(6 / 5, 0.02));
|
expect(coverSize.height / coverSize.width, closeTo(6 / 5, 0.02));
|
||||||
|
final coverClip = tester.widget<ClipRRect>(
|
||||||
|
find.byKey(const Key('home-feed-cover-clip-anime-card')),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coverClip.borderRadius,
|
||||||
|
BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
|
);
|
||||||
final badge = find.byKey(const Key('home-feed-badge-anime-anime-card'));
|
final badge = find.byKey(const Key('home-feed-badge-anime-anime-card'));
|
||||||
expect(badge, findsOneWidget);
|
expect(badge, findsOneWidget);
|
||||||
final badgeContainer = tester.widget<Container>(badge);
|
final badgeContainer = tester.widget<Container>(badge);
|
||||||
@@ -61,9 +68,16 @@ void main() {
|
|||||||
final viewsText = tester.widget<Text>(
|
final viewsText = tester.widget<Text>(
|
||||||
find.descendant(of: views, matching: find.byType(Text)),
|
find.descendant(of: views, matching: find.byType(Text)),
|
||||||
);
|
);
|
||||||
expect(viewsText.data, startsWith('44.'));
|
expect(viewsText.data, '44.5w');
|
||||||
expect(viewsText.style?.fontSize, AppDimens.cardViewsFont);
|
expect(viewsText.style?.fontSize, AppDimens.cardViewsFont);
|
||||||
expect(viewsText.style?.color, AppColors.navSelected);
|
expect(viewsText.style?.color, AppColors.navSelected);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: views,
|
||||||
|
matching: find.byIcon(Icons.auto_awesome_rounded),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
final title = tester.widget<Text>(
|
final title = tester.widget<Text>(
|
||||||
find.byKey(const Key('home-feed-title-anime-card')),
|
find.byKey(const Key('home-feed-title-anime-card')),
|
||||||
);
|
);
|
||||||
@@ -71,14 +85,15 @@ void main() {
|
|||||||
expect(title.style?.fontSize, AppDimens.cardTitleFont);
|
expect(title.style?.fontSize, AppDimens.cardTitleFont);
|
||||||
expect(title.style?.fontWeight, FontWeight.w500);
|
expect(title.style?.fontWeight, FontWeight.w500);
|
||||||
expect(title.style?.height, AppDimens.cardTitleLineHeight);
|
expect(title.style?.height, AppDimens.cardTitleLineHeight);
|
||||||
final tags = tester.widget<Text>(
|
final ribbon = find.byKey(const Key('drama-genre-ribbon-anime-card-0'));
|
||||||
find.byKey(const Key('home-feed-tags-anime-card')),
|
expect(ribbon, findsOneWidget);
|
||||||
|
final ribbonText = tester.widget<Text>(
|
||||||
|
find.descendant(of: ribbon, matching: find.byType(Text)),
|
||||||
);
|
);
|
||||||
expect(tags.data, 'Romance | Urban');
|
expect(ribbonText.data, 'Romance');
|
||||||
expect(tags.style?.fontSize, AppDimens.cardTagFont);
|
expect(ribbonText.style?.fontSize, AppDimens.cardBadgeFont);
|
||||||
expect(tags.style?.color, AppColors.textTertiary);
|
expect(ribbonText.style?.color, AppColors.textPrimary);
|
||||||
expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx);
|
expect(tester.getTopLeft(title).dx, tester.getTopLeft(cover).dx);
|
||||||
expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx);
|
|
||||||
|
|
||||||
await tester.tap(card);
|
await tester.tap(card);
|
||||||
expect(taps, 1);
|
expect(taps, 1);
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-section-new')));
|
await tester.tap(find.byKey(const Key('home-section-new')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(const Key('new-releases-page')), findsNothing);
|
expect(find.byKey(const Key('new-releases-page')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
||||||
expect(firstDramaId(), 'page1-9');
|
expect(firstDramaId(), 'page1-9');
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-section-ranking')));
|
await tester.tap(find.byKey(const Key('home-section-ranking')));
|
||||||
@@ -118,6 +119,7 @@ void main() {
|
|||||||
await tester.tap(find.byKey(const Key('home-section-categories')));
|
await tester.tap(find.byKey(const Key('home-section-categories')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.byKey(const Key('filter-page')), findsNothing);
|
expect(find.byKey(const Key('filter-page')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
||||||
expect(find.byKey(const Key('home-grid-cards')), findsOneWidget);
|
expect(find.byKey(const Key('home-grid-cards')), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byKey(const Key('home-section-category-古装')));
|
await tester.tap(find.byKey(const Key('home-section-category-古装')));
|
||||||
@@ -154,6 +156,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('home-section-featured')));
|
await tester.tap(find.byKey(const Key('home-section-featured')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget);
|
||||||
|
|
||||||
final afterScrollable = find
|
final afterScrollable = find
|
||||||
.descendant(
|
.descendant(
|
||||||
|
|||||||
@@ -85,6 +85,13 @@ void main() {
|
|||||||
expect(moreText.style?.fontSize, AppDimens.recommendSectionMore);
|
expect(moreText.style?.fontSize, AppDimens.recommendSectionMore);
|
||||||
expect(moreText.style?.color, AppColors.textSecondary);
|
expect(moreText.style?.color, AppColors.textSecondary);
|
||||||
final card = find.byKey(const Key('recommend-card-hits-the-signal-demo'));
|
final card = find.byKey(const Key('recommend-card-hits-the-signal-demo'));
|
||||||
|
final coverClip = tester.widget<ClipRRect>(
|
||||||
|
find.descendant(of: card, matching: find.byType(ClipRRect)),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coverClip.borderRadius,
|
||||||
|
BorderRadius.circular(AppDimens.cardCoverRadius),
|
||||||
|
);
|
||||||
final cover = tester.widget<AspectRatio>(
|
final cover = tester.widget<AspectRatio>(
|
||||||
find.descendant(of: card, matching: find.byType(AspectRatio)),
|
find.descendant(of: card, matching: find.byType(AspectRatio)),
|
||||||
);
|
);
|
||||||
@@ -103,14 +110,22 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(views.style?.fontSize, AppDimens.cardViewsFont);
|
expect(views.style?.fontSize, AppDimens.cardViewsFont);
|
||||||
expect(views.style?.color, AppColors.textPrimary);
|
expect(views.style?.color, AppColors.textPrimary);
|
||||||
final tags = tester.widget<Text>(
|
final viewIcon = find.descendant(
|
||||||
find.byKey(const Key('recommend-tags-hits-the-signal-demo')),
|
of: find.byKey(const Key('recommend-views-hits-the-signal-demo')),
|
||||||
|
matching: find.byIcon(Icons.auto_awesome_rounded),
|
||||||
);
|
);
|
||||||
expect(tags.data, 'Suspense | Mystery');
|
expect(viewIcon, findsOneWidget);
|
||||||
expect(tags.style?.fontSize, AppDimens.cardTagFont);
|
final ribbon = find.byKey(
|
||||||
expect(tags.style?.color, AppColors.textTertiary);
|
const Key('drama-genre-ribbon-hits-the-signal-demo-0'),
|
||||||
|
);
|
||||||
|
expect(ribbon, findsOneWidget);
|
||||||
|
final ribbonText = tester.widget<Text>(
|
||||||
|
find.descendant(of: ribbon, matching: find.byType(Text)),
|
||||||
|
);
|
||||||
|
expect(ribbonText.data, 'Suspense');
|
||||||
|
expect(ribbonText.style?.fontSize, AppDimens.cardBadgeFont);
|
||||||
|
expect(ribbonText.style?.color, AppColors.textPrimary);
|
||||||
expect(tester.getTopLeft(cardTitle).dx, tester.getTopLeft(cover).dx);
|
expect(tester.getTopLeft(cardTitle).dx, tester.getTopLeft(cover).dx);
|
||||||
expect(tester.getTopLeft(tags).dx, tester.getTopLeft(cover).dx);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_switch_inner_tabs_without_losing_grid_filter_state', (
|
testWidgets('should_switch_inner_tabs_without_losing_grid_filter_state', (
|
||||||
|
|||||||
Reference in new issue
Block a user