mirror of
https://github.com/4ian/GDevelop.git
synced 2026-10-12 02:01:17 +00:00
Rework the Asset Store details page to show a preview of the animation(s) of the object (#3930)
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "GDCore/Extensions/Builtin/SpriteExtension/Direction.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Extensions/Builtin/SpriteExtension/Sprite.h"
|
||||
#include "GDCore/Serialization/SerializerElement.h"
|
||||
@@ -29,6 +31,15 @@ const Sprite& Direction::GetSprite(std::size_t nb) const { return sprites[nb]; }
|
||||
|
||||
Sprite& Direction::GetSprite(std::size_t nb) { return sprites[nb]; }
|
||||
|
||||
const std::vector<gd::String>& Direction::GetSpriteNames() const {
|
||||
static std::vector<gd::String> spriteNames;
|
||||
spriteNames.clear();
|
||||
for (std::size_t i = 0; i < sprites.size(); ++i) {
|
||||
spriteNames.push_back(sprites[i].GetImageName());
|
||||
}
|
||||
return spriteNames;
|
||||
}
|
||||
|
||||
void Direction::RemoveSprite(std::size_t index) {
|
||||
if (index < sprites.size()) sprites.erase(sprites.begin() + index);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,13 @@ class GD_CORE_API Direction {
|
||||
*/
|
||||
Sprite& GetSprite(std::size_t nb);
|
||||
|
||||
/**
|
||||
* \brief Return a vector of references to sprite names.
|
||||
*
|
||||
* \return A vector of all sprite names references.
|
||||
*/
|
||||
const std::vector<gd::String>& GetSpriteNames() const;
|
||||
|
||||
/**
|
||||
* \brief Check if the direction contains sprites.
|
||||
*
|
||||
|
||||
@@ -2507,6 +2507,7 @@ interface Direction {
|
||||
boolean IsLooping();
|
||||
void SetLoop(boolean enable);
|
||||
float GetTimeBetweenFrames();
|
||||
[Const, Ref] VectorString GetSpriteNames();
|
||||
void SetTimeBetweenFrames(float time);
|
||||
void SwapSprites(unsigned long first, unsigned long second);
|
||||
void MoveSprite(unsigned long oldIndex, unsigned long newIndex);
|
||||
|
||||
@@ -10,6 +10,7 @@ declare class gdDirection {
|
||||
isLooping(): boolean;
|
||||
setLoop(enable: boolean): void;
|
||||
getTimeBetweenFrames(): number;
|
||||
getSpriteNames(): gdVectorString;
|
||||
setTimeBetweenFrames(time: number): void;
|
||||
swapSprites(first: number, second: number): void;
|
||||
moveSprite(oldIndex: number, newIndex: number): void;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { Column } from '../UI/Grid';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import Chip from '@material-ui/core/Chip';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
@@ -9,47 +9,41 @@ import {
|
||||
type AssetShortHeader,
|
||||
type Asset,
|
||||
type Author,
|
||||
type ObjectAsset,
|
||||
getAsset,
|
||||
isPixelArt,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { getPixelatedImageRendering } from '../Utils/CssHelpers';
|
||||
import LeftLoader from '../UI/LeftLoader';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource';
|
||||
import { type ResourceSource } from '../ResourcesList/ResourceSource';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
import { ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import { ResponsiveWindowMeasurer } from '../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import Window from '../Utils/Window';
|
||||
import CheckeredBackground from '../ResourcesList/CheckeredBackground';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import ArrowBackIos from '@material-ui/icons/ArrowBackIos';
|
||||
import ArrowForwardIos from '@material-ui/icons/ArrowForwardIos';
|
||||
import ThemeContext from '../UI/Theme/ThemeContext';
|
||||
import AnimationPreview from '../ObjectEditor/Editors/SpriteEditor/AnimationPreview';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
|
||||
const FIXED_HEIGHT = 250;
|
||||
|
||||
const styles = {
|
||||
previewImagePixelated: {
|
||||
width: '100%',
|
||||
imageRendering: getPixelatedImageRendering(),
|
||||
padding: 15,
|
||||
},
|
||||
previewBackground: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
verticalPreviewBackground: {
|
||||
width: 250,
|
||||
},
|
||||
horizontalPreviewBackground: {
|
||||
height: 170,
|
||||
width: 300,
|
||||
maxHeight: FIXED_HEIGHT,
|
||||
},
|
||||
chip: {
|
||||
marginBottom: 2,
|
||||
@@ -61,17 +55,26 @@ const styles = {
|
||||
maxHeight: '100%',
|
||||
verticalAlign: 'middle',
|
||||
pointerEvents: 'none',
|
||||
objectFit: 'contain',
|
||||
},
|
||||
arrowContainer: {
|
||||
padding: 6,
|
||||
},
|
||||
// The left arrow SVG icon from Material-UI is not centered.
|
||||
leftArrowSvg: {
|
||||
transform: 'translateX(5px)',
|
||||
},
|
||||
};
|
||||
|
||||
const makeFirstLetterUppercase = (str: string) =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
layout: ?gdLayout,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
onTagSelection: (tag: string) => void,
|
||||
|
||||
assetShortHeader: AssetShortHeader,
|
||||
onAdd: () => void,
|
||||
@@ -80,21 +83,39 @@ type Props = {|
|
||||
isBeingInstalled: boolean,
|
||||
|};
|
||||
|
||||
const getObjectAssetResourcesByName = (
|
||||
objectAsset: ObjectAsset
|
||||
): { [string]: any /*(serialized gdResource)*/ } => {
|
||||
const resourcesByName = {};
|
||||
|
||||
objectAsset.resources.forEach(resource => {
|
||||
resourcesByName[resource.name] = resource;
|
||||
});
|
||||
|
||||
return resourcesByName;
|
||||
};
|
||||
|
||||
export const AssetDetails = ({
|
||||
project,
|
||||
layout,
|
||||
objectsContainer,
|
||||
resourceSources,
|
||||
onChooseResource,
|
||||
resourceExternalEditors,
|
||||
onTagSelection,
|
||||
assetShortHeader,
|
||||
onAdd,
|
||||
onClose,
|
||||
canInstall,
|
||||
isBeingInstalled,
|
||||
}: Props) => {
|
||||
const gdevelopTheme = React.useContext(ThemeContext);
|
||||
const { authors, licenses } = React.useContext(AssetStoreContext);
|
||||
const [asset, setAsset] = React.useState<?Asset>(null);
|
||||
const [isAssetAdded, setIsAssetAdded] = React.useState<boolean>(false);
|
||||
const [
|
||||
selectedAnimationName,
|
||||
setSelectedAnimationName,
|
||||
] = React.useState<?string>(null);
|
||||
const [error, setError] = React.useState<?Error>(null);
|
||||
const loadAsset = React.useCallback(
|
||||
() => {
|
||||
@@ -102,8 +123,14 @@ export const AssetDetails = ({
|
||||
try {
|
||||
const loadedAsset = await getAsset(assetShortHeader);
|
||||
setAsset(loadedAsset);
|
||||
if (loadedAsset.objectType === 'sprite') {
|
||||
// Only sprites have animations and we select the first one.
|
||||
const firstAnimationName =
|
||||
loadedAsset.objectAssets[0].object.animations[0].name;
|
||||
setSelectedAnimationName(firstAnimationName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error while loading asset:', error);
|
||||
console.error('Error while loading asset:', error);
|
||||
setError(error);
|
||||
}
|
||||
})();
|
||||
@@ -111,6 +138,11 @@ export const AssetDetails = ({
|
||||
[assetShortHeader]
|
||||
);
|
||||
|
||||
const isImageResourceSmooth = React.useMemo(
|
||||
() => !isPixelArt(assetShortHeader),
|
||||
[assetShortHeader]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
loadAsset();
|
||||
@@ -118,12 +150,14 @@ export const AssetDetails = ({
|
||||
[loadAsset]
|
||||
);
|
||||
|
||||
const canAddAsset = canInstall && !isBeingInstalled && !!asset;
|
||||
const canAddAsset =
|
||||
canInstall && !isBeingInstalled && !!asset && !isAssetAdded;
|
||||
const onAddAsset = React.useCallback(
|
||||
() => {
|
||||
if (canAddAsset) onAdd();
|
||||
setIsAssetAdded(true);
|
||||
},
|
||||
[onAdd, canAddAsset]
|
||||
[onAdd, canAddAsset, setIsAssetAdded]
|
||||
);
|
||||
|
||||
const assetAuthors: ?Array<Author> =
|
||||
@@ -139,79 +173,36 @@ export const AssetDetails = ({
|
||||
? licenses.find(({ name }) => name === asset.license)
|
||||
: null;
|
||||
|
||||
// For sprite animations.
|
||||
const assetResources =
|
||||
asset && asset.objectAssets[0]
|
||||
? getObjectAssetResourcesByName(asset.objectAssets[0])
|
||||
: {};
|
||||
const assetAnimations = asset
|
||||
? asset.objectAssets[0].object.animations
|
||||
: null;
|
||||
const animation = assetAnimations
|
||||
? assetAnimations.find(({ name }) => name === selectedAnimationName)
|
||||
: null;
|
||||
const direction = animation ? animation.directions[0] : null;
|
||||
const animationResources =
|
||||
asset && direction
|
||||
? direction.sprites.map(sprite => assetResources[sprite.image])
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={<Trans>Add an object from the store</Trans>}
|
||||
onRequestClose={onClose}
|
||||
actions={[
|
||||
<FlatButton key="back" label={<Trans>Back</Trans>} onClick={onClose} />,
|
||||
<LeftLoader
|
||||
isLoading={isBeingInstalled || (!asset && !error)}
|
||||
key="install"
|
||||
>
|
||||
<RaisedButton
|
||||
primary
|
||||
icon={<Add />}
|
||||
label={<Trans>Add to the game</Trans>}
|
||||
onClick={onAddAsset}
|
||||
disabled={!canAddAsset}
|
||||
id="add-asset-button"
|
||||
/>
|
||||
</LeftLoader>,
|
||||
]}
|
||||
onApply={onAddAsset}
|
||||
id={'asset-details-dialog'}
|
||||
>
|
||||
<ScrollView>
|
||||
<Column expand noMargin>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<div
|
||||
style={{
|
||||
...styles.previewBackground,
|
||||
...(windowWidth === 'small'
|
||||
? styles.horizontalPreviewBackground
|
||||
: styles.verticalPreviewBackground),
|
||||
}}
|
||||
>
|
||||
<CheckeredBackground />
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
...(isPixelArt(assetShortHeader)
|
||||
? styles.previewImagePixelated
|
||||
: undefined),
|
||||
}}
|
||||
src={assetShortHeader.previewImageUrls[0]}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
<Column expand>
|
||||
<div>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Column>
|
||||
<Line alignItems="center" noMargin>
|
||||
<Text size="title" displayInlineAsSpan>
|
||||
{assetShortHeader.name}
|
||||
</Text>{' '}
|
||||
{assetShortHeader.shortDescription && (
|
||||
<React.Fragment>
|
||||
-{' '}
|
||||
<Text size="body" displayInlineAsSpan>
|
||||
{assetShortHeader.shortDescription}
|
||||
</Text>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
<span>
|
||||
{assetShortHeader.tags.map(tag => (
|
||||
<Chip size="small" style={styles.chip} label={tag} key={tag} />
|
||||
))}
|
||||
</span>
|
||||
{asset ? (
|
||||
<React.Fragment>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{asset && (
|
||||
<Text size="body">
|
||||
<Trans>By:</Trans>{' '}
|
||||
<Trans>by</Trans>{' '}
|
||||
{!!assetAuthors &&
|
||||
assetAuthors.map(author => {
|
||||
return (
|
||||
@@ -227,10 +218,184 @@ export const AssetDetails = ({
|
||||
);
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</Line>
|
||||
<Line alignItems="center">
|
||||
<div style={{ flexWrap: 'wrap' }}>
|
||||
{assetShortHeader.tags.slice(0, 5).map((tag, index) => (
|
||||
<React.Fragment key={tag}>
|
||||
{index !== 0 && <Spacer />}
|
||||
<Chip
|
||||
size="small"
|
||||
style={styles.chip}
|
||||
label={makeFirstLetterUppercase(tag)}
|
||||
onClick={() => {
|
||||
onTagSelection(tag);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{assetShortHeader.tags.length > 5 && (
|
||||
<>
|
||||
<Spacer />
|
||||
<Chip
|
||||
size="small"
|
||||
style={styles.chip}
|
||||
label={
|
||||
<Trans>
|
||||
+ {assetShortHeader.tags.length - 5} tag(s)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Line>
|
||||
</Column>
|
||||
<Column alignItems="center" justifyContent="center">
|
||||
<LeftLoader
|
||||
isLoading={isBeingInstalled || (!asset && !error)}
|
||||
key="install"
|
||||
>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
isBeingInstalled ? (
|
||||
<Trans>Adding to my project...</Trans>
|
||||
) : isAssetAdded ? (
|
||||
<Trans>Object added to my project</Trans>
|
||||
) : (
|
||||
<Trans>Add object to my project</Trans>
|
||||
)
|
||||
}
|
||||
onClick={onAddAsset}
|
||||
disabled={!canAddAsset}
|
||||
id="add-asset-button"
|
||||
/>
|
||||
</LeftLoader>
|
||||
</Column>
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<Column>
|
||||
<div style={styles.previewBackground}>
|
||||
{asset ? (
|
||||
<>
|
||||
{asset.objectType === 'sprite' &&
|
||||
animationResources &&
|
||||
direction && (
|
||||
<AnimationPreview
|
||||
resourceNames={animationResources.map(
|
||||
({ name }) => name
|
||||
)}
|
||||
getImageResourceSource={(resourceName: string) => {
|
||||
const resource = assetResources[resourceName];
|
||||
return resource ? resource.file : '';
|
||||
}}
|
||||
isImageResourceSmooth={() => isImageResourceSmooth}
|
||||
project={project}
|
||||
timeBetweenFrames={direction.timeBetweenFrames}
|
||||
isLooping // Always loop in the asset store.
|
||||
hideCheckeredBackground
|
||||
hideControls
|
||||
initialZoom={140 / Math.max(asset.width, asset.height)}
|
||||
fixedHeight={FIXED_HEIGHT}
|
||||
/>
|
||||
)}
|
||||
{(asset.objectType === 'tiled' ||
|
||||
asset.objectType === '9patch') && (
|
||||
<CorsAwareImage
|
||||
style={styles.previewImage}
|
||||
src={asset.previewImageUrls[0]}
|
||||
alt={asset.name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</div>
|
||||
{assetAnimations &&
|
||||
assetAnimations.length > 1 &&
|
||||
typeof selectedAnimationName === 'string' && (
|
||||
<Paper
|
||||
elevation={4}
|
||||
variant="outlined"
|
||||
style={{
|
||||
backgroundColor: gdevelopTheme.list.itemsBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<Line justifyContent="center" alignItems="center" noMargin>
|
||||
<div style={styles.arrowContainer}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const previousAnimationIndex = assetAnimations.findIndex(
|
||||
({ name }) => name === selectedAnimationName
|
||||
);
|
||||
const newAnimationIndex =
|
||||
previousAnimationIndex === 0
|
||||
? assetAnimations.length - 1
|
||||
: previousAnimationIndex - 1;
|
||||
setSelectedAnimationName(
|
||||
assetAnimations[newAnimationIndex].name
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowBackIos style={styles.leftArrowSvg} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<SelectField
|
||||
value={selectedAnimationName}
|
||||
onChange={(e, i, newAnimationName: string) => {
|
||||
setSelectedAnimationName(newAnimationName);
|
||||
}}
|
||||
fullWidth
|
||||
textAlign="center"
|
||||
disableUnderline
|
||||
>
|
||||
{assetAnimations.map(animation => (
|
||||
<SelectOption
|
||||
key={animation.name}
|
||||
value={animation.name}
|
||||
primaryText={
|
||||
makeFirstLetterUppercase(animation.name) ||
|
||||
t`Default` // Display default for animations with no name.
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</SelectField>
|
||||
<div style={styles.arrowContainer}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const previousAnimationIndex = assetAnimations.findIndex(
|
||||
({ name }) => name === selectedAnimationName
|
||||
);
|
||||
const newAnimationIndex =
|
||||
previousAnimationIndex ===
|
||||
assetAnimations.length - 1
|
||||
? 0
|
||||
: previousAnimationIndex + 1;
|
||||
setSelectedAnimationName(
|
||||
assetAnimations[newAnimationIndex].name
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIos />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Line>
|
||||
</Paper>
|
||||
)}
|
||||
</Column>
|
||||
<Column expand>
|
||||
{asset ? (
|
||||
<React.Fragment>
|
||||
<Text size="body">
|
||||
{!!assetLicense && (
|
||||
<Trans>
|
||||
License:{' '}
|
||||
Type of License:{' '}
|
||||
{
|
||||
<Link
|
||||
component="button"
|
||||
@@ -259,6 +424,6 @@ export const AssetDetails = ({
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
</Column>
|
||||
</Dialog>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ export const HexColorField = ({
|
||||
floatingLabelFixed
|
||||
helperMarkdownText={helperMarkdownText}
|
||||
type="text"
|
||||
hintText={t`Code like #ff8844`}
|
||||
hintText={t`#ff8844`}
|
||||
value={colorString}
|
||||
onChange={event => handleTextChange(event.target.value)}
|
||||
/>
|
||||
|
||||
@@ -12,25 +12,14 @@ import {
|
||||
} from '../ObjectsList/EnumerateObjects';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
|
||||
import { Tabs, Tab } from '../UI/Tabs';
|
||||
import { AssetStore } from '.';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { installAsset } from './InstallAsset';
|
||||
import { AssetDetails } from './AssetDetails';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
import {
|
||||
sendAssetAddedToProject,
|
||||
sendAssetOpened,
|
||||
sendNewObjectCreated,
|
||||
} from '../Utils/Analytics/EventSender';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import { useResourceFetcher } from '../ProjectsStorage/ResourceFetcher';
|
||||
import { sendNewObjectCreated } from '../Utils/Analytics/EventSender';
|
||||
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
|
||||
@@ -94,11 +83,6 @@ export default function NewObjectDialog({
|
||||
setNewObjectDialogDefaultTab,
|
||||
getNewObjectDialogDefaultTab,
|
||||
} = React.useContext(PreferencesContext);
|
||||
const [assetWasInstalled, setAssetWasInstalled] = React.useState(false);
|
||||
const [openedAsset, setOpenedAsset] = React.useState<null | AssetShortHeader>(
|
||||
null
|
||||
);
|
||||
|
||||
const [currentTab, setCurrentTab] = React.useState(
|
||||
getNewObjectDialogDefaultTab()
|
||||
);
|
||||
@@ -122,69 +106,11 @@ export default function NewObjectDialog({
|
||||
[allObjectMetadata]
|
||||
);
|
||||
|
||||
const resourcesFetcher = useResourceFetcher();
|
||||
React.useEffect(() => setNewObjectDialogDefaultTab(currentTab), [
|
||||
setNewObjectDialogDefaultTab,
|
||||
currentTab,
|
||||
]);
|
||||
|
||||
const [
|
||||
assetBeingInstalled,
|
||||
setAssetBeingInstalled,
|
||||
] = React.useState<?AssetShortHeader>(null);
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
const onInstallAsset = React.useCallback(
|
||||
(assetShortHeader: AssetShortHeader) => {
|
||||
setAssetBeingInstalled(assetShortHeader);
|
||||
(async () => {
|
||||
try {
|
||||
const installOutput = await installAsset({
|
||||
assetShortHeader,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
});
|
||||
sendAssetAddedToProject({
|
||||
id: assetShortHeader.id,
|
||||
name: assetShortHeader.name,
|
||||
});
|
||||
console.log('Asset successfully installed.');
|
||||
|
||||
installOutput.createdObjects.forEach(object => {
|
||||
onObjectAddedFromAsset(object);
|
||||
});
|
||||
|
||||
await resourcesFetcher.ensureResourcesAreFetched(project);
|
||||
|
||||
setOpenedAsset(null);
|
||||
setAssetWasInstalled(true);
|
||||
} catch (error) {
|
||||
console.error('Error while installing the asset:', error);
|
||||
showErrorBox({
|
||||
message: `There was an error while installing the asset "${
|
||||
assetShortHeader.name
|
||||
}". Verify your internet connection or try again later.`,
|
||||
rawError: error,
|
||||
errorId: 'install-asset-error',
|
||||
});
|
||||
}
|
||||
|
||||
setAssetBeingInstalled(null);
|
||||
})();
|
||||
},
|
||||
[
|
||||
resourcesFetcher,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
onObjectAddedFromAsset,
|
||||
]
|
||||
);
|
||||
|
||||
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
|
||||
'intro-object-types'
|
||||
);
|
||||
@@ -212,7 +138,7 @@ export default function NewObjectDialog({
|
||||
>
|
||||
<Column noMargin expand>
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tab label={<Trans>Search Asset Store</Trans>} value="asset-store" />
|
||||
<Tab label={<Trans>Asset Store</Trans>} value="asset-store" />
|
||||
<Tab
|
||||
label={<Trans>New object from scratch</Trans>}
|
||||
value="new-object"
|
||||
@@ -224,13 +150,11 @@ export default function NewObjectDialog({
|
||||
project={project}
|
||||
objectsContainer={objectsContainer}
|
||||
events={events}
|
||||
onOpenDetails={assetShortHeader => {
|
||||
setOpenedAsset(assetShortHeader);
|
||||
sendAssetOpened({
|
||||
id: assetShortHeader.id,
|
||||
name: assetShortHeader.name,
|
||||
});
|
||||
}}
|
||||
layout={layout}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceSources={resourceSources}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
onObjectAddedFromAsset={onObjectAddedFromAsset}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'new-object' && (
|
||||
@@ -263,34 +187,6 @@ export default function NewObjectDialog({
|
||||
</ScrollView>
|
||||
)}
|
||||
</Column>
|
||||
{openedAsset !== null ? (
|
||||
<AssetDetails
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
resourceSources={resourceSources}
|
||||
onChooseResource={onChooseResource}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
assetShortHeader={openedAsset}
|
||||
onAdd={() => onInstallAsset(openedAsset)}
|
||||
onClose={() => setOpenedAsset(null)}
|
||||
canInstall={!assetBeingInstalled}
|
||||
isBeingInstalled={
|
||||
!!assetBeingInstalled && assetBeingInstalled.id === openedAsset.id
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{resourcesFetcher.renderResourceFetcherDialog()}
|
||||
<DismissableInfoBar
|
||||
identifier="asset-installed-explanation"
|
||||
message={
|
||||
<Trans>
|
||||
The object was added to the list of objects. You can now use it on
|
||||
the scene, in events, and customize it.
|
||||
</Trans>
|
||||
}
|
||||
show={assetWasInstalled}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
+288
-111
@@ -8,7 +8,16 @@ import DoubleChevronArrow from '../UI/CustomSvgIcons/DoubleChevronArrow';
|
||||
import { Column, Line, Spacer } from '../UI/Grid';
|
||||
import Background from '../UI/Background';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import { sendAssetPackOpened } from '../Utils/Analytics/EventSender';
|
||||
import {
|
||||
sendAssetAddedToProject,
|
||||
sendAssetOpened,
|
||||
sendAssetPackOpened,
|
||||
} from '../Utils/Analytics/EventSender';
|
||||
import {
|
||||
type ResourceSource,
|
||||
type ChooseResourceFunction,
|
||||
} from '../ResourcesList/ResourceSource';
|
||||
import { type ResourceExternalEditor } from '../ResourcesList/ResourceExternalEditor.flow';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
|
||||
import { type SearchBarInterface } from '../UI/SearchBar';
|
||||
@@ -25,6 +34,12 @@ import { AssetsHome } from './AssetsHome';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import Text from '../UI/Text';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import { AssetDetails } from './AssetDetails';
|
||||
import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import { installAsset } from './InstallAsset';
|
||||
import { useResourceFetcher } from '../ProjectsStorage/ResourceFetcher';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
|
||||
const styles = {
|
||||
searchBar: {
|
||||
@@ -37,16 +52,24 @@ type Props = {
|
||||
project: gdProject,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
events: gdEventsList,
|
||||
onOpenDetails: AssetShortHeader => void,
|
||||
focusOnMount?: boolean,
|
||||
onObjectAddedFromAsset: (object: gdObject) => void,
|
||||
layout: ?gdLayout,
|
||||
resourceSources: Array<ResourceSource>,
|
||||
resourceExternalEditors: Array<ResourceExternalEditor>,
|
||||
onChooseResource: ChooseResourceFunction,
|
||||
};
|
||||
|
||||
export const AssetStore = ({
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
onOpenDetails,
|
||||
focusOnMount,
|
||||
onObjectAddedFromAsset,
|
||||
layout,
|
||||
resourceSources,
|
||||
resourceExternalEditors,
|
||||
onChooseResource,
|
||||
}: Props) => {
|
||||
const {
|
||||
assetPacks,
|
||||
@@ -68,8 +91,110 @@ export const AssetStore = ({
|
||||
|
||||
const searchBar = React.useRef<?SearchBarInterface>(null);
|
||||
const shouldAutofocusSearchbar = useShouldAutofocusSearchbar();
|
||||
const [
|
||||
openedAssetShortHeader,
|
||||
setOpenedAssetShortHeader,
|
||||
] = React.useState<?AssetShortHeader>(null);
|
||||
const [isFiltersPanelOpen, setIsFiltersPanelOpen] = React.useState(false);
|
||||
const [isOnHomePage, setIsOnHomePage] = React.useState(true);
|
||||
const [isFiltersOpen, setIsFiltersOpen] = React.useState(false);
|
||||
|
||||
const [
|
||||
assetBeingInstalled,
|
||||
setAssetBeingInstalled,
|
||||
] = React.useState<?AssetShortHeader>(null);
|
||||
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
|
||||
const resourcesFetcher = useResourceFetcher();
|
||||
|
||||
const onInstallAsset = React.useCallback(
|
||||
(assetShortHeader: AssetShortHeader) => {
|
||||
setAssetBeingInstalled(assetShortHeader);
|
||||
(async () => {
|
||||
try {
|
||||
const installOutput = await installAsset({
|
||||
assetShortHeader,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
});
|
||||
sendAssetAddedToProject({
|
||||
id: assetShortHeader.id,
|
||||
name: assetShortHeader.name,
|
||||
});
|
||||
console.log('Asset successfully installed.');
|
||||
|
||||
installOutput.createdObjects.forEach(object => {
|
||||
onObjectAddedFromAsset(object);
|
||||
});
|
||||
|
||||
await resourcesFetcher.ensureResourcesAreFetched(project);
|
||||
} catch (error) {
|
||||
console.error('Error while installing the asset:', error);
|
||||
showErrorBox({
|
||||
message: `There was an error while installing the asset "${
|
||||
assetShortHeader.name
|
||||
}". Verify your internet connection or try again later.`,
|
||||
rawError: error,
|
||||
errorId: 'install-asset-error',
|
||||
});
|
||||
}
|
||||
|
||||
setAssetBeingInstalled(null);
|
||||
})();
|
||||
},
|
||||
[
|
||||
resourcesFetcher,
|
||||
eventsFunctionsExtensionsState,
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
onObjectAddedFromAsset,
|
||||
]
|
||||
);
|
||||
|
||||
const resetToDefault = () => {
|
||||
setSearchText('');
|
||||
filtersState.setChosenCategory(null);
|
||||
setOpenedAssetShortHeader(null);
|
||||
clearAllFilters(assetFiltersState);
|
||||
setIsFiltersPanelOpen(false);
|
||||
setIsOnHomePage(true);
|
||||
};
|
||||
|
||||
// When a pack is selected from the home page,
|
||||
// we set it as the chosen category and open the filters panel.
|
||||
const selectPack = (tag: string) => {
|
||||
if (!assetPacks) return;
|
||||
|
||||
sendAssetPackOpened(tag);
|
||||
|
||||
const chosenCategory = {
|
||||
node: { name: tag, allChildrenTags: [], children: [] },
|
||||
parentNodes: [],
|
||||
};
|
||||
filtersState.setChosenCategory(chosenCategory);
|
||||
|
||||
setIsOnHomePage(false);
|
||||
setIsFiltersPanelOpen(true);
|
||||
};
|
||||
|
||||
// When a tag is selected from the asset details page,
|
||||
// we set it as the chosen category, clear old filters and open the filters panel.
|
||||
const selectTag = (tag: string) => {
|
||||
const chosenCategory = {
|
||||
node: { name: tag, allChildrenTags: [], children: [] },
|
||||
parentNodes: [],
|
||||
};
|
||||
filtersState.setChosenCategory(chosenCategory);
|
||||
|
||||
clearAllFilters(assetFiltersState);
|
||||
setOpenedAssetShortHeader(null);
|
||||
setIsFiltersPanelOpen(true);
|
||||
};
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
@@ -81,117 +206,169 @@ export const AssetStore = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
placeholder={t`Enter your Search`}
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => setIsOnHomePage(false)}
|
||||
style={styles.searchBar}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
/>
|
||||
{!isOnHomePage && <Spacer />}
|
||||
<Line justifyContent="left" noMargin={isOnHomePage}>
|
||||
{isOnHomePage ? (
|
||||
<Column>
|
||||
<Text size="title">
|
||||
<Trans>Discover</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
) : (
|
||||
<FlatButton
|
||||
icon={<ArrowBack />}
|
||||
label={<Trans>Back to discover</Trans>}
|
||||
primary={false}
|
||||
onClick={() => {
|
||||
filtersState.setChosenCategory(null);
|
||||
setIsFiltersOpen(false);
|
||||
setIsOnHomePage(true);
|
||||
<>
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<>
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
placeholder={t`Search assets`}
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {
|
||||
if (isOnHomePage) setIsOnHomePage(false);
|
||||
}}
|
||||
style={styles.searchBar}
|
||||
ref={searchBar}
|
||||
id="asset-store-search-bar"
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
>
|
||||
<Background
|
||||
noFullHeight
|
||||
noExpand
|
||||
width={!isFiltersOpen ? 50 : windowWidth === 'small' ? 150 : 250}
|
||||
>
|
||||
{!isFiltersOpen ? (
|
||||
<Line justifyContent="center">
|
||||
<IconButton onClick={() => setIsFiltersOpen(true)}>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
</Line>
|
||||
) : (
|
||||
<ScrollView>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<Column>
|
||||
<Line alignItems="center">
|
||||
<Tune />
|
||||
<Subheader>
|
||||
<Trans>Object filters</Trans>
|
||||
</Subheader>
|
||||
</Line>
|
||||
</Column>
|
||||
<IconButton onClick={() => setIsFiltersOpen(false)}>
|
||||
<DoubleChevronArrow />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<AssetStoreFilterPanel
|
||||
assetFiltersState={assetFiltersState}
|
||||
onChoiceChange={() => setIsOnHomePage(false)}
|
||||
/>
|
||||
</Line>
|
||||
</ScrollView>
|
||||
)}
|
||||
</Background>
|
||||
{isOnHomePage && assetPacks ? (
|
||||
<AssetsHome
|
||||
assetPacks={assetPacks}
|
||||
onPackSelection={tag => {
|
||||
const chosenCategory = {
|
||||
node: { name: tag, allChildrenTags: [], children: [] },
|
||||
parentNodes: [],
|
||||
};
|
||||
sendAssetPackOpened(tag);
|
||||
filtersState.setChosenCategory(chosenCategory);
|
||||
setIsFiltersOpen(true);
|
||||
setIsOnHomePage(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<BoxSearchResults
|
||||
baseSize={128}
|
||||
onRetry={fetchAssetsAndFilters}
|
||||
error={error}
|
||||
searchItems={searchResults}
|
||||
renderSearchItem={(assetShortHeader, size) => (
|
||||
<AssetCard
|
||||
size={size}
|
||||
onOpenDetails={() => onOpenDetails(assetShortHeader)}
|
||||
assetShortHeader={assetShortHeader}
|
||||
{!isOnHomePage && <Spacer />}
|
||||
<Line justifyContent="left" noMargin>
|
||||
{isOnHomePage ? (
|
||||
<Column>
|
||||
<Text size="title">
|
||||
<Trans>Discover</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
) : (
|
||||
<FlatButton
|
||||
icon={<ArrowBack />}
|
||||
label={
|
||||
openedAssetShortHeader ? (
|
||||
<Trans>Back</Trans>
|
||||
) : (
|
||||
<Trans>Back to discover</Trans>
|
||||
)
|
||||
}
|
||||
primary={false}
|
||||
onClick={() => {
|
||||
if (openedAssetShortHeader) {
|
||||
// Going back from Asset page to search.
|
||||
setOpenedAssetShortHeader(null);
|
||||
} else {
|
||||
// Going back from search to home.
|
||||
resetToDefault();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
noResultPlaceholder={
|
||||
<NoResultPlaceholder
|
||||
onClear={() => clearAllFilters(assetFiltersState)}
|
||||
/>
|
||||
</Line>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
>
|
||||
{!openedAssetShortHeader && ( // Don't show filters on asset page.
|
||||
<Background
|
||||
noFullHeight
|
||||
noExpand
|
||||
width={
|
||||
!isFiltersPanelOpen
|
||||
? 50
|
||||
: windowWidth === 'small'
|
||||
? 205
|
||||
: 250
|
||||
}
|
||||
>
|
||||
{!isFiltersPanelOpen ? (
|
||||
<Line justifyContent="center">
|
||||
<IconButton onClick={() => setIsFiltersPanelOpen(true)}>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
</Line>
|
||||
) : (
|
||||
<ScrollView>
|
||||
<Line
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Column>
|
||||
<Line alignItems="center">
|
||||
<Tune />
|
||||
<Subheader>
|
||||
<Trans>Object filters</Trans>
|
||||
</Subheader>
|
||||
</Line>
|
||||
</Column>
|
||||
<IconButton
|
||||
onClick={() => setIsFiltersPanelOpen(false)}
|
||||
>
|
||||
<DoubleChevronArrow />
|
||||
</IconButton>
|
||||
</Line>
|
||||
<Line
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<AssetStoreFilterPanel
|
||||
assetFiltersState={assetFiltersState}
|
||||
onChoiceChange={() => {
|
||||
if (isOnHomePage) setIsOnHomePage(false);
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
</ScrollView>
|
||||
)}
|
||||
</Background>
|
||||
)}
|
||||
{isOnHomePage && !assetPacks && <PlaceholderLoader />}
|
||||
{isOnHomePage && assetPacks && (
|
||||
<AssetsHome
|
||||
assetPacks={assetPacks}
|
||||
onPackSelection={selectPack}
|
||||
/>
|
||||
)}
|
||||
{!isOnHomePage && !openedAssetShortHeader && (
|
||||
<BoxSearchResults
|
||||
baseSize={128}
|
||||
onRetry={fetchAssetsAndFilters}
|
||||
error={error}
|
||||
searchItems={searchResults}
|
||||
renderSearchItem={(assetShortHeader, size) => (
|
||||
<AssetCard
|
||||
size={size}
|
||||
onOpenDetails={() => {
|
||||
sendAssetOpened({
|
||||
id: assetShortHeader.id,
|
||||
name: assetShortHeader.name,
|
||||
});
|
||||
setOpenedAssetShortHeader(assetShortHeader);
|
||||
}}
|
||||
assetShortHeader={assetShortHeader}
|
||||
/>
|
||||
)}
|
||||
noResultPlaceholder={
|
||||
<NoResultPlaceholder
|
||||
onClear={() => clearAllFilters(assetFiltersState)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{openedAssetShortHeader && (
|
||||
<AssetDetails
|
||||
project={project}
|
||||
layout={layout}
|
||||
objectsContainer={objectsContainer}
|
||||
resourceSources={resourceSources}
|
||||
resourceExternalEditors={resourceExternalEditors}
|
||||
onTagSelection={selectTag}
|
||||
assetShortHeader={openedAssetShortHeader}
|
||||
onAdd={() => onInstallAsset(openedAssetShortHeader)}
|
||||
onClose={() => setOpenedAssetShortHeader(null)}
|
||||
canInstall={!assetBeingInstalled}
|
||||
isBeingInstalled={
|
||||
!!assetBeingInstalled &&
|
||||
assetBeingInstalled.id === openedAssetShortHeader.id
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
{resourcesFetcher.renderResourceFetcherDialog()}
|
||||
</>
|
||||
)}
|
||||
</ResponsiveWindowMeasurer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ import Checkbox from '../../../UI/Checkbox';
|
||||
import SelectField from '../../../UI/SelectField';
|
||||
import SelectOption from '../../../UI/SelectOption';
|
||||
import SemiControlledTextField from '../../../UI/SemiControlledTextField';
|
||||
import ImagePreview from '../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import ImagePreview, {
|
||||
isProjectImageResourceSmooth,
|
||||
} from '../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import ResourceSelector from '../../../ResourcesList/ResourceSelector';
|
||||
import ResourcesLoader from '../../../ResourcesLoader';
|
||||
import ShapePreview from './ShapePreview';
|
||||
@@ -358,7 +360,15 @@ const Physics2Editor = (props: Props) => {
|
||||
<ImagePreview
|
||||
resourceName={image}
|
||||
project={props.project}
|
||||
resourcesLoader={resourcesLoader}
|
||||
imageResourceSource={resourcesLoader.getResourceFullUrl(
|
||||
props.project,
|
||||
image,
|
||||
{}
|
||||
)}
|
||||
isImageResourceSmooth={isProjectImageResourceSmooth(
|
||||
props.project,
|
||||
image
|
||||
)}
|
||||
fixedHeight={200}
|
||||
renderOverlay={overlayProps => {
|
||||
// The result from `getProperties` is temporary, and because this renderOverlay
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { Column, Line, Spacer } from '../../../../UI/Grid';
|
||||
import ImagePreview from '../../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import Replay from '@material-ui/icons/Replay';
|
||||
@@ -11,21 +11,7 @@ import Timer from '@material-ui/icons/Timer';
|
||||
import TextField from '../../../../UI/TextField';
|
||||
import FlatButton from '../../../../UI/FlatButton';
|
||||
import Text from '../../../../UI/Text';
|
||||
|
||||
type Props = {|
|
||||
spritesContainer: Object,
|
||||
resourcesLoader: Object,
|
||||
project: Object,
|
||||
timeBetweenFrames: number,
|
||||
onChangeTimeBetweenFrames: number => void,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
currentFrameIndex: number,
|
||||
currentFrameElapsedTime: number,
|
||||
paused: boolean,
|
||||
fps: number,
|
||||
|};
|
||||
import useForceUpdate from '../../../../Utils/UseForceUpdate';
|
||||
|
||||
const styles = {
|
||||
timeField: {
|
||||
@@ -37,99 +23,155 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
export default class AnimationPreview extends Component<Props, State> {
|
||||
state = {
|
||||
currentFrameIndex: 0,
|
||||
currentFrameElapsedTime: 0,
|
||||
paused: false,
|
||||
fps: Math.round(1 / this.props.timeBetweenFrames),
|
||||
type Props = {|
|
||||
resourceNames: string[],
|
||||
getImageResourceSource: (resourceName: string) => string,
|
||||
isImageResourceSmooth: (resourceName: string) => boolean,
|
||||
project: gdProject,
|
||||
timeBetweenFrames: number,
|
||||
onChangeTimeBetweenFrames?: number => void,
|
||||
isLooping: boolean,
|
||||
hideCheckeredBackground?: boolean,
|
||||
hideControls?: boolean,
|
||||
initialZoom?: number,
|
||||
fixedHeight?: number,
|
||||
|};
|
||||
|
||||
const AnimationPreview = ({
|
||||
resourceNames,
|
||||
getImageResourceSource,
|
||||
isImageResourceSmooth,
|
||||
project,
|
||||
timeBetweenFrames,
|
||||
onChangeTimeBetweenFrames,
|
||||
isLooping,
|
||||
hideCheckeredBackground,
|
||||
hideControls,
|
||||
initialZoom,
|
||||
fixedHeight,
|
||||
}: Props) => {
|
||||
const forceUdpate = useForceUpdate();
|
||||
|
||||
// Use state for elements that don't need to be read from inside the animation callback.
|
||||
const [fps, setFps] = React.useState<number>(
|
||||
Math.round(1 / timeBetweenFrames)
|
||||
);
|
||||
|
||||
// Use useRef for mutable variables that we want to persist
|
||||
// to be readable from inside the animation callback.
|
||||
const requestRef = React.useRef();
|
||||
const previousTimeRef = React.useRef();
|
||||
const currentFrameElapsedTimeRef = React.useRef(0);
|
||||
const timeBetweenFramesRef = React.useRef(timeBetweenFrames);
|
||||
const pausedRef = React.useRef(false);
|
||||
const currentFrameIndexRef = React.useRef(0);
|
||||
const isLoopingRef = React.useRef(isLooping);
|
||||
|
||||
// When outside variables change, we need to update the animation callback.
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (timeBetweenFrames !== timeBetweenFramesRef.current) {
|
||||
timeBetweenFramesRef.current = timeBetweenFrames;
|
||||
setFps(Math.round(1 / timeBetweenFrames));
|
||||
}
|
||||
if (isLooping !== isLoopingRef.current) {
|
||||
isLoopingRef.current = isLooping;
|
||||
}
|
||||
},
|
||||
[timeBetweenFrames, isLooping]
|
||||
);
|
||||
|
||||
const replay = () => {
|
||||
currentFrameIndexRef.current = 0;
|
||||
currentFrameElapsedTimeRef.current = 0;
|
||||
pausedRef.current = false;
|
||||
forceUdpate();
|
||||
};
|
||||
|
||||
nextUpdate = null;
|
||||
previousUpdateTimeInMs = 0;
|
||||
// Variables used inside the requestAnimationFrame callback
|
||||
// must be declared as mutable with useRef, otherwise they
|
||||
// will not update between calls.
|
||||
const updateAnimation = React.useCallback(
|
||||
(updateTimeInMs: number) => {
|
||||
// Mutable variables used inside the requestAnimationFrame callback
|
||||
const previousUpdateTimeInMs = previousTimeRef.current;
|
||||
const currentFrameIndex = currentFrameIndexRef.current;
|
||||
const currentFrameElapsedTime = currentFrameElapsedTimeRef.current;
|
||||
const currentTimeBetweenFrames = timeBetweenFramesRef.current;
|
||||
const paused = pausedRef.current;
|
||||
const isLooping = isLoopingRef.current;
|
||||
const numberOfFrames = resourceNames.length;
|
||||
|
||||
componentDidMount() {
|
||||
this.nextUpdate = requestAnimationFrame(this._updateAnimation);
|
||||
if (previousUpdateTimeInMs) {
|
||||
const elapsedTime = (updateTimeInMs - previousUpdateTimeInMs) / 1000;
|
||||
|
||||
let newFrameIndex = currentFrameIndex;
|
||||
let newFrameElapsedTime =
|
||||
currentFrameElapsedTime + (paused ? 0 : elapsedTime);
|
||||
|
||||
// Increase frame index if time elapsed is greater than time between frames.
|
||||
if (newFrameElapsedTime > currentTimeBetweenFrames) {
|
||||
const count = Math.floor(
|
||||
newFrameElapsedTime / currentTimeBetweenFrames
|
||||
);
|
||||
newFrameIndex += count;
|
||||
newFrameElapsedTime =
|
||||
newFrameElapsedTime - count * currentTimeBetweenFrames;
|
||||
if (newFrameElapsedTime < 0) newFrameElapsedTime = 0;
|
||||
}
|
||||
|
||||
// Reset to 0 if we reached the end of the animation.
|
||||
if (newFrameIndex >= numberOfFrames) {
|
||||
newFrameIndex = isLooping
|
||||
? newFrameIndex % numberOfFrames
|
||||
: numberOfFrames - 1;
|
||||
}
|
||||
if (newFrameIndex < 0) newFrameIndex = 0; //May happen if there is no frame.
|
||||
|
||||
currentFrameIndexRef.current = newFrameIndex;
|
||||
currentFrameElapsedTimeRef.current = newFrameElapsedTime;
|
||||
// Ensure we trigger an update if the animation changes,
|
||||
// as the refs will not do it.
|
||||
if (currentFrameIndex !== newFrameIndex) {
|
||||
forceUdpate();
|
||||
}
|
||||
}
|
||||
requestRef.current = requestAnimationFrame(updateAnimation);
|
||||
previousTimeRef.current = updateTimeInMs;
|
||||
},
|
||||
[forceUdpate, resourceNames]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
requestRef.current = requestAnimationFrame(updateAnimation);
|
||||
return () => {
|
||||
if (requestRef.current) cancelAnimationFrame(requestRef.current);
|
||||
};
|
||||
},
|
||||
[updateAnimation]
|
||||
);
|
||||
|
||||
// When changing animation, the index can be out of bounds, so reset the animation.
|
||||
if (currentFrameIndexRef.current >= resourceNames.length) {
|
||||
currentFrameIndexRef.current = 0;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.nextUpdate) cancelAnimationFrame(this.nextUpdate);
|
||||
}
|
||||
const resourceName = resourceNames[currentFrameIndexRef.current];
|
||||
|
||||
replay = () =>
|
||||
this.setState({
|
||||
currentFrameIndex: 0,
|
||||
currentFrameElapsedTime: 0,
|
||||
paused: false,
|
||||
});
|
||||
|
||||
play = () =>
|
||||
this.setState({
|
||||
paused: false,
|
||||
});
|
||||
|
||||
pause = () =>
|
||||
this.setState({
|
||||
paused: true,
|
||||
});
|
||||
|
||||
_updateAnimation = (updateTimeInMs: number) => {
|
||||
const elapsedTime = this.previousUpdateTimeInMs
|
||||
? (updateTimeInMs - this.previousUpdateTimeInMs) / 1000
|
||||
: 0;
|
||||
|
||||
const { spritesContainer, timeBetweenFrames } = this.props;
|
||||
const { currentFrameIndex, currentFrameElapsedTime, paused } = this.state;
|
||||
|
||||
let newFrameIndex = currentFrameIndex;
|
||||
let newFrameElapsedTime = currentFrameElapsedTime;
|
||||
newFrameElapsedTime += paused ? 0 : elapsedTime;
|
||||
|
||||
if (newFrameElapsedTime > timeBetweenFrames) {
|
||||
const count = Math.floor(newFrameElapsedTime / timeBetweenFrames);
|
||||
newFrameIndex += count;
|
||||
newFrameElapsedTime = newFrameElapsedTime - count * timeBetweenFrames;
|
||||
if (newFrameElapsedTime < 0) newFrameElapsedTime = 0;
|
||||
}
|
||||
|
||||
if (newFrameIndex >= spritesContainer.getSpritesCount()) {
|
||||
newFrameIndex = spritesContainer.isLooping()
|
||||
? newFrameIndex % spritesContainer.getSpritesCount()
|
||||
: spritesContainer.getSpritesCount() - 1;
|
||||
}
|
||||
if (newFrameIndex < 0) newFrameIndex = 0; //May happen if there is no frame.
|
||||
|
||||
this.setState({
|
||||
currentFrameIndex: newFrameIndex,
|
||||
currentFrameElapsedTime: newFrameElapsedTime,
|
||||
});
|
||||
this.nextUpdate = requestAnimationFrame(this._updateAnimation);
|
||||
this.previousUpdateTimeInMs = updateTimeInMs;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
spritesContainer,
|
||||
resourcesLoader,
|
||||
project,
|
||||
timeBetweenFrames,
|
||||
onChangeTimeBetweenFrames,
|
||||
} = this.props;
|
||||
const { currentFrameIndex, paused, fps } = this.state;
|
||||
|
||||
const hasValidSprite =
|
||||
currentFrameIndex < spritesContainer.getSpritesCount();
|
||||
const sprite = hasValidSprite
|
||||
? spritesContainer.getSprite(currentFrameIndex)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Column expand noOverflowParent>
|
||||
<ImagePreview
|
||||
resourceName={sprite ? sprite.getImageName() : ''}
|
||||
resourcesLoader={resourcesLoader}
|
||||
project={project}
|
||||
/>
|
||||
return (
|
||||
<Column expand noOverflowParent>
|
||||
<ImagePreview
|
||||
resourceName={resourceName}
|
||||
imageResourceSource={getImageResourceSource(resourceName)}
|
||||
isImageResourceSmooth={isImageResourceSmooth(resourceName)}
|
||||
initialZoom={initialZoom}
|
||||
project={project}
|
||||
hideCheckeredBackground={hideCheckeredBackground}
|
||||
hideControls={hideControls}
|
||||
fixedHeight={fixedHeight}
|
||||
/>
|
||||
{!hideControls && (
|
||||
<Line noMargin alignItems="center">
|
||||
<Text>
|
||||
<Trans>FPS:</Trans>
|
||||
@@ -141,9 +183,13 @@ export default class AnimationPreview extends Component<Props, State> {
|
||||
onChange={(e, text) => {
|
||||
const fps = parseFloat(text);
|
||||
if (fps > 0) {
|
||||
this.setState({ fps });
|
||||
onChangeTimeBetweenFrames(parseFloat((1 / fps).toFixed(4)));
|
||||
this.replay();
|
||||
setFps(fps);
|
||||
const newTimeBetweenFrames = parseFloat((1 / fps).toFixed(4));
|
||||
timeBetweenFramesRef.current = newTimeBetweenFrames;
|
||||
if (onChangeTimeBetweenFrames) {
|
||||
onChangeTimeBetweenFrames(newTimeBetweenFrames);
|
||||
}
|
||||
replay();
|
||||
}
|
||||
}}
|
||||
id="direction-time-between-frames"
|
||||
@@ -157,13 +203,16 @@ export default class AnimationPreview extends Component<Props, State> {
|
||||
<Timer style={styles.timeIcon} />
|
||||
<TextField
|
||||
margin="none"
|
||||
value={timeBetweenFrames}
|
||||
value={timeBetweenFramesRef.current}
|
||||
onChange={(e, text) => {
|
||||
const time = parseFloat(text);
|
||||
if (time > 0) {
|
||||
this.setState({ fps: Math.round(1 / time) });
|
||||
onChangeTimeBetweenFrames(time);
|
||||
this.replay();
|
||||
setFps(Math.round(1 / time));
|
||||
timeBetweenFramesRef.current = time;
|
||||
if (onChangeTimeBetweenFrames) {
|
||||
onChangeTimeBetweenFrames(time);
|
||||
}
|
||||
replay();
|
||||
}
|
||||
}}
|
||||
id="direction-time-between-frames"
|
||||
@@ -177,15 +226,20 @@ export default class AnimationPreview extends Component<Props, State> {
|
||||
<FlatButton
|
||||
icon={<Replay />}
|
||||
label={<Trans>Replay</Trans>}
|
||||
onClick={this.replay}
|
||||
onClick={replay}
|
||||
/>
|
||||
<FlatButton
|
||||
icon={paused ? <PlayArrow /> : <Pause />}
|
||||
label={paused ? 'Play' : 'Pause'}
|
||||
onClick={paused ? this.play : this.pause}
|
||||
icon={!!pausedRef.current ? <PlayArrow /> : <Pause />}
|
||||
label={!!pausedRef.current ? 'Play' : 'Pause'}
|
||||
onClick={() => {
|
||||
pausedRef.current = !pausedRef.current;
|
||||
forceUdpate();
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimationPreview;
|
||||
|
||||
@@ -7,7 +7,9 @@ import { Line, Column } from '../../../../UI/Grid';
|
||||
import { mapFor } from '../../../../Utils/MapFor';
|
||||
import PolygonsList from './PolygonsList';
|
||||
import CollisionMasksPreview from './CollisionMasksPreview';
|
||||
import ImagePreview from '../../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import ImagePreview, {
|
||||
isProjectImageResourceSmooth,
|
||||
} from '../../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import {
|
||||
getCurrentElements,
|
||||
allSpritesHaveSameCollisionMasksAs,
|
||||
@@ -194,6 +196,7 @@ const CollisionMasksEditor = (props: Props) => {
|
||||
screenSize === 'small' ? verticalMosaicNodes : horizontalMosaicNodes;
|
||||
|
||||
if (!props.object.getAnimationsCount()) return null;
|
||||
const resourceName = hasValidSprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
preview: {
|
||||
@@ -202,8 +205,16 @@ const CollisionMasksEditor = (props: Props) => {
|
||||
renderEditor: () => (
|
||||
<Background>
|
||||
<ImagePreview
|
||||
resourceName={hasValidSprite ? sprite.getImageName() : ''}
|
||||
resourcesLoader={props.resourcesLoader}
|
||||
resourceName={resourceName}
|
||||
imageResourceSource={props.resourcesLoader.getResourceFullUrl(
|
||||
props.project,
|
||||
resourceName,
|
||||
{}
|
||||
)}
|
||||
isImageResourceSmooth={isProjectImageResourceSmooth(
|
||||
props.project,
|
||||
resourceName
|
||||
)}
|
||||
project={props.project}
|
||||
onSize={setCurrentSpriteSize}
|
||||
renderOverlay={overlayProps =>
|
||||
|
||||
@@ -13,6 +13,7 @@ import AnimationPreview from './AnimationPreview';
|
||||
import ResourcesLoader from '../../../ResourcesLoader';
|
||||
import { type ResourceExternalEditor } from '../../../ResourcesList/ResourceExternalEditor.flow';
|
||||
import { ResponsiveWindowMeasurer } from '../../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import { isProjectImageResourceSmooth } from '../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
@@ -169,13 +170,19 @@ export default class DirectionTools extends Component<Props, State> {
|
||||
flexBody
|
||||
>
|
||||
<AnimationPreview
|
||||
spritesContainer={direction}
|
||||
resourcesLoader={resourcesLoader}
|
||||
resourceNames={direction.getSpriteNames().toJSArray()}
|
||||
getImageResourceSource={(name: string) =>
|
||||
resourcesLoader.getResourceFullUrl(project, name, {})
|
||||
}
|
||||
isImageResourceSmooth={(name: string) =>
|
||||
isProjectImageResourceSmooth(project, name)
|
||||
}
|
||||
project={project}
|
||||
timeBetweenFrames={this.state.timeBetweenFrames}
|
||||
onChangeTimeBetweenFrames={text =>
|
||||
this.setState({ timeBetweenFrames: text })
|
||||
}
|
||||
isLooping={direction.isLooping()}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { Line, Column } from '../../../../UI/Grid';
|
||||
import { mapFor } from '../../../../Utils/MapFor';
|
||||
import PointsList from './PointsList';
|
||||
import PointsPreview from './PointsPreview';
|
||||
import ImagePreview from '../../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import ImagePreview, {
|
||||
isProjectImageResourceSmooth,
|
||||
} from '../../../../ResourcesList/ResourcePreview/ImagePreview';
|
||||
import {
|
||||
getCurrentElements,
|
||||
allSpritesHaveSamePointsAs,
|
||||
@@ -171,6 +173,7 @@ const PointsEditor = (props: Props) => {
|
||||
screenSize === 'small' ? verticalMosaicNodes : horizontalMosaicNodes;
|
||||
|
||||
if (!props.object.getAnimationsCount()) return null;
|
||||
const resourceName = hasValidSprite ? sprite.getImageName() : '';
|
||||
|
||||
const editors: { [string]: Editor } = {
|
||||
preview: {
|
||||
@@ -179,8 +182,16 @@ const PointsEditor = (props: Props) => {
|
||||
renderEditor: () => (
|
||||
<Background>
|
||||
<ImagePreview
|
||||
resourceName={hasValidSprite ? sprite.getImageName() : ''}
|
||||
resourcesLoader={props.resourcesLoader}
|
||||
resourceName={resourceName}
|
||||
imageResourceSource={props.resourcesLoader.getResourceFullUrl(
|
||||
props.project,
|
||||
resourceName,
|
||||
{}
|
||||
)}
|
||||
isImageResourceSmooth={isProjectImageResourceSmooth(
|
||||
props.project,
|
||||
resourceName
|
||||
)}
|
||||
project={props.project}
|
||||
renderOverlay={overlayProps =>
|
||||
hasValidSprite && (
|
||||
|
||||
@@ -218,6 +218,8 @@ export default class ObjectsList extends React.Component<Props, State> {
|
||||
|
||||
object.setTags(getStringFromTags(this.props.selectedObjectTags));
|
||||
onObjectCreated(object);
|
||||
|
||||
this.forceUpdateList();
|
||||
};
|
||||
|
||||
onAddNewObject = () => {
|
||||
|
||||
@@ -129,7 +129,6 @@ export default class ResourcePropertiesEditor extends React.Component<
|
||||
return (
|
||||
<ResourcePreview
|
||||
resourceName={resources[0].getName()}
|
||||
resourcePath={resources[0].getFile()}
|
||||
resourcesLoader={resourcesLoader}
|
||||
project={project}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Trans } from '@lingui/macro';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Measure from 'react-measure';
|
||||
import * as React from 'react';
|
||||
import ResourcesLoader from '../../ResourcesLoader';
|
||||
import { Column } from '../../UI/Grid';
|
||||
import MiniToolbar from '../../UI/MiniToolbar';
|
||||
import ZoomIn from '@material-ui/icons/ZoomIn';
|
||||
@@ -65,8 +64,9 @@ const styles = {
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
resourceName: string,
|
||||
resourcePath?: string,
|
||||
resourcesLoader: typeof ResourcesLoader,
|
||||
imageResourceSource: string,
|
||||
isImageResourceSmooth: boolean,
|
||||
initialZoom?: number,
|
||||
fixedHeight?: number,
|
||||
renderOverlay?: ({|
|
||||
imageWidth: number,
|
||||
@@ -76,17 +76,11 @@ type Props = {|
|
||||
imageZoomFactor: number,
|
||||
|}) => React.Node,
|
||||
onSize?: (number, number) => void,
|
||||
hideCheckeredBackground?: boolean,
|
||||
hideControls?: boolean,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
errored: boolean,
|
||||
imageWidth: ?number,
|
||||
imageHeight: ?number,
|
||||
imageSource: ?string,
|
||||
imageZoomFactor: number,
|
||||
|};
|
||||
|
||||
const resourceIsSmooth = (
|
||||
export const isProjectImageResourceSmooth = (
|
||||
project: gdProject,
|
||||
resourceName: string
|
||||
): boolean => {
|
||||
@@ -97,123 +91,90 @@ const resourceIsSmooth = (
|
||||
return imageResource.isSmooth();
|
||||
};
|
||||
|
||||
const loadStateFrom = (newProps: {
|
||||
project: gdProject,
|
||||
resourceName: string,
|
||||
resourcesLoader: typeof ResourcesLoader,
|
||||
}) => {
|
||||
return {
|
||||
errored: false,
|
||||
imageSource: newProps.resourcesLoader.getResourceFullUrl(
|
||||
newProps.project,
|
||||
newProps.resourceName,
|
||||
{}
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Display the preview for a resource of a project with kind "image".
|
||||
*/
|
||||
const ImagePreview = (props: Props) => {
|
||||
const [state, setState] = React.useState<State>({
|
||||
errored: false,
|
||||
imageWidth: null,
|
||||
imageHeight: null,
|
||||
imageZoomFactor: 1,
|
||||
...loadStateFrom(props),
|
||||
});
|
||||
const isResizeObserverReady = React.useRef<boolean>(false);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
...loadStateFrom({
|
||||
resourceName: props.resourceName,
|
||||
project: props.project,
|
||||
resourcesLoader: props.resourcesLoader,
|
||||
}),
|
||||
}));
|
||||
},
|
||||
[
|
||||
props.resourceName,
|
||||
props.project,
|
||||
props.resourcesLoader,
|
||||
props.resourcePath,
|
||||
]
|
||||
const ImagePreview = ({
|
||||
project,
|
||||
resourceName,
|
||||
imageResourceSource,
|
||||
isImageResourceSmooth,
|
||||
fixedHeight,
|
||||
renderOverlay,
|
||||
onSize,
|
||||
hideCheckeredBackground,
|
||||
hideControls,
|
||||
initialZoom,
|
||||
}: Props) => {
|
||||
const [errored, setErrored] = React.useState(false);
|
||||
const [imageWidth, setImageWidth] = React.useState(null);
|
||||
const [imageHeight, setImageHeight] = React.useState(null);
|
||||
const [imageZoomFactor, setImageZoomFactor] = React.useState(
|
||||
initialZoom || 1
|
||||
);
|
||||
const [isResizeObserverReady, setIsResizeObserverReady] = React.useState(
|
||||
false
|
||||
);
|
||||
|
||||
const handleImageError = () => {
|
||||
setState(state => ({ ...state, errored: true }));
|
||||
setErrored(true);
|
||||
};
|
||||
|
||||
const adaptZoomToImage = (
|
||||
containerHeight: number,
|
||||
containerWidth: number
|
||||
) => {
|
||||
const { imageHeight, imageWidth } = state;
|
||||
let zoomFactor;
|
||||
if (!imageHeight || !imageWidth) {
|
||||
zoomFactor = 1;
|
||||
} else {
|
||||
const idealZoomFactors = [
|
||||
containerWidth / (imageWidth + 2 * MARGIN),
|
||||
containerHeight / (imageHeight + 2 * MARGIN),
|
||||
];
|
||||
zoomFactor = getBoundedZoomFactor(Math.min(...idealZoomFactors));
|
||||
}
|
||||
setState(state => ({
|
||||
...state,
|
||||
imageZoomFactor: zoomFactor,
|
||||
}));
|
||||
const zoomFactor =
|
||||
!imageHeight || !imageWidth
|
||||
? 1
|
||||
: getBoundedZoomFactor(
|
||||
Math.min(
|
||||
containerWidth / (imageWidth + 2 * MARGIN),
|
||||
containerHeight / (imageHeight + 2 * MARGIN)
|
||||
)
|
||||
);
|
||||
setImageZoomFactor(zoomFactor);
|
||||
};
|
||||
|
||||
const handleImageLoaded = (e: any) => {
|
||||
const imgElement = e.target;
|
||||
|
||||
const imageWidth = imgElement
|
||||
const newImageWidth = imgElement
|
||||
? imgElement.naturalWidth || imgElement.clientWidth
|
||||
: 0;
|
||||
const imageHeight = imgElement
|
||||
const newImageHeight = imgElement
|
||||
? imgElement.naturalHeight || imgElement.clientHeight
|
||||
: 0;
|
||||
setState(state => ({ ...state, imageWidth, imageHeight }));
|
||||
if (props.onSize) props.onSize(imageWidth, imageHeight);
|
||||
setImageHeight(newImageHeight);
|
||||
setImageWidth(newImageWidth);
|
||||
if (onSize) onSize(newImageWidth, newImageHeight);
|
||||
};
|
||||
|
||||
const zoomBy = (imageZoomFactorDelta: number) => {
|
||||
zoomTo(state.imageZoomFactor + imageZoomFactorDelta);
|
||||
zoomTo(imageZoomFactor + imageZoomFactorDelta);
|
||||
};
|
||||
|
||||
const zoomTo = (imageZoomFactor: number) => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
imageZoomFactor: getBoundedZoomFactor(imageZoomFactor),
|
||||
}));
|
||||
setImageZoomFactor(getBoundedZoomFactor(imageZoomFactor));
|
||||
};
|
||||
|
||||
const theme = React.useContext(GDevelopThemeContext);
|
||||
const frameBorderColor = theme.imagePreview.frameBorderColor || '#aaa';
|
||||
const previewBorderColor = theme.imagePreview.borderColor || '#aaa';
|
||||
|
||||
return (
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }) => {
|
||||
const containerWidth = contentRect.bounds.width;
|
||||
const containerHeight = contentRect.bounds.height;
|
||||
if (
|
||||
!!containerWidth &&
|
||||
!!containerHeight &&
|
||||
!isResizeObserverReady.current
|
||||
) {
|
||||
adaptZoomToImage(containerHeight, containerWidth);
|
||||
isResizeObserverReady.current = true;
|
||||
// Once the image is loaded, adapt the zoom to the image size.
|
||||
if (!isResizeObserverReady && !!containerWidth && !!containerHeight) {
|
||||
if (!initialZoom) {
|
||||
adaptZoomToImage(containerHeight, containerWidth);
|
||||
}
|
||||
setIsResizeObserverReady(true);
|
||||
}
|
||||
const { resourceName, renderOverlay, fixedHeight, project } = props;
|
||||
const { imageHeight, imageWidth, imageSource, imageZoomFactor } = state;
|
||||
|
||||
const imageLoaded = !!imageWidth && !!imageHeight && !state.errored;
|
||||
const imageLoaded = !!imageWidth && !!imageHeight && !errored;
|
||||
|
||||
// Centre-align the image and overlays
|
||||
const imagePositionTop = Math.max(
|
||||
@@ -236,7 +197,7 @@ const ImagePreview = (props: Props) => {
|
||||
width: imageWidth ? imageWidth * imageZoomFactor : undefined,
|
||||
height: imageHeight ? imageHeight * imageZoomFactor : undefined,
|
||||
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
|
||||
...(!resourceIsSmooth(project, resourceName)
|
||||
...(!isImageResourceSmooth
|
||||
? styles.previewImagePixelated
|
||||
: undefined),
|
||||
};
|
||||
@@ -263,57 +224,55 @@ const ImagePreview = (props: Props) => {
|
||||
|
||||
return (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<MiniToolbar>
|
||||
<IconButton
|
||||
onClick={() => zoomBy(-0.2)}
|
||||
tooltip={t`Zoom out (you can also use Ctrl + Mouse wheel)`}
|
||||
>
|
||||
<ZoomOut />
|
||||
</IconButton>
|
||||
<div style={styles.sliderContainer}>
|
||||
<Slider
|
||||
min={Math.log10(MIN_ZOOM_FACTOR)}
|
||||
max={Math.log10(MAX_ZOOM_FACTOR)}
|
||||
step={0.05}
|
||||
value={Math.log10(state.imageZoomFactor)}
|
||||
onChange={value => {
|
||||
console.log(value);
|
||||
zoomTo(Math.pow(10, value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => zoomBy(+0.2)}
|
||||
tooltip={t`Zoom in (you can also use Ctrl + Mouse wheel)`}
|
||||
>
|
||||
<ZoomIn />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => zoomTo(1)}
|
||||
tooltip={t`Restore original size`}
|
||||
>
|
||||
<ZoomOutMap />
|
||||
</IconButton>
|
||||
</MiniToolbar>
|
||||
{!hideControls && (
|
||||
<MiniToolbar>
|
||||
<IconButton
|
||||
onClick={() => zoomBy(-0.2)}
|
||||
tooltip={t`Zoom out (you can also use Ctrl + Mouse wheel)`}
|
||||
>
|
||||
<ZoomOut />
|
||||
</IconButton>
|
||||
<div style={styles.sliderContainer}>
|
||||
<Slider
|
||||
min={Math.log10(MIN_ZOOM_FACTOR)}
|
||||
max={Math.log10(MAX_ZOOM_FACTOR)}
|
||||
step={0.05}
|
||||
value={Math.log10(imageZoomFactor)}
|
||||
onChange={value => {
|
||||
zoomTo(Math.pow(10, value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => zoomBy(+0.2)}
|
||||
tooltip={t`Zoom in (you can also use Ctrl + Mouse wheel)`}
|
||||
>
|
||||
<ZoomIn />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => zoomTo(1)}
|
||||
tooltip={t`Restore original size`}
|
||||
>
|
||||
<ZoomOutMap />
|
||||
</IconButton>
|
||||
</MiniToolbar>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
...styles.contentContainer,
|
||||
height: fixedHeight || '100%',
|
||||
}}
|
||||
>
|
||||
<CheckeredBackground />
|
||||
{!hideCheckeredBackground && <CheckeredBackground />}
|
||||
<div
|
||||
dir={
|
||||
'ltr' /* Force LTR layout to avoid issues with image positioning */
|
||||
}
|
||||
style={{
|
||||
...styles.imagePreviewContainer,
|
||||
border: `1px solid ${previewBorderColor}`,
|
||||
}}
|
||||
style={styles.imagePreviewContainer}
|
||||
ref={measureRef}
|
||||
onWheel={event => {
|
||||
const { deltaY } = event;
|
||||
if (shouldZoom(event)) {
|
||||
if (!hideControls && shouldZoom(event)) {
|
||||
zoomBy(-deltaY / 500);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -322,18 +281,18 @@ const ImagePreview = (props: Props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!!state.errored && (
|
||||
{!!errored && (
|
||||
<PlaceholderMessage>
|
||||
<Text>
|
||||
<Trans>Unable to load the image</Trans>
|
||||
</Text>
|
||||
</PlaceholderMessage>
|
||||
)}
|
||||
{!state.errored && (
|
||||
{!errored && (
|
||||
<CorsAwareImage
|
||||
style={imageStyle}
|
||||
alt={resourceName}
|
||||
src={imageSource}
|
||||
src={imageResourceSource}
|
||||
onError={handleImageError}
|
||||
onLoad={handleImageLoaded}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as React from 'react';
|
||||
import ResourcesLoader from '../../ResourcesLoader';
|
||||
import { type ResourceKind } from '../ResourceSource';
|
||||
import ImagePreview from './ImagePreview';
|
||||
import ImagePreview, { isProjectImageResourceSmooth } from './ImagePreview';
|
||||
import GenericIconPreview from './GenericIconPreview';
|
||||
import Audiotrack from '@material-ui/icons/Audiotrack';
|
||||
import InsertDriveFile from '@material-ui/icons/InsertDriveFile';
|
||||
@@ -12,7 +12,6 @@ import FontDownload from '@material-ui/icons/FontDownload';
|
||||
type Props = {|
|
||||
project: gdProject,
|
||||
resourceName: string,
|
||||
resourcePath?: string,
|
||||
resourcesLoader: typeof ResourcesLoader,
|
||||
onSize?: (number, number) => void,
|
||||
|};
|
||||
@@ -31,8 +30,7 @@ export default class ResourcePreview extends React.PureComponent<Props, State> {
|
||||
UNSAFE_componentWillReceiveProps(newProps: Props) {
|
||||
if (
|
||||
newProps.resourceName !== this.props.resourceName ||
|
||||
newProps.project !== this.props.project ||
|
||||
newProps.resourcePath !== this.props.resourcePath
|
||||
newProps.project !== this.props.project
|
||||
) {
|
||||
this.setState(this._loadFrom(newProps));
|
||||
}
|
||||
@@ -59,9 +57,16 @@ export default class ResourcePreview extends React.PureComponent<Props, State> {
|
||||
<ImagePreview
|
||||
project={this.props.project}
|
||||
resourceName={this.props.resourceName}
|
||||
resourcesLoader={this.props.resourcesLoader}
|
||||
imageResourceSource={this.props.resourcesLoader.getResourceFullUrl(
|
||||
this.props.project,
|
||||
this.props.resourceName,
|
||||
{}
|
||||
)}
|
||||
isImageResourceSmooth={isProjectImageResourceSmooth(
|
||||
this.props.project,
|
||||
this.props.resourceName
|
||||
)}
|
||||
onSize={this.props.onSize}
|
||||
resourcePath={this.props.resourcePath}
|
||||
/>
|
||||
);
|
||||
case 'audio':
|
||||
|
||||
@@ -51,7 +51,7 @@ type MemoizedTagsTreeProps = {|
|
||||
allItemsLabel: React.Node,
|
||||
chosenCategory: ?ChosenCategory,
|
||||
setChosenCategory: (?ChosenCategory) => void,
|
||||
onChoiceChange?: () => void,
|
||||
onChoiceChange?: (?ChosenCategory) => void,
|
||||
allFilters: Filters,
|
||||
|};
|
||||
|
||||
@@ -70,19 +70,21 @@ const MemoizedTagsTree = React.memo<MemoizedTagsTreeProps>(function TagsTree({
|
||||
: ''
|
||||
}
|
||||
defaultExpanded={[]}
|
||||
onNodeSelect={() => onChoiceChange && onChoiceChange()}
|
||||
onNodeSelect={() => {}}
|
||||
>
|
||||
<TreeItem
|
||||
nodeId=""
|
||||
label={allItemsLabel}
|
||||
onLabelClick={() => {
|
||||
setChosenCategory(null);
|
||||
onChoiceChange && onChoiceChange();
|
||||
}}
|
||||
/>
|
||||
<TagsTreeItems
|
||||
tagsTreeNodes={allFilters.tagsTree}
|
||||
onChoose={setChosenCategory}
|
||||
onChoose={category => {
|
||||
setChosenCategory(category);
|
||||
if (onChoiceChange) onChoiceChange(category);
|
||||
}}
|
||||
parentNodes={[]}
|
||||
/>
|
||||
</TreeView>
|
||||
@@ -92,7 +94,7 @@ const MemoizedTagsTree = React.memo<MemoizedTagsTreeProps>(function TagsTree({
|
||||
type Props = {|
|
||||
allItemsLabel: React.Node,
|
||||
filtersState: FiltersState,
|
||||
onChoiceChange?: () => void,
|
||||
onChoiceChange?: (?ChosenCategory) => void,
|
||||
allFilters: ?Filters,
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
@@ -6,6 +6,18 @@ import TextField from '@material-ui/core/TextField';
|
||||
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
|
||||
import { computeTextFieldStyleProps } from './TextField';
|
||||
import { MarkdownText } from './MarkdownText';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
|
||||
const INVALID_VALUE = '';
|
||||
const stopPropagation = event => event.stopPropagation();
|
||||
|
||||
const useSelectCenterStyles = makeStyles({
|
||||
root: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export type SelectFieldInterface = {| focus: () => void |};
|
||||
|
||||
type ValueProps = {|
|
||||
value: number | string,
|
||||
@@ -32,6 +44,8 @@ type Props = {|
|
||||
},
|
||||
inputStyle?: {| fontSize?: 14, color?: string |},
|
||||
margin?: 'none' | 'dense',
|
||||
disableUnderline?: true,
|
||||
textAlign?: 'center',
|
||||
|
||||
floatingLabelText?: React.Node,
|
||||
helperMarkdownText?: ?string,
|
||||
@@ -41,11 +55,6 @@ type Props = {|
|
||||
hintText?: MessageDescriptor,
|
||||
|};
|
||||
|
||||
export type SelectFieldInterface = {| focus: () => void |};
|
||||
|
||||
const INVALID_VALUE = '';
|
||||
const stopPropagation = event => event.stopPropagation();
|
||||
|
||||
/**
|
||||
* A select field based on Material-UI select field.
|
||||
* To be used with `SelectOption`.
|
||||
@@ -61,6 +70,7 @@ const SelectField = React.forwardRef<Props, SelectFieldInterface>(
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus,
|
||||
}));
|
||||
const selectCenterStyles = useSelectCenterStyles();
|
||||
|
||||
const onChange = props.onChange || undefined;
|
||||
|
||||
@@ -105,12 +115,16 @@ const SelectField = React.forwardRef<Props, SelectFieldInterface>(
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
InputProps={{ style: props.inputStyle }}
|
||||
InputProps={{
|
||||
style: props.inputStyle,
|
||||
disableUnderline: !!props.disableUnderline,
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
SelectProps={{
|
||||
native: true,
|
||||
classes: props.textAlign === 'center' ? selectCenterStyles : {},
|
||||
}}
|
||||
style={props.style}
|
||||
inputRef={inputRef}
|
||||
@@ -129,4 +143,5 @@ const SelectField = React.forwardRef<Props, SelectFieldInterface>(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default SelectField;
|
||||
|
||||
@@ -8,10 +8,7 @@ import { User as FirebaseUser } from 'firebase/auth';
|
||||
import { type Profile } from '../Utils/GDevelopServices/Authentication';
|
||||
import { type Release } from '../Utils/GDevelopServices/Release';
|
||||
import { type Build } from '../Utils/GDevelopServices/Build';
|
||||
import {
|
||||
type ExtensionShortHeader,
|
||||
type SerializedExtension,
|
||||
} from '../Utils/GDevelopServices/Extension';
|
||||
import { type ExtensionShortHeader } from '../Utils/GDevelopServices/Extension';
|
||||
import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example';
|
||||
import { type Game, type ShowcasedGame } from '../Utils/GDevelopServices/Game';
|
||||
import { type GameMetrics } from '../Utils/GDevelopServices/Analytics';
|
||||
@@ -430,16 +427,16 @@ export const fakeAsset1: Asset = {
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
objectType: 'sprite',
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
objectAssets: [
|
||||
{
|
||||
@@ -457,16 +454,16 @@ export const fakeAssetWithBehaviorCustomizations1: Asset = {
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
objectType: 'sprite',
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
objectAssets: [
|
||||
{
|
||||
@@ -504,16 +501,16 @@ export const fakeAssetWithUnknownBehaviorCustomizations1: Asset = {
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
objectType: 'sprite',
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
objectAssets: [
|
||||
{
|
||||
@@ -540,16 +537,16 @@ export const fakeAssetWithFlashBehaviorCustomizations1: Asset = {
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
objectType: 'sprite',
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
version: '1.0.0',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
objectAssets: [
|
||||
{
|
||||
@@ -576,16 +573,16 @@ export const fakeAssetWithEventCustomizationsAndFlashExtension1: Asset = {
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
objectType: 'sprite',
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
version: '1.0.0',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
objectAssets: [
|
||||
{
|
||||
@@ -655,16 +652,16 @@ export const fakeAssetWithEventCustomizationsAndUnknownExtension1: Asset = {
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
description: "A very nice way to start a shoot'em up.",
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
objectType: 'sprite',
|
||||
gdevelopVersion: '5.0.0-beta100',
|
||||
authors: ['test author'],
|
||||
license: 'MIT',
|
||||
version: '1.0.0',
|
||||
tags: ['space shooter', 'tag2'],
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
objectAssets: [
|
||||
{
|
||||
@@ -708,48 +705,27 @@ export const fakeAssetWithEventCustomizationsAndUnknownExtension1: Asset = {
|
||||
};
|
||||
|
||||
export const fakeAssetShortHeader1: AssetShortHeader = {
|
||||
id: '123',
|
||||
name: 'My spaceship',
|
||||
shortDescription:
|
||||
'A spaceship that can be moved with the keyboard or by touching the screen',
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
tags: ['space shooter', 'tag2'],
|
||||
license: 'MIT',
|
||||
id: 'a4eb5460ffc062ece1f3ff45d24b07e40e9d4247d21602de70973049eb4f6ee5',
|
||||
license:
|
||||
'CC-BY 4.0 (Attribution to the artist is required, click for details)',
|
||||
animationsCount: 6,
|
||||
maxFramesCount: 6,
|
||||
height: 36,
|
||||
width: 36,
|
||||
name: 'Dino Doux',
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
};
|
||||
|
||||
export const fakeAssetShortHeader2: AssetShortHeader = {
|
||||
id: '456',
|
||||
name: 'Zombie',
|
||||
shortDescription: 'A zombie attacking the player and wandering around.',
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
tags: ['survival', 'tag2'],
|
||||
license: 'MIT',
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
dominantColors: [255],
|
||||
};
|
||||
|
||||
export const fakeAssetShortHeader3: AssetShortHeader = {
|
||||
id: '789',
|
||||
name: 'Sword',
|
||||
shortDescription: 'A small sword.',
|
||||
previewImageUrls: ['res/GD-logo.png'],
|
||||
tags: ['medieval', 'tag2'],
|
||||
license: 'MIT',
|
||||
objectType: 'sprite',
|
||||
animationsCount: 1,
|
||||
maxFramesCount: 1,
|
||||
width: 32,
|
||||
height: 32,
|
||||
previewImageUrls: [
|
||||
'https://resources.gdevelop-app.com/assets/24x24 Dino Characters/Dino Doux_Crouch.png',
|
||||
],
|
||||
shortDescription: 'with 6 animations',
|
||||
tags: [
|
||||
'24x24 dino characters',
|
||||
'side view',
|
||||
'pixel art',
|
||||
'character',
|
||||
'player',
|
||||
'enemy',
|
||||
],
|
||||
dominantColors: [255],
|
||||
};
|
||||
|
||||
|
||||
+31
-33
@@ -8,7 +8,7 @@ import { AssetDetails } from '../../../../AssetStore/AssetDetails';
|
||||
import { fakeAssetShortHeader1 } from '../../../../fixtures/GDevelopServicesTestData';
|
||||
import { testProject } from '../../../GDevelopJsInitializerDecorator';
|
||||
import fakeResourceExternalEditors from '../../../FakeResourceExternalEditors';
|
||||
const gd: libGDevelop = global.gd;
|
||||
import { AssetStoreStateProvider } from '../../../../AssetStore/AssetStoreContext';
|
||||
|
||||
export default {
|
||||
title: 'AssetStore/AssetStore/AssetDetails',
|
||||
@@ -17,39 +17,37 @@ export default {
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<AssetDetails
|
||||
canInstall={true}
|
||||
isBeingInstalled={false}
|
||||
onAdd={action('onAdd')}
|
||||
onClose={action('onClose')}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
<AssetStoreStateProvider>
|
||||
<AssetDetails
|
||||
project={testProject.project}
|
||||
layout={testProject.testLayout}
|
||||
objectsContainer={testProject.testLayout}
|
||||
resourceSources={[]}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onTagSelection={() => {}}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
onAdd={() => {}}
|
||||
onClose={() => {}}
|
||||
canInstall={true}
|
||||
isBeingInstalled={false}
|
||||
/>
|
||||
</AssetStoreStateProvider>
|
||||
);
|
||||
|
||||
export const BeingInstalled = () => (
|
||||
<AssetDetails
|
||||
canInstall={false}
|
||||
isBeingInstalled={true}
|
||||
onAdd={action('onAdd')}
|
||||
onClose={action('onClose')}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onChooseResource={() => {
|
||||
action('onChooseResource');
|
||||
return Promise.reject();
|
||||
}}
|
||||
resourceSources={[]}
|
||||
/>
|
||||
<AssetStoreStateProvider>
|
||||
<AssetDetails
|
||||
project={testProject.project}
|
||||
layout={testProject.testLayout}
|
||||
objectsContainer={testProject.testLayout}
|
||||
resourceSources={[]}
|
||||
resourceExternalEditors={fakeResourceExternalEditors}
|
||||
onTagSelection={() => {}}
|
||||
assetShortHeader={fakeAssetShortHeader1}
|
||||
onAdd={() => {}}
|
||||
onClose={() => {}}
|
||||
canInstall={true}
|
||||
isBeingInstalled={true}
|
||||
/>
|
||||
</AssetStoreStateProvider>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,11 @@ export const Default = () => (
|
||||
onOpenDetails={action('onOpenDetails')}
|
||||
events={testProject.testLayout.getEvents()}
|
||||
project={testProject.project}
|
||||
layout={testProject.testLayout}
|
||||
onChooseResource={() => Promise.reject('unimplemented')}
|
||||
resourceSources={[]}
|
||||
onObjectAddedFromAsset={() => {}}
|
||||
resourceExternalEditors={[]}
|
||||
objectsContainer={testProject.testLayout}
|
||||
/>
|
||||
</AssetStoreStateProvider>
|
||||
|
||||
Reference in New Issue
Block a user