mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): hide the video controls again after a few idle seconds (#1846)
This commit is contained in:
1 parent
aa1c636cc2
commit
5cb893245f
3 files changed
+59
-29
No files matched your search
+9
-2
@@ -42,17 +42,24 @@ describe('MediaControlsVisibilityStateMachine', () => {
|
||||
expect(selectMediaControlsVisible(snapshot, signals())).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps controls visible when disabled, paused, interacting, or hovered', () => {
|
||||
it('keeps controls visible when disabled, paused, or interacting', () => {
|
||||
let snapshot = createMediaControlsVisibilitySnapshot();
|
||||
snapshot = send(snapshot, {type: 'controls.hide'});
|
||||
|
||||
expect(selectMediaControlsVisible(snapshot, signals({disabled: true}))).toBe(true);
|
||||
expect(selectMediaControlsVisible(snapshot, signals({isPlaying: false}))).toBe(true);
|
||||
expect(selectMediaControlsVisible(snapshot, signals({isInteracting: true}))).toBe(true);
|
||||
});
|
||||
|
||||
it('lets a resting pointer inside the player still lose the controls', () => {
|
||||
let snapshot = createMediaControlsVisibilitySnapshot();
|
||||
snapshot = send(snapshot, {type: 'controls.mouseEnter'});
|
||||
snapshot = send(snapshot, {type: 'controls.hide'});
|
||||
expect(selectMediaControlsVisible(snapshot, signals())).toBe(true);
|
||||
|
||||
snapshot = send(snapshot, {type: 'controls.hide'});
|
||||
|
||||
expect(getMediaControlsVisibilityValue(snapshot)).toBe('hidden');
|
||||
expect(selectMediaControlsVisible(snapshot, signals())).toBe(false);
|
||||
});
|
||||
|
||||
it('hides on pointer leave only while playback is active and not interacting', () => {
|
||||
|
||||
+5
-19
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {assign, getInitialSnapshot, type SnapshotFrom, setup, transition} from 'xstate';
|
||||
import {getInitialSnapshot, type SnapshotFrom, setup, transition} from 'xstate';
|
||||
|
||||
export interface MediaControlsVisibilitySignals {
|
||||
disabled: boolean;
|
||||
@@ -18,19 +18,10 @@ export type MediaControlsVisibilityEvent =
|
||||
| {type: 'controls.mouseLeave'; signals: MediaControlsVisibilitySignals}
|
||||
| {type: 'controls.touchStart'; signals: MediaControlsVisibilitySignals};
|
||||
|
||||
interface MediaControlsVisibilityContext {
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
export const mediaControlsVisibilityStateMachine = setup({
|
||||
types: {} as {
|
||||
context: MediaControlsVisibilityContext;
|
||||
events: MediaControlsVisibilityEvent;
|
||||
},
|
||||
actions: {
|
||||
markHovered: assign(() => ({isHovered: true})),
|
||||
markUnhovered: assign(() => ({isHovered: false})),
|
||||
},
|
||||
guards: {
|
||||
shouldHideOnPointerLeave: ({event}) =>
|
||||
event.type === 'controls.mouseLeave' && event.signals.isPlaying && !event.signals.isInteracting,
|
||||
@@ -39,7 +30,6 @@ export const mediaControlsVisibilityStateMachine = setup({
|
||||
},
|
||||
}).createMachine({
|
||||
id: 'mediaControlsVisibility',
|
||||
context: () => ({isHovered: false}),
|
||||
initial: 'visible',
|
||||
states: {
|
||||
visible: {
|
||||
@@ -47,11 +37,8 @@ export const mediaControlsVisibilityStateMachine = setup({
|
||||
'controls.show': {target: 'visible'},
|
||||
'controls.hide': {target: 'hidden'},
|
||||
'controls.mouseMove': {target: 'visible'},
|
||||
'controls.mouseEnter': {target: 'visible', actions: 'markHovered'},
|
||||
'controls.mouseLeave': [
|
||||
{target: 'hidden', guard: 'shouldHideOnPointerLeave', actions: 'markUnhovered'},
|
||||
{target: 'visible', actions: 'markUnhovered'},
|
||||
],
|
||||
'controls.mouseEnter': {target: 'visible'},
|
||||
'controls.mouseLeave': [{target: 'hidden', guard: 'shouldHideOnPointerLeave'}, {target: 'visible'}],
|
||||
'controls.touchStart': [{target: 'hidden', guard: 'shouldToggleOnTouch'}, {target: 'visible'}],
|
||||
},
|
||||
},
|
||||
@@ -60,8 +47,8 @@ export const mediaControlsVisibilityStateMachine = setup({
|
||||
'controls.show': {target: 'visible'},
|
||||
'controls.hide': {target: 'hidden'},
|
||||
'controls.mouseMove': {target: 'visible'},
|
||||
'controls.mouseEnter': {target: 'visible', actions: 'markHovered'},
|
||||
'controls.mouseLeave': {target: 'hidden', actions: 'markUnhovered'},
|
||||
'controls.mouseEnter': {target: 'visible'},
|
||||
'controls.mouseLeave': {target: 'hidden'},
|
||||
'controls.touchStart': {target: 'visible'},
|
||||
},
|
||||
},
|
||||
@@ -96,7 +83,6 @@ export function selectMediaControlsVisible(
|
||||
signals.disabled ||
|
||||
!signals.isPlaying ||
|
||||
signals.isInteracting ||
|
||||
snapshot.context.isHovered ||
|
||||
getMediaControlsVisibilityValue(snapshot) === 'visible'
|
||||
);
|
||||
}
|
||||
+45
-8
@@ -8,7 +8,10 @@ import {
|
||||
selectMediaControlsVisible,
|
||||
transitionMediaControlsVisibilitySnapshot,
|
||||
} from '@app/features/voice/components/media_player/MediaControlsVisibilityStateMachine';
|
||||
import {useCallback, useMemo, useState} from 'react';
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
|
||||
export const CONTROLS_AUTOHIDE_DELAY_MS = 3000;
|
||||
export const CONTROLS_AUTOHIDE_FULLSCREEN_DELAY_MS = 1000;
|
||||
|
||||
interface UseControlsVisibilityOptions {
|
||||
autohideDelay?: number;
|
||||
@@ -30,7 +33,12 @@ export interface UseControlsVisibilityReturn {
|
||||
}
|
||||
|
||||
export function useControlsVisibility(options: UseControlsVisibilityOptions = {}): UseControlsVisibilityReturn {
|
||||
const {disabled = false, isPlaying = false, isInteracting = false} = options;
|
||||
const {
|
||||
autohideDelay = CONTROLS_AUTOHIDE_DELAY_MS,
|
||||
disabled = false,
|
||||
isPlaying = false,
|
||||
isInteracting = false,
|
||||
} = options;
|
||||
const signals = useMemo<MediaControlsVisibilitySignals>(
|
||||
() => ({disabled, isPlaying, isInteracting}),
|
||||
[disabled, isPlaying, isInteracting],
|
||||
@@ -39,27 +47,56 @@ export function useControlsVisibility(options: UseControlsVisibilityOptions = {}
|
||||
const send = useCallback((event: MediaControlsVisibilityEvent) => {
|
||||
setSnapshot((currentSnapshot) => transitionMediaControlsVisibilitySnapshot(currentSnapshot, event));
|
||||
}, []);
|
||||
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const clearHideTimer = useCallback(() => {
|
||||
if (hideTimerRef.current === null) return;
|
||||
clearTimeout(hideTimerRef.current);
|
||||
hideTimerRef.current = null;
|
||||
}, []);
|
||||
const armHideTimer = useCallback(() => {
|
||||
clearHideTimer();
|
||||
if (disabled || !isPlaying || isInteracting) return;
|
||||
hideTimerRef.current = setTimeout(() => {
|
||||
hideTimerRef.current = null;
|
||||
send({type: 'controls.hide'});
|
||||
}, autohideDelay);
|
||||
}, [autohideDelay, clearHideTimer, disabled, isInteracting, isPlaying, send]);
|
||||
useEffect(() => {
|
||||
if (disabled || !isPlaying || isInteracting) {
|
||||
clearHideTimer();
|
||||
send({type: 'controls.show'});
|
||||
return clearHideTimer;
|
||||
}
|
||||
armHideTimer();
|
||||
return clearHideTimer;
|
||||
}, [armHideTimer, clearHideTimer, disabled, isInteracting, isPlaying, send]);
|
||||
const showControls = useCallback(() => {
|
||||
send({type: 'controls.show'});
|
||||
}, [send]);
|
||||
armHideTimer();
|
||||
}, [armHideTimer, send]);
|
||||
const hideControls = useCallback(() => {
|
||||
clearHideTimer();
|
||||
send({type: 'controls.hide'});
|
||||
}, [send]);
|
||||
}, [clearHideTimer, send]);
|
||||
const visibilityValue = getMediaControlsVisibilityValue(snapshot);
|
||||
const handleMouseMove = useCallback(() => {
|
||||
if (visibilityValue === 'hidden') {
|
||||
send({type: 'controls.mouseMove'});
|
||||
}
|
||||
}, [send, visibilityValue]);
|
||||
armHideTimer();
|
||||
}, [armHideTimer, send, visibilityValue]);
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
send({type: 'controls.mouseEnter'});
|
||||
}, [send]);
|
||||
armHideTimer();
|
||||
}, [armHideTimer, send]);
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
clearHideTimer();
|
||||
send({type: 'controls.mouseLeave', signals});
|
||||
}, [send, signals]);
|
||||
}, [clearHideTimer, send, signals]);
|
||||
const handleTouchStart = useCallback(() => {
|
||||
send({type: 'controls.touchStart', signals});
|
||||
}, [send, signals]);
|
||||
armHideTimer();
|
||||
}, [armHideTimer, send, signals]);
|
||||
const controlsVisible = selectMediaControlsVisible(snapshot, signals);
|
||||
return {
|
||||
controlsVisible,
|
||||
|
||||
Reference in new issue
Block a user