merge: 对齐短剧列表字号与间距
This commit is contained in:
commit
10477562d4
17 files changed
+576
-148
No files matched your search
@@ -1070,3 +1070,41 @@ $ flutter test
|
||||
解读:登录且钱包快照存在时钱包红标仍显示;首页四个 tab、推荐三个子 tab、
|
||||
追剧页和我的页均断言不存在两侧悬浮组件。`pubspec.lock` 未提交,
|
||||
未操作真机。
|
||||
|
||||
## 2026-10-22 首页与短剧列表视觉字号对齐
|
||||
|
||||
- 分支:`feature/visual-type-scale`
|
||||
- 基线:`develop` / `origin/develop` = `d03dcf1`
|
||||
|
||||
### Token 与组件
|
||||
|
||||
- 新增 `textTertiary = #9E9E9E`,以及搜索、页签、筛选 chip、分区标题、
|
||||
卡片标题/播放量/标签/角标、封面比例和网格间距的公共 token。
|
||||
- 搜索框统一为高 40、圆角 8、placeholder 13;一级分类 16,选中
|
||||
白字 w700,未选中 `#9E9E9E` w400;二级标签 13 并采用参考图选中/未选中
|
||||
底色。
|
||||
- 三列首页网格与全部短剧网格统一左右边距 14、列间距 6、行间距 18;
|
||||
封面 5:6、圆角 6。
|
||||
- 首页、剧单横滑、追剧双列卡片统一标题 15/w500/1.3、播放量 11 白字、
|
||||
标签 11/高 18/左右 6,封面到标题 8、标题到标签 6。
|
||||
- 剧单分区标题 17/w700,副标题 11 `#8E8E93`,更多 12 `#8E8E93`;
|
||||
分区间距保持 24。
|
||||
- 全部短剧筛选 chip 统一高 30、圆角 15、字号 13;我的页标题 17,
|
||||
追剧顶部页签 16。
|
||||
|
||||
### 门禁命令与原始输出
|
||||
|
||||
```text
|
||||
$ flutter analyze
|
||||
Analyzing reelshort-visual-type...
|
||||
No issues found! (ran in 1.8s)
|
||||
```
|
||||
|
||||
```text
|
||||
$ flutter test
|
||||
00:57 +648: All tests passed!
|
||||
```
|
||||
|
||||
解读:首页搜索/页签/二级标签、三类卡片字体与标签、5:6 封面、网格间距、
|
||||
筛选 chip、我的标题和追剧双列 320 宽度适配均有测试覆盖。目标组件均低于
|
||||
500 行;`pubspec.lock` 未提交,未操作真机。
|
||||
@@ -79,6 +79,7 @@ abstract final class AppColors {
|
||||
/// 次级文字与列表 chevron。
|
||||
static const textPrimary = Color(0xFFFFFFFF);
|
||||
static const textSecondary = Color(0xFF8E8E93);
|
||||
static const textTertiary = Color(0xFF9E9E9E);
|
||||
|
||||
/// 原生开屏与冷启动过渡底色。
|
||||
static const splashInk = ink;
|
||||
|
||||
@@ -54,7 +54,9 @@ abstract final class AppDimens {
|
||||
static const double progressRadius = 2;
|
||||
static const double progressLineHeight = 3;
|
||||
|
||||
static const double searchHeight = 44;
|
||||
static const double searchHeight = 40;
|
||||
static const double searchRadius = 8;
|
||||
static const double searchPlaceholderFont = 13;
|
||||
static const double navDot = 3;
|
||||
static const double glassBlur = 24;
|
||||
|
||||
@@ -75,23 +77,51 @@ abstract final class AppDimens {
|
||||
static const double cardShadowY = 0;
|
||||
|
||||
/// 首页探索网格。
|
||||
static const double homeGridGap = 8;
|
||||
static const double homeCardRadius = 8;
|
||||
static const double feedPageHorizontal = 14;
|
||||
static const double homeGridGap = 6;
|
||||
static const double homeGridRowGap = 18;
|
||||
static const double homeCardRadius = 6;
|
||||
static const double homeCardPadding = 8;
|
||||
static const double homeCardTitle = 13;
|
||||
static const double homeCardMeta = 10;
|
||||
static const double homeCardBadge = 9;
|
||||
static const double homeTagFont = 10;
|
||||
static const double cardTitleFont = 15;
|
||||
static const double cardTitleLineHeight = 1.3;
|
||||
static const double cardViewsFont = 11;
|
||||
static const double cardViewsIcon = 11;
|
||||
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 = 8;
|
||||
static const double cardTitleTagGap = 6;
|
||||
static const double cardCoverAspectRatio = 5 / 6;
|
||||
static const double homeCardBodyExtent = 84;
|
||||
static const double homeSectionFont = 14;
|
||||
static const double homePrimaryTabFont = 16;
|
||||
static const double homeSecondaryTagFont = 13;
|
||||
static const double homeSectionIndicator = 2;
|
||||
static const double homeIconSlot = 44;
|
||||
static const double homeHeaderIcon = 22;
|
||||
|
||||
/// 推荐分区。
|
||||
static const double recommendSectionTitle = 17;
|
||||
static const double recommendSectionSubtitle = 11;
|
||||
static const double recommendSectionMore = 12;
|
||||
static const double recommendRowHeight = 252;
|
||||
static const double recommendCardWidth = 122;
|
||||
|
||||
/// 全部短剧筛选与追剧双列。
|
||||
static const double filterChipFont = 13;
|
||||
static const double filterChipHeight = 30;
|
||||
static const double filterChipRadius = 15;
|
||||
static const double followingTabFont = 16;
|
||||
static const double followingGridAspectRatio = 0.5;
|
||||
|
||||
/// 「我的」页中性视觉。
|
||||
static const double mineCardRadius = 16;
|
||||
static const double mineRowHeight = 56;
|
||||
static const double minePageTitle = 21;
|
||||
static const double minePageTitle = 17;
|
||||
static const double mineSectionTitle = 15;
|
||||
static const double mineAvatar = 56;
|
||||
static const double mineUserName = 20;
|
||||
|
||||
@@ -14,6 +14,7 @@ class FollowingGridCard extends StatelessWidget {
|
||||
required this.title,
|
||||
required this.posterUrl,
|
||||
required this.viewCount,
|
||||
required this.tags,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
this.progress,
|
||||
@@ -25,6 +26,7 @@ class FollowingGridCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String posterUrl;
|
||||
final int viewCount;
|
||||
final List<String> tags;
|
||||
final String subtitle;
|
||||
final VoidCallback onTap;
|
||||
final double? progress;
|
||||
@@ -40,7 +42,9 @@ class FollowingGridCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
AspectRatio(
|
||||
key: Key('following-cover-$dramaId'),
|
||||
aspectRatio: AppDimens.cardCoverAspectRatio,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
@@ -75,13 +79,14 @@ class FollowingGridCard extends StatelessWidget {
|
||||
const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: AppColors.textPrimary,
|
||||
size: AppDimens.homeCardMeta,
|
||||
size: AppDimens.cardViewsIcon,
|
||||
),
|
||||
Text(
|
||||
formatInteractionCount(viewCount),
|
||||
key: Key('following-views-$dramaId'),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.homeCardMeta,
|
||||
fontSize: AppDimens.cardViewsFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -109,24 +114,30 @@ class FollowingGridCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
const SizedBox(height: AppDimens.cardCoverTitleGap),
|
||||
Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.homeCardTitle,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: AppDimens.cardTitleFont,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: AppDimens.cardTitleLineHeight,
|
||||
),
|
||||
),
|
||||
if (tags.isNotEmpty) ...[
|
||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
||||
_tags(),
|
||||
],
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeCardMeta,
|
||||
color: AppColors.textTertiary,
|
||||
fontSize: AppDimens.cardViewsFont,
|
||||
),
|
||||
),
|
||||
if (progress != null) ...[
|
||||
@@ -148,4 +159,41 @@ class FollowingGridCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tags() {
|
||||
final visible = tags.take(2).toList(growable: false);
|
||||
return Row(
|
||||
children: [
|
||||
for (final (index, tag) in visible.indexed)
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: index == visible.length - 1 ? 0 : AppDimens.spaceXs,
|
||||
),
|
||||
child: Container(
|
||||
key: Key('following-tag-$dramaId-$index'),
|
||||
height: AppDimens.cardTagHeight,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.cardTagHorizontalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardTagRadius),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textTertiary,
|
||||
fontSize: AppDimens.cardTagFont,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/models/watch_record.dart';
|
||||
import '../../core/data/repositories/catalog_repository.dart';
|
||||
import '../../core/data/repositories/watch_history_repository.dart';
|
||||
@@ -22,6 +23,8 @@ import '../../l10n/app_localizations.dart';
|
||||
import '../player/open_player.dart';
|
||||
import 'following_grid_card.dart';
|
||||
|
||||
final RegExp _tagSplitter = RegExp(r'[,/、·]');
|
||||
|
||||
enum FollowingTab { favorites, history, reserve }
|
||||
|
||||
class FollowingPage extends StatefulWidget {
|
||||
@@ -117,51 +120,56 @@ class _FollowingPageState extends State<FollowingPage> {
|
||||
return SizedBox(
|
||||
key: const Key('following-header'),
|
||||
height: 52,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final tab in tabs)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
key: Key('following-tab-${tab.$3}'),
|
||||
onTap: () => setState(() {
|
||||
_tab = tab.$1;
|
||||
_editing = false;
|
||||
}),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
tab.$2,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: _tab == tab.$1
|
||||
? AppColors.textPrimary
|
||||
: AppColors.textSecondary,
|
||||
fontSize: 16,
|
||||
fontWeight: _tab == tab.$1
|
||||
? FontWeight.w700
|
||||
: FontWeight.w500,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
for (final tab in tabs)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
key: Key('following-tab-${tab.$3}'),
|
||||
onTap: () => setState(() {
|
||||
_tab = tab.$1;
|
||||
_editing = false;
|
||||
}),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
tab.$2,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: _tab == tab.$1
|
||||
? AppColors.textPrimary
|
||||
: AppColors.textTertiary,
|
||||
fontSize: AppDimens.followingTabFont,
|
||||
fontWeight: _tab == tab.$1
|
||||
? FontWeight.w700
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Container(
|
||||
width: _tab == tab.$1 ? 20 : 0,
|
||||
height: 2,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 3),
|
||||
Container(
|
||||
width: _tab == tab.$1 ? 20 : 0,
|
||||
height: 2,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: const Key('following-edit'),
|
||||
tooltip: _editing ? l10n.done : l10n.edit,
|
||||
onPressed: () => setState(() => _editing = !_editing),
|
||||
color: AppColors.textPrimary,
|
||||
iconSize: 24,
|
||||
icon: Icon(_editing ? Icons.check_rounded : Icons.edit_outlined),
|
||||
),
|
||||
IconButton(
|
||||
key: const Key('following-edit'),
|
||||
tooltip: _editing ? l10n.done : l10n.edit,
|
||||
onPressed: () => setState(() => _editing = !_editing),
|
||||
color: AppColors.textPrimary,
|
||||
iconSize: 24,
|
||||
icon: Icon(_editing ? Icons.check_rounded : Icons.edit_outlined),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -233,6 +241,7 @@ class _FavoritesGrid extends StatelessWidget {
|
||||
title: drama.title,
|
||||
posterUrl: drama.posterUrl,
|
||||
viewCount: drama.viewCount,
|
||||
tags: _dramaTags(drama),
|
||||
editing: editing,
|
||||
subtitle: l10n.favoriteEpisodeProgress(
|
||||
episode,
|
||||
@@ -304,6 +313,7 @@ class _HistoryGrid extends StatelessWidget {
|
||||
title: title,
|
||||
posterUrl: record.displayCover,
|
||||
viewCount: record.drama?.viewCount ?? 0,
|
||||
tags: _dramaTags(record.drama),
|
||||
progress: record.progress.fraction,
|
||||
editing: editing,
|
||||
subtitle: l10n.watchedToEpisode(record.episodeNumber),
|
||||
@@ -322,6 +332,17 @@ class _HistoryGrid extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _dramaTags(Drama? drama) {
|
||||
if (drama == null) return const [];
|
||||
final seen = <String>{};
|
||||
return [drama.genre, drama.category]
|
||||
.expand((value) => value.split(_tagSplitter))
|
||||
.map((value) => value.trim())
|
||||
.where((value) => value.isNotEmpty && seen.add(value))
|
||||
.take(2)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<void> _openHistory(
|
||||
BuildContext context,
|
||||
WatchRecordsController controller,
|
||||
@@ -416,17 +437,17 @@ class _PagedGrid extends StatelessWidget {
|
||||
else ...[
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceMd,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: AppDimens.spaceMd,
|
||||
mainAxisSpacing: AppDimens.spaceMd,
|
||||
childAspectRatio: 0.56,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
childAspectRatio: AppDimens.followingGridAspectRatio,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
|
||||
@@ -10,7 +10,6 @@ import '../../../core/widgets/remote_image.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import 'home_drama_classification.dart';
|
||||
|
||||
const double _coverAspectRatio = 5 / 6;
|
||||
final RegExp _tagSplitter = RegExp(r'[,/、·]');
|
||||
|
||||
enum HomeDramaBadge { hot, newRelease, anime }
|
||||
@@ -60,12 +59,12 @@ class HomeFeedCard extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.navSelected,
|
||||
fontSize: AppDimens.homeCardTitle,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
fontSize: AppDimens.cardTitleFont,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: AppDimens.cardTitleLineHeight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
||||
_tags(),
|
||||
],
|
||||
),
|
||||
@@ -81,7 +80,7 @@ class HomeFeedCard extends StatelessWidget {
|
||||
Widget _cover(AppLocalizations l10n) {
|
||||
final badge = _badge();
|
||||
return AspectRatio(
|
||||
aspectRatio: _coverAspectRatio,
|
||||
aspectRatio: AppDimens.cardCoverAspectRatio,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -128,18 +127,18 @@ class HomeFeedCard extends StatelessWidget {
|
||||
return Container(
|
||||
key: Key('home-feed-badge-${badge.name}-${drama.id}'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.spaceSm,
|
||||
vertical: 2,
|
||||
horizontal: AppDimens.cardBadgeHorizontalPadding,
|
||||
vertical: AppDimens.cardBadgeVerticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(AppDimens.chipRadius),
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardBadgeRadius),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppColors.navSelected,
|
||||
fontSize: AppDimens.homeCardBadge,
|
||||
fontSize: AppDimens.cardBadgeFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
@@ -160,7 +159,7 @@ class HomeFeedCard extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
size: AppDimens.homeCardMeta,
|
||||
size: AppDimens.cardViewsIcon,
|
||||
color: AppColors.navSelected,
|
||||
),
|
||||
const SizedBox(width: 1),
|
||||
@@ -168,7 +167,7 @@ class HomeFeedCard extends StatelessWidget {
|
||||
formatInteractionCount(drama.viewCount),
|
||||
style: const TextStyle(
|
||||
color: AppColors.navSelected,
|
||||
fontSize: AppDimens.homeCardMeta,
|
||||
fontSize: AppDimens.cardViewsFont,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
@@ -191,18 +190,22 @@ class HomeFeedCard extends StatelessWidget {
|
||||
),
|
||||
child: Container(
|
||||
key: Key('home-feed-tag-${drama.id}-$index'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||
height: AppDimens.cardTagHeight,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.cardTagHorizontalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.chipRadius),
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardTagRadius),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeTagFont,
|
||||
color: AppColors.textTertiary,
|
||||
fontSize: AppDimens.cardTagFont,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -73,7 +73,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||
builder: (context, constraints) {
|
||||
final itemWidth =
|
||||
(constraints.maxWidth -
|
||||
AppDimens.pageHorizontal * 2 -
|
||||
AppDimens.feedPageHorizontal * 2 -
|
||||
AppDimens.homeGridGap * 2) /
|
||||
3;
|
||||
final itemExtent = itemWidth * 6 / 5 + AppDimens.homeCardBodyExtent;
|
||||
@@ -117,7 +117,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.pageHorizontal,
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('home-grid-cards'),
|
||||
@@ -125,7 +125,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
|
||||
@@ -78,9 +78,9 @@ class HomeGridHeader extends StatelessWidget {
|
||||
Widget _searchRow(AppLocalizations l10n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
),
|
||||
child: Row(
|
||||
@@ -88,7 +88,7 @@ class HomeGridHeader extends StatelessWidget {
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
key: const Key('home-search-entry'),
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
borderRadius: BorderRadius.circular(AppDimens.searchRadius),
|
||||
onTap: callbacks.onSearch,
|
||||
child: Container(
|
||||
height: AppDimens.searchHeight,
|
||||
@@ -97,7 +97,7 @@ class HomeGridHeader extends StatelessWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.inputRadius),
|
||||
borderRadius: BorderRadius.circular(AppDimens.searchRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -114,7 +114,7 @@ class HomeGridHeader extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.body,
|
||||
fontSize: AppDimens.searchPlaceholderFont,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -191,7 +191,7 @@ class HomeGridHeader extends StatelessWidget {
|
||||
formatInteractionCount(badge),
|
||||
style: const TextStyle(
|
||||
color: AppColors.navSelected,
|
||||
fontSize: AppDimens.homeCardBadge,
|
||||
fontSize: AppDimens.cardBadgeFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -206,7 +206,9 @@ class HomeGridHeader extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
key: const Key('home-primary-sections'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppDimens.pageHorizontal),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_primaryItem(
|
||||
@@ -264,9 +266,9 @@ class HomeGridHeader extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? AppColors.navSelected
|
||||
: AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeSectionFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
: AppColors.textTertiary,
|
||||
fontSize: AppDimens.homePrimaryTabFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
@@ -288,9 +290,9 @@ class HomeGridHeader extends StatelessWidget {
|
||||
Widget _tagRow(AppLocalizations l10n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceMd,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
),
|
||||
child: Row(
|
||||
@@ -347,15 +349,15 @@ class HomeGridHeader extends StatelessWidget {
|
||||
vertical: AppDimens.spaceSm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.contentChip : AppColors.contentSurface,
|
||||
color: selected ? AppColors.contentSurface : AppColors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
),
|
||||
child: Text(
|
||||
tag.label,
|
||||
style: TextStyle(
|
||||
color: selected ? AppColors.navSelected : AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeSectionFont - 2,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: selected ? AppColors.navSelected : AppColors.textTertiary,
|
||||
fontSize: AppDimens.homeSecondaryTagFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -134,7 +134,7 @@ class _RecommendPageState extends State<RecommendPage> {
|
||||
height: 56,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.pageHorizontal,
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -189,9 +189,9 @@ class _RecommendPageState extends State<RecommendPage> {
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? AppColors.textPrimary
|
||||
: AppColors.textSecondary,
|
||||
fontSize: AppDimens.listTitle,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
: AppColors.textTertiary,
|
||||
fontSize: AppDimens.homePrimaryTabFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
|
||||
@@ -47,7 +47,7 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
builder: (context, constraints) {
|
||||
final itemWidth =
|
||||
(constraints.maxWidth -
|
||||
AppDimens.pageHorizontal * 2 -
|
||||
AppDimens.feedPageHorizontal * 2 -
|
||||
AppDimens.homeGridGap * 2) /
|
||||
3;
|
||||
final itemExtent = itemWidth * 6 / 5 + AppDimens.homeCardBodyExtent;
|
||||
@@ -77,14 +77,14 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.pageHorizontal,
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
sliver: SliverGrid(
|
||||
key: const Key('all-dramas-grid'),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
@@ -122,9 +122,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
final genres = _availableGenres(l10n);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceSm,
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceMd,
|
||||
),
|
||||
child: Column(
|
||||
@@ -207,21 +207,21 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
|
||||
return InkWell(
|
||||
key: key,
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
borderRadius: BorderRadius.circular(AppDimens.filterChipRadius),
|
||||
child: Container(
|
||||
height: 32,
|
||||
height: AppDimens.filterChipHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.contentChip : AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||
color: selected ? AppColors.contentSurface : AppColors.transparent,
|
||||
borderRadius: BorderRadius.circular(AppDimens.filterChipRadius),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected ? AppColors.textPrimary : AppColors.textSecondary,
|
||||
fontSize: AppDimens.caption,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
color: selected ? AppColors.textPrimary : AppColors.textTertiary,
|
||||
fontSize: AppDimens.filterChipFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -10,6 +10,8 @@ import '../../../core/widgets/remote_image.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../home/widgets/home_drama_classification.dart';
|
||||
|
||||
final RegExp _tagSplitter = RegExp(r'[,/、·]');
|
||||
|
||||
class RecommendSectionCard extends StatelessWidget {
|
||||
const RecommendSectionCard({
|
||||
super.key,
|
||||
@@ -26,7 +28,7 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return SizedBox(
|
||||
width: 122,
|
||||
width: AppDimens.recommendCardWidth,
|
||||
child: InkWell(
|
||||
key: Key('recommend-card-$sectionKey-${drama.id}'),
|
||||
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
|
||||
@@ -38,7 +40,7 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppDimens.homeCardRadius),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 4,
|
||||
aspectRatio: AppDimens.cardCoverAspectRatio,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -55,20 +57,20 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
child: Container(
|
||||
key: Key('recommend-hot-$sectionKey-${drama.id}'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
horizontal: AppDimens.cardBadgeHorizontalPadding,
|
||||
vertical: AppDimens.cardBadgeVerticalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.badgeHot,
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppDimens.chipRadius,
|
||||
AppDimens.cardBadgeRadius,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
l10n.homeBadgeHot,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.homeCardBadge,
|
||||
fontSize: AppDimens.cardBadgeFont,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -78,19 +80,20 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceXs),
|
||||
const SizedBox(height: AppDimens.cardCoverTitleGap),
|
||||
Row(
|
||||
key: Key('recommend-views-$sectionKey-${drama.id}'),
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: AppColors.textSecondary,
|
||||
size: AppDimens.homeCardMeta,
|
||||
color: AppColors.textPrimary,
|
||||
size: AppDimens.cardViewsIcon,
|
||||
),
|
||||
Text(
|
||||
formatInteractionCount(drama.viewCount),
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.homeCardMeta,
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.cardViewsFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -103,14 +106,65 @@ class RecommendSectionCard extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.homeCardTitle,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
fontSize: AppDimens.cardTitleFont,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: AppDimens.cardTitleLineHeight,
|
||||
),
|
||||
),
|
||||
if (_cardTags().isNotEmpty) ...[
|
||||
const SizedBox(height: AppDimens.cardTitleTagGap),
|
||||
_tags(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tags() {
|
||||
final tags = _cardTags();
|
||||
return Row(
|
||||
children: [
|
||||
for (final (index, tag) in tags.indexed)
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: index == tags.length - 1 ? 0 : AppDimens.spaceXs,
|
||||
),
|
||||
child: Container(
|
||||
key: Key('recommend-tag-$sectionKey-${drama.id}-$index'),
|
||||
height: AppDimens.cardTagHeight,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.cardTagHorizontalPadding,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.cardTagRadius),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textTertiary,
|
||||
fontSize: AppDimens.cardTagFont,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _cardTags() {
|
||||
final seen = <String>{};
|
||||
return [drama.genre, drama.category]
|
||||
.expand((value) => value.split(_tagSplitter))
|
||||
.map((value) => value.trim())
|
||||
.where((value) => value.isNotEmpty && seen.add(value))
|
||||
.take(2)
|
||||
.toList(growable: false);
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.pageHorizontal,
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -145,7 +145,7 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: AppDimens.sectionTitle,
|
||||
fontSize: AppDimens.recommendSectionTitle,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -154,7 +154,7 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
subtitle,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.cardViews,
|
||||
fontSize: AppDimens.recommendSectionSubtitle,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -173,7 +173,8 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
Text(
|
||||
l10n.more,
|
||||
style: const TextStyle(
|
||||
fontSize: AppDimens.caption,
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: AppDimens.recommendSectionMore,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -186,12 +187,12 @@ class RecommendSectionsView extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: AppDimens.spaceMd),
|
||||
SizedBox(
|
||||
height: 218,
|
||||
height: AppDimens.recommendRowHeight,
|
||||
child: ListView.separated(
|
||||
key: Key('recommend-row-$key'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimens.pageHorizontal,
|
||||
horizontal: AppDimens.feedPageHorizontal,
|
||||
),
|
||||
itemCount: dramas.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:fruitshorts/core/data/models/watch_progress.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
@@ -41,6 +43,50 @@ void main() {
|
||||
expect(find.byKey(const Key('following-tab-reserve')), findsOneWidget);
|
||||
expect(find.byKey(const Key('following-favorites-grid')), findsOneWidget);
|
||||
expect(find.byType(CustomScrollView), findsWidgets);
|
||||
final tab = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('following-tab-favorites')),
|
||||
matching: find.text('Following'),
|
||||
),
|
||||
);
|
||||
expect(tab.style?.fontSize, AppDimens.followingTabFont);
|
||||
expect(tab.style?.fontWeight, FontWeight.w700);
|
||||
expect(tab.style?.color, AppColors.textPrimary);
|
||||
final grid = tester.widget<SliverGrid>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('following-favorites-grid')),
|
||||
matching: find.byType(SliverGrid),
|
||||
),
|
||||
);
|
||||
final delegate =
|
||||
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
||||
expect(delegate.crossAxisSpacing, AppDimens.homeGridGap);
|
||||
expect(delegate.mainAxisSpacing, AppDimens.homeGridRowGap);
|
||||
expect(delegate.childAspectRatio, AppDimens.followingGridAspectRatio);
|
||||
final card = find.byKey(const Key('following-card-the-signal-demo'));
|
||||
final cover = tester.widget<AspectRatio>(
|
||||
find.byKey(const Key('following-cover-the-signal-demo')),
|
||||
);
|
||||
expect(cover.aspectRatio, AppDimens.cardCoverAspectRatio);
|
||||
final title = tester.widget<Text>(
|
||||
find.descendant(of: card, matching: find.text('The Signal')),
|
||||
);
|
||||
expect(title.style?.fontSize, AppDimens.cardTitleFont);
|
||||
expect(title.style?.fontWeight, FontWeight.w500);
|
||||
expect(title.style?.height, AppDimens.cardTitleLineHeight);
|
||||
final views = tester.widget<Text>(
|
||||
find.byKey(const Key('following-views-the-signal-demo')),
|
||||
);
|
||||
expect(views.style?.fontSize, AppDimens.cardViewsFont);
|
||||
expect(views.style?.color, AppColors.textPrimary);
|
||||
final tag = find.byKey(const Key('following-tag-the-signal-demo-0'));
|
||||
expect(tag, findsOneWidget);
|
||||
expect(tester.getSize(tag).height, AppDimens.cardTagHeight);
|
||||
final tagText = tester.widget<Text>(
|
||||
find.descendant(of: tag, matching: find.byType(Text)),
|
||||
);
|
||||
expect(tagText.style?.fontSize, AppDimens.cardTagFont);
|
||||
expect(tagText.style?.color, AppColors.textTertiary);
|
||||
|
||||
await tester.tap(find.byKey(const Key('following-edit')));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -171,6 +217,33 @@ void main() {
|
||||
);
|
||||
expect(find.byIcon(Icons.edit_outlined), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_fit_following_cards_on_compact_viewport', (tester) async {
|
||||
tester.view.physicalSize = const Size(320, 640);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
_signIn();
|
||||
final catalog = FakeCatalogRepository();
|
||||
await tester.pumpWidget(
|
||||
_app(
|
||||
catalog: catalog,
|
||||
favorites: InMemoryFavoriteRepository(
|
||||
cards: catalog.dramas.take(1).toList(),
|
||||
saved: {catalog.dramas.first.id},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('tab-follow')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('following-card-the-signal-demo')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
void _signIn() {
|
||||
|
||||
@@ -40,9 +40,19 @@ void main() {
|
||||
final cover = find.byKey(const Key('home-feed-cover-anime-card'));
|
||||
final coverSize = tester.getSize(cover);
|
||||
expect(coverSize.height / coverSize.width, closeTo(6 / 5, 0.02));
|
||||
final badge = find.byKey(const Key('home-feed-badge-anime-anime-card'));
|
||||
expect(badge, findsOneWidget);
|
||||
final badgeContainer = tester.widget<Container>(badge);
|
||||
expect(
|
||||
find.byKey(const Key('home-feed-badge-anime-anime-card')),
|
||||
findsOneWidget,
|
||||
(badgeContainer.decoration! as BoxDecoration).borderRadius,
|
||||
BorderRadius.circular(AppDimens.cardBadgeRadius),
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<Text>(find.descendant(of: badge, matching: find.byType(Text)))
|
||||
.style
|
||||
?.fontSize,
|
||||
AppDimens.cardBadgeFont,
|
||||
);
|
||||
expect(find.text('Anime'), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-feed-views-anime-card')), findsOneWidget);
|
||||
@@ -51,16 +61,31 @@ void main() {
|
||||
find.descendant(of: views, matching: find.byType(Text)),
|
||||
);
|
||||
expect(viewsText.data, startsWith('44.'));
|
||||
expect(
|
||||
tester
|
||||
.widget<Text>(find.byKey(const Key('home-feed-title-anime-card')))
|
||||
.maxLines,
|
||||
2,
|
||||
expect(viewsText.style?.fontSize, AppDimens.cardViewsFont);
|
||||
expect(viewsText.style?.color, AppColors.navSelected);
|
||||
final title = tester.widget<Text>(
|
||||
find.byKey(const Key('home-feed-title-anime-card')),
|
||||
);
|
||||
expect(find.byKey(const Key('home-feed-tag-anime-card-0')), findsOneWidget);
|
||||
expect(title.maxLines, 2);
|
||||
expect(title.style?.fontSize, AppDimens.cardTitleFont);
|
||||
expect(title.style?.fontWeight, FontWeight.w500);
|
||||
expect(title.style?.height, AppDimens.cardTitleLineHeight);
|
||||
final firstTag = find.byKey(const Key('home-feed-tag-anime-card-0'));
|
||||
expect(firstTag, findsOneWidget);
|
||||
expect(find.byKey(const Key('home-feed-tag-anime-card-1')), findsOneWidget);
|
||||
expect(find.text('Romance'), findsOneWidget);
|
||||
expect(find.text('Urban'), findsOneWidget);
|
||||
final tagContainer = tester.widget<Container>(firstTag);
|
||||
expect(tester.getSize(firstTag).height, AppDimens.cardTagHeight);
|
||||
expect(
|
||||
(tagContainer.decoration! as BoxDecoration).borderRadius,
|
||||
BorderRadius.circular(AppDimens.cardTagRadius),
|
||||
);
|
||||
final tagText = tester.widget<Text>(
|
||||
find.descendant(of: firstTag, matching: find.byType(Text)),
|
||||
);
|
||||
expect(tagText.style?.fontSize, AppDimens.cardTagFont);
|
||||
expect(tagText.style?.color, AppColors.textTertiary);
|
||||
|
||||
await tester.tap(card);
|
||||
expect(taps, 1);
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/core/data/services/playback_thumbnail_service.dart';
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
||||
@@ -82,8 +84,8 @@ void main() {
|
||||
final delegate =
|
||||
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
||||
expect(delegate.crossAxisCount, 3);
|
||||
expect(delegate.crossAxisSpacing, 8);
|
||||
expect(delegate.mainAxisSpacing, 8);
|
||||
expect(delegate.crossAxisSpacing, AppDimens.homeGridGap);
|
||||
expect(delegate.mainAxisSpacing, AppDimens.homeGridRowGap);
|
||||
expect(find.byKey(const Key('home-section-featured')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-section-new')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-section-categories')), findsOneWidget);
|
||||
@@ -92,6 +94,67 @@ void main() {
|
||||
expect(find.byKey(const Key('home-section-category-社群')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-grid-filter')), findsOneWidget);
|
||||
final searchBox = find.descendant(
|
||||
of: find.byKey(const Key('home-search-entry')),
|
||||
matching: find.byType(Container),
|
||||
);
|
||||
expect(tester.getSize(searchBox).height, AppDimens.searchHeight);
|
||||
expect(
|
||||
(tester.widget<Container>(searchBox).decoration! as BoxDecoration)
|
||||
.borderRadius,
|
||||
BorderRadius.circular(AppDimens.searchRadius),
|
||||
);
|
||||
final placeholder = tester.widget<Text>(
|
||||
find.text('Search dramas, actors, genres...'),
|
||||
);
|
||||
expect(placeholder.style?.fontSize, AppDimens.searchPlaceholderFont);
|
||||
expect(placeholder.style?.color, AppColors.textSecondary);
|
||||
final selectedPrimary = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('home-section-featured')),
|
||||
matching: find.text('Featured'),
|
||||
),
|
||||
);
|
||||
final unselectedPrimary = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('home-section-new')),
|
||||
matching: find.text('New'),
|
||||
),
|
||||
);
|
||||
expect(selectedPrimary.style?.fontSize, AppDimens.homePrimaryTabFont);
|
||||
expect(selectedPrimary.style?.fontWeight, FontWeight.w700);
|
||||
expect(selectedPrimary.style?.color, AppColors.navSelected);
|
||||
expect(unselectedPrimary.style?.fontWeight, FontWeight.w400);
|
||||
expect(unselectedPrimary.style?.color, AppColors.textTertiary);
|
||||
final secondaryTag = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('home-tag-hot')),
|
||||
matching: find.text('Hot'),
|
||||
),
|
||||
);
|
||||
expect(secondaryTag.style?.fontSize, AppDimens.homeSecondaryTagFont);
|
||||
expect(secondaryTag.style?.fontWeight, FontWeight.w400);
|
||||
expect(secondaryTag.style?.color, AppColors.textTertiary);
|
||||
final unselectedTagChip = tester.widget<Container>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('home-tag-hot')),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
(unselectedTagChip.decoration! as BoxDecoration).color,
|
||||
AppColors.transparent,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('home-tag-hot')));
|
||||
await tester.pump();
|
||||
final selectedTag = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('home-tag-hot')),
|
||||
matching: find.text('Hot'),
|
||||
),
|
||||
);
|
||||
expect(selectedTag.style?.color, AppColors.navSelected);
|
||||
expect(selectedTag.style?.fontWeight, FontWeight.w700);
|
||||
expect(find.byType(PageView), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
@@ -128,10 +191,7 @@ void main() {
|
||||
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||
expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget);
|
||||
expect(find.byType(PlayerPage), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode,
|
||||
isTrue,
|
||||
);
|
||||
expect(tester.widget<PlayerPage>(find.byType(PlayerPage)).homeMode, isTrue);
|
||||
expect(find.byKey(const Key('recommend-section-hits')), findsNothing);
|
||||
|
||||
await tester.tap(find.byKey(const Key('recommend-tab-playlist')));
|
||||
|
||||
@@ -41,6 +41,14 @@ void main() {
|
||||
expect(find.byKey(const Key('mine-vip')), findsOneWidget);
|
||||
expect(find.byKey(const Key('mine-vip-open')), findsOneWidget);
|
||||
expect(find.byKey(const Key('mine-menu')), findsOneWidget);
|
||||
final title = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('mine-page-surface')),
|
||||
matching: find.text('My'),
|
||||
),
|
||||
);
|
||||
expect(title.style?.fontSize, AppDimens.minePageTitle);
|
||||
expect(title.style?.fontWeight, FontWeight.w700);
|
||||
expect(find.byKey(const Key('mine-mini-player')), findsNothing);
|
||||
for (final oldShortcut in const [
|
||||
Key('mine-watchlist'),
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:fruitshorts/app.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||
import 'package:fruitshorts/core/widgets/app_bottom_nav.dart';
|
||||
import 'package:fruitshorts/pages/player/player_view.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
@@ -66,6 +68,53 @@ void main() {
|
||||
expect(find.text('New This Week'), findsOneWidget);
|
||||
expect(find.text('Costume Drama Awards'), findsOneWidget);
|
||||
expect(find.byType(AppBottomNav), findsOneWidget);
|
||||
final sectionTitle = tester.widget<Text>(find.text("This Week's Hits"));
|
||||
expect(sectionTitle.style?.fontSize, AppDimens.recommendSectionTitle);
|
||||
expect(sectionTitle.style?.fontWeight, FontWeight.w700);
|
||||
final sectionSubtitle = tester.widget<Text>(
|
||||
find.text('Binge the hottest stories of the week!'),
|
||||
);
|
||||
expect(sectionSubtitle.style?.fontSize, AppDimens.recommendSectionSubtitle);
|
||||
expect(sectionSubtitle.style?.color, AppColors.textSecondary);
|
||||
final moreText = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('recommend-more-hits')),
|
||||
matching: find.text('More'),
|
||||
),
|
||||
);
|
||||
expect(moreText.style?.fontSize, AppDimens.recommendSectionMore);
|
||||
expect(moreText.style?.color, AppColors.textSecondary);
|
||||
final card = find.byKey(const Key('recommend-card-hits-the-signal-demo'));
|
||||
final cover = tester.widget<AspectRatio>(
|
||||
find.descendant(of: card, matching: find.byType(AspectRatio)),
|
||||
);
|
||||
expect(cover.aspectRatio, AppDimens.cardCoverAspectRatio);
|
||||
final cardTitle = tester.widget<Text>(
|
||||
find.byKey(const Key('recommend-title-hits-the-signal-demo')),
|
||||
);
|
||||
expect(cardTitle.style?.fontSize, AppDimens.cardTitleFont);
|
||||
expect(cardTitle.style?.fontWeight, FontWeight.w500);
|
||||
expect(cardTitle.style?.height, AppDimens.cardTitleLineHeight);
|
||||
final views = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('recommend-views-hits-the-signal-demo')),
|
||||
matching: find.byType(Text),
|
||||
),
|
||||
);
|
||||
expect(views.style?.fontSize, AppDimens.cardViewsFont);
|
||||
expect(views.style?.color, AppColors.textPrimary);
|
||||
final tag = find.byKey(const Key('recommend-tag-hits-the-signal-demo-0'));
|
||||
expect(tag, findsOneWidget);
|
||||
expect(tester.getSize(tag).height, AppDimens.cardTagHeight);
|
||||
final tagText = tester.widget<Text>(
|
||||
find.descendant(of: tag, matching: find.byType(Text)),
|
||||
);
|
||||
expect(tagText.style?.fontSize, AppDimens.cardTagFont);
|
||||
expect(tagText.style?.color, AppColors.textTertiary);
|
||||
expect(
|
||||
(tester.widget<Container>(tag).decoration! as BoxDecoration).borderRadius,
|
||||
BorderRadius.circular(AppDimens.cardTagRadius),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should_switch_inner_tabs_without_losing_grid_filter_state', (
|
||||
@@ -88,6 +137,21 @@ void main() {
|
||||
await tester.tap(find.byKey(const Key('all-sort-new')));
|
||||
await tester.pump();
|
||||
expect(_textStyle(tester, 'New').fontWeight, FontWeight.w700);
|
||||
expect(_textStyle(tester, 'New').fontSize, AppDimens.filterChipFont);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const Key('all-sort-new'))).height,
|
||||
AppDimens.filterChipHeight,
|
||||
);
|
||||
final chipContainer = tester.widget<Container>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('all-sort-new')),
|
||||
matching: find.byType(Container),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
(chipContainer.decoration! as BoxDecoration).borderRadius,
|
||||
BorderRadius.circular(AppDimens.filterChipRadius),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const Key('recommend-tab-playlist')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in new issue
Block a user