fix(player): 收敛播放页入口加载态
This commit is contained in:
1 parent
ee4e573f4f
commit
b291044acc
7 files changed
+164
-90
No files matched your search
@@ -142,11 +142,11 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
bool _presentationActive = false;
|
bool _presentationActive = false;
|
||||||
bool _leavingHandoff = false;
|
bool _leavingHandoff = false;
|
||||||
bool _preparingFullPlayer = false;
|
bool _preparingFullPlayer = false;
|
||||||
int? _loadingIndex;
|
|
||||||
int? _videoReadyIndex;
|
int? _videoReadyIndex;
|
||||||
bool _loadingVisible = false;
|
bool _loadingVisible = false;
|
||||||
bool _loadingRemoved = true;
|
bool _loadingRemoved = true;
|
||||||
bool _loadingFadeScheduled = false;
|
bool _loadingFadeScheduled = false;
|
||||||
|
bool _loggedLoadingUiVisible = false;
|
||||||
final Set<String> _precachedPosterUrls = {};
|
final Set<String> _precachedPosterUrls = {};
|
||||||
|
|
||||||
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
||||||
@@ -179,10 +179,16 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true,
|
initialUserWantsPlay: handoff?.sourceUserWantsPlay ?? true,
|
||||||
);
|
);
|
||||||
_pageController = logic.pages;
|
_pageController = logic.pages;
|
||||||
if (!widget.embedded) {
|
if (!widget.embedded &&
|
||||||
_loadingIndex = logic.session.currentIndex;
|
!logic.isEpisodeLocked(logic.session.currentEpisode)) {
|
||||||
_loadingVisible = true;
|
_loadingVisible = true;
|
||||||
_loadingRemoved = false;
|
_loadingRemoved = false;
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint(
|
||||||
|
'[player-entry] phase=loading-shown-once '
|
||||||
|
'index=${logic.session.currentIndex}',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +358,15 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
notification.dragDetails != null) {
|
notification.dragDetails != null) {
|
||||||
_swipeStartedAt = DateTime.now();
|
_swipeStartedAt = DateTime.now();
|
||||||
_swipePrewarmCaptured = false;
|
_swipePrewarmCaptured = false;
|
||||||
|
if (!widget.embedded && !_loadingRemoved) {
|
||||||
|
setState(() {
|
||||||
|
_loadingVisible = false;
|
||||||
|
_loadingRemoved = true;
|
||||||
|
});
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[player-entry] phase=loading-dismissed-on-swipe');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (notification is ScrollUpdateNotification &&
|
} else if (notification is ScrollUpdateNotification &&
|
||||||
!_swipePrewarmCaptured &&
|
!_swipePrewarmCaptured &&
|
||||||
notification.dragDetails != null) {
|
notification.dragDetails != null) {
|
||||||
@@ -544,14 +559,12 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
final interactions = context.watch<InteractionController>();
|
final interactions = context.watch<InteractionController>();
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final indices = logic.visibleIndices;
|
final indices = logic.visibleIndices;
|
||||||
final currentIndex = logic.session.currentIndex;
|
final activePort = logic.session.active;
|
||||||
if (!widget.embedded && _loadingIndex != currentIndex) {
|
final activeVideo = activePort is VideoPlayerPort ? activePort : null;
|
||||||
_loadingIndex = currentIndex;
|
final canWaitForVideoFrame =
|
||||||
_videoReadyIndex = null;
|
activeVideo != null &&
|
||||||
_loadingVisible = true;
|
activeVideo.controller.value.isInitialized &&
|
||||||
_loadingRemoved = false;
|
!activeVideo.controller.value.hasError;
|
||||||
_loadingFadeScheduled = false;
|
|
||||||
}
|
|
||||||
final entryUnavailable =
|
final entryUnavailable =
|
||||||
logic.ticketError != null ||
|
logic.ticketError != null ||
|
||||||
logic.blockedEpisodeId != null ||
|
logic.blockedEpisodeId != null ||
|
||||||
@@ -560,10 +573,10 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
if (!widget.embedded &&
|
if (!widget.embedded &&
|
||||||
!_loadingRemoved &&
|
!_loadingRemoved &&
|
||||||
!_loadingFadeScheduled &&
|
!_loadingFadeScheduled &&
|
||||||
entryUnavailable) {
|
entryUnavailable &&
|
||||||
|
!canWaitForVideoFrame) {
|
||||||
_scheduleLoadingFade('entry-unavailable');
|
_scheduleLoadingFade('entry-unavailable');
|
||||||
}
|
}
|
||||||
final activePort = logic.session.active;
|
|
||||||
if (!widget.embedded &&
|
if (!widget.embedded &&
|
||||||
!_loadingRemoved &&
|
!_loadingRemoved &&
|
||||||
!_loadingFadeScheduled &&
|
!_loadingFadeScheduled &&
|
||||||
@@ -631,8 +644,17 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
slots: _renderSlots(indices),
|
slots: _renderSlots(indices),
|
||||||
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
onCurrentVideoVisible: _handleCurrentVideoVisible,
|
||||||
showNeighbors: widget.embedded || _loadingRemoved,
|
|
||||||
),
|
),
|
||||||
|
if (!widget.embedded && !_loadingRemoved)
|
||||||
|
Center(
|
||||||
|
child: IgnorePointer(
|
||||||
|
key: const Key('player-entry-loading-overlay'),
|
||||||
|
child: PlayerLoadingOverlay(
|
||||||
|
opacity: _loadingVisible ? 1 : 0,
|
||||||
|
onFadeEnd: _handleLoadingFadeEnd,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
pageLayer,
|
pageLayer,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -675,14 +697,6 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!widget.embedded && !_loadingRemoved)
|
|
||||||
Positioned.fill(
|
|
||||||
child: PlayerLoadingOverlay(
|
|
||||||
key: ValueKey('player-loading-${_loadingIndex ?? currentIndex}'),
|
|
||||||
opacity: _loadingVisible ? 1 : 0,
|
|
||||||
onFadeEnd: _handleLoadingFadeEnd,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
final player = AnnotatedRegion<SystemUiOverlayStyle>(
|
final player = AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
@@ -717,7 +731,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleCurrentVideoVisible(int? index) {
|
void _handleCurrentVideoVisible(int? index) {
|
||||||
if (!mounted) return;
|
if (!mounted || _loadingRemoved) return;
|
||||||
final currentIndex = logic.session.currentIndex;
|
final currentIndex = logic.session.currentIndex;
|
||||||
if (index == null ||
|
if (index == null ||
|
||||||
index != currentIndex ||
|
index != currentIndex ||
|
||||||
@@ -733,6 +747,14 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
}
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted ||
|
||||||
|
_loadingRemoved ||
|
||||||
|
index != logic.session.currentIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (kDebugMode) {
|
||||||
|
debugPrint('[player-entry] phase=video-frame-painted index=$index');
|
||||||
|
}
|
||||||
_startLoadingFade(currentIndex);
|
_startLoadingFade(currentIndex);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -938,11 +960,20 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
if (selected && videoReady) _logSwipeReveal();
|
if (selected && videoReady) _logSwipeReveal();
|
||||||
final ready = !locked && (videoReady || port != null);
|
final ready = !locked && (videoReady || port != null);
|
||||||
final theater = logic.pip || logic.pipPreparing;
|
final theater = logic.pip || logic.pipPreparing;
|
||||||
|
final entryLoading = !widget.embedded && !_loadingRemoved;
|
||||||
|
if (kDebugMode && entryLoading && selected && !_loggedLoadingUiVisible) {
|
||||||
|
_loggedLoadingUiVisible = true;
|
||||||
|
debugPrint(
|
||||||
|
'[player-entry] phase=loading-ui-visible '
|
||||||
|
'index=$index top=1 rail=1 progress=1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
if (selected &&
|
if (selected &&
|
||||||
|
!entryLoading &&
|
||||||
!locked &&
|
!locked &&
|
||||||
!theater &&
|
!theater &&
|
||||||
!ready &&
|
!ready &&
|
||||||
@@ -962,7 +993,7 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 票据三态之一:起播前正在向后端要签名地址
|
// 票据三态之一:起播前正在向后端要签名地址
|
||||||
if (selected && !theater && logic.ticketLoading)
|
if (selected && !entryLoading && !theater && logic.ticketLoading)
|
||||||
const Center(
|
const Center(
|
||||||
child: SizedBox.square(
|
child: SizedBox.square(
|
||||||
dimension: _playerLoadingSize,
|
dimension: _playerLoadingSize,
|
||||||
|
|||||||
@@ -17,36 +17,26 @@ class PlayerLoadingOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IgnorePointer(
|
return AnimatedOpacity(
|
||||||
key: const Key('player-entry-loading-overlay'),
|
opacity: opacity,
|
||||||
child: AnimatedOpacity(
|
duration: const Duration(milliseconds: 140),
|
||||||
opacity: opacity,
|
curve: Curves.easeOut,
|
||||||
duration: const Duration(milliseconds: 140),
|
onEnd: onFadeEnd,
|
||||||
curve: Curves.easeOut,
|
child: Container(
|
||||||
onEnd: onFadeEnd,
|
key: const Key('player-loading-surface'),
|
||||||
child: ColoredBox(
|
width: AppDimens.refreshIndicatorSize,
|
||||||
color: AppColors.mediaBlack,
|
height: AppDimens.refreshIndicatorSize,
|
||||||
child: Center(
|
decoration: BoxDecoration(
|
||||||
child: Container(
|
color: AppColors.refreshIndicatorSurface,
|
||||||
key: const Key('player-loading-surface'),
|
borderRadius: BorderRadius.circular(AppDimens.refreshIndicatorRadius),
|
||||||
width: AppDimens.refreshIndicatorSize,
|
),
|
||||||
height: AppDimens.refreshIndicatorSize,
|
alignment: Alignment.center,
|
||||||
decoration: BoxDecoration(
|
child: const SizedBox.square(
|
||||||
color: AppColors.refreshIndicatorSurface,
|
dimension: AppDimens.refreshSpinnerSize,
|
||||||
borderRadius: BorderRadius.circular(
|
child: CircularProgressIndicator(
|
||||||
AppDimens.refreshIndicatorRadius,
|
key: Key('player-entry-loading'),
|
||||||
),
|
color: Colors.white,
|
||||||
),
|
strokeWidth: AppDimens.refreshSpinnerStroke,
|
||||||
alignment: Alignment.center,
|
|
||||||
child: const SizedBox.square(
|
|
||||||
dimension: AppDimens.refreshSpinnerSize,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
key: Key('player-entry-loading'),
|
|
||||||
color: Colors.white,
|
|
||||||
strokeWidth: AppDimens.refreshSpinnerStroke,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
|||||||
thumbnailsUrl: data.episode.thumbnailsUrl,
|
thumbnailsUrl: data.episode.thumbnailsUrl,
|
||||||
episodeId: data.episode.id,
|
episodeId: data.episode.id,
|
||||||
inactiveTrackColor: AppColors.inactiveTrack,
|
inactiveTrackColor: AppColors.inactiveTrack,
|
||||||
|
fallbackDuration: Duration(seconds: data.episode.durationSec),
|
||||||
onScrubStart: actions.onScrubStart,
|
onScrubStart: actions.onScrubStart,
|
||||||
onScrubEnd: actions.onScrubEnd,
|
onScrubEnd: actions.onScrubEnd,
|
||||||
onSeek: actions.onSeek,
|
onSeek: actions.onSeek,
|
||||||
|
|||||||
@@ -53,13 +53,11 @@ class PlayerRenderStack extends StatefulWidget {
|
|||||||
required this.pageController,
|
required this.pageController,
|
||||||
required this.slots,
|
required this.slots,
|
||||||
this.onCurrentVideoVisible,
|
this.onCurrentVideoVisible,
|
||||||
this.showNeighbors = true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final PageController pageController;
|
final PageController pageController;
|
||||||
final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots;
|
final Map<PlayerRenderSlotRole, PlayerRenderSlotData> slots;
|
||||||
final ValueChanged<int?>? onCurrentVideoVisible;
|
final ValueChanged<int?>? onCurrentVideoVisible;
|
||||||
final bool showNeighbors;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerRenderStack> createState() => _PlayerRenderStackState();
|
State<PlayerRenderStack> createState() => _PlayerRenderStackState();
|
||||||
@@ -117,13 +115,11 @@ class _PlayerRenderStackState extends State<PlayerRenderStack> {
|
|||||||
: MediaQuery.sizeOf(context).height;
|
: MediaQuery.sizeOf(context).height;
|
||||||
final pageOffset = _pageOffset();
|
final pageOffset = _pageOffset();
|
||||||
final currentPage = _currentSlot.pagePosition ?? pageOffset;
|
final currentPage = _currentSlot.pagePosition ?? pageOffset;
|
||||||
final slots = widget.showNeighbors
|
final slots = [
|
||||||
? [
|
_slotData(PlayerRenderSlotRole.previous),
|
||||||
_slotData(PlayerRenderSlotRole.previous),
|
_slotData(PlayerRenderSlotRole.current),
|
||||||
_slotData(PlayerRenderSlotRole.current),
|
_slotData(PlayerRenderSlotRole.next),
|
||||||
_slotData(PlayerRenderSlotRole.next),
|
];
|
||||||
]
|
|
||||||
: [_slotData(PlayerRenderSlotRole.current)];
|
|
||||||
return Stack(
|
return Stack(
|
||||||
key: const Key('player-render-stack'),
|
key: const Key('player-render-stack'),
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SeekProgressBar extends StatefulWidget {
|
|||||||
this.spriteAxisService,
|
this.spriteAxisService,
|
||||||
this.episodeId,
|
this.episodeId,
|
||||||
this.thumbnailsUrl,
|
this.thumbnailsUrl,
|
||||||
|
this.fallbackDuration = Duration.zero,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ValueListenable<VideoPlayerValue>? valueListenable;
|
final ValueListenable<VideoPlayerValue>? valueListenable;
|
||||||
@@ -57,6 +58,7 @@ class SeekProgressBar extends StatefulWidget {
|
|||||||
final SpriteThumbnailAxisService? spriteAxisService;
|
final SpriteThumbnailAxisService? spriteAxisService;
|
||||||
final String? episodeId;
|
final String? episodeId;
|
||||||
final String? thumbnailsUrl;
|
final String? thumbnailsUrl;
|
||||||
|
final Duration fallbackDuration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SeekProgressBar> createState() => _SeekProgressBarState();
|
State<SeekProgressBar> createState() => _SeekProgressBarState();
|
||||||
@@ -220,7 +222,10 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTrack(BuildContext context, VideoPlayerValue video) {
|
Widget _buildTrack(BuildContext context, VideoPlayerValue video) {
|
||||||
final total = video.duration.inMilliseconds;
|
final duration = video.duration > Duration.zero
|
||||||
|
? video.duration
|
||||||
|
: widget.fallbackDuration;
|
||||||
|
final total = duration.inMilliseconds;
|
||||||
final scrubbing = _dragging;
|
final scrubbing = _dragging;
|
||||||
final loading = !widget.locked && !scrubbing && _buffering;
|
final loading = !widget.locked && !scrubbing && _buffering;
|
||||||
final enabled =
|
final enabled =
|
||||||
@@ -234,7 +239,7 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
|||||||
final fraction = _previewFraction.value ?? played;
|
final fraction = _previewFraction.value ?? played;
|
||||||
final previewPosition = _previewFraction.value == null
|
final previewPosition = _previewFraction.value == null
|
||||||
? video.position
|
? video.position
|
||||||
: seekPositionForFraction(fraction, video.duration);
|
: seekPositionForFraction(fraction, duration);
|
||||||
final showPreview =
|
final showPreview =
|
||||||
scrubbing &&
|
scrubbing &&
|
||||||
widget.showTimes &&
|
widget.showTimes &&
|
||||||
@@ -248,11 +253,26 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
|||||||
final track = Column(
|
final track = Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (widget.showTimes && !video.isInitialized)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
'00:00 / ${_format(duration)}',
|
||||||
|
key: const Key('player-progress-uninitialized-time'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.onPrimary,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
if (widget.showTimes && scrubbing && !showPreview)
|
if (widget.showTimes && scrubbing && !showPreview)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${_format(previewPosition)} / ${_format(video.duration)}',
|
'${_format(previewPosition)} / ${_format(duration)}',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: AppColors.onPrimary,
|
color: AppColors.onPrimary,
|
||||||
@@ -350,7 +370,7 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
|||||||
thumbnailsUrl: widget.thumbnailsUrl,
|
thumbnailsUrl: widget.thumbnailsUrl,
|
||||||
episodeId: widget.episodeId!,
|
episodeId: widget.episodeId!,
|
||||||
position: previewPosition,
|
position: previewPosition,
|
||||||
duration: video.duration,
|
duration: duration,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -139,6 +139,15 @@ void main() {
|
|||||||
expect(feedPlayer.disposed, isFalse);
|
expect(feedPlayer.disposed, isFalse);
|
||||||
expect(feedPlayer.playing, isFalse);
|
expect(feedPlayer.playing, isFalse);
|
||||||
|
|
||||||
|
final fullCurrentSurface = find.descendant(
|
||||||
|
of: fullPlayer,
|
||||||
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
||||||
|
);
|
||||||
|
final fullCurrentSurfaceState = tester.state(fullCurrentSurface);
|
||||||
|
final loadingOverlay = find.byKey(
|
||||||
|
const Key('player-entry-loading-overlay'),
|
||||||
|
);
|
||||||
|
expect(loadingOverlay, findsNothing);
|
||||||
final fullPlayerPage = find
|
final fullPlayerPage = find
|
||||||
.descendant(
|
.descendant(
|
||||||
of: _fullPlayerFinder(),
|
of: _fullPlayerFinder(),
|
||||||
@@ -149,15 +158,14 @@ void main() {
|
|||||||
)
|
)
|
||||||
.first;
|
.first;
|
||||||
for (var index = 0; index < 4; index++) {
|
for (var index = 0; index < 4; index++) {
|
||||||
await tester.drag(
|
await tester.drag(fullPlayerPage, const Offset(0, -560));
|
||||||
fullPlayerPage,
|
|
||||||
const Offset(0, -560),
|
|
||||||
);
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
expect(loadingOverlay, findsNothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(fullLogic.session.currentEpisode.number, 5);
|
expect(fullLogic.session.currentEpisode.number, 5);
|
||||||
|
expect(tester.state(fullCurrentSurface), same(fullCurrentSurfaceState));
|
||||||
expect(
|
expect(
|
||||||
fullLogic.queue.entryAt(fullLogic.session.currentIndex).drama.id,
|
fullLogic.queue.entryAt(fullLogic.session.currentIndex).drama.id,
|
||||||
sourceDramaId,
|
sourceDramaId,
|
||||||
@@ -260,7 +268,7 @@ void main() {
|
|||||||
await _pumpApp(
|
await _pumpApp(
|
||||||
tester,
|
tester,
|
||||||
playerFactory: (episode) => episode.id == 'the-signal-demo-2'
|
playerFactory: (episode) => episode.id == 'the-signal-demo-2'
|
||||||
? _GatedPlayer(initialization.future)
|
? _GatedVideoPlayer(episode, initialization.future)
|
||||||
: routeFactory.create(episode),
|
: routeFactory.create(episode),
|
||||||
);
|
);
|
||||||
await tester.tap(find.byKey(const Key('tab-recommend')));
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
||||||
@@ -270,13 +278,18 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byKey(const Key('player-episode-2')));
|
await tester.tap(find.byKey(const Key('player-episode-2')));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 16));
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
final loadingOverlay = find.byKey(
|
final loadingOverlay = find.byKey(
|
||||||
const Key('player-entry-loading-overlay'),
|
const Key('player-entry-loading-overlay'),
|
||||||
);
|
);
|
||||||
final fullPlayer = find.byType(PlayerPage).last;
|
final fullPlayer = find.byType(PlayerPage).last;
|
||||||
final fullLogic = _pageLogic(tester, fullPlayer);
|
final fullLogic = _pageLogic(tester, fullPlayer);
|
||||||
|
final fullCurrentSurface = find.descendant(
|
||||||
|
of: fullPlayer,
|
||||||
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
||||||
|
);
|
||||||
|
final fullCurrentSurfaceState = tester.state(fullCurrentSurface);
|
||||||
final fullPlayerPage = find
|
final fullPlayerPage = find
|
||||||
.descendant(
|
.descendant(
|
||||||
of: fullPlayer,
|
of: fullPlayer,
|
||||||
@@ -288,6 +301,7 @@ void main() {
|
|||||||
.first;
|
.first;
|
||||||
expect(loadingOverlay, findsOneWidget);
|
expect(loadingOverlay, findsOneWidget);
|
||||||
expect(tester.widget<IgnorePointer>(loadingOverlay).ignoring, isTrue);
|
expect(tester.widget<IgnorePointer>(loadingOverlay).ignoring, isTrue);
|
||||||
|
expect(tester.getSize(loadingOverlay), const Size(40, 40));
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<AnimatedOpacity>(
|
.widget<AnimatedOpacity>(
|
||||||
@@ -315,39 +329,51 @@ void main() {
|
|||||||
.color,
|
.color,
|
||||||
Colors.white,
|
Colors.white,
|
||||||
);
|
);
|
||||||
expect(
|
|
||||||
tester
|
|
||||||
.widget<ColoredBox>(
|
|
||||||
find.descendant(
|
|
||||||
of: loadingOverlay,
|
|
||||||
matching: find.byType(ColoredBox),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.color,
|
|
||||||
AppColors.mediaBlack,
|
|
||||||
);
|
|
||||||
final loadingSurface = tester.widget<Container>(
|
final loadingSurface = tester.widget<Container>(
|
||||||
find.byKey(const Key('player-loading-surface')),
|
find.byKey(const Key('player-loading-surface')),
|
||||||
);
|
);
|
||||||
final decoration = loadingSurface.decoration! as BoxDecoration;
|
final decoration = loadingSurface.decoration! as BoxDecoration;
|
||||||
expect(decoration.color, AppColors.refreshIndicatorSurface);
|
expect(decoration.color, AppColors.refreshIndicatorSurface);
|
||||||
expect(decoration.borderRadius, BorderRadius.circular(20));
|
expect(decoration.borderRadius, BorderRadius.circular(20));
|
||||||
|
expect(
|
||||||
|
find.descendant(of: loadingOverlay, matching: find.byType(ColoredBox)),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
|
||||||
// IgnorePointer 让加载中的播放暂停与上下滑仍能透传。
|
expect(find.byKey(const Key('player-back')).hitTestable(), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.byKey(const Key('player-episode-heading')).hitTestable(),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.byKey(const Key('player-drama-title')).hitTestable(),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find
|
||||||
|
.descendant(
|
||||||
|
of: find.byKey(const Key('player-rail-like')),
|
||||||
|
matching: find.byType(IconButton),
|
||||||
|
)
|
||||||
|
.hitTestable(),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
||||||
|
expect(find.text('00:00 / 01:00'), findsOneWidget);
|
||||||
|
|
||||||
|
// IgnorePointer 让加载中的播放暂停与上下滑穿透,滑动会关闭入口 loading。
|
||||||
expect(fullLogic.session.userWantsPlay, isTrue);
|
expect(fullLogic.session.userWantsPlay, isTrue);
|
||||||
await tester.tap(find.byKey(const Key('player-video-tap')).hitTestable());
|
await tester.tap(find.byKey(const Key('player-video-tap')).hitTestable());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(fullLogic.session.userWantsPlay, isFalse);
|
expect(fullLogic.session.userWantsPlay, isFalse);
|
||||||
final indexBeforeSwipe = fullLogic.session.currentIndex;
|
final indexBeforeSwipe = fullLogic.session.currentIndex;
|
||||||
await tester.drag(
|
await tester.drag(fullPlayerPage, const Offset(0, -560));
|
||||||
fullPlayerPage,
|
|
||||||
const Offset(0, -560),
|
|
||||||
);
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
expect(fullLogic.session.currentIndex, isNot(indexBeforeSwipe));
|
expect(fullLogic.session.currentIndex, isNot(indexBeforeSwipe));
|
||||||
|
expect(tester.state(fullCurrentSurface), same(fullCurrentSurfaceState));
|
||||||
|
|
||||||
// IgnorePointer 让加载中的返回按钮仍可点击。
|
// 切集完成后返回按钮仍可正常点击。
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(loadingOverlay, findsNothing);
|
expect(loadingOverlay, findsNothing);
|
||||||
await tester.tap(find.byKey(const Key('player-back')).hitTestable());
|
await tester.tap(find.byKey(const Key('player-back')).hitTestable());
|
||||||
@@ -1900,6 +1926,15 @@ class _GatedPlayer implements PlayerPort {
|
|||||||
Future<void> dispose() async {}
|
Future<void> dispose() async {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _GatedVideoPlayer extends VideoPlayerPort {
|
||||||
|
_GatedVideoPlayer(super.episode, this.initialization);
|
||||||
|
|
||||||
|
final Future<void> initialization;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initialize() => initialization;
|
||||||
|
}
|
||||||
|
|
||||||
const _lockedDrama = Drama(
|
const _lockedDrama = Drama(
|
||||||
id: 'locked-demo',
|
id: 'locked-demo',
|
||||||
title: 'Locked Demo',
|
title: 'Locked Demo',
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ Drama _drama(
|
|||||||
number: index + 1,
|
number: index + 1,
|
||||||
title: 'Episode ${index + 1}',
|
title: 'Episode ${index + 1}',
|
||||||
isFree: true,
|
isFree: true,
|
||||||
|
durationSec: 60,
|
||||||
videoUrl: 'https://example.test/$format-$index.mp4',
|
videoUrl: 'https://example.test/$format-$index.mp4',
|
||||||
likeCount: likeCount,
|
likeCount: likeCount,
|
||||||
commentCount: commentCount,
|
commentCount: commentCount,
|
||||||
|
|||||||
Reference in new issue
Block a user