/**
* 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;