mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +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);
|
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();
|
let snapshot = createMediaControlsVisibilitySnapshot();
|
||||||
snapshot = send(snapshot, {type: 'controls.hide'});
|
snapshot = send(snapshot, {type: 'controls.hide'});
|
||||||
|
|
||||||
expect(selectMediaControlsVisible(snapshot, signals({disabled: true}))).toBe(true);
|
expect(selectMediaControlsVisible(snapshot, signals({disabled: true}))).toBe(true);
|
||||||
expect(selectMediaControlsVisible(snapshot, signals({isPlaying: false}))).toBe(true);
|
expect(selectMediaControlsVisible(snapshot, signals({isPlaying: false}))).toBe(true);
|
||||||
expect(selectMediaControlsVisible(snapshot, signals({isInteracting: true}))).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.mouseEnter'});
|
||||||
snapshot = send(snapshot, {type: 'controls.hide'});
|
|
||||||
expect(selectMediaControlsVisible(snapshot, signals())).toBe(true);
|
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', () => {
|
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
|
// 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 {
|
export interface MediaControlsVisibilitySignals {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@@ -18,19 +18,10 @@ export type MediaControlsVisibilityEvent =
|
|||||||
| {type: 'controls.mouseLeave'; signals: MediaControlsVisibilitySignals}
|
| {type: 'controls.mouseLeave'; signals: MediaControlsVisibilitySignals}
|
||||||
| {type: 'controls.touchStart'; signals: MediaControlsVisibilitySignals};
|
| {type: 'controls.touchStart'; signals: MediaControlsVisibilitySignals};
|
||||||
|
|
||||||
interface MediaControlsVisibilityContext {
|
|
||||||
isHovered: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mediaControlsVisibilityStateMachine = setup({
|
export const mediaControlsVisibilityStateMachine = setup({
|
||||||
types: {} as {
|
types: {} as {
|
||||||
context: MediaControlsVisibilityContext;
|
|
||||||
events: MediaControlsVisibilityEvent;
|
events: MediaControlsVisibilityEvent;
|
||||||
},
|
},
|
||||||
actions: {
|
|
||||||
markHovered: assign(() => ({isHovered: true})),
|
|
||||||
markUnhovered: assign(() => ({isHovered: false})),
|
|
||||||
},
|
|
||||||
guards: {
|
guards: {
|
||||||
shouldHideOnPointerLeave: ({event}) =>
|
shouldHideOnPointerLeave: ({event}) =>
|
||||||
event.type === 'controls.mouseLeave' && event.signals.isPlaying && !event.signals.isInteracting,
|
event.type === 'controls.mouseLeave' && event.signals.isPlaying && !event.signals.isInteracting,
|
||||||
@@ -39,7 +30,6 @@ export const mediaControlsVisibilityStateMachine = setup({
|
|||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
id: 'mediaControlsVisibility',
|
id: 'mediaControlsVisibility',
|
||||||
context: () => ({isHovered: false}),
|
|
||||||
initial: 'visible',
|
initial: 'visible',
|
||||||
states: {
|
states: {
|
||||||
visible: {
|
visible: {
|
||||||
@@ -47,11 +37,8 @@ export const mediaControlsVisibilityStateMachine = setup({
|
|||||||
'controls.show': {target: 'visible'},
|
'controls.show': {target: 'visible'},
|
||||||
'controls.hide': {target: 'hidden'},
|
'controls.hide': {target: 'hidden'},
|
||||||
'controls.mouseMove': {target: 'visible'},
|
'controls.mouseMove': {target: 'visible'},
|
||||||
'controls.mouseEnter': {target: 'visible', actions: 'markHovered'},
|
'controls.mouseEnter': {target: 'visible'},
|
||||||
'controls.mouseLeave': [
|
'controls.mouseLeave': [{target: 'hidden', guard: 'shouldHideOnPointerLeave'}, {target: 'visible'}],
|
||||||
{target: 'hidden', guard: 'shouldHideOnPointerLeave', actions: 'markUnhovered'},
|
|
||||||
{target: 'visible', actions: 'markUnhovered'},
|
|
||||||
],
|
|
||||||
'controls.touchStart': [{target: 'hidden', guard: 'shouldToggleOnTouch'}, {target: 'visible'}],
|
'controls.touchStart': [{target: 'hidden', guard: 'shouldToggleOnTouch'}, {target: 'visible'}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -60,8 +47,8 @@ export const mediaControlsVisibilityStateMachine = setup({
|
|||||||
'controls.show': {target: 'visible'},
|
'controls.show': {target: 'visible'},
|
||||||
'controls.hide': {target: 'hidden'},
|
'controls.hide': {target: 'hidden'},
|
||||||
'controls.mouseMove': {target: 'visible'},
|
'controls.mouseMove': {target: 'visible'},
|
||||||
'controls.mouseEnter': {target: 'visible', actions: 'markHovered'},
|
'controls.mouseEnter': {target: 'visible'},
|
||||||
'controls.mouseLeave': {target: 'hidden', actions: 'markUnhovered'},
|
'controls.mouseLeave': {target: 'hidden'},
|
||||||
'controls.touchStart': {target: 'visible'},
|
'controls.touchStart': {target: 'visible'},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -96,7 +83,6 @@ export function selectMediaControlsVisible(
|
|||||||
signals.disabled ||
|
signals.disabled ||
|
||||||
!signals.isPlaying ||
|
!signals.isPlaying ||
|
||||||
signals.isInteracting ||
|
signals.isInteracting ||
|
||||||
snapshot.context.isHovered ||
|
|
||||||
getMediaControlsVisibilityValue(snapshot) === 'visible'
|
getMediaControlsVisibilityValue(snapshot) === 'visible'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+45
-8
@@ -8,7 +8,10 @@ import {
|
|||||||
selectMediaControlsVisible,
|
selectMediaControlsVisible,
|
||||||
transitionMediaControlsVisibilitySnapshot,
|
transitionMediaControlsVisibilitySnapshot,
|
||||||
} from '@app/features/voice/components/media_player/MediaControlsVisibilityStateMachine';
|
} 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 {
|
interface UseControlsVisibilityOptions {
|
||||||
autohideDelay?: number;
|
autohideDelay?: number;
|
||||||
@@ -30,7 +33,12 @@ export interface UseControlsVisibilityReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useControlsVisibility(options: UseControlsVisibilityOptions = {}): 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>(
|
const signals = useMemo<MediaControlsVisibilitySignals>(
|
||||||
() => ({disabled, isPlaying, isInteracting}),
|
() => ({disabled, isPlaying, isInteracting}),
|
||||||
[disabled, isPlaying, isInteracting],
|
[disabled, isPlaying, isInteracting],
|
||||||
@@ -39,27 +47,56 @@ export function useControlsVisibility(options: UseControlsVisibilityOptions = {}
|
|||||||
const send = useCallback((event: MediaControlsVisibilityEvent) => {
|
const send = useCallback((event: MediaControlsVisibilityEvent) => {
|
||||||
setSnapshot((currentSnapshot) => transitionMediaControlsVisibilitySnapshot(currentSnapshot, event));
|
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(() => {
|
const showControls = useCallback(() => {
|
||||||
send({type: 'controls.show'});
|
send({type: 'controls.show'});
|
||||||
}, [send]);
|
armHideTimer();
|
||||||
|
}, [armHideTimer, send]);
|
||||||
const hideControls = useCallback(() => {
|
const hideControls = useCallback(() => {
|
||||||
|
clearHideTimer();
|
||||||
send({type: 'controls.hide'});
|
send({type: 'controls.hide'});
|
||||||
}, [send]);
|
}, [clearHideTimer, send]);
|
||||||
const visibilityValue = getMediaControlsVisibilityValue(snapshot);
|
const visibilityValue = getMediaControlsVisibilityValue(snapshot);
|
||||||
const handleMouseMove = useCallback(() => {
|
const handleMouseMove = useCallback(() => {
|
||||||
if (visibilityValue === 'hidden') {
|
if (visibilityValue === 'hidden') {
|
||||||
send({type: 'controls.mouseMove'});
|
send({type: 'controls.mouseMove'});
|
||||||
}
|
}
|
||||||
}, [send, visibilityValue]);
|
armHideTimer();
|
||||||
|
}, [armHideTimer, send, visibilityValue]);
|
||||||
const handleMouseEnter = useCallback(() => {
|
const handleMouseEnter = useCallback(() => {
|
||||||
send({type: 'controls.mouseEnter'});
|
send({type: 'controls.mouseEnter'});
|
||||||
}, [send]);
|
armHideTimer();
|
||||||
|
}, [armHideTimer, send]);
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
clearHideTimer();
|
||||||
send({type: 'controls.mouseLeave', signals});
|
send({type: 'controls.mouseLeave', signals});
|
||||||
}, [send, signals]);
|
}, [clearHideTimer, send, signals]);
|
||||||
const handleTouchStart = useCallback(() => {
|
const handleTouchStart = useCallback(() => {
|
||||||
send({type: 'controls.touchStart', signals});
|
send({type: 'controls.touchStart', signals});
|
||||||
}, [send, signals]);
|
armHideTimer();
|
||||||
|
}, [armHideTimer, send, signals]);
|
||||||
const controlsVisible = selectMediaControlsVisible(snapshot, signals);
|
const controlsVisible = selectMediaControlsVisible(snapshot, signals);
|
||||||
return {
|
return {
|
||||||
controlsVisible,
|
controlsVisible,
|
||||||
|
|||||||
Reference in new issue
Block a user