diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 0926f0d4f61..9a9b690fdec 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -32,6 +32,7 @@ import DragLayer from '../../containers/drag-layer.jsx'; import ConnectionModal from '../../containers/connection-modal.jsx'; import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx'; import BlockDisplayModal from '../../containers/block-display-modal.jsx'; +import URLLoaderModal from '../url-loader-modal/url-loader-modal.jsx'; import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; import {resolveStageSize} from '../../lib/screen-utils'; @@ -121,6 +122,7 @@ const GUIComponent = props => { onShare, onShowPrivacyPolicy, onStartSelectingFileUpload, + onStartSelectingUrlLoad, onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, @@ -132,6 +134,9 @@ const GUIComponent = props => { telemetryModalVisible, theme, tipsLibraryVisible, + urlLoaderModalVisible, + closeUrlLoaderModal, + onUrlLoaderSubmit, vm, // Exclude Redux-related props from being passed to DOM setSelectedBlocks: _setSelectedBlocks, @@ -187,6 +192,12 @@ const GUIComponent = props => { onShowPrivacyPolicy={onShowPrivacyPolicy} /> ) : null} + {urlLoaderModalVisible ? ( + + ) : null} {loading ? ( ) : null} @@ -260,6 +271,7 @@ const GUIComponent = props => { onSeeCommunity={onSeeCommunity} onShare={onShare} onStartSelectingFileUpload={onStartSelectingFileUpload} + onStartSelectingUrlLoad={onStartSelectingUrlLoad} onToggleLoginOpen={onToggleLoginOpen} /> @@ -466,6 +478,7 @@ GUIComponent.propTypes = { onShare: PropTypes.func, onShowPrivacyPolicy: PropTypes.func, onStartSelectingFileUpload: PropTypes.func, + onStartSelectingUrlLoad: PropTypes.func, onTabSelect: PropTypes.func, onTelemetryModalCancel: PropTypes.func, onTelemetryModalOptIn: PropTypes.func, @@ -480,6 +493,9 @@ GUIComponent.propTypes = { telemetryModalVisible: PropTypes.bool, theme: PropTypes.string, tipsLibraryVisible: PropTypes.bool, + urlLoaderModalVisible: PropTypes.bool, + closeUrlLoaderModal: PropTypes.func, + onUrlLoaderSubmit: PropTypes.func, vm: PropTypes.instanceOf(VM).isRequired }; GUIComponent.defaultProps = { diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index a5cfacb4488..47b9926b3f5 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -188,7 +188,8 @@ class MenuBar extends React.Component { 'handleKeyPress', 'handleRestoreOption', 'getSaveToComputerHandler', - 'restoreOptionMessage' + 'restoreOptionMessage', + 'handleClickLoadFromUrl' ]); } componentDidMount () { @@ -277,6 +278,11 @@ class MenuBar extends React.Component { } }; } + handleClickLoadFromUrl () { + if (this.props.onStartSelectingUrlLoad) { + this.props.onStartSelectingUrlLoad(); + } + } restoreOptionMessage (deletedItem) { switch (deletedItem) { case 'Sprite': @@ -504,6 +510,15 @@ class MenuBar extends React.Component { /> )} + + + @@ -931,6 +946,7 @@ MenuBar.propTypes = { onSetTimeTravelMode: PropTypes.func, onShare: PropTypes.func, onStartSelectingFileUpload: PropTypes.func, + onStartSelectingUrlLoad: PropTypes.func, onToggleLoginOpen: PropTypes.func, projectTitle: PropTypes.string, renderLogin: PropTypes.func, diff --git a/src/components/url-loader-modal/url-loader-icon.svg b/src/components/url-loader-modal/url-loader-icon.svg new file mode 100644 index 00000000000..bf6a8750fb3 --- /dev/null +++ b/src/components/url-loader-modal/url-loader-icon.svg @@ -0,0 +1,25 @@ + + + + URL Loader + URL Loader Icon + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/url-loader-modal/url-loader-modal.css b/src/components/url-loader-modal/url-loader-modal.css new file mode 100644 index 00000000000..cb4c7801a64 --- /dev/null +++ b/src/components/url-loader-modal/url-loader-modal.css @@ -0,0 +1,119 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.modal-content { + width: 500px; + height: auto; + line-height: 1.75; +} + +.header { + background-color: $motion-primary; +} + +.body { + background: $ui-white; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.promptSection { + text-align: left; +} + +.promptText { + font-size: 0.875rem; + color: $text-primary; + line-height: 1.5; +} + +.inputSection { + display: flex; + flex-direction: column; +} + +.urlInput { + width: 100%; + padding: 0.75rem; + border: 2px solid $ui-black-transparent; + border-radius: 0.25rem; + font-size: 0.875rem; + font-family: inherit; + outline: none; + box-sizing: border-box; +} + +.urlInput:focus { + border-color: $motion-primary; + box-shadow: 0 0 0 1px $motion-primary; +} + +.urlInput::placeholder { + color: $text-primary-transparent; +} + +.urlInput.inputError { + border-color: $error-primary; + box-shadow: 0 0 0 1px $error-primary; +} + +.errorMessage { + margin-top: 0.5rem; + font-size: 0.75rem; + color: $error-primary; + line-height: 1.4; +} + +.buttonSection { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.cancelButton, +.openButton { + padding: 0.5rem 1rem; + border: none; + border-radius: 0.25rem; + font-size: 0.875rem; + font-weight: bold; + cursor: pointer; + transition: background-color 0.1s ease; + min-width: 80px; +} + +.cancelButton { + background: $ui-white; + color: $text-primary; + border: 1px solid $ui-black-transparent; +} + +.cancelButton:hover { + background: $ui-secondary; +} + +.cancelButton:active { + background: $ui-black-transparent; +} + +.openButton { + background: $motion-primary; + color: $ui-white; +} + +.openButton:hover:not(.disabled) { + background: $motion-tertiary; +} + +.openButton:active:not(.disabled) { + background: $motion-tertiary; +} + +.openButton.disabled { + background: $ui-black-transparent; + color: $text-primary-transparent; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/components/url-loader-modal/url-loader-modal.jsx b/src/components/url-loader-modal/url-loader-modal.jsx new file mode 100644 index 00000000000..afc330b2ba7 --- /dev/null +++ b/src/components/url-loader-modal/url-loader-modal.jsx @@ -0,0 +1,171 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; + +import Box from '../box/box.jsx'; +import Modal from '../../containers/modal.jsx'; +import urlLoaderIcon from './url-loader-icon.svg'; + +import styles from './url-loader-modal.css'; + +const messages = defineMessages({ + title: { + defaultMessage: 'Load from URL', + description: 'Title for the URL loader modal', + id: 'gui.urlLoader.title' + }, + prompt: { + defaultMessage: 'Enter a Scratch project URL:', + description: 'Prompt message for URL input', + id: 'gui.urlLoader.urlPrompt' + }, + urlPlaceholder: { + defaultMessage: 'https://scratch.mit.edu/projects/1234567890/', + description: 'Placeholder text for URL input field', + id: 'gui.urlLoader.urlPlaceholder' + }, + openButton: { + defaultMessage: 'Open', + description: 'Label for open button', + id: 'gui.urlLoader.openButton' + }, + cancelButton: { + defaultMessage: 'Cancel', + description: 'Label for cancel button', + id: 'gui.urlLoader.cancelButton' + }, + invalidUrlError: { + defaultMessage: 'Please enter a valid Scratch project URL.', + description: 'Error message for invalid URL', + id: 'gui.urlLoader.invalidUrl' + } +}); + +class URLLoaderModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleUrlChange', + 'handleOpenClick', + 'handleCancelClick', + 'handleKeyPress', + 'clearError' + ]); + + this.state = { + url: '', + error: null + }; + } + + handleUrlChange (event) { + this.setState({ + url: event.target.value, + error: null // Clear error when user types + }); + } + + clearError () { + this.setState({error: null}); + } + + handleOpenClick () { + const {url} = this.state; + if (url.trim()) { + this.props.onLoadUrl(url.trim(), error => { + if (error) { + this.setState({error: error}); + } + }); + } + } + + handleCancelClick () { + this.props.onRequestClose(); + } + + handleKeyPress (event) { + if (event.key === 'Enter') { + this.handleOpenClick(); + } + } + + render () { + const {intl, onRequestClose} = this.props; + const {url, error} = this.state; + + return ( + + + +
+ +
+
+ + + + {error && ( +
+ +
+ )} +
+ + + + + +
+
+ ); + } +} + +URLLoaderModal.propTypes = { + intl: intlShape.isRequired, + onRequestClose: PropTypes.func.isRequired, + onLoadUrl: PropTypes.func.isRequired +}; + +export default injectIntl(URLLoaderModal); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index 3a031abf23c..d74ba6ec6ad 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -24,12 +24,15 @@ import { closeBackdropLibrary, closeTelemetryModal, openExtensionLibrary, - closeDebugModal + closeDebugModal, + openUrlLoaderModal, + closeUrlLoaderModal } from '../reducers/modals'; import FontLoaderHOC from '../lib/font-loader-hoc.jsx'; import LocalizationHOC from '../lib/localization-hoc.jsx'; import SBFileUploaderHOC from '../lib/sb-file-uploader-hoc.jsx'; +import URLLoaderHOC from '../lib/url-loader-hoc.jsx'; import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx'; import TitledHOC from '../lib/titled-hoc.jsx'; import ProjectSaverHOC from '../lib/project-saver-hoc.jsx'; @@ -177,6 +180,7 @@ const mapStateToProps = state => { telemetryModalVisible: state.scratchGui.modals.telemetryModal, tipsLibraryVisible: state.scratchGui.modals.tipsLibrary, rubyTabVisible: state.scratchGui.editorTab.activeTabIndex === RUBY_TAB_INDEX, + urlLoaderModalVisible: state.scratchGui.modals.urlLoaderModal, vm: state.scratchGui.vm }; }; @@ -190,7 +194,9 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()), onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()), onRequestCloseDebugModal: () => dispatch(closeDebugModal()), - onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()) + onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()), + openUrlLoaderModal: () => dispatch(openUrlLoaderModal()), + closeUrlLoaderModal: () => dispatch(closeUrlLoaderModal()) }); const ConnectedGUI = injectIntl(connect( @@ -212,6 +218,7 @@ const WrappedGui = compose( vmListenerHOC, vmManagerHOC, SBFileUploaderHOC, + URLLoaderHOC, cloudManagerHOC, systemPreferencesHOC )(ConnectedGUI); diff --git a/src/lib/ruby-generator/translate.js b/src/lib/ruby-generator/translate.js index ecf7709cd85..981e7978119 100644 --- a/src/lib/ruby-generator/translate.js +++ b/src/lib/ruby-generator/translate.js @@ -7,7 +7,7 @@ export default function (Generator) { Generator.translate_getTranslate = function (block) { const words = Generator.valueToCode(block, 'WORDS', Generator.ORDER_NONE) || null; const language = Generator.valueToCode(block, 'LANGUAGE', Generator.ORDER_NONE); - return `translate(${words}, ${language})\n`; + return [`translate(${words}, ${language})`, Generator.ORDER_FUNCTION_CALL]; }; Generator.translate_menu_languages = Generator.text2speech_menu_languages; diff --git a/src/lib/url-loader-hoc.jsx b/src/lib/url-loader-hoc.jsx new file mode 100644 index 00000000000..b536219781e --- /dev/null +++ b/src/lib/url-loader-hoc.jsx @@ -0,0 +1,295 @@ +import bindAll from 'lodash.bindall'; +import React from 'react'; +import PropTypes from 'prop-types'; +import {defineMessages, intlShape, injectIntl} from 'react-intl'; +import {connect} from 'react-redux'; +import log from '../lib/log'; +import sharedMessages from './shared-messages'; + +import {extractScratchProjectId} from './url-parser'; + +import { + LoadingStates, + getIsLoadingUpload, + getIsShowingWithoutId, + onLoadedProject, + projectError, + setProjectId, + requestProjectUpload +} from '../reducers/project-state'; +import {setProjectTitle} from '../reducers/project-title'; +import { + openLoadingProject, + closeLoadingProject, + openUrlLoaderModal, + closeUrlLoaderModal +} from '../reducers/modals'; +import { + closeFileMenu +} from '../reducers/menus'; + +const messages = defineMessages({ + loadError: { + id: 'gui.urlLoader.loadError', + defaultMessage: 'The project URL that was entered failed to load.', + description: 'An error that displays when a project URL fails to load.' + }, + invalidUrl: { + id: 'gui.urlLoader.invalidUrl', + defaultMessage: 'Please enter a valid Scratch project URL.', + description: 'An error that displays when an invalid URL is entered.' + } +}); + +/** + * Higher Order Component to provide behavior for loading project from URL into editor. + * @param {React.Component} WrappedComponent the component to add URL loading functionality to + * @returns {React.Component} WrappedComponent with URL loading functionality added + * + * + * + * + */ +const URLLoaderHOC = function (WrappedComponent) { + class URLLoaderComponent extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleStartSelectingUrlLoad', + 'handleUrlSubmit', + 'loadProjectFromUrl', + 'handleFinishedLoadingUpload' + ]); + } + componentDidUpdate (prevProps) { + if (this.props.isLoadingUpload && !prevProps.isLoadingUpload) { + this.handleFinishedLoadingUpload(); + } + } + + // Step 1: Start the URL loading process + handleStartSelectingUrlLoad () { + this.props.openUrlLoaderModal(); + this.props.closeFileMenu(); + } + + // Step 2: Handle URL submission from modal + handleUrlSubmit (url, errorCallback) { + const { + intl, + isShowingWithoutId, + loadingState, + projectChanged, + userOwnsProject + } = this.props; + + const projectId = extractScratchProjectId(url); + if (!projectId) { + // Instead of alert, pass error to modal via callback + if (errorCallback) { + errorCallback(intl.formatMessage(messages.invalidUrl)); + } + return; + } + + this.projectIdToLoad = projectId; + this.projectUrlToLoad = url; + + // If user owns the project, or user has changed the project, + // we must confirm with the user that they really intend to + // replace it. + let uploadAllowed = true; + if (userOwnsProject || (projectChanged && isShowingWithoutId)) { + uploadAllowed = confirm( // eslint-disable-line no-alert + intl.formatMessage(sharedMessages.replaceProjectWarning) + ); + } + + if (uploadAllowed) { + // Start the loading process + this.props.requestProjectUpload(loadingState); + // Close modal only when validation passes and user confirms + this.props.closeUrlLoaderModal(); + } else { + // Close modal if user cancels the replacement + this.props.closeUrlLoaderModal(); + } + } + + // Step 3: Load project from URL (called from componentDidUpdate) + handleFinishedLoadingUpload () { + if (this.projectIdToLoad) { + this.loadProjectFromUrl(this.projectIdToLoad); + return; + } + this.props.cancelFileUpload(this.props.loadingState); + } + + // Step 4: Actually load the project data + loadProjectFromUrl (projectId) { + this.props.onLoadingStarted(); + + // Set project ID in Redux state first (like project-fetcher-hoc.jsx) + this.props.setProjectId(projectId.toString()); + + // Use the same approach as project-fetcher-hoc.jsx + // First get the project token via the proxy API + const options = { + method: 'GET', + uri: `https://api.smalruby.app/scratch-api-proxy/projects/${projectId}`, + json: true + }; + + fetch(options.uri, { + method: options.method, + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.json(); + }) + .then(data => { + const projectToken = data.project_token; + + // Now load the project using storage system (like project-fetcher-hoc.jsx) + const storage = this.props.vm.runtime.storage; + storage.setProjectToken(projectToken); + + return storage.load(storage.AssetType.Project, projectId, storage.DataFormat.JSON); + }) + .then(projectAsset => { + if (projectAsset) { + // Load project directly to VM (like sb-file-uploader-hoc.jsx for LOADING_VM_FILE_UPLOAD) + return this.props.vm.loadProject(projectAsset.data); + } + throw new Error('Could not find project'); + }) + .then(() => { + // Set project title based on the project data or URL + const projectTitle = `Project ${this.projectIdToLoad}`; + this.props.onSetProjectTitle(projectTitle); + + // Use onLoadedProject for LOADING_VM_FILE_UPLOAD state + this.props.onLoadedProject(this.props.loadingState, true, true); + }) + .catch(error => { + log.warn('URL loader error:', error); + this.props.onError(error); + alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert + }) + .then(() => { + this.props.onLoadingFinished(); + // Clear the project reference + this.projectIdToLoad = null; + this.projectUrlToLoad = null; + }); + } + + render () { + const { + /* eslint-disable no-unused-vars */ + cancelFileUpload, + closeFileMenu: closeFileMenuProp, + isLoadingUpload, + isShowingWithoutId, + loadingState, + onLoadingFinished, + onLoadingStarted, + onSetProjectTitle, + projectChanged, + requestProjectUpload: requestProjectUploadProp, + userOwnsProject, + onStartSelectingUrlLoad: onStartSelectingUrlLoadProp, + /* eslint-enable no-unused-vars */ + ...componentProps + } = this.props; + return ( + + + + ); + } + } + + URLLoaderComponent.propTypes = { + canSave: PropTypes.bool, + cancelFileUpload: PropTypes.func, + closeFileMenu: PropTypes.func, + closeUrlLoaderModal: PropTypes.func, + intl: intlShape.isRequired, + isLoadingUpload: PropTypes.bool, + isShowingWithoutId: PropTypes.bool, + loadingState: PropTypes.oneOf(LoadingStates), + onError: PropTypes.func, + onLoadedProject: PropTypes.func, + onLoadingFinished: PropTypes.func, + onLoadingStarted: PropTypes.func, + onSetProjectTitle: PropTypes.func, + onStartSelectingUrlLoad: PropTypes.func, + openUrlLoaderModal: PropTypes.func, + projectChanged: PropTypes.bool, + requestProjectUpload: PropTypes.func, + setProjectId: PropTypes.func, + userOwnsProject: PropTypes.bool, + vm: PropTypes.shape({ + loadProject: PropTypes.func, + runtime: PropTypes.shape({ + storage: PropTypes.object + }) + }) + }; + + const mapStateToProps = (state, ownProps) => { + const loadingState = state.scratchGui.projectState.loadingState; + const user = state.session && state.session.session && state.session.session.user; + return { + isLoadingUpload: getIsLoadingUpload(loadingState), + isShowingWithoutId: getIsShowingWithoutId(loadingState), + loadingState: loadingState, + projectChanged: state.scratchGui.projectChanged, + userOwnsProject: ownProps.authorUsername && user && + (ownProps.authorUsername === user.username), + vm: state.scratchGui.vm + }; + }; + + const mapDispatchToProps = dispatch => ({ + cancelFileUpload: loadingState => dispatch(onLoadedProject(loadingState, false, false)), + closeFileMenu: () => dispatch(closeFileMenu()), + closeUrlLoaderModal: () => dispatch(closeUrlLoaderModal()), + onError: error => dispatch(projectError(error)), + onLoadedProject: (loadingState, canSave, success) => + dispatch(onLoadedProject(loadingState, canSave, success)), + onLoadingFinished: () => { + dispatch(closeLoadingProject()); + dispatch(closeFileMenu()); + }, + onLoadingStarted: () => dispatch(openLoadingProject()), + onSetProjectTitle: title => dispatch(setProjectTitle(title)), + openUrlLoaderModal: () => dispatch(openUrlLoaderModal()), + requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)), + setProjectId: projectId => dispatch(setProjectId(projectId)) + }); + + const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( + {}, stateProps, dispatchProps, ownProps + ); + + return injectIntl(connect( + mapStateToProps, + mapDispatchToProps, + mergeProps + )(URLLoaderComponent)); +}; + +export { + URLLoaderHOC as default +}; diff --git a/src/lib/url-parser.js b/src/lib/url-parser.js new file mode 100644 index 00000000000..53529d01251 --- /dev/null +++ b/src/lib/url-parser.js @@ -0,0 +1,37 @@ +/** + * Utility functions for parsing project URLs + */ + +/** + * Extract Scratch project ID from Scratch project URL + * @param {string} url - Scratch project URL + * @returns {string|null} - Project ID or null if invalid + */ +export const extractScratchProjectId = url => { + if (!url || typeof url !== 'string') { + return null; + } + + const patterns = [ + // Standard project URL: https://scratch.mit.edu/projects/1209008277/ + /^https?:\/\/scratch\.mit\.edu\/projects\/(\d+)\/?$/, + // Project URL with additional path: https://scratch.mit.edu/projects/1209008277/editor/ + /^https?:\/\/scratch\.mit\.edu\/projects\/(\d+)\/.*$/ + ]; + + for (const pattern of patterns) { + const match = url.trim().match(pattern); + if (match && match[1]) { + return match[1]; + } + } + + return null; +}; + +/** + * Validate if URL is a valid Scratch project URL + * @param {string} url - URL to validate + * @returns {boolean} - True if valid Scratch project URL + */ +export const isValidScratchProjectUrl = url => extractScratchProjectId(url) !== null; diff --git a/src/locales/en.js b/src/locales/en.js index 782b3c58c28..0407086ae3d 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -175,5 +175,14 @@ export default { 'gui.smalruby3.blockDisplayModal.operator_contains': '(apple) contains (a)?', 'gui.smalruby3.blockDisplayModal.operator_mod': '( ) mod ( )', 'gui.smalruby3.blockDisplayModal.operator_round': 'round ( )', - 'gui.smalruby3.blockDisplayModal.operator_mathop': '[abs ▼] of ( )' + 'gui.smalruby3.blockDisplayModal.operator_mathop': '[abs ▼] of ( )', + + // URL Loader messages + 'gui.urlLoader.loadError': 'The project URL that was entered failed to load.', + 'gui.urlLoader.invalidUrl': 'Please enter a valid Scratch project URL.', + 'gui.urlLoader.urlPrompt': 'Enter a Scratch project URL (e.g., https://scratch.mit.edu/projects/1209008277/):', + 'gui.urlLoader.title': 'Load from URL', + 'gui.urlLoader.urlPlaceholder': 'https://scratch.mit.edu/projects/1234567890/', + 'gui.urlLoader.openButton': 'Open', + 'gui.urlLoader.cancelButton': 'Cancel' }; diff --git a/src/locales/ja-Hira.js b/src/locales/ja-Hira.js index f4c776d8b7f..5e82057a706 100644 --- a/src/locales/ja-Hira.js +++ b/src/locales/ja-Hira.js @@ -1,4 +1,12 @@ export default { + 'gui.menuBar.loadFromUrl': 'URLからよみこむ', + 'gui.urlLoader.loadError': 'プロジェクトURLのよみこみにしっぱいしました。', + 'gui.urlLoader.invalidUrl': 'ゆうこうなScratchプロジェクトURLをにゅうりょくしてください。', + 'gui.urlLoader.urlPrompt': 'ScratchプロジェクトのURLをにゅうりょくしてください:', + 'gui.urlLoader.title': 'URLからよみこむ', + 'gui.urlLoader.urlPlaceholder': 'https://scratch.mit.edu/projects/1234567890/', + 'gui.urlLoader.openButton': 'ひらく', + 'gui.urlLoader.cancelButton': 'キャンセル', 'gui.menuBar.seeProjectPage': 'プロジェクトページをみる', 'gui.loader.creating': 'プロジェクトをさくせいちゅう...', 'gui.smalruby3.crashMessage.description': 'もうしわけありません。スモウルビーがクラッシュしたようです。このバグはじどうてきにスモウルビーチームにほうこくされました。ページをさいよみこみしてください。', diff --git a/src/locales/ja.js b/src/locales/ja.js index 66536e4b851..bb039ea2a19 100644 --- a/src/locales/ja.js +++ b/src/locales/ja.js @@ -1,4 +1,12 @@ export default { + 'gui.menuBar.loadFromUrl': 'URLから読み込む', + 'gui.urlLoader.loadError': 'プロジェクトURLの読み込みに失敗しました。', + 'gui.urlLoader.invalidUrl': '有効なScratchプロジェクトURLを入力してください。', + 'gui.urlLoader.urlPrompt': 'ScratchプロジェクトのURLを入力してください:', + 'gui.urlLoader.title': 'URLから読み込む', + 'gui.urlLoader.urlPlaceholder': 'https://scratch.mit.edu/projects/1234567890/', + 'gui.urlLoader.openButton': '開く', + 'gui.urlLoader.cancelButton': 'キャンセル', 'gui.menuBar.seeProjectPage': 'プロジェクトページを見る', 'gui.loader.creating': 'プロジェクトを作成中...', 'gui.smalruby3.crashMessage.description': '申し訳ありません。スモウルビーがクラッシュしたようです。このバグは自動的にスモウルビーチームに報告されました。ページを再読み込みしてください。', diff --git a/src/reducers/modals.js b/src/reducers/modals.js index 5bec3699a1e..a103f676828 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -12,6 +12,7 @@ const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; const MODAL_SOUND_RECORDER = 'soundRecorder'; const MODAL_CONNECTION = 'connectionModal'; const MODAL_TIPS_LIBRARY = 'tipsLibrary'; +const MODAL_URL_LOADER = 'urlLoaderModal'; const initialState = { [MODAL_BACKDROP_LIBRARY]: false, @@ -24,7 +25,8 @@ const initialState = { [MODAL_SPRITE_LIBRARY]: false, [MODAL_SOUND_RECORDER]: false, [MODAL_CONNECTION]: false, - [MODAL_TIPS_LIBRARY]: false + [MODAL_TIPS_LIBRARY]: false, + [MODAL_URL_LOADER]: false }; const reducer = function (state, action) { @@ -87,6 +89,9 @@ const openConnectionModal = function () { const openTipsLibrary = function () { return openModal(MODAL_TIPS_LIBRARY); }; +const openUrlLoaderModal = function () { + return openModal(MODAL_URL_LOADER); +}; const closeBackdropLibrary = function () { return closeModal(MODAL_BACKDROP_LIBRARY); }; @@ -120,6 +125,9 @@ const closeTipsLibrary = function () { const closeConnectionModal = function () { return closeModal(MODAL_CONNECTION); }; +const closeUrlLoaderModal = function () { + return closeModal(MODAL_URL_LOADER); +}; export { reducer as default, initialState as modalsInitialState, @@ -134,6 +142,7 @@ export { openTelemetryModal, openTipsLibrary, openConnectionModal, + openUrlLoaderModal, closeBackdropLibrary, closeCostumeLibrary, closeDebugModal, @@ -144,5 +153,6 @@ export { closeSoundRecorder, closeTelemetryModal, closeTipsLibrary, - closeConnectionModal + closeConnectionModal, + closeUrlLoaderModal }; diff --git a/src/reducers/project-state.js b/src/reducers/project-state.js index 76955d589d5..497f5d93b6f 100644 --- a/src/reducers/project-state.js +++ b/src/reducers/project-state.js @@ -436,7 +436,8 @@ const onLoadedProject = (loadingState, canSave, success) => { // failed to load default project; show error return {type: START_ERROR}; default: - return; + // For states like SHOWING_WITH_ID, return a no-op action + return {type: RETURN_TO_SHOWING}; } };