From 623c184243b9beb3c4570ec5d4af5b757e04f99e Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 10:09:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(player):=20=E4=BC=98=E5=8C=96=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=8C=87=E7=A4=BA=E5=99=A8=E4=B8=8E=E9=94=81=E5=AE=9A?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-10-22-player-loading-polish.md | 30 ++++++ docs/plans/PROGRESS.md | 33 +++++++ lib/pages/player/player_view.dart | 25 +++-- .../player/widgets/home_feed_overlay.dart | 3 + .../widgets/home_feed_overlay_parts.dart | 69 ++++++++++---- .../widgets/loading_progress_track.dart | 4 +- lib/pages/player/widgets/player_overlay.dart | 1 + .../player/widgets/player_overlay_models.dart | 4 + .../player/widgets/seek_progress_bar.dart | 43 ++++++++- test/home/home_preview_layout_test.dart | 78 ++++++++++++++++ test/playback/seek_progress_bar_test.dart | 42 +++++++++ test/player/loading_progress_track_test.dart | 3 +- test/player/player_fullscreen_test.dart | 91 ++++++++++++++++++- 13 files changed, 393 insertions(+), 33 deletions(-) create mode 100644 docs/plans/2026-10-22-player-loading-polish.md diff --git a/docs/plans/2026-10-22-player-loading-polish.md b/docs/plans/2026-10-22-player-loading-polish.md new file mode 100644 index 0000000..b98133a --- /dev/null +++ b/docs/plans/2026-10-22-player-loading-polish.md @@ -0,0 +1,30 @@ +# 播放器 loading 与锁定进度条调整 + +## 目标 + +- 播放器票据与 session loading 圈统一为 24×24、线宽 2。 +- 缓冲/加载进度高光改为白色。 +- 未解锁剧集显示静态 inactive 轨道,不进入 loading,不允许拖动 seek。 + +## 实施步骤 + +1. 更新 `LoadingProgressTrack` 默认高光颜色,并补颜色断言。 +2. 为 `SeekProgressBar`、`PlayerOverlayData`、`HomeFeedOverlay` 和 `_FeedProgressBar` 增加 locked 状态。 +3. `PlayerPage` 使用 `logic.session.isLocked || episode.locked` 传入两个浮层。 +4. 缩小 `player-ticket-loading` 与 `player-session-loading` 的尺寸和线宽。 +5. 补首页/全屏锁定态、缓冲高光和两个 loading 指示器尺寸测试。 +6. 执行 `dart analyze lib test` 与 `flutter test --no-pub`,回填 PROGRESS 后提交合并。 + +## 风险 / 待确认 + +- 锁定态只影响进度条 UI 与手势,不改播放、解锁或接口逻辑。 +- 首页浮层锁定线沿用当前 `Colors.white24` inactive 色,避免改变非锁定轨道样式。 + +## 执行结果 + +- 两个播放器 loading 圈均为 24×24、线宽 2、主色不变。 +- loading 高光默认改为白色,普通轨道与首页 white24 轨道颜色未变。 +- 首页与全屏播放器均按剧集锁定态渲染静态空轨道并禁用 seek。 +- 已覆盖锁定轨道高度、无 loading、无拖动 seek、loading 指示器尺寸和高光颜色。 +- `dart analyze lib test` 零告警,`flutter test --no-pub` 576 项全过。 +- 未启停服务,未操作真机。 diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index c3bef1c..4c4cdbb 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -266,3 +266,36 @@ $ flutter test --no-pub 解读:吐司单例替换、点击穿透和自动消失通过;认证异常与本地校验路径通过; 全量静态分析零告警,573 项测试全过。`pubspec.lock` 已还原,未启停服务, 未操作真机。 + +## 2026-10-22 播放器 loading 与锁定进度条调整 + +- 已从 `develop`(`7b068e8`)创建 `feature/player-loading-polish`。 +- 播放器票据与 session loading 圈统一为 24×24、线宽 2。 +- loading 高光默认改为白色,普通轨道色与首页 white24 轨道保持不变。 +- 首页和全屏播放器进度条增加 locked 状态;锁定态显示静态 inactive 线,不进入 loading, + 禁用拖动与 seek。 +- 已补 loading 指示器尺寸、缓冲高光颜色、首页/全屏锁定态和锁定轨道高度测试。 + +执行命令与原始输出: + +```text +$ flutter test --no-pub test/player/loading_progress_track_test.dart test/playback/seek_progress_bar_test.dart +00:00 +8: All tests passed! + +$ flutter test --no-pub test/home/home_preview_layout_test.dart +00:01 +7: All tests passed! + +$ flutter test --no-pub test/player/player_fullscreen_test.dart +00:09 +37: All tests passed! + +$ dart analyze lib test +Analyzing lib, test... +No issues found! + +$ flutter test --no-pub +00:50 +576: All tests passed! +``` + +解读:加载高光、锁定静态轨道、拖动禁用和两个 loading 指示器尺寸均通过; +全量静态分析零告警,576 项测试全过。`pubspec.lock` 已还原,未启停服务, +未操作真机。 diff --git a/lib/pages/player/player_view.dart b/lib/pages/player/player_view.dart index 9f23d17..4a7807e 100644 --- a/lib/pages/player/player_view.dart +++ b/lib/pages/player/player_view.dart @@ -42,6 +42,8 @@ import '../../core/data/repositories/playback_repository.dart'; part 'player_view_actions.dart'; const double _boostPlaybackSpeed = 2.0; +const double _playerLoadingSize = 24; +const double _playerLoadingStrokeWidth = 2; class PlayerPage extends StatefulWidget { const PlayerPage({ @@ -372,6 +374,7 @@ class _PlayerPageState extends State final videoPort = port is VideoPlayerPort ? port : null; final ready = videoPort?.controller.value.isInitialized ?? false; final theater = logic.pip || logic.pipPreparing; + final locked = logic.session.isLocked || episode.locked; return Stack( fit: StackFit.expand, @@ -400,9 +403,13 @@ class _PlayerPageState extends State // 票据三态之一:起播前正在向后端要签名地址 if (selected && logic.ticketLoading) const Center( - child: CircularProgressIndicator( - key: Key('player-ticket-loading'), - color: AppColors.blue, + child: SizedBox.square( + dimension: _playerLoadingSize, + child: CircularProgressIndicator( + key: Key('player-ticket-loading'), + color: AppColors.blue, + strokeWidth: _playerLoadingStrokeWidth, + ), ), ), // 票据三态之二:拿票失败(断网/服务异常)→ 可重试,不假装在加载 @@ -435,9 +442,13 @@ class _PlayerPageState extends State ), if (selected && logic.session.loading) const Center( - child: CircularProgressIndicator( - key: Key('player-session-loading'), - color: AppColors.blue, + child: SizedBox.square( + dimension: _playerLoadingSize, + child: CircularProgressIndicator( + key: Key('player-session-loading'), + color: AppColors.blue, + strokeWidth: _playerLoadingStrokeWidth, + ), ), ), if (selected && !ready && logic.session.error != null) @@ -499,6 +510,7 @@ class _PlayerPageState extends State drama: drama, episode: episode, controller: ready ? videoPort!.controller : null, + locked: locked, visible: _chromeVisible, boosting: _boosting, onBoostStart: _startBoost, @@ -552,6 +564,7 @@ class _PlayerPageState extends State drama: drama, episode: episode, controller: ready ? videoPort!.controller : null, + locked: locked, liked: interactions.isEpisodeLiked(episode.id), likeCount: interactions.episodeLikeCount(episode.id) ?? diff --git a/lib/pages/player/widgets/home_feed_overlay.dart b/lib/pages/player/widgets/home_feed_overlay.dart index d841bce..e6c99db 100644 --- a/lib/pages/player/widgets/home_feed_overlay.dart +++ b/lib/pages/player/widgets/home_feed_overlay.dart @@ -38,6 +38,7 @@ class HomeFeedOverlay extends StatefulWidget { required this.drama, required this.episode, required this.controller, + required this.locked, required this.liked, required this.saved, required this.visible, @@ -64,6 +65,7 @@ class HomeFeedOverlay extends StatefulWidget { final Drama drama; final Episode episode; final ValueListenable? controller; + final bool locked; final bool liked; final int? likeCount; final int? favoriteCount; @@ -387,6 +389,7 @@ class _HomeFeedOverlayState extends State { bottom: 0, child: _FeedProgressBar( controller: widget.controller, + locked: widget.locked, height: _progressHeight, hitHeight: _progressHitHeight, onScrubStart: widget.onScrubStart, diff --git a/lib/pages/player/widgets/home_feed_overlay_parts.dart b/lib/pages/player/widgets/home_feed_overlay_parts.dart index 690876b..37b3370 100644 --- a/lib/pages/player/widgets/home_feed_overlay_parts.dart +++ b/lib/pages/player/widgets/home_feed_overlay_parts.dart @@ -6,11 +6,13 @@ const double _thumbSize = 6; const double _thumbActiveSize = 14; const double _trackActiveHeight = 6; const double _lineCenterFromBottom = 8; +const Key _inactiveTrackKey = Key('home-feed-progress-inactive'); /// 整集条底部的进度线:20px 手势区内可点按/拖动 seek。 class _FeedProgressBar extends StatefulWidget { const _FeedProgressBar({ required this.controller, + required this.locked, required this.height, required this.hitHeight, this.onScrubStart, @@ -18,6 +20,7 @@ class _FeedProgressBar extends StatefulWidget { }); final ValueListenable? controller; + final bool locked; final double height; final double hitHeight; final VoidCallback? onScrubStart; @@ -50,6 +53,16 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { _buffering = false; _attachSource(); } + if (oldWidget.locked != widget.locked) { + _bufferTimer?.cancel(); + _bufferTimer = null; + _buffering = false; + if (widget.locked) { + _preview = null; + } else { + _syncBufferState(notify: false); + } + } } @override @@ -67,9 +80,12 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { final source = widget.controller; final scrubbing = _preview != null; final loading = - source == null || - (!scrubbing && (!source.value.isInitialized || _buffering)); - final track = source == null + !widget.locked && + (source == null || + (!scrubbing && (!source.value.isInitialized || _buffering))); + final track = widget.locked + ? _inactiveTrack() + : source == null ? _loadingTrack() : ValueListenableBuilder( valueListenable: source, @@ -86,22 +102,23 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { return _track(_preview ?? played, width); }, ); + final interactive = !widget.locked && !loading; return GestureDetector( key: const Key('home-feed-progress'), behavior: HitTestBehavior.opaque, - onHorizontalDragStart: loading - ? null - : (details) { + onHorizontalDragStart: interactive + ? (details) { widget.onScrubStart?.call(); _update(details.localPosition.dx / width); - }, - onHorizontalDragUpdate: loading - ? null - : (details) => _update(details.localPosition.dx / width), - onHorizontalDragEnd: loading ? null : (_) => _commit(_preview), - onHorizontalDragCancel: loading - ? null - : () => setState(() => _preview = null), + } + : null, + onHorizontalDragUpdate: interactive + ? (details) => _update(details.localPosition.dx / width) + : null, + onHorizontalDragEnd: interactive ? (_) => _commit(_preview) : null, + onHorizontalDragCancel: interactive + ? () => setState(() => _preview = null) + : null, child: track, ); }, @@ -121,7 +138,7 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { void _syncBufferState({bool notify = true}) { final source = widget.controller; - if (source == null) { + if (widget.locked || source == null) { _bufferTimer?.cancel(); _bufferTimer = null; _setBuffering(false, notify: notify); @@ -180,12 +197,32 @@ class _FeedProgressBarState extends State<_FeedProgressBar> { ); } + Widget _inactiveTrack() { + final bottom = _lineCenterFromBottom - widget.height / 2; + return SizedBox( + height: widget.hitHeight, + child: Stack( + children: [ + Positioned( + left: 0, + right: 0, + bottom: bottom, + child: SizedBox( + height: widget.height, + child: ColoredBox(key: _inactiveTrackKey, color: Colors.white24), + ), + ), + ], + ), + ); + } + void _update(double fraction) { setState(() => _preview = fraction.clamp(0.0, 1.0).toDouble()); } void _commit(double? fraction) { - if (fraction == null) return; + if (widget.locked || fraction == null) return; final total = widget.controller?.value.duration.inMilliseconds ?? 0; if (total > 0) { widget.onSeek?.call( diff --git a/lib/pages/player/widgets/loading_progress_track.dart b/lib/pages/player/widgets/loading_progress_track.dart index 5d48d51..d54cb91 100644 --- a/lib/pages/player/widgets/loading_progress_track.dart +++ b/lib/pages/player/widgets/loading_progress_track.dart @@ -2,15 +2,13 @@ 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, + this.highlightColor = Colors.white, }); final double height; diff --git a/lib/pages/player/widgets/player_overlay.dart b/lib/pages/player/widgets/player_overlay.dart index 9896595..5d3fdcb 100644 --- a/lib/pages/player/widgets/player_overlay.dart +++ b/lib/pages/player/widgets/player_overlay.dart @@ -263,6 +263,7 @@ class PlayerPlaybackOverlay extends StatelessWidget { semanticsLabel: data.l10n.seekVideo, showTimes: true, compact: true, + locked: data.locked, inactiveTrackColor: Colors.white24, onScrubStart: actions.onScrubStart, onSeek: actions.onSeek, diff --git a/lib/pages/player/widgets/player_overlay_models.dart b/lib/pages/player/widgets/player_overlay_models.dart index 0bdc23e..1a2dd92 100644 --- a/lib/pages/player/widgets/player_overlay_models.dart +++ b/lib/pages/player/widgets/player_overlay_models.dart @@ -23,6 +23,7 @@ class PlayerOverlayData { required this.playbackSpeed, required this.totalEpisodes, required this.boosting, + required this.locked, this.authorNote = '', }); @@ -65,6 +66,9 @@ class PlayerOverlayData { /// 是否处于长按加速态。 final bool boosting; + /// 当前剧集是否锁定。 + final bool locked; + /// 作者声明;没有后端字段时为空。 final String authorNote; } diff --git a/lib/pages/player/widgets/seek_progress_bar.dart b/lib/pages/player/widgets/seek_progress_bar.dart index a4fcbb0..553462a 100644 --- a/lib/pages/player/widgets/seek_progress_bar.dart +++ b/lib/pages/player/widgets/seek_progress_bar.dart @@ -9,6 +9,8 @@ import 'package:video_player/video_player.dart'; import '../../../core/theme/app_colors.dart'; import 'loading_progress_track.dart'; +const Key _inactiveTrackKey = Key('progress-inactive'); + /// Keeps seeks within the playable range so dragging to the edge does not /// trigger the player's completion callback before playback resumes. Duration seekPositionForFraction(double fraction, Duration duration) { @@ -29,6 +31,7 @@ class SeekProgressBar extends StatefulWidget { this.showTimes = true, this.compact = false, this.inactiveTrackColor = AppColors.border, + this.locked = false, }); final ValueListenable? valueListenable; @@ -38,6 +41,7 @@ class SeekProgressBar extends StatefulWidget { final bool showTimes; final bool compact; final Color inactiveTrackColor; + final bool locked; @override State createState() => _SeekProgressBarState(); @@ -67,6 +71,16 @@ class _SeekProgressBarState extends State { _buffering = false; _attachSource(); } + if (oldWidget.locked != widget.locked) { + _bufferTimer?.cancel(); + _bufferTimer = null; + _buffering = false; + if (widget.locked) { + _previewFraction.value = null; + } else { + _syncBufferState(notify: false); + } + } } @override @@ -90,7 +104,7 @@ class _SeekProgressBarState extends State { void _syncBufferState({bool notify = true}) { final source = widget.valueListenable; - if (source == null) { + if (widget.locked || source == null) { _bufferTimer?.cancel(); _bufferTimer = null; _setBuffering(false, notify: notify); @@ -133,6 +147,7 @@ class _SeekProgressBarState extends State { } Future _finish(double fraction, Duration duration) async { + if (widget.locked) return; final source = widget.valueListenable; _previewFraction.value = fraction; try { @@ -166,8 +181,13 @@ class _SeekProgressBarState extends State { Widget _buildTrack(BuildContext context, VideoPlayerValue video) { final total = video.duration.inMilliseconds; final scrubbing = _previewFraction.value != null; - final loading = !scrubbing && (!video.isInitialized || _buffering); - final enabled = video.isInitialized && total > 0 && widget.onSeek != null; + final loading = + !widget.locked && !scrubbing && (!video.isInitialized || _buffering); + final enabled = + !widget.locked && + video.isInitialized && + total > 0 && + widget.onSeek != null; final played = total > 0 ? (video.position.inMilliseconds / total).clamp(0.0, 1.0) : 0.0; @@ -203,7 +223,9 @@ class _SeekProgressBarState extends State { ), SizedBox( height: trackAreaHeight, - child: loading + child: widget.locked + ? _inactiveTrack(trackHeight) + : loading ? Center( child: LoadingProgressTrack( height: trackHeight, @@ -251,6 +273,19 @@ class _SeekProgressBarState extends State { ); } + Widget _inactiveTrack(double height) { + return Center( + child: SizedBox( + width: double.infinity, + height: height, + child: ColoredBox( + key: _inactiveTrackKey, + color: widget.inactiveTrackColor, + ), + ), + ); + } + String _format(Duration value) { final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0'); diff --git a/test/home/home_preview_layout_test.dart b/test/home/home_preview_layout_test.dart index 4064f01..bf2d5fb 100644 --- a/test/home/home_preview_layout_test.dart +++ b/test/home/home_preview_layout_test.dart @@ -106,6 +106,7 @@ void main() { isFree: true, ), controller: null, + locked: false, liked: true, saved: false, visible: true, @@ -183,6 +184,80 @@ void main() { expect(find.byKey(const Key('home-feed-progress-thumb')), findsNothing); }); + testWidgets('should_show_static_track_without_seek_when_feed_is_locked', ( + tester, + ) async { + var scrubStarts = 0; + var seeks = 0; + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light(), + locale: const Locale('en'), + supportedLocales: AppLocalizations.supportedLocales, + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: Builder( + builder: (context) => Scaffold( + body: HomeFeedOverlay( + l10n: AppLocalizations.of(context)!, + drama: const Drama( + id: 'locked-feed', + title: 'Locked Feed', + synopsis: 'synopsis', + posterUrl: '', + ), + episode: const Episode( + id: 'locked-feed-1', + number: 1, + title: 'Locked', + isFree: false, + locked: true, + ), + controller: null, + locked: true, + liked: false, + saved: false, + visible: true, + boosting: false, + onTogglePlay: () {}, + onBoostStart: () {}, + onBoostEnd: () {}, + onLike: () {}, + onSave: () {}, + onComments: () {}, + onShare: () {}, + onDetail: () {}, + onOpenFullPlayer: () {}, + onMore: () {}, + onScrubStart: () => scrubStarts++, + onSeek: (_) => seeks++, + ), + ), + ), + ), + ); + + expect(find.byKey(const Key('progress-loading')), findsNothing); + expect(find.byKey(const Key('progress-loading-highlight')), findsNothing); + expect( + find.byKey(const Key('home-feed-progress-inactive')), + findsOneWidget, + ); + expect( + tester + .getSize(find.byKey(const Key('home-feed-progress-inactive'))) + .height, + 2, + ); + + await tester.drag( + find.byKey(const Key('home-feed-progress')), + const Offset(120, 0), + ); + await tester.pumpAndSettle(); + expect(scrubStarts, 0); + expect(seeks, 0); + }); + testWidgets('should_seek_and_open_player_from_feed_actions', (tester) async { var opened = 0; Duration? seeked; @@ -219,6 +294,7 @@ void main() { isFree: true, ), controller: controller, + locked: false, liked: false, saved: false, visible: true, @@ -355,6 +431,7 @@ void main() { isFree: true, ), controller: controller, + locked: false, liked: false, saved: false, visible: true, @@ -505,6 +582,7 @@ void main() { isFree: true, ), controller: controller, + locked: false, liked: false, saved: false, visible: false, diff --git a/test/playback/seek_progress_bar_test.dart b/test/playback/seek_progress_bar_test.dart index 860423f..5682333 100644 --- a/test/playback/seek_progress_bar_test.dart +++ b/test/playback/seek_progress_bar_test.dart @@ -2,6 +2,7 @@ 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/seek_progress_bar.dart'; import 'package:video_player/video_player.dart'; @@ -34,6 +35,47 @@ void main() { expect(find.byType(Slider), findsNothing); }); + testWidgets('should_show_inactive_track_and_disable_seek_when_locked', ( + tester, + ) async { + final video = ValueNotifier( + const VideoPlayerValue( + duration: Duration(seconds: 12), + position: Duration(seconds: 3), + isInitialized: true, + ), + ); + addTearDown(video.dispose); + var seeks = 0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SeekProgressBar( + valueListenable: video, + semanticsLabel: 'Seek video', + locked: true, + onSeek: (_) async => seeks++, + ), + ), + ), + ); + + expect(find.byKey(const Key('progress-loading')), findsNothing); + expect(find.byKey(const Key('progress-loading-highlight')), findsNothing); + expect(find.byType(Slider), findsNothing); + final inactive = tester.widget( + find.byKey(const Key('progress-inactive')), + ); + expect(inactive.color, AppColors.border); + + await tester.drag( + find.byKey(const Key('progress-inactive')), + const Offset(120, 0), + ); + await tester.pumpAndSettle(); + expect(seeks, 0); + }); + testWidgets('should_debounce_buffering_before_showing_loading_track', ( tester, ) async { diff --git a/test/player/loading_progress_track_test.dart b/test/player/loading_progress_track_test.dart index cacf042..b461a8b 100644 --- a/test/player/loading_progress_track_test.dart +++ b/test/player/loading_progress_track_test.dart @@ -3,7 +3,6 @@ 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() { @@ -37,7 +36,7 @@ void main() { find.byKey(const Key('progress-loading-highlight')), ) .color, - AppColors.blue, + Colors.white, ); final initialLeft = tester diff --git a/test/player/player_fullscreen_test.dart b/test/player/player_fullscreen_test.dart index 8b763c8..854ee69 100644 --- a/test/player/player_fullscreen_test.dart +++ b/test/player/player_fullscreen_test.dart @@ -65,6 +65,53 @@ void main() { _expectPlayerNavigationBar(tester); }); + testWidgets('should_render_player_loading_indicators_at_compact_size', ( + tester, + ) async { + final playback = _PendingPlaybackRepository(); + final initialization = Completer(); + await _pumpApp( + tester, + playback: playback, + playerFactory: (_) => _GatedPlayer(initialization.future), + settle: false, + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + final ticketLoading = find.byKey(const Key('player-ticket-loading')); + expect(ticketLoading, findsOneWidget); + expect(tester.getSize(ticketLoading), const Size(24, 24)); + expect( + tester.widget(ticketLoading).strokeWidth, + 2, + ); + + playback.ticket.complete( + const PlayTicket( + episodeId: 'city-lights-demo-1', + accessible: true, + accessType: PlayTicket.free, + playUrl: '/files/episode/fake-city.mp4?e=1&s=abc', + qualities: ['AUTO'], + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + + final sessionLoading = find.byKey(const Key('player-session-loading')); + expect(sessionLoading, findsOneWidget); + expect(tester.getSize(sessionLoading), const Size(24, 24)); + expect( + tester.widget(sessionLoading).strokeWidth, + 2, + ); + + initialization.complete(); + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + }); + testWidgets('should_pause_and_resume_player_when_page_route_covers_it', ( tester, ) async { @@ -956,6 +1003,16 @@ void main() { tester, cardKey: const Key('home-drama-card-locked-demo'), ); + expect(find.byKey(const Key('progress-loading')), findsNothing); + expect(find.byKey(const Key('progress-loading-highlight')), findsNothing); + expect(find.byKey(const Key('progress-inactive')), findsOneWidget); + expect(find.byType(Slider), findsNothing); + expect( + tester + .widget(find.byKey(const Key('player-progress'))) + .locked, + isTrue, + ); await tester.tap(find.byKey(const Key('player-episodes'))); await tester.pumpAndSettle(); @@ -1293,10 +1350,11 @@ Future _pumpApp( WidgetTester tester, { Size size = const Size(390, 844), FakeCatalogRepository? catalog, - FakePlaybackRepository? playback, + PlaybackRepository? playback, FakeInteractionRepository? interactions, FakeCommentRepository? commentRepository, PlayerFactory? playerFactory, + bool settle = true, }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; @@ -1317,7 +1375,7 @@ Future _pumpApp( haptics: FakeHaptics(), ), ); - await tester.pumpAndSettle(); + if (settle) await tester.pumpAndSettle(); } const _countingDrama = Drama( @@ -1361,6 +1419,35 @@ class _RecordingPlayer implements PlayerPort { Future dispose() async {} } +class _PendingPlaybackRepository implements PlaybackRepository { + final Completer ticket = Completer(); + + @override + Future ticketFor(String episodeId, {bool forceRefresh = false}) => + ticket.future; + + @override + bool isFresh(String episodeId) => false; +} + +class _GatedPlayer implements PlayerPort { + _GatedPlayer(this.initialization); + + final Future initialization; + + @override + Future initialize() => initialization; + + @override + Future play() async {} + + @override + Future pause() async {} + + @override + Future dispose() async {} +} + const _lockedDrama = Drama( id: 'locked-demo', title: 'Locked Demo',