mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
Add audio preview in the resources editor and when editing actions (#8219)
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user