mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add possibility to load WIP in-app tutorial from local file (#6603)
This commit is contained in:
@@ -1,47 +1,19 @@
|
||||
// @flow
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
const fs = optionalRequire('fs');
|
||||
const remote = optionalRequire('@electron/remote');
|
||||
const dialog = remote ? remote.dialog : null;
|
||||
|
||||
const readJSONFile = (filepath: string): Promise<Object> => {
|
||||
if (!fs) return Promise.reject('Filesystem is not supported.');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filepath, { encoding: 'utf8' }, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
try {
|
||||
const dataObject = JSON.parse(data);
|
||||
return resolve(dataObject);
|
||||
} catch (ex) {
|
||||
return reject(filepath + ' is a corrupted/malformed file.');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
import { openFilePicker, readJSONFile } from '../../Utils/FileSystem';
|
||||
|
||||
export default class LocalEventsFunctionsExtensionOpener {
|
||||
static chooseEventsFunctionExtensionFile = (): Promise<?string> => {
|
||||
if (!dialog) return Promise.reject('Not supported');
|
||||
const browserWindow = remote.getCurrentWindow();
|
||||
|
||||
return dialog
|
||||
.showOpenDialog(browserWindow, {
|
||||
title: 'Import an extension in the project',
|
||||
properties: ['openFile'],
|
||||
message: 'Choose an extension file to import (.json file)',
|
||||
filters: [
|
||||
{
|
||||
name: 'GDevelop 5 "events based" extension',
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(({ filePaths }) => {
|
||||
if (!filePaths || !filePaths.length) return null;
|
||||
return filePaths[0];
|
||||
});
|
||||
return openFilePicker({
|
||||
title: 'Import an extension in the project',
|
||||
properties: ['openFile'],
|
||||
message: 'Choose an extension file to import (.json file)',
|
||||
filters: [
|
||||
{
|
||||
name: 'GDevelop 5 "events based" extension',
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
}).then(filePath => filePath);
|
||||
};
|
||||
|
||||
static readEventsFunctionExtensionFile = (
|
||||
|
@@ -21,6 +21,7 @@ export type InAppTutorialState = {|
|
||||
startProjectData: { [key: string]: string },
|
||||
inAppTutorialsFetchingError: string | null,
|
||||
fetchInAppTutorials: () => Promise<void>,
|
||||
onLoadInAppTutorialFromLocalFile: () => Promise<void>,
|
||||
|};
|
||||
|
||||
export const initialInAppTutorialState: InAppTutorialState = {
|
||||
@@ -33,6 +34,7 @@ export const initialInAppTutorialState: InAppTutorialState = {
|
||||
startProjectData: {},
|
||||
inAppTutorialsFetchingError: null,
|
||||
fetchInAppTutorials: async () => {},
|
||||
onLoadInAppTutorialFromLocalFile: async () => {},
|
||||
};
|
||||
|
||||
const InAppTutorialContext = React.createContext<InAppTutorialState>(
|
||||
|
@@ -9,6 +9,11 @@ import {
|
||||
type InAppTutorial,
|
||||
} from '../Utils/GDevelopServices/InAppTutorial';
|
||||
import { IN_APP_TUTORIALS_FETCH_TIMEOUT } from '../Utils/GlobalFetchTimeouts';
|
||||
import optionalRequire from '../Utils/OptionalRequire';
|
||||
import Window from '../Utils/Window';
|
||||
import { openFilePicker, readJSONFile } from '../Utils/FileSystem';
|
||||
import { checkInAppTutorialFileJsonSchema } from './SchemaChecker';
|
||||
const electron = optionalRequire('electron');
|
||||
|
||||
type Props = {| children: React.Node |};
|
||||
|
||||
@@ -35,27 +40,30 @@ const InAppTutorialProvider = (props: Props) => {
|
||||
[inAppTutorialShortHeaders]
|
||||
);
|
||||
|
||||
const startTutorial = async ({
|
||||
tutorialId,
|
||||
initialStepIndex,
|
||||
initialProjectData,
|
||||
}: {|
|
||||
tutorialId: string,
|
||||
initialStepIndex: number,
|
||||
initialProjectData: { [key: string]: string },
|
||||
|}) => {
|
||||
if (!inAppTutorialShortHeaders) return;
|
||||
const startTutorial = React.useCallback(
|
||||
async ({
|
||||
tutorialId,
|
||||
initialStepIndex,
|
||||
initialProjectData,
|
||||
}: {|
|
||||
tutorialId: string,
|
||||
initialStepIndex: number,
|
||||
initialProjectData: { [key: string]: string },
|
||||
|}) => {
|
||||
if (!inAppTutorialShortHeaders) return;
|
||||
|
||||
const inAppTutorialShortHeader = getInAppTutorialShortHeader(tutorialId);
|
||||
const inAppTutorialShortHeader = getInAppTutorialShortHeader(tutorialId);
|
||||
|
||||
if (!inAppTutorialShortHeader) return;
|
||||
if (!inAppTutorialShortHeader) return;
|
||||
|
||||
const inAppTutorial = await fetchInAppTutorial(inAppTutorialShortHeader);
|
||||
setStartStepIndex(initialStepIndex);
|
||||
setStartProjectData(initialProjectData);
|
||||
setTutorial(inAppTutorial);
|
||||
setCurrentlyRunningInAppTutorial(tutorialId);
|
||||
};
|
||||
const inAppTutorial = await fetchInAppTutorial(inAppTutorialShortHeader);
|
||||
setStartStepIndex(initialStepIndex);
|
||||
setStartProjectData(initialProjectData);
|
||||
setTutorial(inAppTutorial);
|
||||
setCurrentlyRunningInAppTutorial(tutorialId);
|
||||
},
|
||||
[getInAppTutorialShortHeader, inAppTutorialShortHeaders]
|
||||
);
|
||||
|
||||
const endTutorial = () => {
|
||||
setTutorial(null);
|
||||
@@ -73,6 +81,46 @@ const InAppTutorialProvider = (props: Props) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onLoadInAppTutorialFromLocalFile = React.useCallback(
|
||||
async () => {
|
||||
if (!electron) {
|
||||
Window.showMessageBox(
|
||||
'This option is available on the desktop app only.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const filePath = await openFilePicker({
|
||||
title: 'Open a guided lesson',
|
||||
properties: ['openFile'],
|
||||
message: 'Choose a guided lesson (.json file)',
|
||||
filters: [{ name: 'GDevelop 5 in-app tutorial', extensions: ['json'] }],
|
||||
});
|
||||
const guidedLesson = await readJSONFile(filePath);
|
||||
const errors = checkInAppTutorialFileJsonSchema(guidedLesson);
|
||||
if (errors.length) {
|
||||
console.error(
|
||||
"Guided lesson file doesn't respect the format. See errors:",
|
||||
errors
|
||||
);
|
||||
Window.showMessageBox(
|
||||
"Guided lesson file doesn't respect the format. Check developer console for details."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (guidedLesson.initialTemplateUrl) {
|
||||
console.warn(
|
||||
'Starting tutorial from file. The tutorial has the field initialTemplateUrl set so make sure the project is already open in the editor.'
|
||||
);
|
||||
}
|
||||
startTutorial({
|
||||
tutorialId: guidedLesson.id,
|
||||
initialProjectData: guidedLesson.initialProjectData || {},
|
||||
initialStepIndex: 0,
|
||||
});
|
||||
},
|
||||
[startTutorial]
|
||||
);
|
||||
|
||||
// Preload the in-app tutorial short headers when the app loads.
|
||||
React.useEffect(
|
||||
() => {
|
||||
@@ -97,6 +145,7 @@ const InAppTutorialProvider = (props: Props) => {
|
||||
startStepIndex,
|
||||
inAppTutorialsFetchingError: fetchingError,
|
||||
fetchInAppTutorials: loadInAppTutorials,
|
||||
onLoadInAppTutorialFromLocalFile,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
75
newIDE/app/src/InAppTutorial/SchemaChecker.js
Normal file
75
newIDE/app/src/InAppTutorial/SchemaChecker.js
Normal file
@@ -0,0 +1,75 @@
|
||||
// @flow
|
||||
|
||||
const editorIdentifiers = [
|
||||
'Scene',
|
||||
'EventsSheet',
|
||||
'Home',
|
||||
'ExternalEvents',
|
||||
'ExternalLayout',
|
||||
'Extension',
|
||||
'Resources',
|
||||
];
|
||||
|
||||
const topLevelKeys = [
|
||||
'id',
|
||||
'flow',
|
||||
'editorSwitches',
|
||||
'endDialog',
|
||||
'availableLocales',
|
||||
];
|
||||
const flowStepKeys = [
|
||||
'elementToHighlightId',
|
||||
'id',
|
||||
'isTriggerFlickering',
|
||||
'isCheckpoint',
|
||||
'deprecated',
|
||||
'nextStepTrigger',
|
||||
'shortcuts',
|
||||
'dialog',
|
||||
'mapProjectData',
|
||||
'tooltip',
|
||||
'skippable',
|
||||
'isOnClosableDialog',
|
||||
];
|
||||
export const checkInAppTutorialFileJsonSchema = (object: Object) => {
|
||||
const errors = [];
|
||||
topLevelKeys.forEach(topLevelKey => {
|
||||
if (!object[topLevelKey]) {
|
||||
errors.push(`Top level key ${topLevelKey} missing in file.`);
|
||||
}
|
||||
});
|
||||
if (object.editorSwitches) {
|
||||
Object.keys(object.editorSwitches).forEach(editorSwitchKey => {
|
||||
const editorSwitch = object.editorSwitches[editorSwitchKey];
|
||||
if (!editorSwitch.editor) {
|
||||
errors.push(
|
||||
`Editor switch ${editorSwitchKey} doesn't have editor key.`
|
||||
);
|
||||
}
|
||||
if (!editorIdentifiers.includes(editorSwitch.editor)) {
|
||||
errors.push(
|
||||
`Editor switch ${editorSwitchKey} with identifier ${
|
||||
editorSwitch.editor
|
||||
} unknown.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (object.flow) {
|
||||
object.flow.forEach((step, index) => {
|
||||
const stepKeys = Object.keys(step);
|
||||
const unknownStepKeys = stepKeys.filter(
|
||||
stepKey => !flowStepKeys.includes(stepKey)
|
||||
);
|
||||
if (unknownStepKeys.length) {
|
||||
errors.push(
|
||||
`Flow step with id ${step.id ||
|
||||
'unknown'} at index ${index} has unknown key(s) ${unknownStepKeys.join(
|
||||
','
|
||||
)})`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
};
|
@@ -18,7 +18,7 @@ import {
|
||||
type WindowSizeType,
|
||||
} from '../../../../UI/Responsive/ResponsiveWindowMeasurer';
|
||||
import Text from '../../../../UI/Text';
|
||||
import { Column, Spacer } from '../../../../UI/Grid';
|
||||
import { Column, Line, Spacer } from '../../../../UI/Grid';
|
||||
import { type Tutorial } from '../../../../Utils/GDevelopServices/Tutorial';
|
||||
import { type SubscriptionPlanWithPricingSystems } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { CardWidget } from '../CardWidget';
|
||||
@@ -35,6 +35,8 @@ import PlaceholderLoader from '../../../../UI/PlaceholderLoader';
|
||||
import PromotionsSlideshow from '../../../../Promotions/PromotionsSlideshow';
|
||||
import { PrivateTutorialViewDialog } from '../../../../AssetStore/PrivateTutorials/PrivateTutorialViewDialog';
|
||||
import { EarnBadges } from './EarnBadges';
|
||||
import FlatButton from '../../../../UI/FlatButton';
|
||||
import InAppTutorialContext from '../../../../InAppTutorial/InAppTutorialContext';
|
||||
|
||||
const styles = {
|
||||
textTutorialContent: {
|
||||
@@ -198,7 +200,13 @@ const RecommendationList = ({
|
||||
achievements,
|
||||
} = authenticatedUser;
|
||||
const { tutorials } = React.useContext(TutorialContext);
|
||||
const { getTutorialProgress } = React.useContext(PreferencesContext);
|
||||
const {
|
||||
getTutorialProgress,
|
||||
values: { showInAppTutorialDeveloperMode },
|
||||
} = React.useContext(PreferencesContext);
|
||||
const { onLoadInAppTutorialFromLocalFile } = React.useContext(
|
||||
InAppTutorialContext
|
||||
);
|
||||
|
||||
const [
|
||||
selectedTutorial,
|
||||
@@ -276,9 +284,17 @@ const RecommendationList = ({
|
||||
|
||||
items.push(
|
||||
<SectionRow key="guided-lessons">
|
||||
<Text size="section-title" noMargin>
|
||||
<Trans>Build game mechanics</Trans>
|
||||
</Text>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text size="section-title" noMargin>
|
||||
<Trans>Build game mechanics</Trans>
|
||||
</Text>
|
||||
{showInAppTutorialDeveloperMode && (
|
||||
<FlatButton
|
||||
label={<Trans>Load local lesson</Trans>}
|
||||
onClick={onLoadInAppTutorialFromLocalFile}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
<GuidedLessons
|
||||
selectInAppTutorial={selectInAppTutorial}
|
||||
lessonsIds={guidedLessonsIds}
|
||||
|
@@ -33,6 +33,8 @@ import AuthenticatedUserContext from '../../../../Profile/AuthenticatedUserConte
|
||||
import { type Limits } from '../../../../Utils/GDevelopServices/Usage';
|
||||
import { PrivateTutorialViewDialog } from '../../../../AssetStore/PrivateTutorials/PrivateTutorialViewDialog';
|
||||
import { EducationCard } from './EducationCard';
|
||||
import InAppTutorialContext from '../../../../InAppTutorial/InAppTutorialContext';
|
||||
import PreferencesContext from '../../../Preferences/PreferencesContext';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
tile: {
|
||||
@@ -144,6 +146,12 @@ const MainPage = ({
|
||||
selectInAppTutorial,
|
||||
}: Props) => {
|
||||
const { limits } = React.useContext(AuthenticatedUserContext);
|
||||
const { onLoadInAppTutorialFromLocalFile } = React.useContext(
|
||||
InAppTutorialContext
|
||||
);
|
||||
const {
|
||||
values: { showInAppTutorialDeveloperMode },
|
||||
} = React.useContext(PreferencesContext);
|
||||
const classes = useStyles();
|
||||
const {
|
||||
windowSize,
|
||||
@@ -223,9 +231,17 @@ const MainPage = ({
|
||||
</Line>
|
||||
</SectionRow>
|
||||
<SectionRow>
|
||||
<Text noMargin size="section-title">
|
||||
<Trans>Guided lessons</Trans>
|
||||
</Text>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text noMargin size="section-title">
|
||||
<Trans>Guided lessons</Trans>
|
||||
</Text>
|
||||
{showInAppTutorialDeveloperMode && (
|
||||
<FlatButton
|
||||
label={<Trans>Load local lesson</Trans>}
|
||||
onClick={onLoadInAppTutorialFromLocalFile}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
<GuidedLessons selectInAppTutorial={selectInAppTutorial} />
|
||||
</SectionRow>
|
||||
<>
|
||||
|
@@ -212,6 +212,7 @@ export type PreferencesValues = {|
|
||||
showCommunityExtensions: boolean,
|
||||
showGetStartedSectionByDefault: boolean,
|
||||
showEventBasedObjectsEditor: boolean,
|
||||
showInAppTutorialDeveloperMode: boolean,
|
||||
showDeprecatedInstructionWarning: boolean,
|
||||
openDiagnosticReportAutomatically: boolean,
|
||||
use3DEditor: boolean,
|
||||
@@ -289,6 +290,7 @@ export type Preferences = {|
|
||||
setShowGetStartedSectionByDefault: (enabled: boolean) => void,
|
||||
setShowEventBasedObjectsEditor: (enabled: boolean) => void,
|
||||
getShowEventBasedObjectsEditor: () => boolean,
|
||||
setShowInAppTutorialDeveloperMode: (enabled: boolean) => void,
|
||||
setOpenDiagnosticReportAutomatically: (enabled: boolean) => void,
|
||||
getOpenDiagnosticReportAutomatically: () => boolean,
|
||||
setShowDeprecatedInstructionWarning: (enabled: boolean) => void,
|
||||
@@ -358,6 +360,7 @@ export const initialPreferences = {
|
||||
showCommunityExtensions: false,
|
||||
showGetStartedSectionByDefault: true,
|
||||
showEventBasedObjectsEditor: false,
|
||||
showInAppTutorialDeveloperMode: false,
|
||||
openDiagnosticReportAutomatically: true,
|
||||
showDeprecatedInstructionWarning: false,
|
||||
use3DEditor: isWebGLSupported(),
|
||||
@@ -419,6 +422,7 @@ export const initialPreferences = {
|
||||
setShowGetStartedSectionByDefault: (enabled: boolean) => {},
|
||||
setShowEventBasedObjectsEditor: (enabled: boolean) => {},
|
||||
getShowEventBasedObjectsEditor: () => false,
|
||||
setShowInAppTutorialDeveloperMode: (enabled: boolean) => {},
|
||||
setShowDeprecatedInstructionWarning: (enabled: boolean) => {},
|
||||
getOpenDiagnosticReportAutomatically: () => true,
|
||||
setOpenDiagnosticReportAutomatically: (enabled: boolean) => {},
|
||||
|
@@ -66,6 +66,7 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
setEventsSheetCancelInlineParameter,
|
||||
setShowCommunityExtensions,
|
||||
setShowEventBasedObjectsEditor,
|
||||
setShowInAppTutorialDeveloperMode,
|
||||
setOpenDiagnosticReportAutomatically,
|
||||
setShowDeprecatedInstructionWarning,
|
||||
setUse3DEditor,
|
||||
@@ -358,16 +359,6 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<Toggle
|
||||
onToggle={(e, check) => setShowEventBasedObjectsEditor(check)}
|
||||
toggled={values.showEventBasedObjectsEditor}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Show custom objects in the extension editor (experimental)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{!!electron && (
|
||||
<Toggle
|
||||
onToggle={(e, check) =>
|
||||
@@ -455,18 +446,46 @@ const PreferencesDialog = ({ i18n, onClose }: Props) => {
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
)}
|
||||
<Text size="block-title">
|
||||
<Trans>Contributor options</Trans>
|
||||
</Text>
|
||||
<Toggle
|
||||
onToggle={(e, check) => setShowEventBasedObjectsEditor(check)}
|
||||
toggled={values.showEventBasedObjectsEditor}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Show custom objects in the extension editor (experimental)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<Toggle
|
||||
onToggle={(e, check) => setShowInAppTutorialDeveloperMode(check)}
|
||||
toggled={values.showInAppTutorialDeveloperMode}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Show button to load guided lesson from file and test it.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
{Window.isDev() && (
|
||||
<Toggle
|
||||
onToggle={(e, check) => setUseGDJSDevelopmentWatcher(check)}
|
||||
toggled={values.useGDJSDevelopmentWatcher}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Watch changes in game engine (GDJS) sources and auto import
|
||||
them (dev only)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Text size="block-title">
|
||||
<Trans>Developer options</Trans>
|
||||
</Text>
|
||||
<Toggle
|
||||
onToggle={(e, check) => setUseGDJSDevelopmentWatcher(check)}
|
||||
toggled={values.useGDJSDevelopmentWatcher}
|
||||
labelPosition="right"
|
||||
label={
|
||||
<Trans>
|
||||
Watch changes in game engine (GDJS) sources and auto import
|
||||
them (dev only)
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
|
@@ -157,6 +157,9 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
getShowEventBasedObjectsEditor: this._getShowEventBasedObjectsEditor.bind(
|
||||
this
|
||||
),
|
||||
setShowInAppTutorialDeveloperMode: this._setShowInAppTutorialDeveloperMode.bind(
|
||||
this
|
||||
),
|
||||
setOpenDiagnosticReportAutomatically: this._setOpenDiagnosticReportAutomatically.bind(
|
||||
this
|
||||
),
|
||||
@@ -446,6 +449,18 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
return this.state.values.showEventBasedObjectsEditor;
|
||||
}
|
||||
|
||||
_setShowInAppTutorialDeveloperMode(showInAppTutorialDeveloperMode: boolean) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
showInAppTutorialDeveloperMode,
|
||||
},
|
||||
}),
|
||||
() => this._persistValuesToLocalStorage(this.state)
|
||||
);
|
||||
}
|
||||
|
||||
_setOpenDiagnosticReportAutomatically(
|
||||
openDiagnosticReportAutomatically: boolean
|
||||
) {
|
||||
|
@@ -2,44 +2,18 @@
|
||||
import optionalRequire from '../../Utils/OptionalRequire';
|
||||
import { type FileMetadata } from '../index';
|
||||
import { unsplit } from '../../Utils/ObjectSplitter';
|
||||
import { openFilePicker, readJSONFile } from '../../Utils/FileSystem';
|
||||
const fs = optionalRequire('fs');
|
||||
const path = optionalRequire('path');
|
||||
const remote = optionalRequire('@electron/remote');
|
||||
const dialog = remote ? remote.dialog : null;
|
||||
|
||||
const readJSONFile = (filepath: string): Promise<Object> => {
|
||||
if (!fs) return Promise.reject('Filesystem is not supported.');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filepath, { encoding: 'utf8' }, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
try {
|
||||
const dataObject = JSON.parse(data);
|
||||
return resolve(dataObject);
|
||||
} catch (ex) {
|
||||
return reject(filepath + ' is a corrupted/malformed file.');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const onOpenWithPicker = (): Promise<?FileMetadata> => {
|
||||
if (!dialog) return Promise.reject('Not supported');
|
||||
const browserWindow = remote.getCurrentWindow();
|
||||
|
||||
return dialog
|
||||
.showOpenDialog(browserWindow, {
|
||||
title: 'Open a project',
|
||||
properties: ['openFile'],
|
||||
message:
|
||||
'If you want to open your GDevelop 4 project, be sure to save it as a .json file',
|
||||
filters: [{ name: 'GDevelop 5 project', extensions: ['json'] }],
|
||||
})
|
||||
.then(({ filePaths }) => {
|
||||
if (!filePaths || !filePaths.length) return null;
|
||||
return { fileIdentifier: filePaths[0] };
|
||||
});
|
||||
return openFilePicker({
|
||||
title: 'Open a project',
|
||||
properties: ['openFile'],
|
||||
message:
|
||||
'If you want to open your GDevelop 4 project, be sure to save it as a .json file',
|
||||
filters: [{ name: 'GDevelop 5 project', extensions: ['json'] }],
|
||||
}).then(filePath => (filePath ? { fileIdentifier: filePath } : null));
|
||||
};
|
||||
|
||||
export const onOpen = (
|
||||
|
45
newIDE/app/src/Utils/FileSystem.js
Normal file
45
newIDE/app/src/Utils/FileSystem.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
import optionalRequire from './OptionalRequire';
|
||||
const fs = optionalRequire('fs');
|
||||
const fsPromises = fs ? fs.promises : null;
|
||||
const remote = optionalRequire('@electron/remote');
|
||||
const dialog = remote ? remote.dialog : null;
|
||||
|
||||
export const readJSONFile = async (filepath: string): Promise<Object> => {
|
||||
if (!fsPromises) throw new Error('Filesystem is not supported.');
|
||||
|
||||
try {
|
||||
const data = await fsPromises.readFile(filepath, { encoding: 'utf8' });
|
||||
const dataObject = JSON.parse(data);
|
||||
return dataObject;
|
||||
} catch (ex) {
|
||||
throw new Error(filepath + ' is a corrupted/malformed file.');
|
||||
}
|
||||
};
|
||||
|
||||
export const openFilePicker = ({
|
||||
title,
|
||||
properties,
|
||||
message,
|
||||
filters,
|
||||
}: {|
|
||||
title: string,
|
||||
properties: string[],
|
||||
message: string,
|
||||
filters: {| name: string, extensions: string[] |}[],
|
||||
|}) => {
|
||||
if (!dialog) return Promise.reject('Not supported');
|
||||
const browserWindow = remote.getCurrentWindow();
|
||||
|
||||
return dialog
|
||||
.showOpenDialog(browserWindow, {
|
||||
title,
|
||||
properties,
|
||||
message,
|
||||
filters,
|
||||
})
|
||||
.then(({ filePaths }) => {
|
||||
if (!filePaths || !filePaths.length) return null;
|
||||
return filePaths[0];
|
||||
});
|
||||
};
|
@@ -6,8 +6,7 @@ import optionalRequire from '../OptionalRequire';
|
||||
import { type MessageDescriptor } from '../i18n/MessageDescriptor.flow';
|
||||
import { type MessageByLocale } from '../i18n/MessageByLocale';
|
||||
import Window from '../Window';
|
||||
const fs = optionalRequire('fs');
|
||||
const fsPromises = fs ? fs.promises : null;
|
||||
import { readJSONFile } from '../FileSystem';
|
||||
const path = optionalRequire('path');
|
||||
const remote = optionalRequire('@electron/remote');
|
||||
const app = remote ? remote.app : null;
|
||||
@@ -140,18 +139,6 @@ export type InAppTutorial = {|
|
||||
availableLocales?: Array<string>,
|
||||
|};
|
||||
|
||||
const readJSONFile = async (filepath: string): Promise<Object> => {
|
||||
if (!fsPromises) throw new Error('Filesystem is not supported.');
|
||||
|
||||
try {
|
||||
const data = await fsPromises.readFile(filepath, { encoding: 'utf8' });
|
||||
const dataObject = JSON.parse(data);
|
||||
return dataObject;
|
||||
} catch (ex) {
|
||||
throw new Error(filepath + ' is a corrupted/malformed file.');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLocalFileIfDesktop = async (filename: string): Promise<?Object> => {
|
||||
const shouldRetrieveTutorialsLocally = !!remote && !Window.isDev();
|
||||
if (!shouldRetrieveTutorialsLocally) return null;
|
||||
@@ -163,7 +150,8 @@ const fetchLocalFileIfDesktop = async (filename: string): Promise<?Object> => {
|
||||
const filePath = path.join(
|
||||
appPath,
|
||||
'..', // If on dev env, replace with '../../app/resources' to test.
|
||||
`inAppTutorials/${filename}.json`
|
||||
'inAppTutorials',
|
||||
`${filename}.json`
|
||||
);
|
||||
const data = await readJSONFile(filePath);
|
||||
return data;
|
||||
|
@@ -33,6 +33,9 @@ const inAppTutorialDecorator: StoryDecorator = (Story, context) => {
|
||||
fetchInAppTutorials: async () => {
|
||||
action('Fetch in app tutorials')();
|
||||
},
|
||||
onLoadInAppTutorialFromLocalFile: async () => {
|
||||
action('load in app tutorial from local file')();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
|
@@ -107,6 +107,9 @@ const WrappedHomePage = ({
|
||||
fetchInAppTutorials: async () => {
|
||||
action('fetch tutorials')();
|
||||
},
|
||||
onLoadInAppTutorialFromLocalFile: async () => {
|
||||
action('load in app tutorial from local file')();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AuthenticatedUserContext.Provider value={user}>
|
||||
|
Reference in New Issue
Block a user