fix(player): 恢复独立播放页双击点赞
This commit is contained in:
1 parent
b291044acc
commit
63296d2a92
8 files changed
+348
-145
No files matched your search
@@ -1228,6 +1228,9 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
? null
|
||||
: () => unawaited(logic.leave(context)),
|
||||
onTogglePlay: _togglePlayback,
|
||||
onDoubleTapLike: locked
|
||||
? null
|
||||
: (_) => _handleDoubleTapLike(context, interactions, episode),
|
||||
onClear: () => setState(() => _chromeVisible = false),
|
||||
onExitClear: () => setState(() => _chromeVisible = true),
|
||||
onDetail: () => unawaited(logic.openDetail(context)),
|
||||
|
||||
@@ -14,7 +14,7 @@ import '../../../core/theme/app_colors.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import 'home_feed_recommend_panel.dart';
|
||||
import 'loading_progress_track.dart';
|
||||
import 'player_double_tap_hearts.dart';
|
||||
import 'player_tap_gesture_region.dart';
|
||||
import 'seek_progress_bar.dart';
|
||||
import 'scrub_preview_card.dart';
|
||||
|
||||
@@ -110,19 +110,8 @@ class HomeFeedOverlay extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
static const Duration _singleTapDelay = Duration(milliseconds: 400);
|
||||
|
||||
bool _scrubbing = false;
|
||||
bool _wasVisibleBeforeScrub = true;
|
||||
Timer? _singleTapTimer;
|
||||
DateTime? _lastDownAt;
|
||||
DateTime? _blockSingleTapUntil;
|
||||
bool _doubleTapDown = false;
|
||||
bool _likedThisPair = false;
|
||||
int _downSerial = 0;
|
||||
int _tapSeq = 0;
|
||||
int _nextHeartBurstId = 0;
|
||||
final List<({int id, Offset position})> _heartBursts = [];
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant HomeFeedOverlay oldWidget) {
|
||||
@@ -132,13 +121,6 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapSeq++;
|
||||
_singleTapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
@@ -146,21 +128,16 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Listener(
|
||||
onPointerUp: (_) => widget.onBoostEnd(),
|
||||
onPointerCancel: (_) => widget.onBoostEnd(),
|
||||
onPointerDown: _handlePointerDown,
|
||||
child: GestureDetector(
|
||||
key: const Key('home-feed-gesture'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapUp: _handleTapUp,
|
||||
onLongPressStart: (details) =>
|
||||
_handleLongPressStart(details, constraints.maxWidth),
|
||||
onLongPressEnd: (_) => widget.onBoostEnd(),
|
||||
onLongPressCancel: widget.onBoostEnd,
|
||||
),
|
||||
),
|
||||
child: PlayerTapGestureRegion(
|
||||
gestureKey: const Key('home-feed-gesture'),
|
||||
episodeId: widget.episode.id,
|
||||
enabled: !_scrubbing,
|
||||
doubleTapEnabled: !widget.locked,
|
||||
now: widget.now,
|
||||
onSingleTap: widget.onTogglePlay,
|
||||
onDoubleTapLike: widget.onDoubleTapLike,
|
||||
onLongPressStart: _handleLongPressStart,
|
||||
onInteractionEnd: widget.onBoostEnd,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -222,25 +199,6 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
onSeek: widget.onSeek,
|
||||
),
|
||||
),
|
||||
if (_heartBursts.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
for (final burst in _heartBursts)
|
||||
Positioned(
|
||||
left: burst.position.dx,
|
||||
top: burst.position.dy - 10,
|
||||
child: PlayerDoubleTapHeartBurst(
|
||||
key: ValueKey(burst.id),
|
||||
onCompleted: () => _removeHeartBurst(burst.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -262,83 +220,8 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
widget.onScrubEnd?.call(wasVisible);
|
||||
}
|
||||
|
||||
DateTime _now() => widget.now?.call() ?? DateTime.now();
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
final now = _now();
|
||||
final previous = _lastDownAt;
|
||||
_downSerial++;
|
||||
_lastDownAt = now;
|
||||
final repeated =
|
||||
previous != null && now.difference(previous) <= _singleTapDelay;
|
||||
if (!repeated) {
|
||||
if (_singleTapBlocked(now)) return;
|
||||
_doubleTapDown = false;
|
||||
_likedThisPair = false;
|
||||
return;
|
||||
}
|
||||
_doubleTapDown = true;
|
||||
_blockSingleTapUntil = now.add(_singleTapDelay);
|
||||
_cancelPendingSingleTap();
|
||||
}
|
||||
|
||||
bool _singleTapBlocked(DateTime now) {
|
||||
final blockedUntil = _blockSingleTapUntil;
|
||||
return blockedUntil != null && now.isBefore(blockedUntil);
|
||||
}
|
||||
|
||||
void _cancelPendingSingleTap() {
|
||||
_tapSeq++;
|
||||
_singleTapTimer?.cancel();
|
||||
_singleTapTimer = null;
|
||||
}
|
||||
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
final now = _now();
|
||||
if (_doubleTapDown || _singleTapBlocked(now)) {
|
||||
_showDoubleTapLike(details.localPosition);
|
||||
return;
|
||||
}
|
||||
final serial = _downSerial;
|
||||
final tapSeq = ++_tapSeq;
|
||||
_singleTapTimer?.cancel();
|
||||
late final Timer pending;
|
||||
pending = Timer(_singleTapDelay, () {
|
||||
if (!identical(_singleTapTimer, pending)) return;
|
||||
_singleTapTimer = null;
|
||||
if (!mounted || tapSeq != _tapSeq || serial != _downSerial) return;
|
||||
if (_doubleTapDown || _singleTapBlocked(_now())) return;
|
||||
_lastDownAt = null;
|
||||
widget.onTogglePlay();
|
||||
});
|
||||
_singleTapTimer = pending;
|
||||
}
|
||||
|
||||
void _showDoubleTapLike(Offset position) {
|
||||
if (_likedThisPair) return;
|
||||
_likedThisPair = true;
|
||||
_blockSingleTapUntil = _now().add(_singleTapDelay);
|
||||
_cancelPendingSingleTap();
|
||||
_showHearts(position);
|
||||
}
|
||||
|
||||
void _showHearts(Offset position) {
|
||||
final id = _nextHeartBurstId++;
|
||||
setState(() => _heartBursts.add((id: id, position: position)));
|
||||
widget.onDoubleTapLike?.call(position);
|
||||
}
|
||||
|
||||
void _removeHeartBurst(int id) {
|
||||
if (!mounted) return;
|
||||
setState(() => _heartBursts.removeWhere((burst) => burst.id == id));
|
||||
}
|
||||
|
||||
void _handleLongPressStart(LongPressStartDetails details, double width) {
|
||||
final third = width / 3;
|
||||
_tapSeq++;
|
||||
_cancelPendingSingleTap();
|
||||
_doubleTapDown = false;
|
||||
_lastDownAt = null;
|
||||
final dx = details.localPosition.dx;
|
||||
if (dx < third || dx > width - third) {
|
||||
widget.onBoostStart();
|
||||
|
||||
@@ -37,6 +37,8 @@ const List<_HeartParticle> _hearts = [
|
||||
class PlayerDoubleTapHeartBurst extends StatefulWidget {
|
||||
const PlayerDoubleTapHeartBurst({super.key, required this.onCompleted});
|
||||
|
||||
static const int particleCount = 3;
|
||||
|
||||
final VoidCallback onCompleted;
|
||||
|
||||
@override
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../../l10n/app_localizations.dart';
|
||||
import 'player_center_toggle.dart';
|
||||
import 'interaction_rail.dart';
|
||||
import 'player_overlay_models.dart';
|
||||
import 'player_tap_gesture_region.dart';
|
||||
import 'seek_progress_bar.dart';
|
||||
|
||||
part 'player_overlay_parts.dart';
|
||||
@@ -56,16 +57,15 @@ class PlayerPlaybackOverlay extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => GestureDetector(
|
||||
key: const Key('player-video-tap'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: actions.onTogglePlay,
|
||||
onLongPressStart: (details) =>
|
||||
_handleLongPressStart(details, constraints.maxWidth),
|
||||
onLongPressEnd: (_) => actions.onBoostEnd(),
|
||||
onLongPressCancel: actions.onBoostEnd,
|
||||
),
|
||||
child: PlayerTapGestureRegion(
|
||||
gestureKey: const Key('player-video-tap'),
|
||||
episodeId: data.episode.id,
|
||||
enabled: !scrubbing,
|
||||
doubleTapEnabled: !data.locked,
|
||||
onSingleTap: actions.onTogglePlay,
|
||||
onDoubleTapLike: actions.onDoubleTapLike,
|
||||
onLongPressStart: _handleLongPressStart,
|
||||
onInteractionEnd: actions.onBoostEnd,
|
||||
),
|
||||
),
|
||||
_top(context),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
@@ -87,6 +89,7 @@ class PlayerOverlayActions {
|
||||
const PlayerOverlayActions({
|
||||
required this.onBack,
|
||||
required this.onTogglePlay,
|
||||
this.onDoubleTapLike,
|
||||
required this.onClear,
|
||||
required this.onExitClear,
|
||||
required this.onDetail,
|
||||
@@ -111,6 +114,9 @@ class PlayerOverlayActions {
|
||||
/// 切换播放状态。
|
||||
final VoidCallback onTogglePlay;
|
||||
|
||||
/// 双击画面点赞;位置用于飘心。
|
||||
final ValueChanged<Offset>? onDoubleTapLike;
|
||||
|
||||
/// 进入清屏。
|
||||
final VoidCallback onClear;
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'player_double_tap_hearts.dart';
|
||||
|
||||
typedef PlayerLongPressStartCallback =
|
||||
void Function(LongPressStartDetails details, double width);
|
||||
|
||||
/// 播放画面共用的单击/双击仲裁层。
|
||||
class PlayerTapGestureRegion extends StatefulWidget {
|
||||
const PlayerTapGestureRegion({
|
||||
super.key,
|
||||
required this.gestureKey,
|
||||
required this.episodeId,
|
||||
required this.onSingleTap,
|
||||
required this.onLongPressStart,
|
||||
required this.onInteractionEnd,
|
||||
this.onDoubleTapLike,
|
||||
this.doubleTapEnabled = true,
|
||||
this.enabled = true,
|
||||
this.now,
|
||||
});
|
||||
|
||||
final Key gestureKey;
|
||||
final String episodeId;
|
||||
final VoidCallback onSingleTap;
|
||||
final ValueChanged<Offset>? onDoubleTapLike;
|
||||
final PlayerLongPressStartCallback onLongPressStart;
|
||||
final VoidCallback onInteractionEnd;
|
||||
final bool doubleTapEnabled;
|
||||
final bool enabled;
|
||||
final DateTime Function()? now;
|
||||
|
||||
@override
|
||||
State<PlayerTapGestureRegion> createState() => _PlayerTapGestureRegionState();
|
||||
}
|
||||
|
||||
class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> {
|
||||
static const Duration _singleTapDelay = Duration(milliseconds: 320);
|
||||
|
||||
Timer? _singleTapTimer;
|
||||
DateTime? _lastDownAt;
|
||||
DateTime? _blockSingleTapUntil;
|
||||
bool _doubleTapDown = false;
|
||||
bool _likedThisPair = false;
|
||||
int _downSerial = 0;
|
||||
int _tapSeq = 0;
|
||||
int _nextHeartBurstId = 0;
|
||||
final List<({int id, Offset position})> _heartBursts = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tapSeq++;
|
||||
_singleTapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
ignoring: !widget.enabled,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Listener(
|
||||
onPointerUp: (_) => widget.onInteractionEnd(),
|
||||
onPointerCancel: (_) => widget.onInteractionEnd(),
|
||||
onPointerDown: _handlePointerDown,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
key: widget.gestureKey,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapUp: _handleTapUp,
|
||||
onLongPressStart: (details) =>
|
||||
_handleLongPressStart(details, constraints.maxWidth),
|
||||
onLongPressEnd: (_) => widget.onInteractionEnd(),
|
||||
onLongPressCancel: widget.onInteractionEnd,
|
||||
),
|
||||
if (_heartBursts.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
for (final burst in _heartBursts)
|
||||
Positioned(
|
||||
left: burst.position.dx,
|
||||
top: burst.position.dy - 10,
|
||||
child: PlayerDoubleTapHeartBurst(
|
||||
key: ValueKey(burst.id),
|
||||
onCompleted: () => _removeHeartBurst(burst.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _now() => widget.now?.call() ?? DateTime.now();
|
||||
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
final now = _now();
|
||||
final previous = _lastDownAt;
|
||||
_downSerial++;
|
||||
_lastDownAt = now;
|
||||
final repeated =
|
||||
previous != null && now.difference(previous) <= _singleTapDelay;
|
||||
if (!repeated) {
|
||||
if (_singleTapBlocked(now)) return;
|
||||
_doubleTapDown = false;
|
||||
_likedThisPair = false;
|
||||
return;
|
||||
}
|
||||
_doubleTapDown = true;
|
||||
_blockSingleTapUntil = now.add(_singleTapDelay);
|
||||
_cancelPendingSingleTap();
|
||||
}
|
||||
|
||||
bool _singleTapBlocked(DateTime now) {
|
||||
final blockedUntil = _blockSingleTapUntil;
|
||||
return blockedUntil != null && now.isBefore(blockedUntil);
|
||||
}
|
||||
|
||||
void _cancelPendingSingleTap() {
|
||||
_tapSeq++;
|
||||
_singleTapTimer?.cancel();
|
||||
_singleTapTimer = null;
|
||||
}
|
||||
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
final now = _now();
|
||||
if (_doubleTapDown || _singleTapBlocked(now)) {
|
||||
_handleDoubleTapLike(details.localPosition);
|
||||
return;
|
||||
}
|
||||
final serial = _downSerial;
|
||||
final tapSeq = ++_tapSeq;
|
||||
_singleTapTimer?.cancel();
|
||||
late final Timer pending;
|
||||
pending = Timer(_singleTapDelay, () {
|
||||
if (!identical(_singleTapTimer, pending)) return;
|
||||
_singleTapTimer = null;
|
||||
if (!mounted || tapSeq != _tapSeq || serial != _downSerial) return;
|
||||
if (_doubleTapDown || _singleTapBlocked(_now())) return;
|
||||
_lastDownAt = null;
|
||||
widget.onSingleTap();
|
||||
});
|
||||
_singleTapTimer = pending;
|
||||
}
|
||||
|
||||
void _handleDoubleTapLike(Offset position) {
|
||||
_blockSingleTapUntil = _now().add(_singleTapDelay);
|
||||
_cancelPendingSingleTap();
|
||||
if (!widget.doubleTapEnabled || _likedThisPair) return;
|
||||
_likedThisPair = true;
|
||||
final id = _nextHeartBurstId++;
|
||||
setState(() => _heartBursts.add((id: id, position: position)));
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[player] phase=double-tap-like episodeId=${widget.episodeId}',
|
||||
);
|
||||
debugPrint(
|
||||
'[player] phase=heart-burst '
|
||||
'count=${PlayerDoubleTapHeartBurst.particleCount}',
|
||||
);
|
||||
}
|
||||
widget.onDoubleTapLike?.call(position);
|
||||
}
|
||||
|
||||
void _removeHeartBurst(int id) {
|
||||
if (!mounted) return;
|
||||
setState(() => _heartBursts.removeWhere((burst) => burst.id == id));
|
||||
}
|
||||
|
||||
void _handleLongPressStart(LongPressStartDetails details, double width) {
|
||||
_tapSeq++;
|
||||
_cancelPendingSingleTap();
|
||||
_doubleTapDown = false;
|
||||
_lastDownAt = null;
|
||||
widget.onLongPressStart(details, width);
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import '../support/fake_haptics.dart';
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('double tap within 400ms never pauses', (tester) async {
|
||||
for (final delay in [120, 250, 300, 380]) {
|
||||
testWidgets('double tap within 320ms never pauses', (tester) async {
|
||||
for (final delay in [120, 250, 300]) {
|
||||
final repository = FakeInteractionRepository();
|
||||
final haptics = FakeHaptics();
|
||||
final key = GlobalKey<_HarnessState>();
|
||||
@@ -74,6 +74,39 @@ void main() {
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(key.currentState!.pauseToggles, 2);
|
||||
});
|
||||
|
||||
testWidgets('liked episode keeps heart feedback without liking again', (
|
||||
tester,
|
||||
) async {
|
||||
final repository = FakeInteractionRepository();
|
||||
final haptics = FakeHaptics();
|
||||
final key = GlobalKey<_HarnessState>();
|
||||
final clock = _TestClock();
|
||||
await _pumpHarness(tester, repository, haptics, key, clock);
|
||||
final center = tester.getCenter(find.byKey(const Key('home-feed-gesture')));
|
||||
|
||||
await tester.tapAt(center);
|
||||
clock.advance(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.tapAt(center);
|
||||
await tester.pump();
|
||||
expect(repository.likeTargets, hasLength(1));
|
||||
await tester.pump(const Duration(milliseconds: 800));
|
||||
|
||||
clock.advance(const Duration(milliseconds: 500));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(center);
|
||||
clock.advance(const Duration(milliseconds: 100));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.tapAt(center);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(repository.likeTargets, hasLength(1));
|
||||
expect(haptics.lightImpacts, 2);
|
||||
expect(key.currentState!.pauseToggles, 0);
|
||||
expect(find.byKey(const Key('double-tap-heart-0')), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpHarness(
|
||||
|
||||
@@ -364,7 +364,7 @@ void main() {
|
||||
// IgnorePointer 让加载中的播放暂停与上下滑穿透,滑动会关闭入口 loading。
|
||||
expect(fullLogic.session.userWantsPlay, isTrue);
|
||||
await tester.tap(find.byKey(const Key('player-video-tap')).hitTestable());
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
expect(fullLogic.session.userWantsPlay, isFalse);
|
||||
final indexBeforeSwipe = fullLogic.session.currentIndex;
|
||||
await tester.drag(fullPlayerPage, const Offset(0, -560));
|
||||
@@ -632,7 +632,7 @@ void main() {
|
||||
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
||||
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
@@ -655,6 +655,82 @@ void main() {
|
||||
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_double_tap_like_once_without_pausing_full_player', (
|
||||
tester,
|
||||
) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.withData({
|
||||
'auth.accessToken': 'test-access',
|
||||
'auth.refreshToken': 'test-refresh',
|
||||
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
||||
});
|
||||
final interactions = FakeInteractionRepository(
|
||||
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
|
||||
);
|
||||
await _pumpApp(tester, interactions: interactions);
|
||||
await _openSeriesPlayer(tester);
|
||||
|
||||
final logic = _pageLogic(tester, find.byType(PlayerPage).last);
|
||||
final gesture = find.byKey(const Key('player-video-tap')).hitTestable();
|
||||
final position = tester.getCenter(gesture);
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
|
||||
for (var index = 0; index < 4; index++) {
|
||||
await tester.tapAt(position);
|
||||
}
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
||||
expect(interactions.likeTargets, ['EPISODE:city-lights-demo-1']);
|
||||
for (var index = 0; index < 3; index++) {
|
||||
expect(find.byKey(Key('double-tap-heart-$index')), findsOneWidget);
|
||||
}
|
||||
expect(
|
||||
tester
|
||||
.widget<Image>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('player-rail-like')),
|
||||
matching: find.byType(Image),
|
||||
),
|
||||
)
|
||||
.color,
|
||||
AppColors.doubleTapHeartRed,
|
||||
);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 800));
|
||||
expect(find.byKey(const Key('double-tap-heart-0')), findsNothing);
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should_double_tap_like_in_clear_screen', (tester) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.withData({
|
||||
'auth.accessToken': 'test-access',
|
||||
'auth.refreshToken': 'test-refresh',
|
||||
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
||||
});
|
||||
final interactions = FakeInteractionRepository();
|
||||
await _pumpApp(tester, interactions: interactions);
|
||||
await _openSeriesPlayer(tester);
|
||||
final logic = _pageLogic(tester, find.byType(PlayerPage).last);
|
||||
|
||||
await _clearFromMore(tester);
|
||||
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
||||
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
expect(interactions.likeTargets, hasLength(1));
|
||||
expect(find.byKey(const Key('double-tap-heart-0')), findsOneWidget);
|
||||
expect(find.byKey(const Key('double-tap-heart-1')), findsOneWidget);
|
||||
expect(find.byKey(const Key('double-tap-heart-2')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_controls_visible_until_clear_screen_is_tapped', (
|
||||
tester,
|
||||
) async {
|
||||
@@ -666,7 +742,7 @@ void main() {
|
||||
expect(find.byType(PlayerCenterToggle), findsNothing);
|
||||
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
@@ -710,13 +786,13 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
||||
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
|
||||
expect(find.byType(PlayerCenterToggle), findsNothing);
|
||||
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
||||
@@ -1649,8 +1725,10 @@ void main() {
|
||||
testWidgets('should_open_unlock_panel_with_honest_method_states', (
|
||||
tester,
|
||||
) async {
|
||||
final interactions = FakeInteractionRepository();
|
||||
await _pumpApp(
|
||||
tester,
|
||||
interactions: interactions,
|
||||
playback: FakePlaybackRepository(
|
||||
ticket: const PlayTicket(
|
||||
episodeId: 'city-lights-demo-1',
|
||||
@@ -1663,6 +1741,12 @@ void main() {
|
||||
await _openSeriesPlayer(tester);
|
||||
|
||||
expect(find.byKey(const Key('player-unlock-notice')), findsOneWidget);
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.tapAt(const Offset(80, 300));
|
||||
await tester.pump(const Duration(milliseconds: 350));
|
||||
expect(find.byKey(const Key('double-tap-heart-0')), findsNothing);
|
||||
expect(interactions.likeTargets, isEmpty);
|
||||
|
||||
await tester.tap(find.byKey(const Key('player-open-unlock')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
||||
Reference in new issue
Block a user