/** * 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 TWThemeHOC from '../lib/tw-theme-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 VoteFrame from './vote-frame.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 styles from './interface.css'; import restore from './restore.js'; const urlparams = new URLSearchParams(location.search); const restoring = urlparams.get('restore'); const restoreHandler = urlparams.get('handler'); if (String(restoring) === 'true') { // console.log(restore) restore(restoreHandler); } let announcement = null; if (process.env.ANNOUNCEMENT) { announcement = document.createElement('p'); // This is safe because process.env.ANNOUNCEMENT is set at build time. announcement.innerHTML = process.env.ANNOUNCEMENT; } const handleClickAddonSettings = () => { const path = process.env.ROUTING_STYLE === 'wildcard' ? 'addons' : 'addons.html'; window.open(`${process.env.ROOT}${path}`); }; const xmlEscape = function (unsafe) { return unsafe.replace(/[<>&'"]/g, c => { switch (c) { case '<': return '<'; case '>': return '>'; case '&': return '&'; case '\'': return '''; case '"': return '"'; } }); }; const formatProjectTitle = _title => { const title = xmlEscape(String(_title)); const emojiRegex = /:(\w+):/g; return title.replace(emojiRegex, match => { const emojiName = match.replace(/:/gmi, ''); return `:${emojiName}:`; }); }; const messages = defineMessages({ defaultTitle: { defaultMessage: 'Editor', description: 'Title of homepage', id: 'pm.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(); /* todo: fix this and make it work properly */ // const projectDetailCache = {}; // const getProjectDetailsById = async (id) => { // // if we have already gotten the details of this project, avoid making another request since they likely never changed // if (projectDetailCache[String(id)] != null) return projectDetailCache[String(id)]; // // TODO: when this is fixed change this to the new api // const response = await fetch(`https://projects.penguinmod.com/api/projects/getPublished?id=${id}`); // // Don't continue if the api never returned 200-299 since we would cache an error as project details // if (!response.ok) return {}; // const project = await response.json(); // projectDetailCache[String(id)] = project; // return projectDetailCache[String(id)]; // }; const Footer = () => ( ); const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const numberSuffixes = [ 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th', 'th' ]; const addNumberSuffix = num => { if (!num) return `${num}`; if (num < 20 && num > 10) return `${num}th`; return num + numberSuffixes[(num - 1) % 10]; }; 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 = `PenguinMod - ${this.props.intl.formatMessage(messages.defaultTitle)}`; } else { document.title = `${title} - PenguinMod`; } } copyProjectLink (id) { if ('clipboard' in navigator && 'writeText' in navigator.clipboard) { navigator.clipboard.writeText(`https://projects.penguinmod.com/${id}`); } } render () { const { /* eslint-disable no-unused-vars */ intl, hasCloudVariables, title, description, extraProjectInfo, remixedProjectInfo, isFullScreen, isLoading, isPlayerOnly, isRtl, onClickTheme, projectId, /* eslint-enable no-unused-vars */ ...props } = this.props; const isHomepage = isPlayerOnly && !isFullScreen; const isEditor = !isPlayerOnly; const isUpdated = extraProjectInfo.isUpdated; const projectReleaseYear = extraProjectInfo.releaseDate.getFullYear(); const projectReleaseMonth = monthNames[extraProjectInfo.releaseDate.getMonth()]; const projectReleaseDay = addNumberSuffix(extraProjectInfo.releaseDate.getDate()); const hour24 = extraProjectInfo.releaseDate.getHours(); const projectReleaseHour = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24); const projectReleaseHalf = extraProjectInfo.releaseDate.getHours() > 11 ? 'PM' : 'AM'; const projectReleaseMinute = extraProjectInfo.releaseDate.getMinutes(); return (
{isHomepage ? (
) : null}
{isHomepage && announcement ? : null} {isHomepage && projectId !== '0' && title && extraProjectInfo && extraProjectInfo.author &&
{extraProjectInfo.author}
} {isHomepage ? ( {/* project not approved message */} {(!extraProjectInfo.accepted) && (

This project is currently under review. Content may not be suitable for all ages, and you should be careful when running the project.

)} {/* remix info */} {(extraProjectInfo.isRemix && remixedProjectInfo.loaded) && ( )} {isBrowserSupported() ? null : ( )} {hasCloudVariables && projectId !== '0' && (
)} {description.instructions || description.credits ? (
) : null} {extraProjectInfo.author && ( )} {projectId !== '0' && extraProjectInfo.author && (
{`${isUpdated ? 'Updated' : 'Uploaded'} ${projectReleaseMonth} ${projectReleaseDay} ${projectReleaseYear} at ${projectReleaseHour}:${projectReleaseMinute < 10 ? '0' : ''}${projectReleaseMinute} ${projectReleaseHalf}`}
! {'Report'}
)}
See more projects
) : 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 }), extraProjectInfo: PropTypes.shape({ accepted: PropTypes.bool, isRemix: PropTypes.bool, remixId: PropTypes.string, tooLarge: PropTypes.bool, author: PropTypes.string, releaseDate: PropTypes.shape(Date), isUpdated: PropTypes.bool }), remixedProjectInfo: PropTypes.shape({ loaded: PropTypes.bool, name: PropTypes.string, author: PropTypes.string }), isFullScreen: PropTypes.bool, isLoading: PropTypes.bool, isPlayerOnly: PropTypes.bool, isRtl: PropTypes.bool, onClickTheme: PropTypes.func, projectId: PropTypes.string }; const mapStateToProps = state => ({ hasCloudVariables: state.scratchGui.tw.hasCloudVariables, customStageSize: state.scratchGui.customStageSize, title: state.scratchGui.projectTitle, description: state.scratchGui.tw.description, extraProjectInfo: state.scratchGui.tw.extraProjectInfo, remixedProjectInfo: state.scratchGui.tw.remixedProjectInfo, 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, TWThemeHOC, TWPackagerIntegrationHOC )(ConnectedInterface); export default WrappedInterface;