diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 2bbdb2d..96c5af8 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -319,7 +319,6 @@ class _PlayerPageState extends State ), PlayerVideoSurface( controller: videoPort?.controller, - posterUrl: drama.posterUrl, videoUrl: videoPort?.controller.dataSource ?? episode.videoUrl, theater: theater, onTap: () => diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index 35a3374..a4f2d75 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -10,6 +10,7 @@ import '../../../core/formatting/localized_content.dart'; import '../../../core/theme/app_colors.dart'; import '../../../l10n/app_localizations.dart'; import 'interaction_rail.dart'; +import 'loading_progress_track.dart'; part 'home_feed_overlay_parts.dart'; diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index c5cc0fe..c7b8709 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -37,10 +37,13 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { final width = constraints.maxWidth; final source = widget.controller; final track = source == null - ? _track(0, width) + ? _loadingTrack() : ValueListenableBuilder( valueListenable: source, builder: (context, value, _) { + if (!value.isInitialized || value.isBuffering) { + return _loadingTrack(); + } final total = value.duration.inMilliseconds; final played = total <= 0 ? 0.0 @@ -67,6 +70,23 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { ); } + Widget _loadingTrack() { + final bottom = _lineCenterFromBottom - widget.height / 2; + return SizedBox( + height: widget.hitHeight, + child: Stack( + children: [ + Positioned( + left: 0, + right: 0, + bottom: bottom, + child: LoadingProgressTrack(height: widget.height), + ), + ], + ), + ); + } + void _update(double fraction) { setState(() => _preview = fraction.clamp(0.0, 1.0).toDouble()); } diff --git a/lib/pages/player/widgets/loading_progress_track.dart b/lib/pages/player/widgets/loading_progress_track.dart new file mode 100644 index 0000000..5d48d51 --- /dev/null +++ b/lib/pages/player/widgets/loading_progress_track.dart @@ -0,0 +1,102 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../../../core/theme/app_colors.dart'; + +/// 播放未就绪时沿进度轨道循环移动的加载高光。 +class LoadingProgressTrack extends StatefulWidget { + const LoadingProgressTrack({ + super.key = const Key('progress-loading'), + required this.height, + this.trackColor = Colors.white24, + this.highlightColor = AppColors.blue, + }); + + final double height; + final Color trackColor; + final Color highlightColor; + + @override + State createState() => _LoadingProgressTrackState(); +} + +class _LoadingProgressTrackState extends State + with SingleTickerProviderStateMixin, WidgetsBindingObserver { + late final AnimationController _controller; + late final Animation _position; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + ); + _position = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); + WidgetsBinding.instance.addObserver(this); + _syncAnimation(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + _syncAnimation(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _controller.dispose(); + super.dispose(); + } + + void _syncAnimation() { + if (WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { + if (!_controller.isAnimating) _controller.repeat(); + } else { + _controller.stop(); + } + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: widget.height, + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.height / 2), + child: LayoutBuilder( + builder: (context, constraints) { + final highlightWidth = constraints.maxWidth * 0.28; + final travel = constraints.maxWidth + highlightWidth; + return AnimatedBuilder( + animation: _position, + builder: (context, _) { + final left = -highlightWidth + travel * _position.value; + return Stack( + fit: StackFit.expand, + children: [ + ColoredBox( + key: const Key('progress-loading-track'), + color: widget.trackColor, + ), + Positioned( + left: left, + top: 0, + bottom: 0, + width: highlightWidth, + child: ColoredBox( + key: const Key('progress-loading-highlight'), + color: widget.highlightColor, + ), + ), + ], + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/player/widgets/player_video_surface.dart b/lib/pages/player/widgets/player_video_surface.dart index cdd66c3..1226e3f 100644 --- a/lib/pages/player/widgets/player_video_surface.dart +++ b/lib/pages/player/widgets/player_video_surface.dart @@ -4,25 +4,21 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; -import '../../../core/widgets/remote_image.dart'; - const Duration _videoFadeDuration = Duration(milliseconds: 200); const double _errorHorizontal = 32; const int _errorMaxLines = 3; -/// 封面底层与视频层共用同一控制器,视频首帧就绪后再淡入。 +/// 黑色底层与视频层共用同一控制器,视频首帧就绪后再淡入。 class PlayerVideoSurface extends StatefulWidget { const PlayerVideoSurface({ super.key, required this.controller, - required this.posterUrl, required this.videoUrl, required this.theater, required this.onTap, }); final VideoPlayerController? controller; - final String posterUrl; final String videoUrl; final bool theater; final VoidCallback onTap; @@ -57,7 +53,10 @@ class _PlayerVideoSurfaceState extends State { return Stack( fit: StackFit.expand, children: [ - _FallbackBackground(posterUrl: widget.posterUrl), + const ColoredBox( + key: Key('player-fallback-background'), + color: Colors.black, + ), AnimatedOpacity( key: const Key('player-video-opacity'), opacity: reveal ? 1 : 0, @@ -164,35 +163,3 @@ class _VideoLayer extends StatelessWidget { ); } } - -class _FallbackBackground extends StatelessWidget { - const _FallbackBackground({required this.posterUrl}); - - final String posterUrl; - - @override - Widget build(BuildContext context) { - if (posterUrl.trim().isEmpty) { - return const ColoredBox( - key: Key('player-fallback-background'), - color: Colors.black, - ); - } - return Stack( - key: const Key('player-fallback-background'), - fit: StackFit.expand, - children: [ - RemoteImage( - posterUrl, - key: const Key('player-fallback-cover'), - fit: BoxFit.cover, - fallbackColor: Colors.black, - ), - const ColoredBox( - key: Key('player-fallback-scrim'), - color: Color(0x4D000000), - ), - ], - ); - } -} diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index faa2e85..fa7e8bd 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import '../../../core/theme/app_colors.dart'; +import 'loading_progress_track.dart'; /// Keeps seeks within the playable range so dragging to the edge does not /// trigger the player's completion callback before playback resumes. @@ -92,6 +93,7 @@ class _SeekProgressBarState extends State { Widget _buildTrack(BuildContext context, VideoPlayerValue video) { final total = video.duration.inMilliseconds; + final loading = !video.isInitialized || video.isBuffering; final enabled = video.isInitialized && total > 0 && widget.onSeek != null; final played = total > 0 ? (video.position.inMilliseconds / total).clamp(0.0, 1.0) @@ -100,7 +102,15 @@ class _SeekProgressBarState extends State { final previewPosition = _previewFraction.value == null ? video.position : seekPositionForFraction(fraction, video.duration); - final scrubbing = _previewFraction.value != null; + final scrubbing = !loading && _previewFraction.value != null; + final trackHeight = widget.compact + ? (scrubbing ? 3.0 : 1.5) + : (scrubbing ? 4.0 : 3.0); + final trackAreaHeight = loading + ? (widget.compact ? 20.0 : 44.0) + : widget.compact + ? (scrubbing ? 32.0 : 20.0) + : (scrubbing ? 48.0 : 44.0); return Column( mainAxisSize: MainAxisSize.min, @@ -120,47 +130,50 @@ class _SeekProgressBarState extends State { ), ), SizedBox( - height: widget.compact - ? (scrubbing ? 32 : 20) - : (scrubbing ? 48 : 44), - child: SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: widget.compact - ? (scrubbing ? 3 : 1.5) - : (scrubbing ? 4 : 3), - activeTrackColor: AppColors.blue, - inactiveTrackColor: widget.inactiveTrackColor, - thumbColor: AppColors.onPrimary, - thumbShape: RoundSliderThumbShape( - enabledThumbRadius: widget.compact - ? (scrubbing ? 6 : 4) - : (scrubbing ? 8 : 6), - ), - overlayShape: RoundSliderOverlayShape( - overlayRadius: widget.compact ? 12 : 18, - ), - ), - child: Semantics( - label: widget.semanticsLabel, - child: Slider( - value: fraction, - // 轨道与上方文案共用同一层水平内边距,避免 Slider 默认内缩。 - padding: EdgeInsets.zero, - onChangeStart: enabled - ? (value) { - _previewFraction.value = value; - widget.onScrubStart?.call(); - } - : null, - onChanged: enabled - ? (value) => _previewFraction.value = value - : null, - onChangeEnd: enabled - ? (value) => unawaited(_finish(value, video.duration)) - : null, - ), - ), - ), + height: trackAreaHeight, + child: loading + ? Center( + child: LoadingProgressTrack( + height: trackHeight, + trackColor: widget.inactiveTrackColor, + ), + ) + : SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: trackHeight, + activeTrackColor: AppColors.blue, + inactiveTrackColor: widget.inactiveTrackColor, + thumbColor: AppColors.onPrimary, + thumbShape: RoundSliderThumbShape( + enabledThumbRadius: widget.compact + ? (scrubbing ? 6 : 4) + : (scrubbing ? 8 : 6), + ), + overlayShape: RoundSliderOverlayShape( + overlayRadius: widget.compact ? 12 : 18, + ), + ), + child: Semantics( + label: widget.semanticsLabel, + child: Slider( + value: fraction, + // 轨道与上方文案共用同一层水平内边距,避免 Slider 默认内缩。 + padding: EdgeInsets.zero, + onChangeStart: enabled + ? (value) { + _previewFraction.value = value; + widget.onScrubStart?.call(); + } + : null, + onChanged: enabled + ? (value) => _previewFraction.value = value + : null, + onChangeEnd: enabled + ? (value) => unawaited(_finish(value, video.duration)) + : null, + ), + ), + ), ), ], ); diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index 17224fc..704dc0d 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -150,6 +150,8 @@ void main() { tester.widget(find.byKey(const Key('rail-comments-count'))).data, '12', ); + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect(find.byKey(const Key('home-feed-progress-thumb')), findsNothing); }); testWidgets('should_seek_and_open_player_from_feed_actions', (tester) async { diff --git a/test/playback/seek_progress_bar_test.dart b/test/playback/seek_progress_bar_test.dart index 53ad74b..eb8524f 100644 --- a/test/playback/seek_progress_bar_test.dart +++ b/test/playback/seek_progress_bar_test.dart @@ -16,55 +16,111 @@ void main() { expect(seekPositionForFraction(1, duration), lessThan(duration)); }); - testWidgets('should_preview_while_dragging_and_seek_on_release_when_slider_is_horizontal', ( + testWidgets('should_show_loading_track_when_controller_is_missing', ( tester, ) async { - final video = ValueNotifier( - const VideoPlayerValue( - duration: Duration(seconds: 12), - position: Duration(seconds: 3), - isInitialized: true, - ), - ); - addTearDown(video.dispose); - var started = 0; - Duration? target; await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Center( - child: SizedBox( - width: 300, - child: SeekProgressBar( - valueListenable: video, - semanticsLabel: 'Seek video', - onScrubStart: () => started++, - onSeek: (position) async { - target = position; - }, - ), - ), + body: SeekProgressBar( + valueListenable: null, + semanticsLabel: 'Seek video', ), ), ), ); - final gesture = await tester.startGesture( - tester.getCenter(find.byType(Slider)), - ); - await gesture.moveBy(const Offset(120, 0)); - await tester.pump(); - expect(started, 1); - expect(target, isNull); - expect(find.textContaining('/ 00:12'), findsOneWidget); - await gesture.up(); - await tester.pumpAndSettle(); - expect(target, isNotNull); - expect(target!, greaterThan(const Duration(seconds: 3))); - expect(target!, lessThan(const Duration(seconds: 12))); - expect(find.textContaining('/ 00:12'), findsNothing); + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect(find.byType(Slider), findsNothing); }); + testWidgets('should_switch_between_loading_track_and_slider', (tester) async { + final video = ValueNotifier( + const VideoPlayerValue( + duration: Duration(seconds: 12), + position: Duration(seconds: 3), + isInitialized: true, + isBuffering: true, + ), + ); + addTearDown(video.dispose); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SeekProgressBar( + valueListenable: video, + semanticsLabel: 'Seek video', + onSeek: (_) async {}, + ), + ), + ), + ); + + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect(find.byType(Slider), findsNothing); + expect(find.textContaining('00:12'), findsNothing); + + video.value = const VideoPlayerValue( + duration: Duration(seconds: 12), + position: Duration(seconds: 3), + isInitialized: true, + ); + await tester.pump(); + + expect(find.byKey(const Key('progress-loading')), findsNothing); + expect(find.byType(Slider), findsOneWidget); + }); + + testWidgets( + 'should_preview_while_dragging_and_seek_on_release_when_slider_is_horizontal', + (tester) async { + final video = ValueNotifier( + const VideoPlayerValue( + duration: Duration(seconds: 12), + position: Duration(seconds: 3), + isInitialized: true, + ), + ); + addTearDown(video.dispose); + var started = 0; + Duration? target; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: SeekProgressBar( + valueListenable: video, + semanticsLabel: 'Seek video', + onScrubStart: () => started++, + onSeek: (position) async { + target = position; + }, + ), + ), + ), + ), + ), + ); + + final gesture = await tester.startGesture( + tester.getCenter(find.byType(Slider)), + ); + await gesture.moveBy(const Offset(120, 0)); + await tester.pump(); + expect(started, 1); + expect(target, isNull); + expect(find.textContaining('/ 00:12'), findsOneWidget); + await gesture.up(); + await tester.pumpAndSettle(); + expect(target, isNotNull); + expect(target!, greaterThan(const Duration(seconds: 3))); + expect(target!, lessThan(const Duration(seconds: 12))); + expect(find.textContaining('/ 00:12'), findsNothing); + }, + ); + testWidgets('should_show_time_only_while_dragging_when_seek_bar_is_compact', ( tester, ) async { diff --git a/test/player/loading_progress_track_test.dart b/test/player/loading_progress_track_test.dart new file mode 100644 index 0000000..cacf042 --- /dev/null +++ b/test/player/loading_progress_track_test.dart @@ -0,0 +1,53 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:fruitshorts/core/theme/app_colors.dart'; +import 'package:fruitshorts/pages/player/widgets/loading_progress_track.dart'; + +void main() { + testWidgets('should_move_highlight_while_loading', (tester) async { + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + addTearDown( + () => tester.binding.handleAppLifecycleStateChanged( + AppLifecycleState.paused, + ), + ); + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox(width: 240, child: LoadingProgressTrack(height: 4)), + ), + ), + ), + ); + + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect( + tester + .widget(find.byKey(const Key('progress-loading-track'))) + .color, + Colors.white24, + ); + expect( + tester + .widget( + find.byKey(const Key('progress-loading-highlight')), + ) + .color, + AppColors.blue, + ); + + final initialLeft = tester + .getTopLeft(find.byKey(const Key('progress-loading-highlight'))) + .dx; + await tester.pump(const Duration(milliseconds: 300)); + final movedLeft = tester + .getTopLeft(find.byKey(const Key('progress-loading-highlight'))) + .dx; + expect(movedLeft, greaterThan(initialLeft)); + expect(tester.getSize(find.byKey(const Key('progress-loading'))).height, 4); + }); +} diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 29f26f2..d8a7b6b 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -18,7 +18,6 @@ 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/playback_session.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; -import 'package:fruitshorts/core/widgets/remote_image.dart'; import 'package:fruitshorts/pages/auth/auth_view.dart'; import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart'; import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart'; @@ -132,15 +131,9 @@ void main() { expect(player.playing, isTrue); }); - testWidgets('should_fall_back_to_black_when_poster_is_empty', (tester) async { - await _pumpApp( - tester, - catalog: FakeCatalogRepository(dramas: const [_noCoverDrama]), - ); - await _openSeriesPlayer( - tester, - cardKey: const Key('home-drama-card-no-cover-demo'), - ); + testWidgets('should_keep_video_surface_background_black', (tester) async { + await _pumpApp(tester); + await _openSeriesPlayer(tester); expect( tester @@ -150,6 +143,8 @@ void main() { .color, Colors.black, ); + expect(find.byKey(const Key('player-fallback-cover')), findsNothing); + expect(find.byKey(const Key('player-fallback-scrim')), findsNothing); }); testWidgets('should_gate_comments_before_opening_sheet_when_signed_out', ( @@ -415,19 +410,9 @@ void main() { expect(find.byKey(const Key('player-background')), findsNothing); final fallback = find.byKey(const Key('player-fallback-background')); expect(fallback, findsOneWidget); - expect(find.byKey(const Key('player-fallback-cover')), findsOneWidget); - expect( - tester - .widget(find.byKey(const Key('player-fallback-cover'))) - .fallbackColor, - Colors.black, - ); - expect( - tester - .widget(find.byKey(const Key('player-fallback-scrim'))) - .color, - const Color(0x4D000000), - ); + expect(tester.widget(fallback).color, Colors.black); + expect(find.byKey(const Key('player-fallback-cover')), findsNothing); + expect(find.byKey(const Key('player-fallback-scrim')), findsNothing); expect( tester .widget(find.byKey(const Key('player-scaffold'))) @@ -476,7 +461,8 @@ void main() { tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))), const Size(40, 40), ); - expect(tester.widget(find.byType(Slider)).padding, EdgeInsets.zero); + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect(find.byType(Slider), findsNothing); expect(bottom.height, lessThanOrEqualTo(200)); expect(bottom.top, greaterThanOrEqualTo(screenHeight * 0.78)); @@ -1092,7 +1078,8 @@ void _expectFullscreenOverlay(WidgetTester tester) { expect(find.byKey(const Key('player-pip')), findsNothing); expect(find.byKey(const Key('player-rail-comments')), findsOneWidget); expect(find.byKey(const Key('player-fullscreen-toggle')), findsNothing); - expect(find.byType(Slider), findsOneWidget); + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect(find.byType(Slider), findsNothing); expect(find.byKey(const Key('player-title')), findsNothing); expect(find.byKey(const Key('player-subtitle')), findsNothing); expect(find.byKey(const Key('player-genre-chip')), findsNothing); @@ -1211,20 +1198,3 @@ const _lockedDrama = Drama( ), ], ); - -const _noCoverDrama = Drama( - id: 'no-cover-demo', - title: 'No Cover Demo', - synopsis: 'No cover synopsis', - posterUrl: '', - detailLoaded: true, - episodes: [ - Episode( - id: 'no-cover-demo-1', - number: 1, - title: 'No Cover Episode', - isFree: true, - videoUrl: 'https://example.test/no-cover-demo-1.mp4', - ), - ], -); diff --git a/test/player/player_video_surface_test.dart b/test/player/player_video_surface_test.dart index 3cc3194..7ab4f3c 100644 --- a/test/player/player_video_surface_test.dart +++ b/test/player/player_video_surface_test.dart @@ -12,8 +12,9 @@ void main() { ) async { await _pumpSurface(tester); - expect(find.byKey(const Key('player-fallback-background')), findsOneWidget); - expect(find.byKey(const Key('player-fallback-cover')), findsOneWidget); + expect(_fallbackColor(tester), Colors.black); + expect(find.byKey(const Key('player-fallback-cover')), findsNothing); + expect(find.byKey(const Key('player-fallback-scrim')), findsNothing); expect(_opacity(tester), 0); expect(find.byKey(const Key('player-debug-video')), findsNothing); }); @@ -48,7 +49,8 @@ void main() { expect(halfway, lessThan(1)); await tester.pump(const Duration(milliseconds: 100)); expect(_fadeValue(tester), 1); - expect(find.byKey(const Key('player-fallback-background')), findsOneWidget); + expect(_fallbackColor(tester), Colors.black); + expect(find.byKey(const Key('player-fallback-cover')), findsNothing); expect(find.byKey(const Key('player-debug-video')), findsNothing); }); @@ -62,7 +64,8 @@ void main() { expect(find.byKey(const Key('player-video-error')), findsOneWidget); expect(find.text('No route to host'), findsOneWidget); - expect(find.byKey(const Key('player-fallback-background')), findsOneWidget); + expect(_fallbackColor(tester), Colors.black); + expect(find.byKey(const Key('player-fallback-cover')), findsNothing); expect(_opacity(tester), 0); expect(find.byKey(const Key('player-debug-video')), findsNothing); }); @@ -82,7 +85,6 @@ Future _pumpSurface( home: Scaffold( body: PlayerVideoSurface( controller: controller, - posterUrl: '/files/poster.jpg', videoUrl: controller.dataSource, theater: false, onTap: () {}, @@ -94,6 +96,12 @@ Future _pumpSurface( return controller; } +Color _fallbackColor(WidgetTester tester) { + return tester + .widget(find.byKey(const Key('player-fallback-background'))) + .color; +} + double _opacity(WidgetTester tester) { return tester .widget(find.byKey(const Key('player-video-opacity'))) diff --git a/test/widget_test.dart b/test/widget_test.dart index 5b910bc..27b9260 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -185,7 +185,8 @@ void main() { await tester.pumpAndSettle(); await _openTheaterPlayer(tester); - expect(find.byType(Slider), findsOneWidget); + expect(find.byKey(const Key('progress-loading')), findsOneWidget); + expect(find.byType(Slider), findsNothing); await tester.drag(find.byType(PageView), const Offset(0, -500)); await tester.pumpAndSettle(); expect(