Add audio preview in the resources editor and when editing actions (#8219)

This commit is contained in:
Aurélien Vivet
2026-02-09 12:50:25 +01:00
committed by GitHub
parent bca03bd376
commit e71ae983d2
5 changed files with 238 additions and 4 deletions
@@ -0,0 +1,38 @@
// @flow
import * as React from 'react';
import SoundPlayer from '../../UI/SoundPlayer';
import GenericIconPreview from './GenericIconPreview';
import Music from '../../UI/CustomSvgIcons/Music';
type Props = {|
resourceName: string,
audioResourceSource: string,
|};
const AudioPreview = ({ resourceName, audioResourceSource }: Props) => {
const [hasError, setHasError] = React.useState(false);
const onSoundLoaded = React.useCallback(() => {
setHasError(false);
}, []);
const onSoundError = React.useCallback(() => {
setHasError(true);
}, []);
if (hasError) {
return <GenericIconPreview renderIcon={props => <Music {...props} />} />;
}
return (
<SoundPlayer
soundSrc={audioResourceSource || null}
onSoundLoaded={onSoundLoaded}
onSoundError={onSoundError}
title={resourceName}
subtitle={null}
/>
);
};
export default AudioPreview;
@@ -6,8 +6,8 @@ import GenericIconPreview from './GenericIconPreview';
import FontDownload from '@material-ui/icons/FontDownload';
import File from '../../UI/CustomSvgIcons/File';
import Video from '../../UI/CustomSvgIcons/Video';
import Music from '../../UI/CustomSvgIcons/Music';
import Model3DPreview from './Model3DPreview';
import AudioPreview from './AudioPreview';
type Props = {|
project: gdProject,
@@ -42,7 +42,16 @@ const ResourcePreview = (props: Props) => {
/>
);
case 'audio':
return <GenericIconPreview renderIcon={props => <Music {...props} />} />;
return (
<AudioPreview
resourceName={resourceName}
audioResourceSource={props.resourcesLoader.getResourceFullUrl(
project,
resourceName,
{}
)}
/>
);
case 'model3D':
return (
<Model3DPreview
@@ -30,6 +30,7 @@ import { ExternalEditorOpenedDialog } from '../UI/ExternalEditorOpenedDialog';
import Add from '../UI/CustomSvgIcons/Add';
import Edit from '../UI/CustomSvgIcons/Edit';
import Cross from '../UI/CustomSvgIcons/Cross';
import CompactSoundPlayer from '../UI/SoundPlayer/CompactSoundPlayer';
import useResourcesChangedWatcher from './UseResourcesChangedWatcher';
import useForceUpdate from '../Utils/UseForceUpdate';
import useAlertDialog from '../UI/Alert/useAlertDialog';
@@ -505,6 +506,15 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
)}
</LineStackLayout>
</Column>
{props.resourceKind === 'audio' && resourceName && (
<CompactSoundPlayer
soundSrc={props.resourcesLoader.getResourceFullUrl(
props.project,
resourceName,
{}
)}
/>
)}
<RaisedButton
label={
resourceName ? (
@@ -0,0 +1,151 @@
// @flow
import * as React from 'react';
import IconButton from '@material-ui/core/IconButton';
import CircularProgress from '@material-ui/core/CircularProgress';
import Play from '../CustomSvgIcons/Play';
import Pause from '../CustomSvgIcons/Pause';
import { useIsMounted } from '../../Utils/UseIsMounted';
type Props = {|
soundSrc: string | null,
onSoundLoaded?: () => void,
onSoundError?: () => void,
|};
const CompactSoundPlayer = ({
soundSrc,
onSoundLoaded,
onSoundError,
}: Props) => {
const audioRef = React.useRef<?HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
const [isAudioLoaded, setIsAudioLoaded] = React.useState(false);
const isMountedRef = useIsMounted();
const abortControllerRef = React.useRef<?AbortController>(null);
const cleanupAudioAndListeners = React.useCallback(() => {
const audio = audioRef.current;
if (!audio) return;
audio.pause();
if (abortControllerRef.current) abortControllerRef.current.abort();
audio.src = '';
audioRef.current = null;
abortControllerRef.current = null;
setIsPlaying(false);
setIsLoading(false);
setIsAudioLoaded(false);
}, []);
const initializeAudio = React.useCallback(
() => {
if (!soundSrc || audioRef.current) return;
setIsLoading(true);
const audio = new Audio(soundSrc);
audio.loop = false;
const abortController = new AbortController();
abortControllerRef.current = abortController;
const handleLoadedMetadata = () => {
if (!isMountedRef.current) return;
setIsLoading(false);
setIsAudioLoaded(true);
if (onSoundLoaded) onSoundLoaded();
};
const handleError = () => {
if (!isMountedRef.current) return;
setIsLoading(false);
setIsPlaying(false);
setIsAudioLoaded(false);
if (onSoundError) onSoundError();
};
const handleEnded = () => {
if (!isMountedRef.current) return;
setIsPlaying(false);
};
audio.addEventListener('loadedmetadata', handleLoadedMetadata, {
signal: abortController.signal,
});
audio.addEventListener('error', handleError, {
signal: abortController.signal,
});
audio.addEventListener('ended', handleEnded, {
signal: abortController.signal,
});
audioRef.current = audio;
audio.play().catch(() => {
if (isMountedRef.current) {
setIsPlaying(false);
}
});
},
[soundSrc, onSoundLoaded, onSoundError]
);
const onPlayPause = React.useCallback(
() => {
if (!soundSrc) return;
if (!isAudioLoaded && !isLoading) {
setIsPlaying(true);
initializeAudio();
} else if (isAudioLoaded) {
setIsPlaying(_isPlaying => !_isPlaying);
}
},
[soundSrc, isAudioLoaded, isLoading, initializeAudio]
);
React.useEffect(
() => {
if (!audioRef.current || !isAudioLoaded) return;
if (isPlaying) {
audioRef.current.play().catch(() => {
setIsPlaying(false);
});
} else {
audioRef.current.pause();
}
},
[isPlaying, isAudioLoaded]
);
React.useEffect(
() => {
return () => {
cleanupAudioAndListeners();
};
},
[soundSrc, cleanupAudioAndListeners]
);
if (!soundSrc) {
return null;
}
if (isLoading) {
return (
<IconButton size="small" disabled>
<CircularProgress size={24} />
</IconButton>
);
}
return (
<IconButton size="small" onClick={onPlayPause}>
{isPlaying ? <Pause /> : <Play />}
</IconButton>
);
};
export default CompactSoundPlayer;
+28 -2
View File
@@ -52,6 +52,7 @@ type Props = {|
title: string | null,
subtitle: React.Node,
onSoundLoaded: () => void,
onSoundError?: () => void,
onSkipForward?: () => void,
onSkipBack?: () => void,
|};
@@ -62,7 +63,15 @@ export type SoundPlayerInterface = {|
const SoundPlayer = React.forwardRef<Props, SoundPlayerInterface>(
(
{ soundSrc, onSoundLoaded, onSkipBack, onSkipForward, title, subtitle },
{
soundSrc,
onSoundLoaded,
onSoundError,
onSkipBack,
onSkipForward,
title,
subtitle,
},
ref
) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
@@ -119,6 +128,13 @@ const SoundPlayer = React.forwardRef<Props, SoundPlayerInterface>(
setIsPlaying(false);
}, []);
const onError = React.useCallback(
() => {
if (onSoundError) onSoundError();
},
[onSoundError]
);
React.useEffect(
() => {
if (!waveSurferRef.current && !mobileAudioRef.current) return;
@@ -175,6 +191,7 @@ const SoundPlayer = React.forwardRef<Props, SoundPlayerInterface>(
audio.addEventListener('ended', onFinishPlaying);
audio.addEventListener('loadstart', onLoad);
audio.addEventListener('loadedmetadata', onAudioReady);
audio.addEventListener('error', onError);
mobileAudioRef.current = audio;
}
waveSurferRef.current = null;
@@ -183,7 +200,15 @@ const SoundPlayer = React.forwardRef<Props, SoundPlayerInterface>(
}
onLoad();
},
[isMobile, soundSrc, onTimeupdate, onFinishPlaying, onLoad, onAudioReady]
[
isMobile,
soundSrc,
onTimeupdate,
onFinishPlaying,
onLoad,
onAudioReady,
onError,
]
);
return (
@@ -250,6 +275,7 @@ const SoundPlayer = React.forwardRef<Props, SoundPlayerInterface>(
onTimeupdate={onTimeupdate}
onLoad={onLoad}
onFinish={onFinishPlaying}
onError={onError}
/>
</div>
)}