mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-12 02:02:42 +00:00
Allow extensions made in the editor to specify dependencies on Cordova libraries or NPM packages (#2393)
Only show in developer changelog
This commit is contained in:
@@ -105,14 +105,15 @@ class GD_CORE_API DependencyMetadata {
|
||||
* \brief Mark the dependency to be included in the export only if one other
|
||||
* dependency is included in the export.
|
||||
*/
|
||||
DependencyMetadata& OnlyIfOtherDependencyIsExported(const gd::String& otherDependency) {
|
||||
DependencyMetadata& OnlyIfOtherDependencyIsExported(
|
||||
const gd::String& otherDependency) {
|
||||
onlyIfOtherDependencyIsExported = otherDependency;
|
||||
return *this;
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Get the name of another dependency that must be exported to have this
|
||||
* one also exported.
|
||||
* \brief Get the name of another dependency that must be exported to have
|
||||
* this one also exported.
|
||||
*/
|
||||
const gd::String& GetOtherDependencyThatMustBeExported() const {
|
||||
return onlyIfOtherDependencyIsExported;
|
||||
@@ -132,6 +133,10 @@ class GD_CORE_API DependencyMetadata {
|
||||
return extraData;
|
||||
}
|
||||
|
||||
void CopyFrom(const DependencyMetadata& dependencyMetadata) {
|
||||
*this = dependencyMetadata;
|
||||
}
|
||||
|
||||
private:
|
||||
gd::String name; ///< The name of the dependency.
|
||||
gd::String exportName; ///< The name used to install the package (example:
|
||||
@@ -144,7 +149,7 @@ class GD_CORE_API DependencyMetadata {
|
||||
bool onlyIfSomeExtraSettingsNonEmpty; ///< If true, only use this dependency
|
||||
///< if at least one of the extra
|
||||
///< settings is set.
|
||||
gd::String onlyIfOtherDependencyIsExported;
|
||||
gd::String onlyIfOtherDependencyIsExported;
|
||||
};
|
||||
} // namespace gd
|
||||
#endif // DEPENDENCYMETADATA_H
|
||||
|
||||
@@ -59,6 +59,10 @@ void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const {
|
||||
element.SetAttribute("previewIconUrl", previewIconUrl);
|
||||
element.SetAttribute("iconUrl", iconUrl);
|
||||
element.SetAttribute("helpPath", helpPath);
|
||||
auto& dependenciesElement = element.AddChild("dependencies");
|
||||
dependenciesElement.ConsiderAsArray();
|
||||
for (auto& dependency : dependencies)
|
||||
SerializeDependencyTo(dependency, dependenciesElement.AddChild(""));
|
||||
|
||||
SerializeEventsFunctionsTo(element.AddChild("eventsFunctions"));
|
||||
eventsBasedBehaviors.SerializeElementsTo(
|
||||
@@ -95,6 +99,13 @@ void EventsFunctionsExtension::UnserializeFrom(
|
||||
}
|
||||
}
|
||||
|
||||
dependencies.clear();
|
||||
const auto& dependenciesElement = element.GetChild("dependencies");
|
||||
dependenciesElement.ConsiderAsArray();
|
||||
for (size_t i = 0; i < dependenciesElement.GetChildrenCount(); ++i)
|
||||
dependencies.push_back(
|
||||
UnserializeDependencyFrom(dependenciesElement.GetChild(i)));
|
||||
|
||||
UnserializeEventsFunctionsFrom(project, element.GetChild("eventsFunctions"));
|
||||
eventsBasedBehaviors.UnserializeElementsFrom(
|
||||
"eventsBasedBehavior", project, element.GetChild("eventsBasedBehaviors"));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "GDCore/Extensions/Metadata/DependencyMetadata.h"
|
||||
#include "GDCore/Project/EventsBasedBehavior.h"
|
||||
#include "GDCore/Project/EventsFunctionsContainer.h"
|
||||
#include "GDCore/String.h"
|
||||
@@ -136,6 +137,35 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
return eventsBasedBehaviors;
|
||||
}
|
||||
|
||||
/** \name Dependencies
|
||||
*/
|
||||
///@{
|
||||
|
||||
/**
|
||||
* \brief Adds a new dependency.
|
||||
*/
|
||||
gd::DependencyMetadata& AddDependency() {
|
||||
gd::DependencyMetadata dependency;
|
||||
dependencies.push_back(dependency);
|
||||
return dependencies.back();
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Adds a new dependency.
|
||||
*/
|
||||
void RemoveDependencyAt(size_t index) {
|
||||
dependencies.erase(dependencies.begin() + index);
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Returns the list of dependencies.
|
||||
*/
|
||||
std::vector<gd::DependencyMetadata>& GetAllDependencies() {
|
||||
return dependencies;
|
||||
};
|
||||
|
||||
///@}
|
||||
|
||||
/** \name Serialization
|
||||
*/
|
||||
///@{
|
||||
@@ -165,6 +195,24 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
*/
|
||||
void Init(const gd::EventsFunctionsExtension& other);
|
||||
|
||||
void SerializeDependencyTo(const gd::DependencyMetadata& dependency,
|
||||
gd::SerializerElement& serializer) const {
|
||||
serializer.SetStringAttribute("type", dependency.GetDependencyType());
|
||||
serializer.SetStringAttribute("exportName", dependency.GetExportName());
|
||||
serializer.SetStringAttribute("name", dependency.GetName());
|
||||
serializer.SetStringAttribute("version", dependency.GetVersion());
|
||||
}
|
||||
|
||||
gd::DependencyMetadata UnserializeDependencyFrom(
|
||||
gd::SerializerElement& serializer) {
|
||||
gd::DependencyMetadata dependency;
|
||||
dependency.SetDependencyType(serializer.GetStringAttribute("type"));
|
||||
dependency.SetExportName(serializer.GetStringAttribute("exportName"));
|
||||
dependency.SetName(serializer.GetStringAttribute("name"));
|
||||
dependency.SetVersion(serializer.GetStringAttribute("version"));
|
||||
return dependency;
|
||||
}
|
||||
|
||||
gd::String version;
|
||||
gd::String extensionNamespace;
|
||||
gd::String shortDescription;
|
||||
@@ -178,6 +226,7 @@ class GD_CORE_API EventsFunctionsExtension : public EventsFunctionsContainer {
|
||||
gd::String helpPath; ///< The relative path to the help for this extension in
|
||||
///< the documentation (or an absolute URL).
|
||||
gd::SerializableWithNameList<EventsBasedBehavior> eventsBasedBehaviors;
|
||||
std::vector<gd::DependencyMetadata> dependencies;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
||||
@@ -1100,6 +1100,8 @@ interface DependencyMetadata {
|
||||
|
||||
[Ref] DependencyMetadata OnlyIfSomeExtraSettingsNonEmpty();
|
||||
[Ref] DependencyMetadata OnlyIfOtherDependencyIsExported([Const] DOMString otherDependency);
|
||||
|
||||
void CopyFrom([Const, Ref] DependencyMetadata dependencyMetadata);
|
||||
};
|
||||
|
||||
interface ParameterMetadata {
|
||||
@@ -2049,6 +2051,10 @@ interface EventsFunctionsExtension {
|
||||
[Ref] EventsFunctionsExtension SetHelpPath([Const] DOMString helpPath);
|
||||
[Const, Ref] DOMString GetHelpPath();
|
||||
|
||||
[Ref] DependencyMetadata AddDependency();
|
||||
void RemoveDependencyAt(unsigned long index);
|
||||
[Ref] VectorDependencyMetadata GetAllDependencies();
|
||||
|
||||
[Ref] EventsBasedBehaviorsList GetEventsBasedBehaviors();
|
||||
|
||||
void SerializeTo([Ref] SerializerElement element);
|
||||
|
||||
@@ -13,6 +13,7 @@ declare class gdDependencyMetadata {
|
||||
getAllExtraSettings(): gdMapStringPropertyDescriptor;
|
||||
onlyIfSomeExtraSettingsNonEmpty(): gdDependencyMetadata;
|
||||
onlyIfOtherDependencyIsExported(otherDependency: string): gdDependencyMetadata;
|
||||
copyFrom(dependencyMetadata: gdDependencyMetadata): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
||||
@@ -22,6 +22,9 @@ declare class gdEventsFunctionsExtension extends gdEventsFunctionsContainer {
|
||||
getIconUrl(): string;
|
||||
setHelpPath(helpPath: string): gdEventsFunctionsExtension;
|
||||
getHelpPath(): string;
|
||||
addDependency(): gdDependencyMetadata;
|
||||
removeDependencyAt(index: number): void;
|
||||
getAllDependencies(): gdVectorDependencyMetadata;
|
||||
getEventsBasedBehaviors(): gdEventsBasedBehaviorsList;
|
||||
serializeTo(element: gdSerializerElement): void;
|
||||
unserializeFrom(project: gdProject, element: gdSerializerElement): void;
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import * as React from 'react';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import TextField from '../UI/TextField';
|
||||
import CloudUpload from '@material-ui/icons/CloudUpload';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import Dialog from '../UI/Dialog';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
import EventsFunctionsExtensionsContext, {
|
||||
type EventsFunctionsExtensionsState,
|
||||
} from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Window from '../Utils/Window';
|
||||
import Text from '../UI/Text';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { ResourceStore } from '../AssetStore/ResourceStore';
|
||||
import { ColumnStackLayout, TextFieldWithButtonLayout } from '../UI/Layout';
|
||||
import axios from 'axios';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
import { useIsMounted } from '../Utils/UseIsMounted';
|
||||
import { showErrorBox } from '../UI/Messages/MessageBox';
|
||||
import {
|
||||
getHelpLink,
|
||||
isRelativePathToDocumentationRoot,
|
||||
isDocumentationAbsoluteUrl,
|
||||
} from '../Utils/HelpLink';
|
||||
|
||||
type Props = {|
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
onClose: () => void,
|
||||
open: boolean,
|
||||
|};
|
||||
|
||||
const exportExtension = (
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension
|
||||
) => {
|
||||
const eventsFunctionsExtensionWriter = eventsFunctionsExtensionsState.getEventsFunctionsExtensionWriter();
|
||||
if (!eventsFunctionsExtensionWriter)
|
||||
return Promise.reject(new Error('Not supported'));
|
||||
|
||||
return eventsFunctionsExtensionWriter
|
||||
.chooseEventsFunctionExtensionFile()
|
||||
.then(pathOrUrl => {
|
||||
if (!pathOrUrl) return;
|
||||
|
||||
eventsFunctionsExtensionWriter
|
||||
.writeEventsFunctionsExtension(eventsFunctionsExtension, pathOrUrl)
|
||||
.then();
|
||||
});
|
||||
};
|
||||
|
||||
const downloadSvgAsBase64 = async (url: string): Promise<string> => {
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
|
||||
const image = btoa(
|
||||
new Uint8Array(response.data).reduce(
|
||||
(data, byte) => data + String.fromCharCode(byte),
|
||||
''
|
||||
)
|
||||
);
|
||||
if (image.length > 100 * 1024) {
|
||||
throw new Error(
|
||||
`Icon is too big (size after base64 conversion: ${image.length})`
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = response.headers
|
||||
? response.headers['content-type'].toLowerCase()
|
||||
: '';
|
||||
if (contentType !== 'image/svg+xml')
|
||||
throw new Error(
|
||||
`Wrong content type. Got: "${contentType}", expected "image/svg+xml"`
|
||||
);
|
||||
|
||||
return `data:${contentType};base64,${image}`;
|
||||
} catch (err) {
|
||||
console.error('Unable to import the icon.', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const openGitHubIssue = () => {
|
||||
const body = `
|
||||
**⚠️ Please edit and complete this before submitting your extension:**
|
||||
|
||||
## Describe the extension
|
||||
A clear and concise description of what the extension is, how useful it is.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Extension has a proper name and description.
|
||||
- [ ] Extension has tags (for example: "platform, brick, breakable").
|
||||
- [ ] All behaviors have a description.
|
||||
- [ ] All functions (actions, conditions, expressions) have descriptions.
|
||||
- [ ] I confirm that this extension can be intergrated to this GitHub repository, distributed and MIT licensed.
|
||||
|
||||
## Extension file
|
||||
|
||||
Finally, attach the .json file of your extension here.
|
||||
|
||||
You also may have to create an account on GitHub before posting.
|
||||
If your extension is high quality and useful, it will be added to the list of GDevelop community extensions.
|
||||
When you're ready, remove this last paragraph and click on "Submit new issue". Thanks 🙌`;
|
||||
Window.openExternalURL(
|
||||
`https://github.com/4ian/GDevelop-extensions/issues/new?body=${encodeURIComponent(
|
||||
body
|
||||
)}&title=New%20extension`
|
||||
);
|
||||
};
|
||||
|
||||
type HelpPathTextFieldProps = {|
|
||||
i18n: I18nType,
|
||||
helpPath: string,
|
||||
onChangeHelpPath: string => void,
|
||||
|};
|
||||
|
||||
const HelpPathTextField = ({
|
||||
i18n,
|
||||
helpPath,
|
||||
onChangeHelpPath,
|
||||
}: HelpPathTextFieldProps) => {
|
||||
const isAbsoluteUrl = isDocumentationAbsoluteUrl(helpPath);
|
||||
const isRelativePath = isRelativePathToDocumentationRoot(helpPath);
|
||||
const helperText = helpPath
|
||||
? (isRelativePath
|
||||
? i18n._(
|
||||
t`This is a relative path that will open in the GDevelop wiki.`
|
||||
)
|
||||
: i18n._(t`This is link to a webpage.`)) +
|
||||
` [${i18n._(t`Click here to test the link.`)}](${getHelpLink(helpPath)})`
|
||||
: i18n._(
|
||||
t`This can either be a URL to a web page, or a path starting with a slash that will be opened in the GDevelop wiki. Leave empty if there is no help page, although it's recommended you eventually write one if you distribute the extension.`
|
||||
);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Help page URL</Trans>}
|
||||
value={helpPath}
|
||||
onChange={(e, text) => {
|
||||
onChangeHelpPath(text);
|
||||
}}
|
||||
errorText={
|
||||
!!helpPath && !isAbsoluteUrl && !isRelativePath ? (
|
||||
<Trans>
|
||||
This is not a URL starting with "http://" or "https://".
|
||||
</Trans>
|
||||
) : null
|
||||
}
|
||||
helperMarkdownText={helperText}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default function OptionsEditorDialog({
|
||||
eventsFunctionsExtension,
|
||||
onClose,
|
||||
open,
|
||||
}: Props) {
|
||||
const [exportDialogOpen, setExportDialogOpen] = React.useState(false);
|
||||
const [resourceStoreOpen, setResourceStoreOpen] = React.useState(false);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const forceUpdate = useForceUpdate();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
const eventsFunctionsExtensionWriter = eventsFunctionsExtensionsState.getEventsFunctionsExtensionWriter();
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Dialog
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/extensions/create" />,
|
||||
eventsFunctionsExtensionWriter ? (
|
||||
<FlatButton
|
||||
icon={<CloudUpload />}
|
||||
key="export"
|
||||
label={<Trans>Export extension</Trans>}
|
||||
onClick={() => {
|
||||
setExportDialogOpen(true);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
) : null,
|
||||
]}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={true}
|
||||
keyboardFocused={true}
|
||||
onClick={() => onClose()}
|
||||
disabled={isLoading}
|
||||
key={'close'}
|
||||
/>,
|
||||
]}
|
||||
cannotBeDismissed={true}
|
||||
open={open}
|
||||
title={<Trans>Edit Extension Options</Trans>}
|
||||
onRequestClose={isLoading ? () => {} : onClose}
|
||||
>
|
||||
<ColumnStackLayout noMargin>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Name</Trans>}
|
||||
value={eventsFunctionsExtension.getName()}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
<TextFieldWithButtonLayout
|
||||
renderButton={style => (
|
||||
<RaisedButton
|
||||
onClick={() => {
|
||||
setResourceStoreOpen(true);
|
||||
}}
|
||||
primary
|
||||
label={<Trans>Choose</Trans>}
|
||||
disabled={isLoading}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
renderTextField={() => (
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Icon URL</Trans>}
|
||||
value={eventsFunctionsExtension.getPreviewIconUrl()}
|
||||
onChange={text => {
|
||||
eventsFunctionsExtension.setPreviewIconUrl(text);
|
||||
}}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Name displayed in editor</Trans>}
|
||||
value={eventsFunctionsExtension.getFullName()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setFullName(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Short description</Trans>}
|
||||
value={eventsFunctionsExtension.getShortDescription()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setShortDescription(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={2}
|
||||
rowsMax={2}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={
|
||||
<Trans>Description (markdown supported)</Trans>
|
||||
}
|
||||
value={eventsFunctionsExtension.getDescription()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setDescription(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={5}
|
||||
rowsMax={5}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Version</Trans>}
|
||||
value={eventsFunctionsExtension.getVersion()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setVersion(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Tags (comma separated)</Trans>}
|
||||
value={eventsFunctionsExtension
|
||||
.getTags()
|
||||
.toJSArray()
|
||||
.join(', ')}
|
||||
onChange={text => {
|
||||
const tags = eventsFunctionsExtension.getTags();
|
||||
tags.clear();
|
||||
text
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(Boolean)
|
||||
.forEach(tag => {
|
||||
tags.push_back(tag);
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<HelpPathTextField
|
||||
i18n={i18n}
|
||||
helpPath={eventsFunctionsExtension.getHelpPath()}
|
||||
onChangeHelpPath={helpPath => {
|
||||
eventsFunctionsExtension.setHelpPath(helpPath);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={
|
||||
<Trans>Author (Name, email or GitHub handle)</Trans>
|
||||
}
|
||||
value={eventsFunctionsExtension.getAuthor()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setAuthor(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
{exportDialogOpen && (
|
||||
<Dialog
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/extensions/share" />,
|
||||
]}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
keyboardFocused={true}
|
||||
onClick={() => {
|
||||
setExportDialogOpen(false);
|
||||
}}
|
||||
key={'close'}
|
||||
/>,
|
||||
]}
|
||||
open
|
||||
cannotBeDismissed={false}
|
||||
onRequestClose={() => {
|
||||
setExportDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Column expand>
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can export the extension to a file to easily import it
|
||||
in another project. If your extension is providing useful
|
||||
and reusable functions or behaviors, consider sharing it
|
||||
with the GDevelop community!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<RaisedButton
|
||||
icon={<CloudUpload />}
|
||||
primary
|
||||
label={<Trans>Export to a file</Trans>}
|
||||
onClick={() => {
|
||||
exportExtension(
|
||||
eventsFunctionsExtensionsState,
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FlatButton
|
||||
label={<Trans>Submit extension to the community</Trans>}
|
||||
onClick={openGitHubIssue}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</Dialog>
|
||||
)}
|
||||
{resourceStoreOpen && (
|
||||
<Dialog
|
||||
title={<Trans>Choose an icon for the extension</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
primary={false}
|
||||
onClick={() => {
|
||||
setResourceStoreOpen(false);
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
noMargin
|
||||
>
|
||||
<ResourceStore
|
||||
onChoose={resource => {
|
||||
setResourceStoreOpen(false);
|
||||
setIsLoading(true);
|
||||
downloadSvgAsBase64(resource.url)
|
||||
.then(
|
||||
base64Svg => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
eventsFunctionsExtension.setPreviewIconUrl(
|
||||
resource.url
|
||||
);
|
||||
eventsFunctionsExtension.setIconUrl(base64Svg);
|
||||
},
|
||||
rawError => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
showErrorBox({
|
||||
message: i18n._(
|
||||
t`Unable to download the icon. Verify your internet connection or try again later.`
|
||||
),
|
||||
rawError,
|
||||
errorId: 'icon-download-error',
|
||||
});
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
setIsLoading(false);
|
||||
});
|
||||
}}
|
||||
resourceKind={'svg'}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</Dialog>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
//@flow
|
||||
import React from 'react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableContainer from '@material-ui/core/TableContainer';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import IconButton from '../../UI/IconButton';
|
||||
import Add from '@material-ui/icons/Add';
|
||||
import Delete from '@material-ui/icons/Delete';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import SelectField from '../../UI/SelectField';
|
||||
import SelectOption from '../../UI/SelectOption';
|
||||
import { Line, Column } from '../../UI/Grid';
|
||||
|
||||
import { mapVector } from '../../Utils/MapFor';
|
||||
import newNameGenerator from '../../Utils/NewNameGenerator';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import BackgroundText from '../../UI/BackgroundText';
|
||||
import { showWarningBox } from '../../UI/Messages/MessageBox';
|
||||
|
||||
const checkNameExists = (
|
||||
name: string,
|
||||
deps: gdVectorDependencyMetadata
|
||||
): boolean => {
|
||||
for (let i = 0; i < deps.size(); i++)
|
||||
if (deps.at(i).getName() === name) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
type Props = {| eventsFunctionsExtension: gdEventsFunctionsExtension |};
|
||||
|
||||
export const ExtensionDependenciesEditor = ({
|
||||
eventsFunctionsExtension,
|
||||
}: Props) => {
|
||||
const deps = eventsFunctionsExtension.getAllDependencies();
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const addDependency = () => {
|
||||
eventsFunctionsExtension
|
||||
.addDependency()
|
||||
.setName(
|
||||
newNameGenerator('New Dependency', (newName) =>
|
||||
checkNameExists(newName, deps)
|
||||
)
|
||||
)
|
||||
.setExportName('my-dependency')
|
||||
.setVersion('1.0.0')
|
||||
.setDependencyType('cordova');
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Line expand>
|
||||
<TableContainer
|
||||
component={({ children }) => (
|
||||
<Paper elevation={4} style={{ minWidth: '100%' }}>
|
||||
{children}
|
||||
</Paper>
|
||||
)}
|
||||
>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Trans>Name</Trans>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Trans>Export name</Trans>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Trans>Version</Trans>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Trans>Dependency type</Trans>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Trans>Action</Trans>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{mapVector<gdDependencyMetadata, TableRow>(
|
||||
eventsFunctionsExtension.getAllDependencies(),
|
||||
(dependency, index) => (
|
||||
<TableRow key={dependency.getName()}>
|
||||
<TableCell>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
value={dependency.getName()}
|
||||
onChange={(newName) => {
|
||||
if (checkNameExists(newName, deps))
|
||||
showWarningBox(
|
||||
`This name is already in use! Please use a unique name.`,
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
else {
|
||||
eventsFunctionsExtension
|
||||
.getAllDependencies()
|
||||
.at(index)
|
||||
.setName(newName);
|
||||
forceUpdate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
value={dependency.getExportName()}
|
||||
onChange={(newExportName) => {
|
||||
dependency.setExportName(newExportName);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
value={dependency.getVersion()}
|
||||
onChange={(newVersion) => {
|
||||
dependency.setVersion(newVersion);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<SelectField
|
||||
value={dependency.getDependencyType()}
|
||||
onChange={(_, __, newType) => {
|
||||
dependency.setDependencyType(newType);
|
||||
forceUpdate();
|
||||
}}
|
||||
>
|
||||
<SelectOption value="npm" primaryText={t`NPM`} />
|
||||
<SelectOption
|
||||
value="cordova"
|
||||
primaryText={t`Cordova`}
|
||||
/>
|
||||
</SelectField>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
tooltip={t`Remove`}
|
||||
onClick={() => {
|
||||
eventsFunctionsExtension.removeDependencyAt(index);
|
||||
forceUpdate();
|
||||
}}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<FlatButton
|
||||
icon={<Add />}
|
||||
label={<Trans>Add</Trans>}
|
||||
onClick={addDependency}
|
||||
/>
|
||||
</TableContainer>
|
||||
</Line>
|
||||
<Line>
|
||||
<BackgroundText>
|
||||
<Trans>
|
||||
Dependencies allow to add additional libraries in the exported
|
||||
games. NPM dependencies will be included for Electron builds
|
||||
(Windows, macOS, Linux) and Cordova dependencies will be included
|
||||
for Cordova builds (Android, iOS). Note that this is intended for
|
||||
usage in JavaScript events only. If you are only using standard
|
||||
events, you should not worry about this.
|
||||
</Trans>
|
||||
</BackgroundText>
|
||||
</Line>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
//@flow
|
||||
import React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
|
||||
import TextField from '../../UI/TextField';
|
||||
import { ColumnStackLayout, TextFieldWithButtonLayout } from '../../UI/Layout';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import { ResourceStore } from '../../AssetStore/ResourceStore';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import {
|
||||
getHelpLink,
|
||||
isRelativePathToDocumentationRoot,
|
||||
isDocumentationAbsoluteUrl,
|
||||
} from '../../Utils/HelpLink';
|
||||
import axios from 'axios';
|
||||
import { useIsMounted } from '../../Utils/UseIsMounted';
|
||||
import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
|
||||
const downloadSvgAsBase64 = async (url: string): Promise<string> => {
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
|
||||
const image = btoa(
|
||||
new Uint8Array(response.data).reduce(
|
||||
(data, byte) => data + String.fromCharCode(byte),
|
||||
''
|
||||
)
|
||||
);
|
||||
if (image.length > 100 * 1024) {
|
||||
throw new Error(
|
||||
`Icon is too big (size after base64 conversion: ${image.length})`
|
||||
);
|
||||
}
|
||||
|
||||
const contentType = response.headers
|
||||
? response.headers['content-type'].toLowerCase()
|
||||
: '';
|
||||
if (contentType !== 'image/svg+xml')
|
||||
throw new Error(
|
||||
`Wrong content type. Got: "${contentType}", expected "image/svg+xml"`
|
||||
);
|
||||
|
||||
return `data:${contentType};base64,${image}`;
|
||||
} catch (err) {
|
||||
console.error('Unable to import the icon.', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
type HelpPathTextFieldProps = {|
|
||||
i18n: I18nType,
|
||||
helpPath: string,
|
||||
onChangeHelpPath: string => void,
|
||||
|};
|
||||
|
||||
const HelpPathTextField = ({
|
||||
i18n,
|
||||
helpPath,
|
||||
onChangeHelpPath,
|
||||
}: HelpPathTextFieldProps) => {
|
||||
const isAbsoluteUrl = isDocumentationAbsoluteUrl(helpPath);
|
||||
const isRelativePath = isRelativePathToDocumentationRoot(helpPath);
|
||||
const helperText = helpPath
|
||||
? (isRelativePath
|
||||
? i18n._(
|
||||
t`This is a relative path that will open in the GDevelop wiki.`
|
||||
)
|
||||
: i18n._(t`This is link to a webpage.`)) +
|
||||
` [${i18n._(t`Click here to test the link.`)}](${getHelpLink(helpPath)})`
|
||||
: i18n._(
|
||||
t`This can either be a URL to a web page, or a path starting with a slash that will be opened in the GDevelop wiki. Leave empty if there is no help page, although it's recommended you eventually write one if you distribute the extension.`
|
||||
);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Help page URL</Trans>}
|
||||
value={helpPath}
|
||||
onChange={(e, text) => {
|
||||
onChangeHelpPath(text);
|
||||
}}
|
||||
errorText={
|
||||
!!helpPath && !isAbsoluteUrl && !isRelativePath ? (
|
||||
<Trans>
|
||||
This is not a URL starting with "http://" or "https://".
|
||||
</Trans>
|
||||
) : null
|
||||
}
|
||||
helperMarkdownText={helperText}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
onLoadChange: (isLoading: boolean) => void,
|
||||
isLoading: boolean,
|
||||
|};
|
||||
|
||||
export const ExtensionOptionsEditor = ({
|
||||
eventsFunctionsExtension,
|
||||
onLoadChange,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const [resourceStoreOpen, setResourceStoreOpen] = React.useState(false);
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }: { i18n: I18nType }) => (
|
||||
<ColumnStackLayout noMargin>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Name</Trans>}
|
||||
value={eventsFunctionsExtension.getName()}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
<TextFieldWithButtonLayout
|
||||
renderButton={style => (
|
||||
<RaisedButton
|
||||
onClick={() => {
|
||||
setResourceStoreOpen(true);
|
||||
}}
|
||||
primary
|
||||
label={<Trans>Choose</Trans>}
|
||||
disabled={isLoading}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
renderTextField={() => (
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Icon URL</Trans>}
|
||||
value={eventsFunctionsExtension.getPreviewIconUrl()}
|
||||
onChange={text => {
|
||||
eventsFunctionsExtension.setPreviewIconUrl(text);
|
||||
}}
|
||||
disabled
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Name displayed in editor</Trans>}
|
||||
value={eventsFunctionsExtension.getFullName()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setFullName(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Short description</Trans>}
|
||||
value={eventsFunctionsExtension.getShortDescription()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setShortDescription(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={2}
|
||||
rowsMax={2}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Description (markdown supported)</Trans>}
|
||||
value={eventsFunctionsExtension.getDescription()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setDescription(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
rows={5}
|
||||
rowsMax={5}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Version</Trans>}
|
||||
value={eventsFunctionsExtension.getVersion()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setVersion(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<SemiControlledTextField
|
||||
floatingLabelText={<Trans>Tags (comma separated)</Trans>}
|
||||
value={eventsFunctionsExtension
|
||||
.getTags()
|
||||
.toJSArray()
|
||||
.join(', ')}
|
||||
onChange={text => {
|
||||
const tags = eventsFunctionsExtension.getTags();
|
||||
tags.clear();
|
||||
text
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(Boolean)
|
||||
.forEach(tag => {
|
||||
tags.push_back(tag);
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<HelpPathTextField
|
||||
i18n={i18n}
|
||||
helpPath={eventsFunctionsExtension.getHelpPath()}
|
||||
onChangeHelpPath={helpPath => {
|
||||
eventsFunctionsExtension.setHelpPath(helpPath);
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={
|
||||
<Trans>Author (Name, email or GitHub handle)</Trans>
|
||||
}
|
||||
value={eventsFunctionsExtension.getAuthor()}
|
||||
onChange={(e, text) => {
|
||||
eventsFunctionsExtension.setAuthor(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
{resourceStoreOpen && (
|
||||
<Dialog
|
||||
title={<Trans>Choose an icon for the extension</Trans>}
|
||||
actions={[
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
primary={false}
|
||||
onClick={() => {
|
||||
setResourceStoreOpen(false);
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
cannotBeDismissed={false}
|
||||
open
|
||||
noMargin
|
||||
>
|
||||
<ResourceStore
|
||||
onChoose={resource => {
|
||||
setResourceStoreOpen(false);
|
||||
onLoadChange(true);
|
||||
downloadSvgAsBase64(resource.url)
|
||||
.then(
|
||||
base64Svg => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
eventsFunctionsExtension.setPreviewIconUrl(
|
||||
resource.url
|
||||
);
|
||||
eventsFunctionsExtension.setIconUrl(base64Svg);
|
||||
},
|
||||
rawError => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
showErrorBox({
|
||||
message: i18n._(
|
||||
t`Unable to download the icon. Verify your internet connection or try again later.`
|
||||
),
|
||||
rawError,
|
||||
errorId: 'icon-download-error',
|
||||
});
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
if (!isMounted.current) return;
|
||||
|
||||
onLoadChange(false);
|
||||
});
|
||||
}}
|
||||
resourceKind={'svg'}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import Dialog from '../../UI/Dialog';
|
||||
import FlatButton from '../../UI/FlatButton';
|
||||
import CloudUpload from '@material-ui/icons/CloudUpload';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import EventsFunctionsExtensionsContext, {
|
||||
type EventsFunctionsExtensionsState,
|
||||
} from '../../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import Window from '../../Utils/Window';
|
||||
import Text from '../../UI/Text';
|
||||
import { ExtensionOptionsEditor } from './ExtensionOptionsEditor';
|
||||
import { Tab, Tabs } from '../../UI/Tabs';
|
||||
import { ExtensionDependenciesEditor } from './ExtensionDependenciesEditor';
|
||||
|
||||
const exportExtension = (
|
||||
eventsFunctionsExtensionsState: EventsFunctionsExtensionsState,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension
|
||||
) => {
|
||||
const eventsFunctionsExtensionWriter = eventsFunctionsExtensionsState.getEventsFunctionsExtensionWriter();
|
||||
if (!eventsFunctionsExtensionWriter)
|
||||
return Promise.reject(new Error('Not supported'));
|
||||
|
||||
return eventsFunctionsExtensionWriter
|
||||
.chooseEventsFunctionExtensionFile()
|
||||
.then(pathOrUrl => {
|
||||
if (!pathOrUrl) return;
|
||||
|
||||
eventsFunctionsExtensionWriter
|
||||
.writeEventsFunctionsExtension(eventsFunctionsExtension, pathOrUrl)
|
||||
.then();
|
||||
});
|
||||
};
|
||||
|
||||
const openGitHubIssue = () => {
|
||||
const body = `
|
||||
**⚠️ Please edit and complete this before submitting your extension:**
|
||||
|
||||
## Describe the extension
|
||||
A clear and concise description of what the extension is, how useful it is.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Extension has a proper name and description.
|
||||
- [ ] Extension has tags (for example: "platform, brick, breakable").
|
||||
- [ ] All behaviors have a description.
|
||||
- [ ] All functions (actions, conditions, expressions) have descriptions.
|
||||
- [ ] I confirm that this extension can be intergrated to this GitHub repository, distributed and MIT licensed.
|
||||
|
||||
## Extension file
|
||||
|
||||
Finally, attach the .json file of your extension here.
|
||||
|
||||
You also may have to create an account on GitHub before posting.
|
||||
If your extension is high quality and useful, it will be added to the list of GDevelop community extensions.
|
||||
When you're ready, remove this last paragraph and click on "Submit new issue". Thanks 🙌`;
|
||||
Window.openExternalURL(
|
||||
`https://github.com/4ian/GDevelop-extensions/issues/new?body=${encodeURIComponent(
|
||||
body
|
||||
)}&title=New%20extension`
|
||||
);
|
||||
};
|
||||
|
||||
type TabName = 'options' | 'dependencies';
|
||||
|
||||
type Props = {|
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
onClose: () => void,
|
||||
open: boolean,
|
||||
|};
|
||||
|
||||
export default function OptionsEditorDialog({
|
||||
eventsFunctionsExtension,
|
||||
onClose,
|
||||
open,
|
||||
}: Props) {
|
||||
const [currentTab, setCurrentTab] = React.useState<TabName>('options');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [exportDialogOpen, setExportDialogOpen] = React.useState(false);
|
||||
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
const eventsFunctionsExtensionWriter = eventsFunctionsExtensionsState.getEventsFunctionsExtensionWriter();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/extensions/create" />,
|
||||
eventsFunctionsExtensionWriter ? (
|
||||
<FlatButton
|
||||
icon={<CloudUpload />}
|
||||
key="export"
|
||||
label={<Trans>Export extension</Trans>}
|
||||
onClick={() => {
|
||||
setExportDialogOpen(true);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
) : null,
|
||||
]}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
primary={true}
|
||||
keyboardFocused={true}
|
||||
onClick={() => onClose()}
|
||||
disabled={isLoading}
|
||||
key={'close'}
|
||||
/>,
|
||||
]}
|
||||
cannotBeDismissed={true}
|
||||
open={open}
|
||||
noTitleMargin
|
||||
title={
|
||||
<Tabs value={currentTab} onChange={setCurrentTab}>
|
||||
<Tab label={<Trans>Options</Trans>} value="options" />
|
||||
<Tab label={<Trans>Dependencies</Trans>} value="dependencies" />
|
||||
</Tabs>
|
||||
}
|
||||
onRequestClose={isLoading ? () => {} : onClose}
|
||||
>
|
||||
{currentTab === 'options' && (
|
||||
<ExtensionOptionsEditor
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
onLoadChange={setIsLoading}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
{currentTab === 'dependencies' && (
|
||||
<ExtensionDependenciesEditor
|
||||
eventsFunctionsExtension={eventsFunctionsExtension}
|
||||
/>
|
||||
)}
|
||||
{exportDialogOpen && (
|
||||
<Dialog
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/extensions/share" />,
|
||||
]}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label={<Trans>Close</Trans>}
|
||||
keyboardFocused={true}
|
||||
onClick={() => {
|
||||
setExportDialogOpen(false);
|
||||
}}
|
||||
key={'close'}
|
||||
/>,
|
||||
]}
|
||||
open
|
||||
cannotBeDismissed={false}
|
||||
onRequestClose={() => {
|
||||
setExportDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Column expand>
|
||||
<Line>
|
||||
<Text>
|
||||
<Trans>
|
||||
You can export the extension to a file to easily import it in
|
||||
another project. If your extension is providing useful and
|
||||
reusable functions or behaviors, consider sharing it with the
|
||||
GDevelop community!
|
||||
</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<RaisedButton
|
||||
icon={<CloudUpload />}
|
||||
primary
|
||||
label={<Trans>Export to a file</Trans>}
|
||||
onClick={() => {
|
||||
exportExtension(
|
||||
eventsFunctionsExtensionsState,
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FlatButton
|
||||
label={<Trans>Submit extension to the community</Trans>}
|
||||
onClick={openGitHubIssue}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</Dialog>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -31,8 +31,22 @@ export const declareExtension = (
|
||||
)
|
||||
.setExtensionHelpPath(eventsFunctionsExtension.getHelpPath())
|
||||
.setIconUrl(eventsFunctionsExtension.getIconUrl());
|
||||
|
||||
declareExtensionDependencies(extension, eventsFunctionsExtension);
|
||||
};
|
||||
|
||||
/**
|
||||
* Declare the dependencies of an extension from an events based extension.
|
||||
*/
|
||||
export const declareExtensionDependencies = (
|
||||
extension: gdPlatformExtension,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension
|
||||
) =>
|
||||
mapVector<gdDependencyMetadata, void>(
|
||||
eventsFunctionsExtension.getAllDependencies(),
|
||||
dependency => extension.addDependency().copyFrom(dependency)
|
||||
);
|
||||
|
||||
const getExtensionIconUrl = (extension: gdPlatformExtension) => {
|
||||
return extension.getIconUrl() || 'res/function24.png';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user