Spaces:
Runtime error
Runtime error
/** | |
* Copyright (C) 2021 Thomas Weber | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License version 3 as | |
* published by the Free Software Foundation. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |
*/ | |
import classNames from 'classnames'; | |
import PropTypes from 'prop-types'; | |
import React from 'react'; | |
import {connect} from 'react-redux'; | |
import {compose} from 'redux'; | |
import {FormattedMessage, defineMessages, injectIntl, intlShape} from 'react-intl'; | |
import {getIsLoading} from '../reducers/project-state.js'; | |
// import DOMElementRenderer from '../containers/dom-element-renderer.jsx'; | |
import AppStateHOC from '../lib/app-state-hoc.jsx'; | |
import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; | |
import TWProjectMetaFetcherHOC from '../lib/tw-project-meta-fetcher-hoc.jsx'; | |
import TWStateManagerHOC from '../lib/tw-state-manager-hoc.jsx'; | |
import SBFileUploaderHOC from '../lib/sb-file-uploader-hoc.jsx'; | |
import TWPackagerIntegrationHOC from '../lib/tw-packager-integration-hoc.jsx'; | |
import SettingsStore from '../addons/settings-store-singleton'; | |
import '../lib/tw-fix-history-api'; | |
import GUI from './render-gui.jsx'; | |
import MenuBar from '../components/menu-bar/menu-bar.jsx'; | |
import ProjectInput from '../components/tw-project-input/project-input.jsx'; | |
import FeaturedProjects from '../components/tw-featured-projects/featured-projects.jsx'; | |
import Description from '../components/tw-description/description.jsx'; | |
import BrowserModal from '../components/browser-modal/browser-modal.jsx'; | |
import CloudVariableBadge from '../containers/tw-cloud-variable-badge.jsx'; | |
import {isBrowserSupported} from '../lib/tw-environment-support-prober'; | |
import AddonChannels from '../addons/channels'; | |
import {loadServiceWorker} from './load-service-worker'; | |
import runAddons from '../addons/entry'; | |
import InvalidEmbed from '../components/tw-invalid-embed/invalid-embed.jsx'; | |
import {APP_NAME, FEEDBACK_URL, GITHUB_URL} from '../lib/brand.js'; | |
import styles from './interface.css'; | |
const isInvalidEmbed = window.parent !== window; | |
// Import window manager dynamically | |
let WindowManager = null; | |
let settingsWindow = null; | |
const loadWindowManager = async () => { | |
if (!WindowManager) { | |
try { | |
const module = await import('../addons/window-system/window-manager.js'); | |
WindowManager = module.default; | |
} catch (e) { | |
console.warn('Window manager not available, falling back to new window:', e); | |
return null; | |
} | |
} | |
return WindowManager; | |
}; | |
const handleClickAddonSettings = async addonId => { | |
const windowManager = await loadWindowManager(); | |
if (!windowManager) { | |
// Fall back to original behavior if window manager isn't available | |
const path = process.env.ROUTING_STYLE === 'wildcard' ? 'addons' : 'addons.html'; | |
const url = `${process.env.ROOT}${path}${typeof addonId === 'string' ? `#${addonId}` : ''}`; | |
window.open(url); | |
return; | |
} | |
// If window already exists, focus it and navigate to addon if specified | |
if (settingsWindow && settingsWindow.isVisible) { | |
settingsWindow.bringToFront(); | |
if (typeof addonId === 'string') { | |
navigateToAddon(addonId); | |
} | |
return; | |
} | |
// Create new settings window | |
settingsWindow = windowManager.createWindow({ | |
title: 'Addon Settings', | |
width: 900, | |
height: 700, | |
minWidth: 600, | |
minHeight: 400, | |
x: Math.max(50, (window.innerWidth - 900) / 2), | |
y: Math.max(50, (window.innerHeight - 700) / 2), | |
onClose: () => { | |
settingsWindow = null; | |
} | |
}); | |
createSettingsContent(addonId); | |
settingsWindow.show(); | |
}; | |
// Make the function available globally for addon integration | |
if (typeof window !== 'undefined') { | |
window.handleClickAddonSettings = handleClickAddonSettings; | |
} | |
const createSettingsContent = (addonId) => { | |
const container = settingsWindow.getContentElement(); | |
container.style.padding = '0'; | |
container.style.overflow = 'hidden'; | |
// Create iframe to load the settings page | |
const iframe = document.createElement('iframe'); | |
iframe.style.width = '100%'; | |
iframe.style.height = '100%'; | |
iframe.style.border = 'none'; | |
iframe.style.borderRadius = '0 0 8px 8px'; // Match window border radius | |
// Construct the settings URL | |
const path = process.env.ROUTING_STYLE === 'wildcard' ? 'addons' : 'addons.html'; | |
const url = `${process.env.ROOT}${path}${typeof addonId === 'string' ? `#${addonId}` : ''}`; | |
iframe.src = url; | |
container.appendChild(iframe); | |
}; | |
const navigateToAddon = (addonId) => { | |
if (settingsWindow) { | |
const iframe = settingsWindow.getContentElement().querySelector('iframe'); | |
if (iframe) { | |
try { | |
const newUrl = iframe.src.split('#')[0] + '#' + addonId; | |
iframe.src = newUrl; | |
} catch (e) { | |
console.warn('Could not navigate to addon:', e); | |
} | |
} | |
} | |
}; | |
const messages = defineMessages({ | |
defaultTitle: { | |
defaultMessage: 'Run Scratch projects faster', | |
description: 'Title of homepage', | |
id: 'tw.guiDefaultTitle' | |
} | |
}); | |
const WrappedMenuBar = compose( | |
SBFileUploaderHOC, | |
TWPackagerIntegrationHOC | |
)(MenuBar); | |
if (AddonChannels.reloadChannel) { | |
AddonChannels.reloadChannel.addEventListener('message', () => { | |
location.reload(); | |
}); | |
} | |
if (AddonChannels.changeChannel) { | |
AddonChannels.changeChannel.addEventListener('message', e => { | |
SettingsStore.setStoreWithVersionCheck(e.data); | |
}); | |
} | |
runAddons(); | |
const Footer = () => ( | |
<footer className={styles.footer}> | |
<div className={styles.footerContent}> | |
<div className={styles.footerText}> | |
<FormattedMessage | |
// eslint-disable-next-line max-len | |
defaultMessage="{APP_NAME} is not affiliated with Scratch, the Scratch Team, or the Scratch Foundation." | |
description="Disclaimer that TurboWarp is not connected to Scratch" | |
id="tw.footer.disclaimer" | |
values={{ | |
APP_NAME | |
}} | |
/> | |
</div> | |
<div className={styles.footerText}> | |
<FormattedMessage | |
// eslint-disable-next-line max-len | |
defaultMessage="Scratch is a project of the Scratch Foundation. It is available for free at {scratchDotOrg}." | |
description="A disclaimer that Scratch requires when referring to Scratch. {scratchDotOrg} is a link with text 'https://scratch.org/'" | |
id="tw.footer.scratchDisclaimer" | |
values={{ | |
scratchDotOrg: ( | |
<a | |
href="https://scratch.org/" | |
target="_blank" | |
rel="noreferrer" | |
> | |
{'https://scratch.org/'} | |
</a> | |
) | |
}} | |
/> | |
</div> | |
<div className={styles.footerColumns}> | |
<div className={styles.footerSection}> | |
<a href="credits.html"> | |
<FormattedMessage | |
defaultMessage="Credits" | |
description="Credits link in footer" | |
id="tw.footer.credits" | |
/> | |
</a> | |
<a href="https://patreon.com/Mistium"> | |
<FormattedMessage | |
defaultMessage="Donate" | |
description="Donation link in footer" | |
id="tw.footer.donate" | |
/> | |
</a> | |
</div> | |
<div className={styles.footerSection}> | |
<a href="https://packager.warp.mistium.com/"> | |
{/* Do not translate */} | |
{'MistWarp Packager'} | |
</a> | |
<a href="https://docs.warp.mistium.com/embedding"> | |
<FormattedMessage | |
defaultMessage="Embedding" | |
description="Link in footer to embedding documentation for embedding link" | |
id="tw.footer.embed" | |
/> | |
</a> | |
<a href="https://docs.warp.mistium.com/url-parameters"> | |
<FormattedMessage | |
defaultMessage="URL Parameters" | |
description="Link in footer to URL parameters documentation" | |
id="tw.footer.parameters" | |
/> | |
</a> | |
<a href="https://docs.warp.mistium.com"> | |
<FormattedMessage | |
defaultMessage="Documentation" | |
description="Link in footer to additional documentation" | |
id="tw.footer.documentation" | |
/> | |
</a> | |
</div> | |
<div className={styles.footerSection}> | |
<a href={FEEDBACK_URL}> | |
<FormattedMessage | |
defaultMessage="Feedback & Bugs" | |
description="Link to feedback/bugs page" | |
id="tw.feedback" | |
/> | |
</a> | |
<a href={GITHUB_URL}> | |
<FormattedMessage | |
defaultMessage="Source Code" | |
description="Link to source code" | |
id="tw.code" | |
/> | |
</a> | |
<a href="privacy.html"> | |
<FormattedMessage | |
defaultMessage="Privacy Policy" | |
description="Link to privacy policy" | |
id="tw.privacy" | |
/> | |
</a> | |
</div> | |
</div> | |
</div> | |
</footer> | |
); | |
class Interface extends React.Component { | |
constructor (props) { | |
super(props); | |
this.handleUpdateProjectTitle = this.handleUpdateProjectTitle.bind(this); | |
} | |
componentDidUpdate (prevProps) { | |
if (prevProps.isLoading && !this.props.isLoading) { | |
loadServiceWorker(); | |
} | |
} | |
handleUpdateProjectTitle (title, isDefault) { | |
if (isDefault || !title) { | |
document.title = `${APP_NAME} - ${this.props.intl.formatMessage(messages.defaultTitle)}`; | |
} else { | |
document.title = `${title} - ${APP_NAME}`; | |
} | |
} | |
render () { | |
if (isInvalidEmbed) { | |
return <InvalidEmbed />; | |
} | |
const { | |
/* eslint-disable no-unused-vars */ | |
intl, | |
hasCloudVariables, | |
description, | |
isFullScreen, | |
isLoading, | |
isPlayerOnly, | |
isRtl, | |
projectId, | |
/* eslint-enable no-unused-vars */ | |
...props | |
} = this.props; | |
const isHomepage = isPlayerOnly && !isFullScreen; | |
const isEditor = !isPlayerOnly; | |
return ( | |
<div | |
className={classNames(styles.container, { | |
[styles.playerOnly]: isHomepage, | |
[styles.editor]: isEditor | |
})} | |
dir={isRtl ? 'rtl' : 'ltr'} | |
> | |
{isHomepage ? ( | |
<div className={styles.menu}> | |
<WrappedMenuBar | |
canChangeLanguage | |
canManageFiles | |
canChangeTheme | |
enableSeeInside | |
onClickAddonSettings={handleClickAddonSettings} | |
/> | |
</div> | |
) : null} | |
<div | |
className={styles.center} | |
style={isPlayerOnly ? ({ | |
// + 2 accounts for 1px border on each side of the stage | |
width: `${Math.max(480, props.customStageSize.width) + 2}px` | |
}) : null} | |
> | |
<GUI | |
onClickAddonSettings={handleClickAddonSettings} | |
onUpdateProjectTitle={this.handleUpdateProjectTitle} | |
backpackVisible | |
backpackHost="_local_" | |
{...props} | |
/> | |
{isHomepage ? ( | |
<React.Fragment> | |
{isBrowserSupported() ? null : ( | |
<BrowserModal isRtl={isRtl} /> | |
)} | |
<div className={styles.section}> | |
<ProjectInput /> | |
</div> | |
{( | |
// eslint-disable-next-line max-len | |
description.instructions === 'unshared' || description.credits === 'unshared' | |
) && ( | |
<div className={classNames(styles.infobox, styles.unsharedUpdate)}> | |
<p> | |
<FormattedMessage | |
defaultMessage="Unshared projects are no longer visible." | |
description="Appears on unshared projects" | |
id="tw.unshared2.1" | |
/> | |
</p> | |
<p> | |
<FormattedMessage | |
defaultMessage="For more information, visit: {link}" | |
description="Appears on unshared projects" | |
id="tw.unshared.2" | |
values={{ | |
link: ( | |
<a | |
href="https://docs.turbowarp.org/unshared-projects" | |
target="_blank" | |
rel="noopener noreferrer" | |
> | |
{'https://docs.turbowarp.org/unshared-projects'} | |
</a> | |
) | |
}} | |
/> | |
</p> | |
<p> | |
<FormattedMessage | |
// eslint-disable-next-line max-len | |
defaultMessage="If the project was shared recently, this message may appear incorrectly for a few minutes." | |
description="Appears on unshared projects" | |
id="tw.unshared.cache" | |
/> | |
</p> | |
<p> | |
<FormattedMessage | |
// eslint-disable-next-line max-len | |
defaultMessage="If this project is actually shared, please report a bug." | |
description="Appears on unshared projects" | |
id="tw.unshared.bug" | |
/> | |
</p> | |
</div> | |
)} | |
{hasCloudVariables && projectId !== '0' && ( | |
<div className={styles.section}> | |
<CloudVariableBadge /> | |
</div> | |
)} | |
{description.instructions || description.credits ? ( | |
<div className={styles.section}> | |
<Description | |
instructions={description.instructions} | |
credits={description.credits} | |
projectId={projectId} | |
/> | |
</div> | |
) : null} | |
<div className={styles.section}> | |
<p> | |
<FormattedMessage | |
// eslint-disable-next-line max-len | |
defaultMessage="{APP_NAME} is a Scratch mod that compiles projects to JavaScript to make them run really fast. Try it out by inputting a project ID or URL above or choosing a featured project below." | |
description="Description of TurboWarp on the homepage" | |
id="tw.home.description" | |
values={{ | |
APP_NAME | |
}} | |
/> | |
</p> | |
</div> | |
<div className={styles.section}> | |
<FeaturedProjects studio="27205657" /> | |
</div> | |
</React.Fragment> | |
) : null} | |
</div> | |
{isHomepage && <Footer />} | |
</div> | |
); | |
} | |
} | |
Interface.propTypes = { | |
intl: intlShape, | |
hasCloudVariables: PropTypes.bool, | |
customStageSize: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number | |
}), | |
description: PropTypes.shape({ | |
credits: PropTypes.string, | |
instructions: PropTypes.string | |
}), | |
isFullScreen: PropTypes.bool, | |
isLoading: PropTypes.bool, | |
isPlayerOnly: PropTypes.bool, | |
isRtl: PropTypes.bool, | |
projectId: PropTypes.string | |
}; | |
const mapStateToProps = state => ({ | |
hasCloudVariables: state.scratchGui.tw.hasCloudVariables, | |
customStageSize: state.scratchGui.customStageSize, | |
description: state.scratchGui.tw.description, | |
isFullScreen: state.scratchGui.mode.isFullScreen, | |
isLoading: getIsLoading(state.scratchGui.projectState.loadingState), | |
isPlayerOnly: state.scratchGui.mode.isPlayerOnly, | |
isRtl: state.locales.isRtl, | |
projectId: state.scratchGui.projectState.projectId | |
}); | |
const mapDispatchToProps = () => ({}); | |
const ConnectedInterface = injectIntl(connect( | |
mapStateToProps, | |
mapDispatchToProps | |
)(Interface)); | |
const WrappedInterface = compose( | |
AppStateHOC, | |
ErrorBoundaryHOC('TW Interface'), | |
TWProjectMetaFetcherHOC, | |
TWStateManagerHOC, | |
TWPackagerIntegrationHOC | |
)(ConnectedInterface); | |
export default WrappedInterface; | |