mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Fix platform icons generation for cloud projects from desktop app (#6551)
This commit is contained in:
@@ -1,33 +1,13 @@
|
||||
//@flow
|
||||
import optionalRequire from '../Utils/OptionalRequire';
|
||||
|
||||
const fs = optionalRequire('fs-extra');
|
||||
|
||||
export const getImageFromPath = (path: string): Promise<HTMLImageElement> => {
|
||||
const imageElement = document.createElement('img');
|
||||
|
||||
const file = fs.readFileSync(path, { encoding: 'base64' });
|
||||
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
imageElement.addEventListener('error', (event: Event) => {
|
||||
reject(event);
|
||||
});
|
||||
imageElement.addEventListener('load', () => {
|
||||
resolve(imageElement);
|
||||
});
|
||||
imageElement.src = `data:image/png;base64,${file}`;
|
||||
});
|
||||
};
|
||||
|
||||
export const resizeImage = (
|
||||
image: HTMLImageElement,
|
||||
outputFile: string,
|
||||
imageAsBlobDataUrl: string,
|
||||
{
|
||||
width,
|
||||
height,
|
||||
transparentBorderSize = 0,
|
||||
}: {| width: number, height: number, transparentBorderSize?: number |}
|
||||
): Promise<boolean> => {
|
||||
): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvasElement = document.createElement('canvas');
|
||||
canvasElement.width = width;
|
||||
@@ -36,20 +16,27 @@ export const resizeImage = (
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
const image = new Image();
|
||||
image.addEventListener('load', () => {
|
||||
try {
|
||||
ctx.drawImage(
|
||||
image,
|
||||
transparentBorderSize,
|
||||
transparentBorderSize,
|
||||
width - 2 * transparentBorderSize,
|
||||
height - 2 * transparentBorderSize
|
||||
);
|
||||
|
||||
ctx.drawImage(
|
||||
image,
|
||||
transparentBorderSize,
|
||||
transparentBorderSize,
|
||||
width - 2 * transparentBorderSize,
|
||||
height - 2 * transparentBorderSize
|
||||
);
|
||||
|
||||
canvasElement.toBlob(blob => {
|
||||
blob.arrayBuffer().then(buffer => {
|
||||
fs.writeFileSync(outputFile, Buffer.from(buffer));
|
||||
resolve(true);
|
||||
});
|
||||
}, 'image/png');
|
||||
canvasElement.toBlob(blob => {
|
||||
resolve(URL.createObjectURL(blob));
|
||||
}, 'image/png');
|
||||
} catch (error) {
|
||||
reject('An error occurred while generating an icon');
|
||||
}
|
||||
});
|
||||
image.addEventListener('error', (e: Event) => {
|
||||
reject('An error occurred while loading the input image');
|
||||
});
|
||||
image.src = imageAsBlobDataUrl;
|
||||
});
|
||||
};
|
||||
|
@@ -5,21 +5,18 @@ import { I18n } from '@lingui/react';
|
||||
|
||||
import * as React from 'react';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Dialog, { DialogPrimaryButton } from '../UI/Dialog';
|
||||
import { Line } from '../UI/Grid';
|
||||
import ResourcesLoader from '../ResourcesLoader';
|
||||
import ResourceSelectorWithThumbnail from '../ResourcesList/ResourceSelectorWithThumbnail';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import { getImageFromPath, resizeImage } from './ImageResizer';
|
||||
import { resizeImage } from './ImageResizer';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import optionalRequire from '../Utils/OptionalRequire';
|
||||
import Text from '../UI/Text';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import ErrorBoundary from '../UI/ErrorBoundary';
|
||||
|
||||
const path = optionalRequire('path');
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
@@ -70,6 +67,8 @@ const iosSizes = [
|
||||
];
|
||||
|
||||
class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
||||
inputRef: HTMLInputElement | null = null;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = this._loadFrom(props.project);
|
||||
@@ -105,118 +104,108 @@ class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_generateFromFile = async () => {
|
||||
const { project, resourceManagementProps } = this.props;
|
||||
|
||||
const resourceSource = resourceManagementProps.resourceSources
|
||||
.filter(source => source.kind === 'image')
|
||||
.filter(source => source.name.startsWith('local-file-opener'))[0];
|
||||
|
||||
if (!resourceSource) {
|
||||
throw new Error(
|
||||
'No supported resource source - only local files are supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const resources = await resourceManagementProps.onChooseResource({
|
||||
initialSourceName: resourceSource.name,
|
||||
multiSelection: false,
|
||||
resourceKind: 'image',
|
||||
});
|
||||
|
||||
if (!resources.length || !path) {
|
||||
_generateFromFile = async e => {
|
||||
if (!this.inputRef || !this.inputRef.files || !this.inputRef.files[0]) {
|
||||
console.error("Could't find selected file. Aborting icon generation.");
|
||||
return;
|
||||
}
|
||||
const chosenFileAsBlobDataUrl = URL.createObjectURL(this.inputRef.files[0]);
|
||||
const { project, resourceManagementProps } = this.props;
|
||||
|
||||
const resourcesManager = project.getResourcesManager();
|
||||
const projectPath = path.dirname(project.getProjectFile());
|
||||
const fullPath = path.resolve(projectPath, resources[0].getFile());
|
||||
|
||||
const image = await getImageFromPath(fullPath);
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
...desktopSizes.map(async size => ({
|
||||
resourceName: `desktop-icon-${size}.png`,
|
||||
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||
width: size,
|
||||
height: size,
|
||||
}),
|
||||
})),
|
||||
...androidSizes.map(async size => ({
|
||||
resourceName: `android-icon-${size}.png`,
|
||||
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||
width: size,
|
||||
height: size,
|
||||
}),
|
||||
})),
|
||||
(async () => ({
|
||||
resourceName: 'android-windowSplashScreenAnimatedIcon.png',
|
||||
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||
width: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
||||
height: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
||||
transparentBorderSize:
|
||||
androidWindowSplashScreenAnimatedIconRecommendedSize / 6,
|
||||
}),
|
||||
}))(),
|
||||
...iosSizes.map(async size => ({
|
||||
resourceName: `ios-icon-${size}.png`,
|
||||
blobDataUrl: await resizeImage(chosenFileAsBlobDataUrl, {
|
||||
width: size,
|
||||
height: size,
|
||||
}),
|
||||
})),
|
||||
]);
|
||||
|
||||
// Important, we are responsible for deleting the resources that were given to us.
|
||||
// Otherwise we have a memory leak.
|
||||
resources.forEach(resource => resource.delete());
|
||||
results.forEach(({ resourceName, blobDataUrl }) => {
|
||||
const metadata = JSON.stringify({
|
||||
extension: '.png',
|
||||
// Used in local app to define the path where to save the image on
|
||||
// the file system.
|
||||
localFilePath: `./assets/${resourceName}`,
|
||||
});
|
||||
if (!resourcesManager.hasResource(resourceName)) {
|
||||
const imageResource = new gd.ImageResource();
|
||||
imageResource.setFile(blobDataUrl);
|
||||
imageResource.setName(resourceName);
|
||||
imageResource.setMetadata(metadata);
|
||||
|
||||
const results = await Promise.all([
|
||||
...desktopSizes.map(size =>
|
||||
resizeImage(image, path.join(projectPath, `desktop-icon-${size}.png`), {
|
||||
width: size,
|
||||
height: size,
|
||||
})
|
||||
),
|
||||
...androidSizes.map(size =>
|
||||
resizeImage(image, path.join(projectPath, `android-icon-${size}.png`), {
|
||||
width: size,
|
||||
height: size,
|
||||
})
|
||||
),
|
||||
resizeImage(
|
||||
image,
|
||||
path.join(projectPath, 'android-windowSplashScreenAnimatedIcon.png'),
|
||||
{
|
||||
width: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
||||
height: androidWindowSplashScreenAnimatedIconRecommendedSize,
|
||||
transparentBorderSize:
|
||||
androidWindowSplashScreenAnimatedIconRecommendedSize / 6,
|
||||
resourcesManager.addResource(imageResource);
|
||||
|
||||
// Important, we are responsible for deleting the resources that we created
|
||||
// Otherwise we have a memory leak, as calling addResource is making a copy of the resource.
|
||||
imageResource.delete();
|
||||
} else {
|
||||
resourcesManager.getResource(resourceName).setFile(blobDataUrl);
|
||||
resourcesManager.getResource(resourceName).setMetadata(metadata);
|
||||
}
|
||||
),
|
||||
...iosSizes.map(size =>
|
||||
resizeImage(image, path.join(projectPath, `ios-icon-${size}.png`), {
|
||||
width: size,
|
||||
height: size,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
if (results.indexOf(false) !== -1) {
|
||||
await resourceManagementProps.onFetchNewlyAddedResources();
|
||||
|
||||
// Make sure the resources are (re)loaded.
|
||||
ResourcesLoader.burstUrlsCacheForResources(
|
||||
project,
|
||||
results.map(({ resourceName }) => resourceName)
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
desktopIconResourceNames: desktopSizes.map(
|
||||
size => `desktop-icon-${size}.png`
|
||||
),
|
||||
androidIconResourceNames: androidSizes.map(
|
||||
size => `android-icon-${size}.png`
|
||||
),
|
||||
androidWindowSplashScreenAnimatedIconResourceName:
|
||||
'android-windowSplashScreenAnimatedIcon.png',
|
||||
iosIconResourceNames: iosSizes.map(size => `ios-icon-${size}.png`),
|
||||
});
|
||||
}, 200 /* Let a bit of time so that image files can be found */);
|
||||
} catch (e) {
|
||||
showErrorBox({
|
||||
message: 'Some icons could not be generated!',
|
||||
message: 'Some icons could not be generated.',
|
||||
rawError: undefined,
|
||||
errorId: 'icon-generation-error',
|
||||
doNotReport: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Add resources to the game
|
||||
const allResourcesNames = [
|
||||
...desktopSizes.map(size => `desktop-icon-${size}.png`),
|
||||
...androidSizes.map(size => `android-icon-${size}.png`),
|
||||
'android-windowSplashScreenAnimatedIcon.png',
|
||||
...iosSizes.map(size => `ios-icon-${size}.png`),
|
||||
];
|
||||
allResourcesNames.forEach(resourceName => {
|
||||
if (!resourcesManager.hasResource(resourceName)) {
|
||||
const imageResource = new gd.ImageResource();
|
||||
imageResource.setFile(resourceName);
|
||||
imageResource.setName(resourceName);
|
||||
|
||||
resourcesManager.addResource(imageResource);
|
||||
|
||||
// Important, we are responsible for deleting the resources that we created
|
||||
// Otherwise we have a memory leak, as calling addResource is making a copy of the resource.
|
||||
imageResource.delete();
|
||||
} else {
|
||||
resourcesManager.getResource(resourceName).setFile(resourceName);
|
||||
} finally {
|
||||
// Reset input value so that if the user selects the same file again,
|
||||
// the onChange callback is called.
|
||||
if (this.inputRef) {
|
||||
this.inputRef.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the resources are (re)loaded.
|
||||
ResourcesLoader.burstUrlsCacheForResources(project, allResourcesNames);
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
desktopIconResourceNames: desktopSizes.map(
|
||||
size => `desktop-icon-${size}.png`
|
||||
),
|
||||
androidIconResourceNames: androidSizes.map(
|
||||
size => `android-icon-${size}.png`
|
||||
),
|
||||
androidWindowSplashScreenAnimatedIconResourceName:
|
||||
'android-windowSplashScreenAnimatedIcon.png',
|
||||
iosIconResourceNames: iosSizes.map(size => `ios-icon-${size}.png`),
|
||||
});
|
||||
}, 200 /* Let a bit of time so that image files can be found */);
|
||||
}
|
||||
};
|
||||
|
||||
onApply = () => {
|
||||
@@ -297,22 +286,20 @@ class PlatformSpecificAssetsDialog extends React.Component<Props, State> {
|
||||
onApply={this.onApply}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<Line justifyContent="center" noMargin>
|
||||
{!!path ? (
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Generate icons from a file</Trans>}
|
||||
onClick={this._generateFromFile}
|
||||
/>
|
||||
) : (
|
||||
<Text>
|
||||
<Trans>
|
||||
Download GDevelop desktop version to generate the Android and
|
||||
iOS icons of your game.
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout
|
||||
alignItems="center"
|
||||
noMargin
|
||||
noColumnMargin
|
||||
>
|
||||
<Text noMargin>
|
||||
<Trans>Generate icons from a file:</Trans>
|
||||
</Text>
|
||||
<input
|
||||
type="file"
|
||||
onChange={this._generateFromFile}
|
||||
ref={_inputRef => (this.inputRef = _inputRef)}
|
||||
/>
|
||||
</ResponsiveLineStackLayout>
|
||||
<Text size="sub-title">
|
||||
<Trans>gd.games thumbnail</Trans>
|
||||
</Text>
|
||||
|
Reference in New Issue
Block a user