fix(home): 修正封面角标与题材标签
This commit is contained in:
1 parent
3abf69e886
commit
e16faa258a
10 files changed
+199
-199
No files matched your search
@@ -92,13 +92,8 @@ abstract final class AppDimens {
|
||||
static const double cardGenreRibbonLeftInset = 8;
|
||||
static const double cardGenreRibbonMaxWidth = 86;
|
||||
static const double cardTagFont = 11;
|
||||
static const double cardTagHeight = 18;
|
||||
static const double cardTagRadius = 9;
|
||||
static const double cardTagHorizontalPadding = 6;
|
||||
static const double cardBadgeFont = 10;
|
||||
static const double cardBadgeRadius = 3;
|
||||
static const double cardBadgeHorizontalPadding = 6;
|
||||
static const double cardBadgeVerticalPadding = 2;
|
||||
static const double cardCoverTitleGap = 6;
|
||||
static const double cardTitleTagGap = 6;
|
||||
static const double cardCoverAspectRatio = 5 / 6;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_colors.dart';
|
||||
import '../theme/app_dimens.dart';
|
||||
|
||||
/// 封面右上角的斜边状态角标。
|
||||
class DramaCoverBadgeRibbon extends StatelessWidget {
|
||||
const DramaCoverBadgeRibbon({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.color,
|
||||
required this.keyPrefix,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final Color color;
|
||||
final String keyPrefix;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipPath(
|
||||
key: Key('drama-cover-badge-ribbon-$keyPrefix'),
|
||||
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: color,
|
||||
child: Text(
|
||||
label,
|
||||
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;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// 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,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import '../../core/formatting/count_format.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_dimens.dart';
|
||||
import '../../core/widgets/drama_genre_ribbon.dart';
|
||||
import '../../core/widgets/remote_image.dart';
|
||||
|
||||
class FollowingGridCard extends StatelessWidget {
|
||||
@@ -95,12 +94,6 @@ class FollowingGridCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!editing)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: DramaGenreRibbon(tags: tags, keyPrefix: dramaId),
|
||||
),
|
||||
if (editing)
|
||||
Positioned(
|
||||
top: -6,
|
||||
@@ -135,6 +128,10 @@ class FollowingGridCard extends StatelessWidget {
|
||||
height: AppDimens.cardTitleLineHeight,
|
||||
),
|
||||
),
|
||||
if (tags.isNotEmpty) ...[
|
||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
||||
_tags(),
|
||||
],
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Text(
|
||||
subtitle,
|
||||
@@ -164,4 +161,19 @@ 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import '../../../core/data/models/drama.dart';
|
||||
import '../../../core/formatting/count_format.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../../core/widgets/drama_genre_ribbon.dart';
|
||||
import '../../../core/widgets/drama_cover_badge_ribbon.dart';
|
||||
import '../../../core/widgets/remote_image.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import 'home_drama_classification.dart';
|
||||
@@ -63,6 +63,8 @@ class HomeFeedCard extends StatelessWidget {
|
||||
height: AppDimens.cardTitleLineHeight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
||||
_tags(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -92,15 +94,14 @@ class HomeFeedCard extends StatelessWidget {
|
||||
),
|
||||
if (badge != null)
|
||||
Positioned(
|
||||
top: AppDimens.spaceXs,
|
||||
left: AppDimens.spaceXs,
|
||||
child: _badgeChip(l10n, badge),
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: DramaCoverBadgeRibbon(
|
||||
label: _badgeLabel(l10n, badge),
|
||||
color: _badgeColor(badge),
|
||||
keyPrefix: '${drama.id}-${badge.name}',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: DramaGenreRibbon(tags: _cardTags(), keyPrefix: drama.id),
|
||||
),
|
||||
Positioned(
|
||||
left: AppDimens.spaceXs,
|
||||
bottom: AppDimens.spaceXs,
|
||||
@@ -123,32 +124,20 @@ class HomeFeedCard extends StatelessWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _badgeChip(AppLocalizations l10n, HomeDramaBadge badge) {
|
||||
final (label, color) = switch (badge) {
|
||||
HomeDramaBadge.hot => (l10n.homeBadgeHot, AppColors.badgeHot),
|
||||
HomeDramaBadge.newRelease => (l10n.homeBadgeNew, AppColors.badgeNew),
|
||||
HomeDramaBadge.anime => (l10n.homeBadgeAnime, AppColors.badgeAnime),
|
||||
String _badgeLabel(AppLocalizations l10n, HomeDramaBadge badge) {
|
||||
return switch (badge) {
|
||||
HomeDramaBadge.hot => l10n.homeBadgeHot,
|
||||
HomeDramaBadge.newRelease => l10n.homeBadgeNew,
|
||||
HomeDramaBadge.anime => l10n.homeBadgeAnime,
|
||||
};
|
||||
}
|
||||
|
||||
Color _badgeColor(HomeDramaBadge badge) {
|
||||
return switch (badge) {
|
||||
HomeDramaBadge.hot => AppColors.badgeHot,
|
||||
HomeDramaBadge.newRelease => AppColors.badgeNew,
|
||||
HomeDramaBadge.anime => AppColors.badgeAnime,
|
||||
};
|
||||
return Container(
|
||||
key: Key('home-feed-badge-${badge.name}-${drama.id}'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.cardBadgeHorizontalPadding,
|
||||
vertical: AppDimens.cardBadgeVerticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardBadgeRadius),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppColors.navSelected,
|
||||
fontSize: AppDimens.cardBadgeFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _views() {
|
||||
@@ -182,6 +171,23 @@ 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() {
|
||||
final seen = <String>{};
|
||||
return [drama.genre, drama.category]
|
||||
|
||||
@@ -6,7 +6,7 @@ import '../../../core/data/models/drama.dart';
|
||||
import '../../../core/formatting/count_format.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../../core/widgets/drama_genre_ribbon.dart';
|
||||
import '../../../core/widgets/drama_cover_badge_ribbon.dart';
|
||||
import '../../../core/widgets/remote_image.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../home/widgets/home_drama_classification.dart';
|
||||
@@ -54,38 +54,14 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
),
|
||||
if (isHotDrama(drama))
|
||||
Positioned(
|
||||
top: AppDimens.spaceXs,
|
||||
left: AppDimens.spaceXs,
|
||||
child: Container(
|
||||
key: Key('recommend-hot-$sectionKey-${drama.id}'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.cardBadgeHorizontalPadding,
|
||||
vertical: AppDimens.cardBadgeVerticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.badgeHot,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppDimens.cardBadgeRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
l10n.homeBadgeHot,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.cardBadgeFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: DramaCoverBadgeRibbon(
|
||||
label: l10n.homeBadgeHot,
|
||||
color: AppColors.badgeHot,
|
||||
keyPrefix: '$sectionKey-${drama.id}-hot',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: DramaGenreRibbon(
|
||||
tags: _cardTags(),
|
||||
keyPrefix: '$sectionKey-${drama.id}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -123,6 +99,8 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
height: AppDimens.cardTitleLineHeight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
||||
_tags(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -138,4 +116,20 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
.take(3)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Widget _tags() {
|
||||
final tags = _cardTags();
|
||||
if (tags.isEmpty) return const SizedBox.shrink();
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user