mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
5 Commits
a05b0cd0ef
...
8bc243171a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8bc243171a | ||
![]() |
747571958d | ||
![]() |
47cd3425ef | ||
![]() |
cac2b8f6e5 | ||
![]() |
47c75bab99 |
@@ -30,7 +30,6 @@ function openBrowserSWPreviewDB() {
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
console.log('[ServiceWorker] Preview database opened successfully');
|
||||
resolve(db);
|
||||
};
|
||||
|
||||
|
@@ -10,6 +10,7 @@ import './UI/icomoon-font.css'; // Styles for Icomoon font.
|
||||
import browserResourceSources from './ResourcesList/BrowserResourceSources';
|
||||
import browserResourceExternalEditors from './ResourcesList/BrowserResourceExternalEditors';
|
||||
import BrowserSWPreviewLauncher from './ExportAndShare/BrowserExporters/BrowserSWPreviewLauncher';
|
||||
import BrowserS3PreviewLauncher from './ExportAndShare/BrowserExporters/BrowserS3PreviewLauncher';
|
||||
import {
|
||||
browserAutomatedExporters,
|
||||
browserManualExporters,
|
||||
@@ -19,6 +20,7 @@ import makeExtensionsLoader from './JsExtensionsLoader/BrowserJsExtensionsLoader
|
||||
import ObjectsEditorService from './ObjectEditor/ObjectsEditorService';
|
||||
import ObjectsRenderingService from './ObjectsRendering/ObjectsRenderingService';
|
||||
import { makeBrowserSWEventsFunctionCodeWriter } from './EventsFunctionsExtensionsLoader/CodeWriters/BrowserSWEventsFunctionCodeWriter';
|
||||
import { makeBrowserS3EventsFunctionCodeWriter } from './EventsFunctionsExtensionsLoader/CodeWriters/BrowserS3EventsFunctionCodeWriter';
|
||||
import Providers from './MainFrame/Providers';
|
||||
import ProjectStorageProviders from './ProjectsStorage/ProjectStorageProviders';
|
||||
import UrlStorageProvider from './ProjectsStorage/UrlStorageProvider';
|
||||
@@ -30,6 +32,7 @@ import BrowserResourceFetcher from './ProjectsStorage/ResourceFetcher/BrowserRes
|
||||
import BrowserEventsFunctionsExtensionOpener from './EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionOpener';
|
||||
import BrowserEventsFunctionsExtensionWriter from './EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter';
|
||||
import BrowserLoginProvider from './LoginProvider/BrowserLoginProvider';
|
||||
import { isServiceWorkerSupported } from './ServiceWorkerSetup';
|
||||
|
||||
export const create = (authentication: Authentication) => {
|
||||
Window.setUpContextMenu();
|
||||
@@ -38,12 +41,17 @@ export const create = (authentication: Authentication) => {
|
||||
|
||||
let app = null;
|
||||
const appArguments = Window.getArguments();
|
||||
const canUseBrowserSW = isServiceWorkerSupported();
|
||||
|
||||
app = (
|
||||
<Providers
|
||||
authentication={authentication}
|
||||
disableCheckForUpdates={!!appArguments['disable-update-check']}
|
||||
makeEventsFunctionCodeWriter={makeBrowserSWEventsFunctionCodeWriter}
|
||||
makeEventsFunctionCodeWriter={
|
||||
canUseBrowserSW
|
||||
? makeBrowserSWEventsFunctionCodeWriter
|
||||
: makeBrowserS3EventsFunctionCodeWriter
|
||||
}
|
||||
eventsFunctionsExtensionWriter={BrowserEventsFunctionsExtensionWriter}
|
||||
eventsFunctionsExtensionOpener={BrowserEventsFunctionsExtensionOpener}
|
||||
>
|
||||
@@ -67,9 +75,13 @@ export const create = (authentication: Authentication) => {
|
||||
}) => (
|
||||
<MainFrame
|
||||
i18n={i18n}
|
||||
renderPreviewLauncher={(props, ref) => (
|
||||
<BrowserSWPreviewLauncher {...props} ref={ref} />
|
||||
)}
|
||||
renderPreviewLauncher={(props, ref) =>
|
||||
canUseBrowserSW ? (
|
||||
<BrowserSWPreviewLauncher {...props} ref={ref} />
|
||||
) : (
|
||||
<BrowserS3PreviewLauncher {...props} ref={ref} />
|
||||
)
|
||||
}
|
||||
renderShareDialog={props => (
|
||||
<ShareDialog
|
||||
project={props.project}
|
||||
@@ -104,7 +116,9 @@ export const create = (authentication: Authentication) => {
|
||||
filterExamples: !Window.isDev(),
|
||||
})}
|
||||
initialFileMetadataToOpen={initialFileMetadataToOpen}
|
||||
initialExampleSlugToOpen={appArguments['create-from-example'] || null}
|
||||
initialExampleSlugToOpen={
|
||||
appArguments['create-from-example'] || null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ProjectStorageProviders>
|
||||
|
@@ -23,8 +23,7 @@ const flushBatchedWrites = debounce(async () => {
|
||||
console.info(
|
||||
`[BrowserSWEventsFunctionCodeWriter] Storing a batch of ${
|
||||
writes.length
|
||||
} extension generated files in IndexedDB...`,
|
||||
writes.map(w => w.path)
|
||||
} extension generated files in IndexedDB...`
|
||||
);
|
||||
|
||||
batchedWrites = [];
|
||||
@@ -85,9 +84,9 @@ export const makeBrowserSWEventsFunctionCodeWriter = ({
|
||||
// At startup, clean up the old generated files for extensions code.
|
||||
(async () => {
|
||||
try {
|
||||
// TODO: maybe don't do it at startup because this could break multiple tabs!
|
||||
// TODO: Also consider doing a preview per tab?
|
||||
await deleteFilesWithPrefix(extensionsCodeUrl + '/');
|
||||
// TODO: This could break multiple tabs! Consider doing a folder per tab/session?
|
||||
const relativePath = extensionsCodeUrl.replace(baseUrl, '');
|
||||
await deleteFilesWithPrefix(relativePath + '/');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[BrowserSWEventsFunctionCodeWriter] Failed to clean generated files for extensions code in "${extensionsCodeUrl}/".`,
|
||||
|
@@ -0,0 +1,34 @@
|
||||
// @flow
|
||||
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
|
||||
|
||||
let nextPreviewWindowId = 0;
|
||||
|
||||
/**
|
||||
* Open a window showing a black "loading..." screen. It's important this is done
|
||||
* NOT in an asynchronous way but JUST after a click. Otherwise, browsers like Safari
|
||||
* will block the window opening.
|
||||
*/
|
||||
export const immediatelyOpenNewPreviewWindow = (
|
||||
project: gdProject
|
||||
): WindowProxy => {
|
||||
const width = project.getGameResolutionWidth();
|
||||
const height = project.getGameResolutionHeight();
|
||||
const left = window.screenX + window.innerWidth / 2 - width / 2;
|
||||
const top = window.screenY + window.innerHeight / 2 - height / 2;
|
||||
|
||||
const targetId = 'GDevelopPreview' + nextPreviewWindowId++;
|
||||
const previewWindow = window.open(
|
||||
'about:blank',
|
||||
targetId,
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
if (!previewWindow) {
|
||||
throw new Error(
|
||||
"Can't open the preview window because of browser restrictions."
|
||||
);
|
||||
}
|
||||
|
||||
displayBlackLoadingScreenOrThrow(previewWindow);
|
||||
|
||||
return previewWindow;
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import BrowserPreviewErrorDialog from './BrowserPreviewErrorDialog';
|
||||
import BrowserPreviewErrorDialog from '../BrowserPreview/BrowserPreviewErrorDialog';
|
||||
import BrowserS3FileSystem from '../BrowserS3FileSystem';
|
||||
import { findGDJS } from '../../../GameEngineFinder/BrowserS3GDJSFinder';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
@@ -14,58 +14,23 @@ import {
|
||||
browserPreviewDebuggerServer,
|
||||
getExistingPreviewWindowForDebuggerId,
|
||||
registerNewPreviewWindow,
|
||||
} from './BrowserPreviewDebuggerServer';
|
||||
} from '../BrowserPreview/BrowserPreviewDebuggerServer';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { getBrowserSWPreviewBaseUrl } from '../BrowserSWPreviewLauncher/BrowserSWPreviewIndexedDB';
|
||||
import { immediatelyOpenNewPreviewWindow } from '../BrowserPreview/BrowserPreviewWindow';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
let nextPreviewWindowId = 0;
|
||||
|
||||
/**
|
||||
* Open a window showing a black "loading..." screen. It's important this is done
|
||||
* NOT in an asynchronous way but JUST after a click. Otherwise, browsers like Safari
|
||||
* will block the window opening.
|
||||
*/
|
||||
export const immediatelyOpenNewPreviewWindow = (
|
||||
project: gdProject
|
||||
): WindowProxy => {
|
||||
const width = project.getGameResolutionWidth();
|
||||
const height = project.getGameResolutionHeight();
|
||||
const left = window.screenX + window.innerWidth / 2 - width / 2;
|
||||
const top = window.screenY + window.innerHeight / 2 - height / 2;
|
||||
|
||||
const targetId = 'GDevelopPreview' + nextPreviewWindowId++;
|
||||
const previewWindow = window.open(
|
||||
'about:blank',
|
||||
targetId,
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
if (!previewWindow) {
|
||||
throw new Error(
|
||||
"Can't open the preview window because of browser restrictions."
|
||||
);
|
||||
}
|
||||
console.log('Preview window opened', previewWindow);
|
||||
|
||||
displayBlackLoadingScreenOrThrow(previewWindow);
|
||||
|
||||
return previewWindow;
|
||||
};
|
||||
|
||||
export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
PreviewLauncherProps,
|
||||
State
|
||||
> {
|
||||
canDoNetworkPreview = () => false;
|
||||
canDoHotReload = () => false;
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
@@ -138,7 +103,7 @@ export default class BrowserS3PreviewLauncher extends React.Component<
|
||||
|
||||
try {
|
||||
await this.getPreviewDebuggerServer().startServer({
|
||||
origin: new URL(getBrowserSWPreviewBaseUrl()).origin,
|
||||
origin: new URL(getBaseUrl()).origin,
|
||||
});
|
||||
} catch (err) {
|
||||
// Ignore any error when running the debugger server - the preview
|
||||
|
@@ -102,16 +102,13 @@ export default class BrowserSWFileSystem {
|
||||
} files in IndexedDB for preview...`
|
||||
);
|
||||
|
||||
let totalBytes = 0;
|
||||
const uploadPromises = this._pendingFiles.map(async file => {
|
||||
const fullPath = `/${file.path}`; // TODO
|
||||
const fullPath = `/${file.path}`;
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(file.content).buffer;
|
||||
|
||||
console.log(
|
||||
`[BrowserSWFileSystem] Storing file: ${fullPath} (${
|
||||
bytes.byteLength
|
||||
} bytes, ${file.contentType})`
|
||||
);
|
||||
totalBytes += bytes.byteLength;
|
||||
|
||||
await putFile(fullPath, bytes, file.contentType);
|
||||
});
|
||||
@@ -121,7 +118,7 @@ export default class BrowserSWFileSystem {
|
||||
console.log(
|
||||
`[BrowserSWFileSystem] Successfully stored all ${
|
||||
this._pendingFiles.length
|
||||
} preview files in IndexedDB.`
|
||||
} preview files in IndexedDB (${Math.ceil(totalBytes / 1000)} kB).`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -142,7 +139,6 @@ export default class BrowserSWFileSystem {
|
||||
};
|
||||
|
||||
clearDir = (path: string) => {
|
||||
// TODO: add to a pending operation list so we ensure it's executed.
|
||||
console.info(`[BrowserSWFileSystem] Clearing directory: ${path}...`);
|
||||
this._pendingDeleteOperations.push(deleteFilesWithPrefix(path));
|
||||
};
|
||||
@@ -205,10 +201,6 @@ export default class BrowserSWFileSystem {
|
||||
const relativePath = fullPath.replace(this.baseUrl, '');
|
||||
const contentType = getContentType(fullPath);
|
||||
|
||||
console.log(
|
||||
`[BrowserSWFileSystem] Queuing file for IndexedDB: ${relativePath} (${contentType})`
|
||||
);
|
||||
|
||||
// Queue the file to be written to IndexedDB
|
||||
this._pendingFiles.push({
|
||||
path: relativePath,
|
||||
|
@@ -1,11 +1,5 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* A utility module for managing local game preview files in IndexedDB.
|
||||
* This provides a clean, promise-based interface for storing and retrieving
|
||||
* game files that will be served by the service worker.
|
||||
*/
|
||||
|
||||
// If updated, also update the service worker template.
|
||||
const DB_NAME = 'gdevelop-browser-sw-preview';
|
||||
const STORE_NAME = 'files';
|
||||
@@ -130,12 +124,20 @@ export const putFile = async (
|
||||
reject(error);
|
||||
};
|
||||
|
||||
transaction.onabort = () => {
|
||||
const error = transaction.error || new Error('Transaction aborted');
|
||||
console.error(
|
||||
'[BrowserSWIndexedDB] Transaction aborted while putting file:',
|
||||
path,
|
||||
error
|
||||
);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
// TODO: add onabort?
|
||||
|
||||
const objectStore = transaction.objectStore(STORE_NAME);
|
||||
const record: FileRecord = { bytes, contentType };
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import BrowserPreviewErrorDialog from '../BrowserS3PreviewLauncher/BrowserPreviewErrorDialog';
|
||||
import BrowserPreviewErrorDialog from '../BrowserPreview/BrowserPreviewErrorDialog';
|
||||
import BrowserSWFileSystem from '../BrowserSWFileSystem';
|
||||
import { findGDJS } from '../../../GameEngineFinder/BrowserS3GDJSFinder';
|
||||
import assignIn from 'lodash/assignIn';
|
||||
@@ -11,57 +11,24 @@ import {
|
||||
import {
|
||||
browserPreviewDebuggerServer,
|
||||
registerNewPreviewWindow,
|
||||
} from '../BrowserS3PreviewLauncher/BrowserPreviewDebuggerServer';
|
||||
} from '../BrowserPreview/BrowserPreviewDebuggerServer';
|
||||
import Window from '../../../Utils/Window';
|
||||
import { displayBlackLoadingScreenOrThrow } from '../../../Utils/BrowserExternalWindowUtils';
|
||||
import { getGDevelopResourceJwtToken } from '../../../Utils/GDevelopServices/Project';
|
||||
import { isNativeMobileApp } from '../../../Utils/Platform';
|
||||
import { getIDEVersionWithHash } from '../../../Version';
|
||||
import { getBrowserSWPreviewBaseUrl } from './BrowserSWPreviewIndexedDB';
|
||||
import { immediatelyOpenNewPreviewWindow } from '../BrowserPreview/BrowserPreviewWindow';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type State = {|
|
||||
error: ?Error,
|
||||
|};
|
||||
|
||||
let nextPreviewWindowId = 0;
|
||||
|
||||
/**
|
||||
* Open a window showing a black "loading..." screen. It's important this is done
|
||||
* NOT in an asynchronous way but JUST after a click. Otherwise, browsers like Safari
|
||||
* will block the window opening.
|
||||
*/
|
||||
export const immediatelyOpenNewPreviewWindow = (
|
||||
project: gdProject
|
||||
): WindowProxy => {
|
||||
const width = project.getGameResolutionWidth();
|
||||
const height = project.getGameResolutionHeight();
|
||||
const left = window.screenX + window.innerWidth / 2 - width / 2;
|
||||
const top = window.screenY + window.innerHeight / 2 - height / 2;
|
||||
|
||||
const targetId = 'GDevelopPreview' + nextPreviewWindowId++;
|
||||
const previewWindow = window.open(
|
||||
'about:blank',
|
||||
targetId,
|
||||
`width=${width},height=${height},left=${left},top=${top}`
|
||||
);
|
||||
if (!previewWindow) {
|
||||
throw new Error(
|
||||
"Can't open the preview window because of browser restrictions."
|
||||
);
|
||||
}
|
||||
|
||||
displayBlackLoadingScreenOrThrow(previewWindow);
|
||||
|
||||
return previewWindow;
|
||||
};
|
||||
|
||||
export default class BrowserSWPreviewLauncher extends React.Component<
|
||||
PreviewLauncherProps,
|
||||
State
|
||||
> {
|
||||
canDoNetworkPreview = () => false;
|
||||
canDoHotReload = () => false;
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
|
@@ -52,7 +52,6 @@ export default class LocalPreviewLauncher extends React.Component<
|
||||
State
|
||||
> {
|
||||
canDoNetworkPreview = () => true;
|
||||
canDoHotReload = () => true;
|
||||
|
||||
state = {
|
||||
networkPreviewDialogOpen: false,
|
||||
|
@@ -112,7 +112,6 @@ export type HotReloaderLog = {|
|
||||
export type PreviewLauncherInterface = {
|
||||
launchPreview: (previewOptions: PreviewOptions) => Promise<any>,
|
||||
canDoNetworkPreview: () => boolean,
|
||||
canDoHotReload: () => boolean,
|
||||
+closePreview?: (windowId: number) => void,
|
||||
+getPreviewDebuggerServer: () => ?PreviewDebuggerServer,
|
||||
};
|
||||
|
@@ -1,60 +1,73 @@
|
||||
// @flow
|
||||
import optionalRequire from './Utils/OptionalRequire';
|
||||
import { isNativeMobileApp } from './Utils/Platform';
|
||||
|
||||
const PUBLIC_URL: string = process.env.PUBLIC_URL || '';
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export function registerServiceWorker() {
|
||||
const enabled = true;
|
||||
const serviceWorker = navigator.serviceWorker;
|
||||
const electron = optionalRequire('electron');
|
||||
const serviceWorker = navigator.serviceWorker;
|
||||
|
||||
if (enabled && serviceWorker) {
|
||||
window.addEventListener('load', () => {
|
||||
// Use a cache-buster for development so that the service worker is
|
||||
// always reloaded when the app is reloaded.
|
||||
const swUrl = isDev
|
||||
? `${PUBLIC_URL}/service-worker.js?dev=${Date.now()}`
|
||||
: `${PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
const alreadyHasAServiceWorker = !!serviceWorker.controller;
|
||||
if (alreadyHasAServiceWorker) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all tabs for this page are closed.'
|
||||
);
|
||||
} else {
|
||||
// Service worker has been installed for the first time.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
serviceWorker.ready.then(registration => {
|
||||
// Forces a check right now for a newer service worker script in development.
|
||||
// If there is one, it will be installed (see the service worker script to verify how in development
|
||||
// a new service worker script does a `self.skipWaiting()` and `self.clients.claim()`).
|
||||
registration.update();
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Service Worker disabled - TODO: fallback to S3?');
|
||||
}
|
||||
export function isServiceWorkerSupported() {
|
||||
return !!serviceWorker;
|
||||
}
|
||||
|
||||
export function registerServiceWorker() {
|
||||
if (isNativeMobileApp() || !!electron) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serviceWorker) {
|
||||
console.warn(
|
||||
'Service Worker not supported on this deployment (probably: not HTTPS and not localhost).'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
// Use a cache-buster for development so that the service worker is
|
||||
// always reloaded when the app is reloaded.
|
||||
const swUrl = isDev
|
||||
? `${PUBLIC_URL}/service-worker.js?dev=${Date.now()}`
|
||||
: `${PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
const alreadyHasAServiceWorker = !!serviceWorker.controller;
|
||||
if (alreadyHasAServiceWorker) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all tabs for this page are closed.'
|
||||
);
|
||||
} else {
|
||||
// Service worker has been installed for the first time.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
serviceWorker.ready.then(registration => {
|
||||
// Forces a check right now for a newer service worker script in development.
|
||||
// If there is one, it will be installed (see the service worker script to verify how in development
|
||||
// a new service worker script does a `self.skipWaiting()` and `self.clients.claim()`).
|
||||
registration.update();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -79,7 +79,7 @@ import EditorMosaicPlayground from './EditorMosaicPlayground';
|
||||
import EditorNavigator from '../UI/EditorMosaic/EditorNavigator';
|
||||
import PropertiesEditor from '../PropertiesEditor';
|
||||
import { OpenConfirmDialog } from '../ProjectsStorage/OpenConfirmDialog';
|
||||
import BrowserPreviewErrorDialog from '../ExportAndShare/BrowserExporters/BrowserS3PreviewLauncher/BrowserPreviewErrorDialog';
|
||||
import BrowserPreviewErrorDialog from '../ExportAndShare/BrowserExporters/BrowserPreview/BrowserPreviewErrorDialog';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Text from '../UI/Text';
|
||||
import IconButton from '../UI/IconButton';
|
||||
|
Reference in New Issue
Block a user