merge: 合并 K95.2 首切频道封面过渡
This commit is contained in:
commit
a2a491d056
5 files changed
+111
-19
No files matched your search
@@ -378,6 +378,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
PlayerVideoSurface(
|
PlayerVideoSurface(
|
||||||
controller: videoPort?.controller,
|
controller: videoPort?.controller,
|
||||||
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
|
videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl,
|
||||||
|
posterUrl: drama.posterUrl,
|
||||||
theater: theater,
|
theater: theater,
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
logic.session.setUserWantsPlay(!logic.session.userWantsPlay),
|
||||||
@@ -427,7 +428,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (selected && logic.session.loading)
|
if (selected && logic.session.loading && !widget.homeMode)
|
||||||
const Center(
|
const Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
key: Key('player-session-loading'),
|
key: Key('player-session-loading'),
|
||||||
|
|||||||
@@ -4,22 +4,26 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import '../../../core/widgets/remote_image.dart';
|
||||||
|
|
||||||
const Duration _videoFadeDuration = Duration(milliseconds: 200);
|
const Duration _videoFadeDuration = Duration(milliseconds: 200);
|
||||||
const double _errorHorizontal = 32;
|
const double _errorHorizontal = 32;
|
||||||
const int _errorMaxLines = 3;
|
const int _errorMaxLines = 3;
|
||||||
|
|
||||||
/// 黑色底层与视频层共用同一控制器,视频首帧就绪后再淡入。
|
/// 封面或黑色底层与视频层共用同一控制器,视频首帧就绪后再淡入。
|
||||||
class PlayerVideoSurface extends StatefulWidget {
|
class PlayerVideoSurface extends StatefulWidget {
|
||||||
const PlayerVideoSurface({
|
const PlayerVideoSurface({
|
||||||
super.key,
|
super.key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.videoUrl,
|
required this.videoUrl,
|
||||||
|
this.posterUrl = '',
|
||||||
required this.theater,
|
required this.theater,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final VideoPlayerController? controller;
|
final VideoPlayerController? controller;
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
|
final String posterUrl;
|
||||||
final bool theater;
|
final bool theater;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@@ -53,10 +57,17 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
const ColoredBox(
|
if (widget.posterUrl.isEmpty)
|
||||||
key: Key('player-fallback-background'),
|
const ColoredBox(
|
||||||
color: Colors.black,
|
key: Key('player-fallback-background'),
|
||||||
),
|
color: Colors.black,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
RemoteImage(
|
||||||
|
widget.posterUrl,
|
||||||
|
key: const Key('player-video-poster'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
key: const Key('player-video-opacity'),
|
key: const Key('player-video-opacity'),
|
||||||
opacity: reveal ? 1 : 0,
|
opacity: reveal ? 1 : 0,
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:fruitshorts/app.dart';
|
import 'package:fruitshorts/app.dart';
|
||||||
import 'package:fruitshorts/core/data/models/feed_page.dart';
|
import 'package:fruitshorts/core/data/models/feed_page.dart';
|
||||||
|
import 'package:fruitshorts/core/data/models/player_port.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.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/playback_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/services/playback_session.dart';
|
||||||
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
||||||
import 'package:fruitshorts/pages/player/player_view.dart';
|
import 'package:fruitshorts/pages/player/player_view.dart';
|
||||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||||
@@ -120,6 +122,37 @@ void main() {
|
|||||||
expect(player.homeMode, isFalse);
|
expect(player.homeMode, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_hide_session_loading_only_on_home_feed', (tester) async {
|
||||||
|
final players = <_PendingPlayer>[];
|
||||||
|
await _pumpApp(
|
||||||
|
tester,
|
||||||
|
playerFactory: (_) {
|
||||||
|
final player = _PendingPlayer();
|
||||||
|
players.add(player);
|
||||||
|
return player;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(PlayerPage), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('player-session-loading')), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 400));
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('player-session-loading')), findsOneWidget);
|
||||||
|
|
||||||
|
for (var round = 0; round < 6; round++) {
|
||||||
|
for (final player in players) {
|
||||||
|
if (!player.initialized.isCompleted) {
|
||||||
|
player.initialized.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_switch_to_anime_channel', (tester) async {
|
testWidgets('should_switch_to_anime_channel', (tester) async {
|
||||||
await _pumpApp(tester);
|
await _pumpApp(tester);
|
||||||
|
|
||||||
@@ -489,6 +522,7 @@ Future<void> _pumpApp(
|
|||||||
WidgetTester tester, {
|
WidgetTester tester, {
|
||||||
Size size = const Size(390, 844),
|
Size size = const Size(390, 844),
|
||||||
FakeCatalogRepository? catalog,
|
FakeCatalogRepository? catalog,
|
||||||
|
PlayerFactory? playerFactory,
|
||||||
}) async {
|
}) async {
|
||||||
tester.view.physicalSize = size;
|
tester.view.physicalSize = size;
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
@@ -497,7 +531,7 @@ Future<void> _pumpApp(
|
|||||||
final repository = catalog ?? FakeCatalogRepository();
|
final repository = catalog ?? FakeCatalogRepository();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
LumaDramaApp(
|
LumaDramaApp(
|
||||||
playerFactory: createWidgetPlayer,
|
playerFactory: playerFactory ?? createWidgetPlayer,
|
||||||
catalogRepository: repository,
|
catalogRepository: repository,
|
||||||
categoryRepository: FakeCategoryRepository(catalog: repository),
|
categoryRepository: FakeCategoryRepository(catalog: repository),
|
||||||
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
||||||
@@ -542,3 +576,19 @@ class _EmptyAnimeCatalog extends FakeCatalogRepository {
|
|||||||
return super.fetchFeed(types: types, cursor: cursor, size: size);
|
return super.fetchFeed(types: types, cursor: cursor, size: size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PendingPlayer implements PlayerPort {
|
||||||
|
final initialized = Completer<void>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initialize() => initialized.future;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> play() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pause() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
}
|
||||||
@@ -131,18 +131,12 @@ void main() {
|
|||||||
expect(player.playing, isTrue);
|
expect(player.playing, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_keep_video_surface_background_black', (tester) async {
|
testWidgets('should_show_poster_before_video_first_frame', (tester) async {
|
||||||
await _pumpApp(tester);
|
await _pumpApp(tester);
|
||||||
await _openSeriesPlayer(tester);
|
await _openSeriesPlayer(tester);
|
||||||
|
|
||||||
expect(
|
expect(find.byKey(const Key('player-video-poster')), findsOneWidget);
|
||||||
tester
|
expect(find.byKey(const Key('player-fallback-background')), findsNothing);
|
||||||
.widget<ColoredBox>(
|
|
||||||
find.byKey(const Key('player-fallback-background')),
|
|
||||||
)
|
|
||||||
.color,
|
|
||||||
Colors.black,
|
|
||||||
);
|
|
||||||
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
||||||
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
||||||
});
|
});
|
||||||
@@ -471,9 +465,8 @@ void main() {
|
|||||||
find.byKey(const Key('player-interaction-rail')),
|
find.byKey(const Key('player-interaction-rail')),
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('player-background')), findsNothing);
|
expect(find.byKey(const Key('player-background')), findsNothing);
|
||||||
final fallback = find.byKey(const Key('player-fallback-background'));
|
expect(find.byKey(const Key('player-video-poster')), findsOneWidget);
|
||||||
expect(fallback, findsOneWidget);
|
expect(find.byKey(const Key('player-fallback-background')), findsNothing);
|
||||||
expect(tester.widget<ColoredBox>(fallback).color, Colors.black);
|
|
||||||
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
||||||
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ import 'package:video_player/video_player.dart';
|
|||||||
import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart';
|
import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
testWidgets('should_show_poster_when_controller_is_missing', (tester) async {
|
||||||
|
await _pumpBareSurface(tester, posterUrl: '/files/poster.png');
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('player-video-poster')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('player-fallback-background')), findsNothing);
|
||||||
|
expect(_opacity(tester), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_keep_black_fallback_when_poster_is_empty', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _pumpBareSurface(tester);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('player-video-poster')), findsNothing);
|
||||||
|
expect(_fallbackColor(tester), Colors.black);
|
||||||
|
expect(_opacity(tester), 0);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_keep_fallback_visible_before_video_is_ready', (
|
testWidgets('should_keep_fallback_visible_before_video_is_ready', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -96,6 +114,25 @@ Future<VideoPlayerController> _pumpSurface(
|
|||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpBareSurface(
|
||||||
|
WidgetTester tester, {
|
||||||
|
String posterUrl = '',
|
||||||
|
}) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: PlayerVideoSurface(
|
||||||
|
controller: null,
|
||||||
|
videoUrl: '',
|
||||||
|
posterUrl: posterUrl,
|
||||||
|
theater: false,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Color _fallbackColor(WidgetTester tester) {
|
Color _fallbackColor(WidgetTester tester) {
|
||||||
return tester
|
return tester
|
||||||
.widget<ColoredBox>(find.byKey(const Key('player-fallback-background')))
|
.widget<ColoredBox>(find.byKey(const Key('player-fallback-background')))
|
||||||
|
|||||||
Reference in new issue
Block a user