fix softlock further

This commit is contained in:
Boof2015
2026-07-28 17:08:54 -04:00
parent 304192fbb6
commit 0df3dfcf6b
+53 -48
View File
@@ -47,7 +47,6 @@ import {
} from '@/components/player/nowPlayingTrackTransition'; } from '@/components/player/nowPlayingTrackTransition';
import { import {
resolveNowPlayingPanRelease, resolveNowPlayingPanRelease,
resolveNowPlayingDismissSpring,
shouldEnableNowPlayingPan, shouldEnableNowPlayingPan,
shouldStartNowPlayingPan, shouldStartNowPlayingPan,
} from '@/components/player/nowPlayingDismiss'; } from '@/components/player/nowPlayingDismiss';
@@ -131,6 +130,8 @@ const MENU_ANIMATION_OUT_MS = 100;
const MENU_ENTER_OFFSET_Y = -8; const MENU_ENTER_OFFSET_Y = -8;
const NOW_PLAYING_SPECTRUM_SMOOTHING = 0.85; const NOW_PLAYING_SPECTRUM_SMOOTHING = 0.85;
const PAN_DISMISS_HANDOFF_BACKSTOP_MS = 120; const PAN_DISMISS_HANDOFF_BACKSTOP_MS = 120;
const PAN_TOUCH_END_BACKSTOP_MS = 80;
const PAN_GESTURE_RECOVERY_MS = 1200;
interface NowPlayingMenuItem { interface NowPlayingMenuItem {
key: string; key: string;
@@ -304,9 +305,7 @@ export function NowPlayingOverlay() {
shouldEnableNowPlayingPan(playerOpen, queueOpen, lyricsBodySwitching) shouldEnableNowPlayingPan(playerOpen, queueOpen, lyricsBodySwitching)
); );
const panDismissRequested = useSharedValue(false); const panDismissRequested = useSharedValue(false);
const panExitPending = useSharedValue(false); const panRecoveryLease = useSharedValue(0);
const panReleaseVelocity = useSharedValue(0);
const panReleaseRemainingDistance = useSharedValue(windowHeight);
const screenHeight = useSharedValue(windowHeight); const screenHeight = useSharedValue(windowHeight);
const companionTouchStartX = useSharedValue(companionStartX); const companionTouchStartX = useSharedValue(companionStartX);
const menuProgress = useSharedValue(0); const menuProgress = useSharedValue(0);
@@ -318,7 +317,7 @@ export function NowPlayingOverlay() {
const suspendPanForChildTransition = () => { const suspendPanForChildTransition = () => {
panEnabled.value = false; panEnabled.value = false;
panDismissRequested.value = false; panDismissRequested.value = false;
panExitPending.value = false; cancelAnimation(panRecoveryLease);
cancelAnimation(translateY); cancelAnimation(translateY);
translateY.value = 0; translateY.value = 0;
}; };
@@ -561,30 +560,9 @@ export function NowPlayingOverlay() {
// after an effect that depends on the value. // after an effect that depends on the value.
useEffect(() => { useEffect(() => {
if (phase === 'closing') { if (phase === 'closing') {
// The gesture and button paths own the exit animation, including its // Gesture/button paths attach their own exit animation. Only animate here
// velocity-matched spring shaping. Only animate here when `closing` // when `closing` arrived from a direct closePlayer() call.
// arrived from a direct closePlayer() with no animation attached. if (!exitAnimated) {
if (exitAnimated && panExitPending.value) {
const releaseSpring = resolveNowPlayingDismissSpring(
panReleaseVelocity.value,
panReleaseRemainingDistance.value
);
panExitPending.value = false;
cancelAnimation(translateY);
translateY.value = withSpring(
screenHeight.value,
{
damping: releaseSpring.damping,
stiffness: releaseSpring.stiffness,
velocity: releaseSpring.velocity,
overshootClamping: true,
energyThreshold: 1e-4,
},
(finished) => {
if (finished) runOnJS(commitClosed)();
}
);
} else if (!exitAnimated) {
translateY.value = withTiming(windowHeight, { duration: 200 }); translateY.value = withTiming(windowHeight, { duration: 200 });
} }
return; return;
@@ -597,11 +575,6 @@ export function NowPlayingOverlay() {
exitAnimated, exitAnimated,
queueOpen, queueOpen,
lyricsMode, lyricsMode,
commitClosed,
panExitPending,
panReleaseRemainingDistance,
panReleaseVelocity,
screenHeight,
translateY, translateY,
]); ]);
@@ -629,6 +602,22 @@ export function NowPlayingOverlay() {
return undefined; return undefined;
}, [phase, openRequest]); }, [phase, openRequest]);
const dismissFromPan = useCallback(() => {
// One JS transaction: the store enters `closing` before the deterministic
// slide is scheduled. Its 220 ms duration always beats the 450 ms fallback
// unmount, so a normal release cannot visibly despawn.
beginDismiss();
cancelAnimation(panRecoveryLease);
cancelAnimation(translateY);
translateY.value = withTiming(
screenHeight.value,
motion.snap,
(finished) => {
if (finished) runOnJS(commitClosed)();
}
);
}, [beginDismiss, commitClosed, panRecoveryLease, screenHeight, translateY]);
const pan = useMemo( const pan = useMemo(
() => Gesture.Pan() () => Gesture.Pan()
.activeOffsetY(14) // engage only on a downward drag .activeOffsetY(14) // engage only on a downward drag
@@ -649,10 +638,31 @@ export function NowPlayingOverlay() {
.onTouchesMove((_event, stateManager) => { .onTouchesMove((_event, stateManager) => {
if (!panEnabled.value) stateManager.fail(); if (!panEnabled.value) stateManager.fail();
}) })
.onTouchesUp(() => {
if (panDismissRequested.value) return;
translateY.value = withDelay(
PAN_TOUCH_END_BACKSTOP_MS,
withTiming(0, motion.snap)
);
})
.onTouchesCancelled(() => {
if (panDismissRequested.value) return;
translateY.value = withTiming(0, motion.snap);
})
.onStart(() => { .onStart(() => {
panDismissRequested.value = false; panDismissRequested.value = false;
panExitPending.value = false;
cancelAnimation(translateY); cancelAnimation(translateY);
cancelAnimation(panRecoveryLease);
panRecoveryLease.value = withDelay(
PAN_GESTURE_RECOVERY_MS,
withTiming(panRecoveryLease.value + 1, { duration: 0 }, (finished) => {
if (!finished || panDismissRequested.value) return;
// This lease is independent of RNGH's terminal callbacks. If a
// nested native gesture drops the handler, the partial drag still
// repairs itself on the UI thread.
translateY.value = withTiming(0, motion.snap);
})
);
}) })
.onUpdate((event) => { .onUpdate((event) => {
if (!panEnabled.value) { if (!panEnabled.value) {
@@ -669,40 +679,35 @@ export function NowPlayingOverlay() {
); );
if (release === 'dismiss') { if (release === 'dismiss') {
panDismissRequested.value = true; panDismissRequested.value = true;
panExitPending.value = true; cancelAnimation(panRecoveryLease);
panReleaseVelocity.value = event.velocityY;
panReleaseRemainingDistance.value =
screenHeight.value - translateY.value;
// If the RN handoff is ever dropped, restore the partial drag instead // If the RN handoff is ever dropped, restore the partial drag instead
// of leaving an open player stranded. The phase effect cancels this // of leaving an open player stranded. dismissFromPan cancels this
// delayed animation only after Zustand has entered `closing`. // delayed animation after synchronously entering `closing`.
translateY.value = withDelay( translateY.value = withDelay(
PAN_DISMISS_HANDOFF_BACKSTOP_MS, PAN_DISMISS_HANDOFF_BACKSTOP_MS,
withTiming(0, motion.snap) withTiming(0, motion.snap)
); );
runOnJS(beginDismiss)(); runOnJS(dismissFromPan)();
return; return;
} }
panDismissRequested.value = false; panDismissRequested.value = false;
panExitPending.value = false; cancelAnimation(panRecoveryLease);
translateY.value = withTiming(0, motion.snap); translateY.value = withTiming(0, motion.snap);
}) })
.onFinalize(() => { .onFinalize(() => {
// Successful dismissals retain the short handoff backstop above. Every // Successful dismissals retain the short handoff backstop above. Every
// other terminal path, including cancellation, re-anchors immediately. // other terminal path, including cancellation, re-anchors immediately.
if (!panDismissRequested.value) { if (!panDismissRequested.value) {
cancelAnimation(panRecoveryLease);
translateY.value = withTiming(0, motion.snap); translateY.value = withTiming(0, motion.snap);
} }
}), }),
[ [
beginDismiss,
companionTouchStartX, companionTouchStartX,
dismissFromPan,
panDismissRequested, panDismissRequested,
panEnabled, panEnabled,
panExitPending, panRecoveryLease,
panReleaseRemainingDistance,
panReleaseVelocity,
screenHeight,
translateY, translateY,
] ]
); );