feat(cover): 支持横版封面并收紧底栏图标间距
flutter-ci / test (push) Canceled after 0s

- Drama 新增 coverLandscape,WatchProgress 新增 dramaCoverLandscape
- 继续观看横幅优先使用横版封面,缺省回退竖版
- 底部导航栏图标与文字间距由 5dp 收紧至 1dp
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-29 17:32:41 +08:00
1 parent 3590cd0c55
commit 26321e3348
8 files changed
+64 -12

No files matched your search

+6
View File
@@ -97,6 +97,7 @@ class Drama {
required this.title, required this.title,
required this.synopsis, required this.synopsis,
required this.posterUrl, required this.posterUrl,
this.coverLandscape = '',
this.episodes = const [], this.episodes = const [],
this.titleLocales = const {}, this.titleLocales = const {},
this.synopsisLocales = const {}, this.synopsisLocales = const {},
@@ -133,6 +134,9 @@ class Drama {
final String tagCode; final String tagCode;
final String tagColor; final String tagColor;
final String posterUrl; final String posterUrl;
/// 横版封面,用于继续观看横幅等横向展示位;为空时回退竖版 `posterUrl`。
final String coverLandscape;
final List<Episode> episodes; final List<Episode> episodes;
final String category; final String category;
final Map<String, String> categoryLocales; final Map<String, String> categoryLocales;
@@ -179,6 +183,7 @@ class Drama {
tagCode: (json['tagCode'] as String? ?? '').trim(), tagCode: (json['tagCode'] as String? ?? '').trim(),
tagColor: json['tagColor'] as String? ?? '', tagColor: json['tagColor'] as String? ?? '',
posterUrl: json['cover'] as String? ?? '', posterUrl: json['cover'] as String? ?? '',
coverLandscape: json['coverLandscape'] as String? ?? '',
episodes: preview is Map<String, dynamic> episodes: preview is Map<String, dynamic>
? [Episode.fromJson(preview)] ? [Episode.fromJson(preview)]
: const [], : const [],
@@ -212,6 +217,7 @@ class Drama {
tagCode: (json['tagCode'] as String? ?? '').trim(), tagCode: (json['tagCode'] as String? ?? '').trim(),
tagColor: json['tagColor'] as String? ?? '', tagColor: json['tagColor'] as String? ?? '',
posterUrl: json['cover'] as String? ?? '', posterUrl: json['cover'] as String? ?? '',
coverLandscape: json['coverLandscape'] as String? ?? '',
category: text.category, category: text.category,
categoryLocales: text.categoryLocales, categoryLocales: text.categoryLocales,
types: text.types, types: text.types,
+6
View File
@@ -16,6 +16,7 @@ class WatchProgress {
this.dramaTitle, this.dramaTitle,
this.titleLocales = const {}, this.titleLocales = const {},
this.dramaCover, this.dramaCover,
this.dramaCoverLandscape,
this.episodeNumber, this.episodeNumber,
this.totalEpisodes, this.totalEpisodes,
this.finished = false, this.finished = false,
@@ -29,6 +30,9 @@ class WatchProgress {
final String? dramaTitle; final String? dramaTitle;
final Map<String, String> titleLocales; final Map<String, String> titleLocales;
final String? dramaCover; final String? dramaCover;
/// 横版封面,用于继续观看横幅等横向位;为空时回退 [dramaCover]。
final String? dramaCoverLandscape;
final int? episodeNumber; final int? episodeNumber;
final int? totalEpisodes; final int? totalEpisodes;
final Duration position; final Duration position;
@@ -60,6 +64,7 @@ class WatchProgress {
dramaTitle: json['dramaTitle']?.toString(), dramaTitle: json['dramaTitle']?.toString(),
titleLocales: localeMap(json['titleLocales']), titleLocales: localeMap(json['titleLocales']),
dramaCover: json['dramaCover']?.toString(), dramaCover: json['dramaCover']?.toString(),
dramaCoverLandscape: json['dramaCoverLandscape']?.toString(),
episodeNumber: json['episodeNumber'] == null episodeNumber: json['episodeNumber'] == null
? null ? null
: jsonToInt(json['episodeNumber']), : jsonToInt(json['episodeNumber']),
@@ -83,6 +88,7 @@ class WatchProgress {
if (dramaTitle != null) 'dramaTitle': dramaTitle, if (dramaTitle != null) 'dramaTitle': dramaTitle,
if (titleLocales.isNotEmpty) 'titleLocales': titleLocales, if (titleLocales.isNotEmpty) 'titleLocales': titleLocales,
if (dramaCover != null) 'dramaCover': dramaCover, if (dramaCover != null) 'dramaCover': dramaCover,
if (dramaCoverLandscape != null) 'dramaCoverLandscape': dramaCoverLandscape,
if (episodeNumber != null) 'episodeNumber': episodeNumber, if (episodeNumber != null) 'episodeNumber': episodeNumber,
if (totalEpisodes != null) 'totalEpisodes': totalEpisodes, if (totalEpisodes != null) 'totalEpisodes': totalEpisodes,
'positionMs': position.inMilliseconds, 'positionMs': position.inMilliseconds,
+8
View File
@@ -46,6 +46,14 @@ class WatchRecord {
return pageCover.isNotEmpty ? pageCover : drama?.posterUrl ?? ''; return pageCover.isNotEmpty ? pageCover : drama?.posterUrl ?? '';
} }
/// 横向展示位(继续观看横幅)优先用横版封面,缺省回退竖版。
String get displayLandscapeCover {
final pageLandscape = progress.dramaCoverLandscape?.trim() ?? '';
if (pageLandscape.isNotEmpty) return pageLandscape;
final landscape = drama?.coverLandscape.trim() ?? '';
return landscape.isNotEmpty ? landscape : displayCover;
}
/// 分页接口已带展示字段时无需等待详情加载。 /// 分页接口已带展示字段时无需等待详情加载。
bool get hasDisplayMetadata => bool get hasDisplayMetadata =>
displayTitle.isNotEmpty || displayTitle.isNotEmpty ||
+1 -1
View File
@@ -37,7 +37,7 @@ abstract final class AppDimens {
static const double previewSectionTitle = 14; static const double previewSectionTitle = 14;
static const double chipFont = 10; static const double chipFont = 10;
static const double navLabel = 13; static const double navLabel = 13;
static const double navLabelGap = 5; static const double navLabelGap = 1;
static const double buttonHeight = 48; static const double buttonHeight = 48;
static const double primaryButtonHeight = 52; static const double primaryButtonHeight = 52;
@@ -33,13 +33,12 @@ class ContinueWatchingBanner extends StatelessWidget {
return SizedBox( return SizedBox(
height: AppDimens.followingBannerHeight, height: AppDimens.followingBannerHeight,
child: ClipRRect( child: ClipRRect(
borderRadius: borderRadius: BorderRadius.circular(AppDimens.followingBannerRadius),
BorderRadius.circular(AppDimens.followingBannerRadius),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
RemoteImage( RemoteImage(
record.displayCover, record.displayLandscapeCover,
fit: BoxFit.cover, fit: BoxFit.cover,
showFallbackIcon: true, showFallbackIcon: true,
fallbackColor: AppColors.contentSurface, fallbackColor: AppColors.contentSurface,
@@ -82,8 +81,7 @@ class ContinueWatchingBanner extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
color: AppColors.bannerTitleWarm, color: AppColors.bannerTitleWarm,
fontSize: fontSize: AppDimens.followingBannerTitleFont,
AppDimens.followingBannerTitleFont,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
), ),
@@ -143,8 +141,7 @@ class _ContinueButton extends StatelessWidget {
height: AppDimens.followingBannerButtonHeight, height: AppDimens.followingBannerButtonHeight,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: horizontal: AppDimens.followingBannerButtonHorizontalPadding,
AppDimens.followingBannerButtonHorizontalPadding,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -188,9 +185,7 @@ class _ProgressBar extends StatelessWidget {
height: AppDimens.followingBannerProgressHeight, height: AppDimens.followingBannerProgressHeight,
width: double.infinity, width: double.infinity,
child: DecoratedBox( child: DecoratedBox(
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Color(0x66FFFFFF)),
color: Color(0x66FFFFFF),
),
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FractionallySizedBox( child: FractionallySizedBox(
+17
View File
@@ -46,6 +46,23 @@ void main() {
expect(drama.episodeCount, 12); expect(drama.episodeCount, 12);
}); });
test('should_parse_landscape_cover_for_horizontal_slots', () {
final card = Drama.fromCardJson(const {
'id': 5,
'title': '闪婚后',
'cover': '/files/cover.jpg',
'coverLandscape': '/files/cover-wide.jpg',
});
final legacy = Drama.fromDetailJson(const {
'id': 6,
'title': '替嫁新娘',
'cover': '/files/cover-2.jpg',
});
expect(card.coverLandscape, '/files/cover-wide.jpg');
expect(legacy.coverLandscape, isEmpty);
});
test('should_count_shipped_episodes_when_detail_response_is_parsed', () { test('should_count_shipped_episodes_when_detail_response_is_parsed', () {
final drama = Drama.fromDetailJson(const { final drama = Drama.fromDetailJson(const {
'id': 5, 'id': 5,
+1 -1
View File
@@ -19,7 +19,7 @@ void main() {
expect(AppDimens.sectionTitle, 16); expect(AppDimens.sectionTitle, 16);
expect(AppDimens.pageTitle, 20); expect(AppDimens.pageTitle, 20);
expect(AppDimens.navLabel, 13); expect(AppDimens.navLabel, 13);
expect(AppDimens.navLabelGap, 5); expect(AppDimens.navLabelGap, 1);
expect(AppDimens.navIcon, 30); expect(AppDimens.navIcon, 30);
expect(AppDimens.buttonHeight, 48); expect(AppDimens.buttonHeight, 48);
expect(AppDimens.primaryButtonHeight, 52); expect(AppDimens.primaryButtonHeight, 52);
@@ -8,6 +8,7 @@ import 'package:fruitshorts/core/data/models/feed_page.dart';
import 'package:fruitshorts/core/data/models/home_feed.dart'; import 'package:fruitshorts/core/data/models/home_feed.dart';
import 'package:fruitshorts/core/data/models/page_result.dart'; import 'package:fruitshorts/core/data/models/page_result.dart';
import 'package:fruitshorts/core/data/models/watch_progress.dart'; import 'package:fruitshorts/core/data/models/watch_progress.dart';
import 'package:fruitshorts/core/data/models/watch_record.dart';
import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart';
@@ -278,6 +279,25 @@ void main() {
expect(controller.error, '账号不存在,请先注册'); expect(controller.error, '账号不存在,请先注册');
}); });
test('should_prefer_page_landscape_cover_and_fall_back_to_portrait', () {
WatchRecord record(String? landscape) => WatchRecord(
progress: WatchProgress(
dramaId: '1',
episodeId: 'episode-1',
position: const Duration(seconds: 12),
duration: const Duration(seconds: 60),
dramaCover: '/files/poster.webp',
dramaCoverLandscape: landscape,
),
);
expect(
record('/files/wide.webp').displayLandscapeCover,
'/files/wide.webp',
);
expect(record(null).displayLandscapeCover, '/files/poster.webp');
});
} }
class ControlledWatchHistory implements WatchHistoryRepository { class ControlledWatchHistory implements WatchHistoryRepository {