diff --git a/lib/core/data/models/drama.dart b/lib/core/data/models/drama.dart index 8dc16b9..6611f72 100644 --- a/lib/core/data/models/drama.dart +++ b/lib/core/data/models/drama.dart @@ -97,6 +97,7 @@ class Drama { required this.title, required this.synopsis, required this.posterUrl, + this.coverLandscape = '', this.episodes = const [], this.titleLocales = const {}, this.synopsisLocales = const {}, @@ -133,6 +134,9 @@ class Drama { final String tagCode; final String tagColor; final String posterUrl; + + /// 横版封面,用于继续观看横幅等横向展示位;为空时回退竖版 `posterUrl`。 + final String coverLandscape; final List episodes; final String category; final Map categoryLocales; @@ -179,6 +183,7 @@ class Drama { tagCode: (json['tagCode'] as String? ?? '').trim(), tagColor: json['tagColor'] as String? ?? '', posterUrl: json['cover'] as String? ?? '', + coverLandscape: json['coverLandscape'] as String? ?? '', episodes: preview is Map ? [Episode.fromJson(preview)] : const [], @@ -212,6 +217,7 @@ class Drama { tagCode: (json['tagCode'] as String? ?? '').trim(), tagColor: json['tagColor'] as String? ?? '', posterUrl: json['cover'] as String? ?? '', + coverLandscape: json['coverLandscape'] as String? ?? '', category: text.category, categoryLocales: text.categoryLocales, types: text.types, diff --git a/lib/core/data/models/watch_progress.dart b/lib/core/data/models/watch_progress.dart index ae9fd71..da73ae3 100644 --- a/lib/core/data/models/watch_progress.dart +++ b/lib/core/data/models/watch_progress.dart @@ -16,6 +16,7 @@ class WatchProgress { this.dramaTitle, this.titleLocales = const {}, this.dramaCover, + this.dramaCoverLandscape, this.episodeNumber, this.totalEpisodes, this.finished = false, @@ -29,6 +30,9 @@ class WatchProgress { final String? dramaTitle; final Map titleLocales; final String? dramaCover; + + /// 横版封面,用于继续观看横幅等横向位;为空时回退 [dramaCover]。 + final String? dramaCoverLandscape; final int? episodeNumber; final int? totalEpisodes; final Duration position; @@ -60,6 +64,7 @@ class WatchProgress { dramaTitle: json['dramaTitle']?.toString(), titleLocales: localeMap(json['titleLocales']), dramaCover: json['dramaCover']?.toString(), + dramaCoverLandscape: json['dramaCoverLandscape']?.toString(), episodeNumber: json['episodeNumber'] == null ? null : jsonToInt(json['episodeNumber']), @@ -83,6 +88,7 @@ class WatchProgress { if (dramaTitle != null) 'dramaTitle': dramaTitle, if (titleLocales.isNotEmpty) 'titleLocales': titleLocales, if (dramaCover != null) 'dramaCover': dramaCover, + if (dramaCoverLandscape != null) 'dramaCoverLandscape': dramaCoverLandscape, if (episodeNumber != null) 'episodeNumber': episodeNumber, if (totalEpisodes != null) 'totalEpisodes': totalEpisodes, 'positionMs': position.inMilliseconds, diff --git a/lib/core/data/models/watch_record.dart b/lib/core/data/models/watch_record.dart index 021ddb1..e5e239f 100644 --- a/lib/core/data/models/watch_record.dart +++ b/lib/core/data/models/watch_record.dart @@ -46,6 +46,14 @@ class WatchRecord { 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 => displayTitle.isNotEmpty || diff --git a/lib/core/theme/app_dimens.dart b/lib/core/theme/app_dimens.dart index 84d6ee0..396bac3 100644 --- a/lib/core/theme/app_dimens.dart +++ b/lib/core/theme/app_dimens.dart @@ -37,7 +37,7 @@ abstract final class AppDimens { static const double previewSectionTitle = 14; static const double chipFont = 10; static const double navLabel = 13; - static const double navLabelGap = 5; + static const double navLabelGap = 1; static const double buttonHeight = 48; static const double primaryButtonHeight = 52; diff --git a/lib/pages/following/widgets/continue_watching_banner.dart b/lib/pages/following/widgets/continue_watching_banner.dart index 22461e2..3688c23 100644 --- a/lib/pages/following/widgets/continue_watching_banner.dart +++ b/lib/pages/following/widgets/continue_watching_banner.dart @@ -33,13 +33,12 @@ class ContinueWatchingBanner extends StatelessWidget { return SizedBox( height: AppDimens.followingBannerHeight, child: ClipRRect( - borderRadius: - BorderRadius.circular(AppDimens.followingBannerRadius), + borderRadius: BorderRadius.circular(AppDimens.followingBannerRadius), child: Stack( fit: StackFit.expand, children: [ RemoteImage( - record.displayCover, + record.displayLandscapeCover, fit: BoxFit.cover, showFallbackIcon: true, fallbackColor: AppColors.contentSurface, @@ -82,8 +81,7 @@ class ContinueWatchingBanner extends StatelessWidget { overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.bannerTitleWarm, - fontSize: - AppDimens.followingBannerTitleFont, + fontSize: AppDimens.followingBannerTitleFont, fontWeight: FontWeight.w400, ), ), @@ -143,8 +141,7 @@ class _ContinueButton extends StatelessWidget { height: AppDimens.followingBannerButtonHeight, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: - AppDimens.followingBannerButtonHorizontalPadding, + horizontal: AppDimens.followingBannerButtonHorizontalPadding, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -188,9 +185,7 @@ class _ProgressBar extends StatelessWidget { height: AppDimens.followingBannerProgressHeight, width: double.infinity, child: DecoratedBox( - decoration: const BoxDecoration( - color: Color(0x66FFFFFF), - ), + decoration: const BoxDecoration(color: Color(0x66FFFFFF)), child: Align( alignment: Alignment.centerLeft, child: FractionallySizedBox( diff --git a/test/catalog/drama_model_test.dart b/test/catalog/drama_model_test.dart index ef2ea7f..d041866 100644 --- a/test/catalog/drama_model_test.dart +++ b/test/catalog/drama_model_test.dart @@ -46,6 +46,23 @@ void main() { 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', () { final drama = Drama.fromDetailJson(const { 'id': 5, diff --git a/test/theme/app_dimens_test.dart b/test/theme/app_dimens_test.dart index df8c7a6..cc63132 100644 --- a/test/theme/app_dimens_test.dart +++ b/test/theme/app_dimens_test.dart @@ -19,7 +19,7 @@ void main() { expect(AppDimens.sectionTitle, 16); expect(AppDimens.pageTitle, 20); expect(AppDimens.navLabel, 13); - expect(AppDimens.navLabelGap, 5); + expect(AppDimens.navLabelGap, 1); expect(AppDimens.navIcon, 30); expect(AppDimens.buttonHeight, 48); expect(AppDimens.primaryButtonHeight, 52); diff --git a/test/watch/watch_records_controller_test.dart b/test/watch/watch_records_controller_test.dart index 5a415fa..f58d788 100644 --- a/test/watch/watch_records_controller_test.dart +++ b/test/watch/watch_records_controller_test.dart @@ -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/page_result.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/watch_history_repository.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; @@ -278,6 +279,25 @@ void main() { 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 {