/** * 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 . */ 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 = () => ( ); 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 ; } 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 (
{isHomepage ? (
) : null}
{isHomepage ? ( {isBrowserSupported() ? null : ( )}
{( // eslint-disable-next-line max-len description.instructions === 'unshared' || description.credits === 'unshared' ) && (

{'https://docs.turbowarp.org/unshared-projects'} ) }} />

)} {hasCloudVariables && projectId !== '0' && (
)} {description.instructions || description.credits ? (
) : null}

) : null}
{isHomepage &&
); } } 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;