scratch-gui / src /playground /render-interface.jsx
soiz1's picture
Upload folder using huggingface_hub
8fd7a1d verified
/**
* 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;