mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: allow scenes to be paused (#58150)
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
f9d4f45179
commit
458d0ffde9
@@ -105,7 +105,8 @@
|
||||
"update-card": "Update your card",
|
||||
"donate-now": "Donate Now",
|
||||
"confirm-amount": "Confirm amount",
|
||||
"play-scene": "Press Play",
|
||||
"play": "Play Video",
|
||||
"pause": "Pause Video",
|
||||
"closed-caption": "Closed caption",
|
||||
"share-on-x": "Share on X",
|
||||
"share-on-bluesky": "Share on BlueSky",
|
||||
@@ -1171,7 +1172,7 @@
|
||||
"focus-instructions-panel": "Focus Instructions Panel",
|
||||
"navigate-previous": "Navigate To Previous Exercise",
|
||||
"navigate-next": "Navigate To Next Exercise",
|
||||
"play-scene": "Play Scene"
|
||||
"play-video": "Play Video"
|
||||
},
|
||||
"signout": {
|
||||
"heading": "Sign out of your account",
|
||||
|
||||
@@ -6,11 +6,10 @@ function ClosedCaptionsIcon(
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 256 256'
|
||||
viewBox='0 0 576 512'
|
||||
fill={props.fill || 'var(--gray-00)'}
|
||||
>
|
||||
<rect width='256' height='256' fill='none' />
|
||||
<path d='M216,40H40A16.01833,16.01833,0,0,0,24,56V200a16.01833,16.01833,0,0,0,16,16H216a16.01833,16.01833,0,0,0,16-16V56A16.01833,16.01833,0,0,0,216,40ZM96,148a19.85259,19.85259,0,0,0,14.28613-6.00293,7.99956,7.99956,0,0,1,11.42774,11.19727,36,36,0,1,1,0-50.38868,7.99956,7.99956,0,0,1-11.42774,11.19727A20.00012,20.00012,0,1,0,96,148Zm72,0a19.85259,19.85259,0,0,0,14.28613-6.00293,7.99956,7.99956,0,0,1,11.42774,11.19727,36,36,0,1,1,0-50.38868,7.99956,7.99956,0,0,1-11.42774,11.19727A20.00012,20.00012,0,1,0,168,148Z' />
|
||||
<path d='M0 96C0 60.7 28.7 32 64 32l448 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM200 208c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48s21.5-48 48-48zm144 48c0-26.5 21.5-48 48-48c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48z' />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
:root {
|
||||
--theme-color: #0a0a23;
|
||||
--yellow-gold: #ffbf00;
|
||||
--gray-00-translucent: rgba(255, 255, 255, 0.85);
|
||||
--gray-00: #ffffff;
|
||||
--gray-05: #f5f6f7;
|
||||
--gray-10: #dfdfe2;
|
||||
@@ -10,6 +11,7 @@
|
||||
--gray-80: #2a2a40;
|
||||
--gray-85: #1b1b32;
|
||||
--gray-90: #0a0a23;
|
||||
--gray-90-translucent: rgba(10, 10, 35, 0.85);
|
||||
--purple-light: #dbb8ff;
|
||||
--purple-dark: #5a01a7;
|
||||
--yellow-light: #ffc300;
|
||||
@@ -43,6 +45,7 @@
|
||||
}
|
||||
|
||||
.dark-palette {
|
||||
--primary-color-translucent: var(--gray-00-translucent);
|
||||
--primary-color: var(--gray-00);
|
||||
--secondary-color: var(--gray-05);
|
||||
--tertiary-color: var(--gray-10);
|
||||
@@ -51,6 +54,7 @@
|
||||
--tertiary-background: var(--gray-80);
|
||||
--secondary-background: var(--gray-85);
|
||||
--primary-background: var(--gray-90);
|
||||
--primary-background-translucent: var(--gray-90-translucent);
|
||||
--highlight-color: var(--blue-light);
|
||||
--highlight-background: var(--blue-dark);
|
||||
--selection-color: var(--blue-light-translucent);
|
||||
@@ -67,6 +71,7 @@
|
||||
}
|
||||
|
||||
.light-palette {
|
||||
--primary-color-translucent: var(--gray-90-translucent);
|
||||
--primary-color: var(--gray-90);
|
||||
--secondary-color: var(--gray-85);
|
||||
--tertiary-color: var(--gray-80);
|
||||
@@ -75,6 +80,7 @@
|
||||
--tertiary-background: var(--gray-10);
|
||||
--secondary-background: var(--gray-05);
|
||||
--primary-background: var(--gray-00);
|
||||
--primary-background-translucent: var(--gray-00-translucent);
|
||||
--highlight-color: var(--blue-dark);
|
||||
--highlight-background: var(--blue-light);
|
||||
--selection-color: var(--blue-dark-translucent);
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Character({
|
||||
const [mouthIsOpen, setMouthIsOpen] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
const onNotify = (eventType: 'play' | 'stop') => {
|
||||
const onNotify = (eventType: 'play' | 'pause' | 'stop') => {
|
||||
if (eventType === 'play') {
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ const domain =
|
||||
'https://cdn.freecodecamp.org/curriculum/english/animation-assets';
|
||||
|
||||
export const sounds = `${domain}/sounds`;
|
||||
export const images = `${domain}/images`;
|
||||
const images = `${domain}/images`;
|
||||
export const backgrounds = `${images}/backgrounds`;
|
||||
export const characters = `${images}/characters`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type Observer = (eventType: 'play' | 'stop') => void;
|
||||
type Observer = (eventType: 'play' | 'pause' | 'stop') => void;
|
||||
|
||||
export class SceneSubject {
|
||||
#observers: Observer[];
|
||||
@@ -16,7 +16,7 @@ export class SceneSubject {
|
||||
|
||||
// For now, we don't need to pass any data to the observers, so notify()
|
||||
// doesn't take any arguments.
|
||||
notify(eventType: 'play' | 'stop') {
|
||||
notify(eventType: 'play' | 'pause' | 'stop') {
|
||||
this.#observers.forEach(observer => observer(eventType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,60 +4,24 @@
|
||||
color: var(--gray-00);
|
||||
}
|
||||
|
||||
.scene-start-screen {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
background-color: rgba(10, 10, 35, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: var(--gray-00);
|
||||
}
|
||||
|
||||
.scene-start-btn,
|
||||
.scene-start-btn:hover,
|
||||
.scene-start-btn:focus,
|
||||
.scene-start-btn:active {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
scale: 0.75;
|
||||
}
|
||||
|
||||
.scene-a11y-btn {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scene-play-btn img {
|
||||
transform: translate(5px, 10px);
|
||||
}
|
||||
|
||||
.scene-a11y-btn svg {
|
||||
width: calc(50px + 3vw);
|
||||
height: calc(50px + 3vw);
|
||||
}
|
||||
|
||||
.scene-dialogue-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
padding: calc(5px + 0.25vw) calc(5px + 4vw);
|
||||
background: rgba(10, 10, 35, 0.9);
|
||||
font-size: calc(0.25vw + 0.75rem);
|
||||
width: 100%;
|
||||
padding: 5px calc(5px + 3.5vw);
|
||||
font-size: calc(0.25vw + 0.75rem);
|
||||
min-height: calc(35px + 1vw + 2rem);
|
||||
flex: 0 0 80%;
|
||||
background: var(--primary-background-translucent);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.scene-dialogue-label {
|
||||
color: var(--blue-light);
|
||||
color: var(--highlight-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.scene-dialogue-align-left {
|
||||
@@ -74,5 +38,34 @@
|
||||
|
||||
.scene-dialogue-text {
|
||||
font-size: calc(0.25vw + 1rem);
|
||||
padding: 5px 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.scene-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-self: center;
|
||||
background: var(--primary-background);
|
||||
padding: 0 10px;
|
||||
min-height: 60px;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.scene-btn,
|
||||
.scene-btn:hover,
|
||||
.scene-btn:focus,
|
||||
.scene-btn:active {
|
||||
color: var(--tertiary-color);
|
||||
background: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
scale: 0.75;
|
||||
}
|
||||
|
||||
.scene-play-btn {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.scene-a11y-btn svg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import React, {
|
||||
useCallback
|
||||
} from 'react';
|
||||
import { Col, Spacer } from '@freecodecamp/ui';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCirclePause, faCirclePlay } from '@fortawesome/free-solid-svg-icons';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FullScene } from '../../../../redux/prop-types';
|
||||
import { Loader } from '../../../../components/helpers';
|
||||
import ClosedCaptionsIcon from '../../../../assets/icons/closedcaptions';
|
||||
import { sounds, images, backgrounds, characterAssets } from './scene-assets';
|
||||
import { sounds, backgrounds, characterAssets } from './scene-assets';
|
||||
import Character from './character';
|
||||
import { SceneSubject } from './scene-subject';
|
||||
|
||||
@@ -46,6 +48,27 @@ export function Scene({
|
||||
? sToMs(audio.finishTimestamp - audio.startTimestamp)
|
||||
: Infinity;
|
||||
|
||||
const pauseAudio = () => {
|
||||
// Until the play() promise resolves, we can't pause the audio
|
||||
if (canPauseRef.current) audioRef.current.pause();
|
||||
canPauseRef.current = false;
|
||||
clearTimeout(startTimerRef.current);
|
||||
clearTimeout(finishTimerRef.current);
|
||||
};
|
||||
|
||||
const pauseAnimation = () => {
|
||||
setIsPlaying(false);
|
||||
// @ts-expect-error cancelAnimationFrame accepts undefined, but TS doesn't
|
||||
// know that
|
||||
window.cancelAnimationFrame(animationRef.current);
|
||||
};
|
||||
|
||||
const resetAudio = useCallback(() => {
|
||||
pauseAudio();
|
||||
audioRef.current.currentTime = audio.startTimestamp || 0;
|
||||
pausedAtRef.current = 0;
|
||||
}, [audio.startTimestamp]);
|
||||
|
||||
// on mount
|
||||
useEffect(() => {
|
||||
const { current } = audioRef;
|
||||
@@ -77,17 +100,14 @@ export function Scene({
|
||||
|
||||
// on unmount
|
||||
return () => {
|
||||
if (current) {
|
||||
current.pause();
|
||||
current.currentTime = 0;
|
||||
current.removeEventListener('canplaythrough', audioLoaded);
|
||||
}
|
||||
resetAudio();
|
||||
current.removeEventListener('canplaythrough', audioLoaded);
|
||||
};
|
||||
}, [audioRef, duration, setup, commands]);
|
||||
}, [duration, setup, commands, resetAudio]);
|
||||
|
||||
const initBackground = setup.background;
|
||||
|
||||
// The charactesr are memoized to prevent the useEffect from running on every
|
||||
// The characters are memoized to prevent the useEffect from running on every
|
||||
// render,
|
||||
const initCharacters = useMemo(
|
||||
() =>
|
||||
@@ -103,12 +123,12 @@ export function Scene({
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [sceneIsReady, setSceneIsReady] = useState(false);
|
||||
const [showDialogue, setShowDialogue] = useState(false);
|
||||
const [accessibilityOn, setAccessibilityOn] = useState(false);
|
||||
const [characters, setCharacters] = useState(initCharacters);
|
||||
const [dialogue, setDialogue] = useState(initDialogue);
|
||||
const [background, setBackground] = useState(initBackground);
|
||||
const startRef = useRef<number>(0);
|
||||
const startClocktimeRef = useRef<number>(0);
|
||||
const pausedAtRef = useRef<number>(0);
|
||||
const startTimerRef = useRef<number>();
|
||||
const finishTimerRef = useRef<number>();
|
||||
const animationRef = useRef<number>();
|
||||
@@ -147,15 +167,10 @@ export function Scene({
|
||||
setSceneIsReady(true);
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
// Until the play() promise resolves, we can't pause the audio
|
||||
if (canPauseRef.current) audioRef.current.pause();
|
||||
canPauseRef.current = false;
|
||||
};
|
||||
|
||||
const handlePlay = useCallback(() => {
|
||||
const pausedAt = pausedAtRef.current;
|
||||
const updateCurrentTime = () => {
|
||||
const time = Date.now() - startRef.current;
|
||||
const time = Date.now() - startClocktimeRef.current;
|
||||
setCurrentTime(time);
|
||||
|
||||
if (isPlayingSceneRef.current) {
|
||||
@@ -168,75 +183,112 @@ export function Scene({
|
||||
if (isPlaying || !sceneIsReady) return;
|
||||
setIsPlaying(true);
|
||||
isPlayingSceneRef.current = true;
|
||||
startRef.current = Date.now();
|
||||
setShowDialogue(true);
|
||||
|
||||
// when we paused, the startRef was the clock time when we started and
|
||||
// pausedAt was the currentTime (i.e. how long we've been playing). That
|
||||
// means to resume we need to set the startRef to the current time minus
|
||||
// the time we've already played.
|
||||
startClocktimeRef.current = Date.now() - pausedAt;
|
||||
updateCurrentTime();
|
||||
|
||||
const audioStartDelay = sToMs(audio.startTime) - pausedAt;
|
||||
|
||||
// @ts-expect-error it's not a node timer
|
||||
startTimerRef.current = setTimeout(() => {
|
||||
if (audioRef.current.paused) {
|
||||
void audioRef.current.play().then(() => {
|
||||
canPauseRef.current = true;
|
||||
|
||||
// If the duration is Infinity, that means the duration is simply the
|
||||
// length of the file. However we need to actively stop the audio to
|
||||
// ensure that cleanup (i.e. resetAudio is called) )
|
||||
const effectiveDuration =
|
||||
duration === Infinity ? sToMs(audioRef.current.duration) : duration;
|
||||
|
||||
// If the delay is positive, the setTimeout will have already waited
|
||||
// that amount of time. However, if it's negative, then the setTimeout
|
||||
// has no delay and we need to account for that when calculating how
|
||||
// much audio is left to play.
|
||||
const effectiveStartDelay = Math.min(0, audioStartDelay);
|
||||
const audioEndDelay = effectiveDuration + effectiveStartDelay;
|
||||
|
||||
if (audioEndDelay < 0) {
|
||||
resetAudio();
|
||||
return;
|
||||
}
|
||||
// @ts-expect-error it's not a node timer
|
||||
finishTimerRef.current = setTimeout(() => {
|
||||
const endTimeStamp = sToMs(audio.finishTimestamp!); // it exists because duration is not Infinity
|
||||
const audioCurrentTime = sToMs(audioRef.current.currentTime);
|
||||
const remainingTime = endTimeStamp - audioCurrentTime;
|
||||
// For some reason, despite the setTimeout resolving at the right
|
||||
// time, the currentTime can be smaller than expected. That means
|
||||
// that if we pause now it will cut off the last part.
|
||||
if (remainingTime < 100) {
|
||||
// 100ms is arbitrary and may need to be adjusted if people still
|
||||
// notice the cut off
|
||||
|
||||
resetAudio();
|
||||
} else {
|
||||
// @ts-expect-error it's not a node timer
|
||||
finishTimerRef.current = setTimeout(() => {
|
||||
resetAudio();
|
||||
}, remainingTime);
|
||||
}
|
||||
}, audioEndDelay);
|
||||
});
|
||||
}
|
||||
}, sToMs(audio.startTime));
|
||||
}, audioStartDelay);
|
||||
}, [audio, duration, isPlaying, resetAudio, sceneIsReady]);
|
||||
|
||||
// @ts-expect-error it's not a node timer
|
||||
finishTimerRef.current = setTimeout(
|
||||
() => {
|
||||
if (duration !== Infinity) {
|
||||
const endTimeStamp = sToMs(audio.finishTimestamp!); // it exists because duration is not Infinity
|
||||
const audioCurrentTime = sToMs(audioRef.current.currentTime);
|
||||
const remainingTime = endTimeStamp - audioCurrentTime;
|
||||
// For some reason, despite the setTimeout resolving at the right
|
||||
// time, the currentTime can be smaller than expected. That means
|
||||
// that if we pause now it will cut off the last part.
|
||||
if (remainingTime < 100) {
|
||||
// 100ms is arbitrary and may need to be adjusted if people still
|
||||
// notice the cut off
|
||||
|
||||
pause();
|
||||
} else {
|
||||
// @ts-expect-error it's not a node timer
|
||||
finishTimerRef.current = setTimeout(() => {
|
||||
pause();
|
||||
}, remainingTime);
|
||||
}
|
||||
}
|
||||
},
|
||||
duration + sToMs(audio.startTime)
|
||||
);
|
||||
}, [audio, duration, isPlaying, sceneIsReady]);
|
||||
const handlePause = useCallback(() => {
|
||||
isPlayingSceneRef.current = false;
|
||||
pausedAtRef.current = currentTime;
|
||||
pauseAudio();
|
||||
pauseAnimation();
|
||||
}, [currentTime]);
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
usedCommandsRef.current.clear();
|
||||
pause();
|
||||
pauseAudio();
|
||||
pauseAnimation();
|
||||
audioRef.current.currentTime = audio.startTimestamp || 0;
|
||||
setCurrentTime(0);
|
||||
setIsPlaying(false);
|
||||
isPlayingSceneRef.current = false;
|
||||
setShowDialogue(false);
|
||||
setDialogue(initDialogue);
|
||||
setCharacters(initCharacters);
|
||||
setBackground(initBackground);
|
||||
}, [audio, initCharacters, initBackground]);
|
||||
|
||||
const resetAnimation = useCallback(() => {
|
||||
usedCommandsRef.current.clear();
|
||||
startClocktimeRef.current = 0;
|
||||
setCurrentTime(0);
|
||||
setDialogue(initDialogue);
|
||||
setCharacters(initCharacters);
|
||||
setBackground(initBackground);
|
||||
}, [initCharacters, initBackground]);
|
||||
|
||||
const resetScene = () => {
|
||||
setIsPlaying(false);
|
||||
isPlayingSceneRef.current = false;
|
||||
pausedAtRef.current = 0;
|
||||
};
|
||||
|
||||
const onNotify = useCallback(
|
||||
(eventType: 'play' | 'stop') => {
|
||||
(eventType: 'play' | 'pause' | 'stop') => {
|
||||
if (eventType === 'play') {
|
||||
handlePlay();
|
||||
} else if (eventType === 'pause') {
|
||||
handlePause();
|
||||
} else {
|
||||
handleStop();
|
||||
}
|
||||
},
|
||||
[handlePlay, handleStop]
|
||||
[handlePlay, handlePause, handleStop]
|
||||
);
|
||||
|
||||
const resetScene = useCallback(() => {
|
||||
sceneSubject.notify('stop');
|
||||
}, [sceneSubject]);
|
||||
|
||||
useEffect(() => {
|
||||
sceneSubject.attach(onNotify);
|
||||
return () => {
|
||||
@@ -279,10 +331,13 @@ export function Scene({
|
||||
}
|
||||
});
|
||||
|
||||
// resetScene only works if called AFTER the commands, otherwise the
|
||||
// commands will undo the reset.
|
||||
if (currentTime >= resetTime) resetScene();
|
||||
}, [currentTime, resetTime, sortedCommands, resetScene]);
|
||||
if (currentTime >= resetTime) {
|
||||
// resetAnimation only works if called AFTER the commands, otherwise the
|
||||
// commands will undo the reset.
|
||||
resetAnimation();
|
||||
resetScene();
|
||||
}
|
||||
}, [currentTime, resetTime, sortedCommands, resetAnimation]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -321,14 +376,14 @@ export function Scene({
|
||||
name={character}
|
||||
position={position}
|
||||
opacity={opacity}
|
||||
isTalking={isPlaying && isTalking}
|
||||
sceneSubject={sceneSubject}
|
||||
isTalking={isTalking}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
{showDialogue && (alwaysShowDialogue || accessibilityOn) && (
|
||||
{(alwaysShowDialogue || accessibilityOn) && (
|
||||
<div
|
||||
className={`scene-dialogue-wrap ${
|
||||
dialogue.align ? `scene-dialogue-align-${dialogue.align}` : ''
|
||||
@@ -338,38 +393,45 @@ export function Scene({
|
||||
<div className='scene-dialogue-text'>{dialogue.text}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPlaying && (
|
||||
<div className='scene-start-screen'>
|
||||
<button
|
||||
className='scene-start-btn scene-play-btn'
|
||||
onClick={() => sceneSubject.notify('play')}
|
||||
>
|
||||
<img
|
||||
src={`${images}/play-button.png`}
|
||||
alt={t('buttons.play-scene')}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!alwaysShowDialogue && (
|
||||
<button
|
||||
className='scene-start-btn scene-a11y-btn'
|
||||
aria-label={t('buttons.closed-caption')}
|
||||
aria-pressed={accessibilityOn}
|
||||
onClick={() => setAccessibilityOn(!accessibilityOn)}
|
||||
>
|
||||
<ClosedCaptionsIcon
|
||||
fill={
|
||||
accessibilityOn ? 'var(--gray-00)' : 'var(--gray-15)'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='scene-controls'>
|
||||
<button
|
||||
className='scene-btn scene-play-btn'
|
||||
onClick={() => sceneSubject.notify(isPlaying ? 'pause' : 'play')}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<>
|
||||
<span className='sr-only'>{t('buttons.pause')}</span>
|
||||
<FontAwesomeIcon icon={faCirclePause} size='3x' />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className='sr-only'>{t('buttons.play')}</span>
|
||||
<FontAwesomeIcon icon={faCirclePlay} size='3x' />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{alwaysShowDialogue ? (
|
||||
<div className='scene-a11y-btn'></div>
|
||||
) : (
|
||||
<button
|
||||
className='scene-btn scene-a11y-btn'
|
||||
aria-label={t('buttons.closed-caption')}
|
||||
aria-pressed={accessibilityOn}
|
||||
onClick={() => setAccessibilityOn(!accessibilityOn)}
|
||||
>
|
||||
<ClosedCaptionsIcon
|
||||
fill={
|
||||
accessibilityOn ? 'var(--tertiary-color)' : 'var(--gray-45)'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Spacer size='m' />
|
||||
</Col>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ function ShortcutsModal({
|
||||
<td>CTRL/Command + Enter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{t('shortcuts.play-scene')}</td>
|
||||
<td>{t('shortcuts.play-video')}</td>
|
||||
<td>CTRL + Space</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
Reference in New Issue
Block a user