feat(detail): 重做详情页视觉结构

This commit is contained in:
Zhou Weidong committed 2026-10-19 15:16:36 +08:00
1 parent 4553606ec2
commit ec163f905d
9 files changed
+1153 -239

No files matched your search

+7
View File
@@ -27,6 +27,13 @@
"verticalMiniSeries": "VERTICAL MINI-SERIES",
"followSeries": "Follow Series",
"seasonOneVerticalCinema": "Season 1 • Vertical Cinema",
"seasonOneOriginalRelease": "Season 1 • Original Release",
"startWatching": "Start Watching",
"autoNextOn": "Auto-Next On",
"viewAll": "View all",
"showLess": "Less",
"views": "views",
"enterFullscreen": "Fullscreen",
"artworkOnly": "Artwork",
"artworkOnlyNotice": "This is a visual catalog item. No video is available yet.",
"videoPending": "Video pending",
+7
View File
@@ -27,6 +27,13 @@
"verticalMiniSeries": "SERIAL MINI VERTIKAL",
"followSeries": "Ikuti Series",
"seasonOneVerticalCinema": "Musim 1 • Sinema Vertikal",
"seasonOneOriginalRelease": "Musim 1 • Rilis Orisinal",
"startWatching": "Mulai Menonton",
"autoNextOn": "Lanjut Otomatis",
"viewAll": "Lihat semua",
"showLess": "Lebih sedikit",
"views": "tayangan",
"enterFullscreen": "Layar penuh",
"artworkOnly": "Gambar",
"artworkOnlyNotice": "Ini hanya gambar katalog. Videonya belum tersedia.",
"videoPending": "Video belum siap",
+42
View File
@@ -262,6 +262,48 @@ abstract class AppLocalizations {
/// **'Season 1 • Vertical Cinema'**
String get seasonOneVerticalCinema;
/// No description provided for @seasonOneOriginalRelease.
///
/// In en, this message translates to:
/// **'Season 1 • Original Release'**
String get seasonOneOriginalRelease;
/// No description provided for @startWatching.
///
/// In en, this message translates to:
/// **'Start Watching'**
String get startWatching;
/// No description provided for @autoNextOn.
///
/// In en, this message translates to:
/// **'Auto-Next On'**
String get autoNextOn;
/// No description provided for @viewAll.
///
/// In en, this message translates to:
/// **'View all'**
String get viewAll;
/// No description provided for @showLess.
///
/// In en, this message translates to:
/// **'Less'**
String get showLess;
/// No description provided for @views.
///
/// In en, this message translates to:
/// **'views'**
String get views;
/// No description provided for @enterFullscreen.
///
/// In en, this message translates to:
/// **'Fullscreen'**
String get enterFullscreen;
/// No description provided for @artworkOnly.
///
/// In en, this message translates to:
+21
View File
@@ -90,6 +90,27 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get seasonOneVerticalCinema => 'Season 1 • Vertical Cinema';
@override
String get seasonOneOriginalRelease => 'Season 1 • Original Release';
@override
String get startWatching => 'Start Watching';
@override
String get autoNextOn => 'Auto-Next On';
@override
String get viewAll => 'View all';
@override
String get showLess => 'Less';
@override
String get views => 'views';
@override
String get enterFullscreen => 'Fullscreen';
@override
String get artworkOnly => 'Artwork';
+21
View File
@@ -90,6 +90,27 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get seasonOneVerticalCinema => 'Musim 1 • Sinema Vertikal';
@override
String get seasonOneOriginalRelease => 'Musim 1 • Rilis Orisinal';
@override
String get startWatching => 'Mulai Menonton';
@override
String get autoNextOn => 'Lanjut Otomatis';
@override
String get viewAll => 'Lihat semua';
@override
String get showLess => 'Lebih sedikit';
@override
String get views => 'tayangan';
@override
String get enterFullscreen => 'Layar penuh';
@override
String get artworkOnly => 'Gambar';
+21
View File
@@ -89,6 +89,27 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get seasonOneVerticalCinema => '第 1 季 · 竖屏影院';
@override
String get seasonOneOriginalRelease => '第 1 季 · 原创上线';
@override
String get startWatching => '开始观看';
@override
String get autoNextOn => '自动连播';
@override
String get viewAll => '查看全部';
@override
String get showLess => '收起';
@override
String get views => '次播放';
@override
String get enterFullscreen => '全屏';
@override
String get artworkOnly => '海报展示';
+7
View File
@@ -27,6 +27,13 @@
"verticalMiniSeries": "竖屏短剧",
"followSeries": "追剧",
"seasonOneVerticalCinema": "第 1 季 · 竖屏影院",
"seasonOneOriginalRelease": "第 1 季 · 原创上线",
"startWatching": "开始观看",
"autoNextOn": "自动连播",
"viewAll": "查看全部",
"showLess": "收起",
"views": "次播放",
"enterFullscreen": "全屏",
"artworkOnly": "海报展示",
"artworkOnlyNotice": "这张海报仅用于目录展示,暂时没有可播放视频。",
"videoPending": "视频待提供",
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,151 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:fruitshorts/core/data/models/drama.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/data/services/catalog_controller.dart';
import 'package:fruitshorts/core/data/services/favorite_controller.dart';
import 'package:fruitshorts/core/data/services/interaction_controller.dart';
import 'package:fruitshorts/core/platform/haptics.dart';
import 'package:fruitshorts/core/theme/app_theme.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart';
import '../support/fake_catalog.dart';
import '../support/fake_haptics.dart';
import '../support/fake_interaction_repositories.dart';
void main() {
testWidgets('should_render_hero_cta_synopsis_and_two_column_episode_grid', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(_host(_detailDrama()));
await tester.pumpAndSettle();
final hero = tester.widget<AspectRatio>(
find.byKey(const Key('detail-hero')),
);
expect(hero.aspectRatio, closeTo(16 / 9, 0.001));
expect(find.byKey(const Key('detail-cta')), findsOneWidget);
expect(find.byKey(const Key('detail-favorite')), findsOneWidget);
expect(find.byKey(const Key('detail-download')), findsOneWidget);
expect(find.byKey(const Key('detail-episodes-range')), findsOneWidget);
expect(find.byKey(const Key('detail-episode-chip-1')), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('detail-episode-chip-1'))),
const Size(46, 44),
);
final synopsis = tester.widget<Text>(
find.byKey(const Key('detail-synopsis')),
);
expect(synopsis.maxLines, 2);
await tester.tap(find.byKey(const Key('detail-synopsis-toggle')));
await tester.pumpAndSettle();
expect(
tester.widget<Text>(find.byKey(const Key('detail-synopsis'))).maxLines,
100,
);
final grid = tester.widget<SliverGrid>(
find.descendant(
of: find.byKey(const Key('detail-episode-grid')),
matching: find.byType(SliverGrid),
),
);
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 2);
expect(find.byKey(const Key('detail-episode-card-1')), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('detail-episode-thumb-1'))),
const Size(96, 64),
);
expect(tester.takeException(), isNull);
});
testWidgets('should_fit_compact_viewport_when_detail_scrolls', (
tester,
) async {
tester.view.physicalSize = const Size(320, 568);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(_host(_detailDrama()));
await tester.pumpAndSettle();
await tester.drag(
find.byKey(const Key('detail-scroll')),
const Offset(0, -520),
);
await tester.pumpAndSettle();
expect(find.byKey(const Key('detail-episode-card-3')), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
Widget _host(Drama drama) {
final catalog = FakeCatalogRepository(dramas: [drama]);
return MultiProvider(
providers: [
Provider<WatchHistoryRepository>.value(
value: InMemoryWatchHistoryRepository(),
),
Provider<PlaybackRepository>.value(value: FakePlaybackRepository()),
Provider<Haptics>.value(value: FakeHaptics()),
ChangeNotifierProvider(
create: (_) => FavoriteController(InMemoryFavoriteRepository()),
),
ChangeNotifierProvider(
create: (_) => InteractionController(FakeInteractionRepository()),
),
ChangeNotifierProvider(create: (_) => CatalogController(catalog)..load()),
],
child: MaterialApp(
theme: AppTheme.dark(),
locale: const Locale('en'),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
home: DramaDetailPage(drama: drama),
),
);
}
Drama _detailDrama() {
return Drama(
id: 'detail-k25',
title: 'Detail K25',
synopsis:
'She agreed to marry a mysterious stranger to save her family, '
'until she discovered his secret identity and the city he controls.',
posterUrl: '/files/detail-k25.png',
genre: 'Urban Romance',
category: 'Romance',
totalEpisodes: 8,
viewCount: 4800000,
detailLoaded: true,
episodes: [
for (var number = 1; number <= 8; number++)
Episode(
id: 'detail-k25-$number',
number: number,
title: 'Episode $number',
isFree: number == 1,
durationSec: 135,
locked: number > 1,
videoUrl: number == 1 ? '/files/detail-k25-1.mp4' : '',
),
],
);
}