Release/v1.0.0 #1

Merged
jianghaifeng merged 128 commits from release/v1.0.0 into main 2026-10-19 13:42:16 +00:00
2 changed files with 135 additions and 20 deletions
Showing only changes of commit f4e1a5424e - Show all commits

No files matched your search

+40 -16
View File
@@ -303,14 +303,41 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
),
),
const SizedBox(height: 8),
Wrap(
key: const Key('detail-episode-chips'),
spacing: 6,
runSpacing: 6,
children: [
for (var index = 0; index < drama.episodes.length; index++)
_episodeChip(context, drama, index, continueIndex),
],
LayoutBuilder(
builder: (context, constraints) {
final sixColumnsWidth = (constraints.maxWidth - 5 * 8) / 6;
final columns = sixColumnsWidth >= 40 ? 6 : 5;
final selectedIndex = continueIndex ?? 0;
final selectedNumber = drama.episodes.isEmpty
? null
: drama.episodes[selectedIndex].number;
final ordered = [
for (var index = 0; index < drama.episodes.length; index++)
(index: index, episode: drama.episodes[index]),
]..sort((a, b) => a.episode.number.compareTo(b.episode.number));
return GridView.builder(
key: const Key('detail-episode-chips'),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: ordered.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.35,
),
itemBuilder: (context, position) {
final item = ordered[position];
return _episodeChip(
context,
item.episode,
item.index,
selectedNumber,
);
},
);
},
),
],
),
@@ -319,20 +346,17 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
Widget _episodeChip(
BuildContext context,
Drama drama,
Episode episode,
int index,
int? selectedIndex,
int? selectedNumber,
) {
final episode = drama.episodes[index];
final selected = index == (selectedIndex ?? 0);
final selected = episode.number == selectedNumber;
final locked = episode.locked || (!episode.isFree && index > 0);
return InkWell(
key: Key('detail-episode-chip-${episode.number}'),
onTap: () => logic.openPlayer(context, index),
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(9),
child: Container(
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
alignment: Alignment.center,
decoration: BoxDecoration(
color: selected
@@ -340,7 +364,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
: locked
? AppColors.secondary.withValues(alpha: 0.16)
: const Color(0xFF29272B),
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(9),
),
child: Text(
'${episode.number}',
+95 -4
View File
@@ -5,20 +5,25 @@ 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/comment_repository.dart';
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
import 'package:fruitshorts/core/data/repositories/interaction_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/data/services/playback_session.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 'package:fruitshorts/pages/player/player_view.dart';
import '../support/fake_catalog.dart';
import '../support/fake_haptics.dart';
import '../support/fake_interaction_repositories.dart';
import '../support/widget_player.dart';
void main() {
testWidgets('should_render_preview_hero_cta_synopsis_and_episode_chips', (
@@ -78,6 +83,14 @@ void main() {
await tester.pumpWidget(_host(_detailDrama()));
await tester.pumpAndSettle();
final grid = tester.widget<GridView>(
find.byKey(const Key('detail-episode-chips')),
);
expect(
(grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount)
.crossAxisCount,
6,
);
await tester.drag(
find.byKey(const Key('detail-scroll')),
const Offset(0, -520),
@@ -87,22 +100,96 @@ void main() {
expect(find.byKey(const Key('detail-episode-chip-8')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('should_render_sorted_six_column_grid_and_open_original_index', (
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(count: 80)));
await tester.pumpAndSettle();
final grid = tester.widget<GridView>(
find.byKey(const Key('detail-episode-chips')),
);
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 6);
expect(delegate.mainAxisSpacing, 8);
expect(delegate.crossAxisSpacing, 8);
expect(delegate.childAspectRatio, closeTo(1.35, 0.001));
final chip1 = find.byKey(const Key('detail-episode-chip-1'));
final chip2 = find.byKey(const Key('detail-episode-chip-2'));
final chip6 = find.byKey(const Key('detail-episode-chip-6'));
final chip7 = find.byKey(const Key('detail-episode-chip-7'));
expect(tester.getTopLeft(chip1).dy, tester.getTopLeft(chip6).dy);
expect(
tester.getTopLeft(chip7).dy,
greaterThan(tester.getTopLeft(chip1).dy),
);
expect(tester.getTopLeft(chip1).dx, lessThan(tester.getTopLeft(chip2).dx));
expect(tester.getSize(chip1), tester.getSize(chip2));
await tester.drag(
find.byKey(const Key('detail-scroll')),
const Offset(0, -420),
);
await tester.pumpAndSettle();
final chip12 = find.byKey(const Key('detail-episode-chip-12'));
expect(chip12, findsOneWidget);
await tester.tap(chip12);
await tester.pumpAndSettle();
final player = tester.widget<PlayerPage>(find.byType(PlayerPage));
expect(player.initialIndex, 11);
});
testWidgets('should_sort_episode_chips_ascending_by_number', (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(count: 80, descending: true)));
await tester.pumpAndSettle();
final chip1 = find.byKey(const Key('detail-episode-chip-1'));
final chip2 = find.byKey(const Key('detail-episode-chip-2'));
final chip6 = find.byKey(const Key('detail-episode-chip-6'));
final chip7 = find.byKey(const Key('detail-episode-chip-7'));
expect(tester.getTopLeft(chip1).dy, tester.getTopLeft(chip6).dy);
expect(tester.getTopLeft(chip1).dx, lessThan(tester.getTopLeft(chip2).dx));
expect(
tester.getTopLeft(chip7).dy,
greaterThan(tester.getTopLeft(chip1).dy),
);
expect(tester.getSize(chip1), tester.getSize(chip2));
});
}
Widget _host(Drama drama) {
final catalog = FakeCatalogRepository(dramas: [drama]);
final interactions = FakeInteractionRepository();
return MultiProvider(
providers: [
Provider<WatchHistoryRepository>.value(
value: InMemoryWatchHistoryRepository(),
),
Provider<PlaybackRepository>.value(value: FakePlaybackRepository()),
Provider<PlayerFactory>.value(value: createWidgetPlayer),
Provider(create: (_) => PlaybackCoordinator()),
Provider<InteractionRepository>.value(value: interactions),
Provider<CommentRepository>.value(value: FakeCommentRepository()),
Provider<Haptics>.value(value: FakeHaptics()),
ChangeNotifierProvider(
create: (_) => FavoriteController(InMemoryFavoriteRepository()),
),
ChangeNotifierProvider(
create: (_) => InteractionController(FakeInteractionRepository()),
create: (_) => InteractionController(interactions),
),
ChangeNotifierProvider(create: (_) => CatalogController(catalog)..load()),
],
@@ -116,7 +203,11 @@ Widget _host(Drama drama) {
);
}
Drama _detailDrama() {
Drama _detailDrama({int count = 8, bool descending = false}) {
final numbers = List<int>.generate(
count,
(index) => descending ? count - index : index + 1,
);
return Drama(
id: 'detail-k25',
title: 'Detail K25',
@@ -126,11 +217,11 @@ Drama _detailDrama() {
posterUrl: '/files/detail-k25.png',
genre: 'Urban Romance',
category: 'Romance',
totalEpisodes: 8,
totalEpisodes: count,
viewCount: 4800000,
detailLoaded: true,
episodes: [
for (var number = 1; number <= 8; number++)
for (final number in numbers)
Episode(
id: 'detail-k25-$number',
number: number,