diff --git a/extensions/src/common/extension/mixins/base/scratchInfo/index.ts b/extensions/src/common/extension/mixins/base/scratchInfo/index.ts index 2715b7b24..16599ec5d 100644 --- a/extensions/src/common/extension/mixins/base/scratchInfo/index.ts +++ b/extensions/src/common/extension/mixins/base/scratchInfo/index.ts @@ -1,7 +1,7 @@ import { castToType } from "$common/cast"; import CustomArgumentManager from "$common/extension/mixins/configurable/customArguments/CustomArgumentManager"; import { ArgumentType, BlockType } from "$common/types/enums"; -import { BlockOperation, ValueOf, Menu, ExtensionMetadata, ExtensionBlockMetadata, ExtensionMenuMetadata, DynamicMenu, BlockMetadata, } from "$common/types"; +import { BlockOperation, ValueOf, Menu, ExtensionMetadata, ExtensionBlockMetadata, ExtensionMenuMetadata, DynamicMenu, BlockMetadata, BlockUtilityWithID, } from "$common/types"; import { registerButtonCallback } from "$common/ui"; import { isString, typesafeCall, } from "$common/utils"; import type BlockUtility from "$root/packages/scratch-vm/src/engine/block-utility"; @@ -12,11 +12,17 @@ import { convertToArgumentInfo, extractArgs, zipArgs } from "./args"; import { convertToDisplayText } from "./text"; import { CustomizableExtensionConstructor, MinimalExtensionInstance, } from ".."; import { ExtensionInstanceWithFunctionality } from "../.."; +import { blockIDKey } from "$common/globals"; export const getImplementationName = (opcode: string) => `internal_${opcode}`; const inlineImageAccessError = "ERROR: This argument represents an inline image and should not be accessed."; +const isBlockUtilityWithID = (query: any): query is BlockUtilityWithID => query?.[blockIDKey] !== undefined; +const nonBlockContextError = "Block method was not given a block utility, and thus was likely called by something OTHER THAN the Scratch Runtime. NOTE: You cannot call block methods directly from within your class due to how block methods are converted to work with scratch. Consider abstracting the logic to a seperate, non-block method which can be invoked directly." +const checkForBlockContext = (blockUtility: BlockUtilityWithID) => isBlockUtilityWithID(blockUtility) ? void 0 : console.error(nonBlockContextError); + + /** * Wraps a blocks operation so that the arguments passed from Scratch are first extracted and then passed as indices in a parameter array. * @param _this What will be bound to the 'this' context of the underlying operation @@ -29,7 +35,8 @@ export const wrapOperation = ( operation: BlockOperation, args: { name: string, type: ValueOf, handler: Handler }[] ) => _this.supports("customArguments") - ? function (this: ExtensionInstanceWithFunctionality<["customArguments"]>, argsFromScratch: Record, blockUtility: BlockUtility) { + ? function (this: ExtensionInstanceWithFunctionality<["customArguments"]>, argsFromScratch: Record, blockUtility: BlockUtilityWithID) { + checkForBlockContext(blockUtility); const castedArguments = args.map(({ name, type, handler }) => { if (type === ArgumentType.Image) return inlineImageAccessError; const param = argsFromScratch[name]; @@ -44,7 +51,8 @@ export const wrapOperation = ( }); return operation.call(_this, ...castedArguments, blockUtility); } - : function (this: T, argsFromScratch: Record, blockUtility: BlockUtility) { + : function (this: T, argsFromScratch: Record, blockUtility: BlockUtilityWithID) { + checkForBlockContext(blockUtility); const castedArguments = args.map(({ name, type, handler }) => type === ArgumentType.Image ? inlineImageAccessError diff --git a/extensions/src/common/utils.ts b/extensions/src/common/utils.ts index b1c74173b..db8d5a827 100644 --- a/extensions/src/common/utils.ts +++ b/extensions/src/common/utils.ts @@ -11,9 +11,9 @@ export const getTextFromMenuItem = (item: MenuItem) => typeof item === "ob export async function fetchWithTimeout( resource: FetchParams["request"], - options: FetchParams["options"] & { timeout: number } + options: FetchParams["options"] & { timeoutMs: number } ) { - const { timeout } = options; + const { timeoutMs: timeout } = options; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); diff --git a/extensions/src/textClassification/Editor.svelte b/extensions/src/textClassification/Editor.svelte index 8b957ed47..32bd82db4 100644 --- a/extensions/src/textClassification/Editor.svelte +++ b/extensions/src/textClassification/Editor.svelte @@ -8,10 +8,6 @@ export let extension: Extension; export let close: () => void; - export const onClose = () => { - console.log("closed!"); - }; - const invoke: ReactiveInvoke = (functionName, ...args) => reactiveInvoke((extension = extension), functionName, args); diff --git a/extensions/src/textClassification/ImportExport.svelte b/extensions/src/textClassification/ImportExport.svelte new file mode 100644 index 000000000..4e402f5ee --- /dev/null +++ b/extensions/src/textClassification/ImportExport.svelte @@ -0,0 +1,47 @@ + + +
+ Export Classifier + + Import Classifier + (file = e.currentTarget.files[0])} + /> + + Done +
+ + diff --git a/extensions/src/textClassification/index.ts b/extensions/src/textClassification/index.ts index 478055fb0..3be3d7505 100644 --- a/extensions/src/textClassification/index.ts +++ b/extensions/src/textClassification/index.ts @@ -1,8 +1,8 @@ /// -import { Environment, extension, ExtensionMenuDisplayDetails, block, wrapClamp, fetchWithTimeout, RuntimeEvent, buttonBlock, untilTimePassed } from "$common"; +import { Environment, extension, ExtensionMenuDisplayDetails, wrapClamp, fetchWithTimeout, RuntimeEvent, buttonBlock, } from "$common"; import type BlockUtility from "$scratch-vm/engine/block-utility"; import { legacyFullSupport, info, } from "./legacy"; -import { getState, setState, tryCopyStateToClone } from "./state"; +import { State, getState, setState, tryCopyStateToClone } from "./state"; import { getSynthesisURL } from "./services/synthesis"; import timer from "./timer"; import voices, { Voice } from "./voices"; @@ -22,7 +22,7 @@ const details: ExtensionMenuDisplayDetails = { description: "Create a text classification model for use in a Scratch project!", }; -const defaultLabel = "No labels"; +const defaultLabels = ["No labels"]; export default class TextClassification extends extension(details, "legacySupport", "ui", "indicators") { labels: string[] = []; @@ -53,12 +53,15 @@ export default class TextClassification extends extension(details, "legacySuppor @buttonBlock("Edit Model") editButton() { this.openUI("Editor", "Edit Text Model") } + @buttonBlock("Load / Save Model") + saveLoadButton() { this.openUI("ImportExport", "Save / Load Text Model") } + @legacyBlock.ifTextMatchesClass((self) => ({ argumentMethods: { 1: { getItems: () => { const { labels } = self; - return labels?.length > 0 ? labels : [defaultLabel]; + return labels?.length > 0 ? labels : defaultLabels; } } } @@ -100,40 +103,13 @@ export default class TextClassification extends extension(details, "legacySuppor @legacyBlock.speakText() async speakText(text: string, { target }: BlockUtility) { - const locale = 'en-US'; - const { audioEngine } = this.runtime; - const { currentVoice } = getState(target); - const { gender, playbackRate } = voices[currentVoice]; - const encoded = encodeURIComponent(JSON.stringify(text).substring(0, 128)); - const endpoint = getSynthesisURL({ gender, locale, text: encoded }); - - await new Promise(async (resolve) => { - try { - const response = await fetchWithTimeout(endpoint, { timeout: 40 }); - if (!response.ok) return console.warn(response.statusText); - const sound = { data: response.body as unknown as Buffer }; - const soundPlayer = await audioEngine.decodeSoundPlayer(sound); - this.soundPlayers.set(soundPlayer.id, soundPlayer); - soundPlayer.setPlaybackRate(playbackRate); - const chain = audioEngine.createEffectChain(); - chain.set('volume', 250); - soundPlayer.connect(chain); - soundPlayer.play(); - soundPlayer.on('stop', () => { - this.soundPlayers.delete(soundPlayer.id); - resolve(); - }); - } - catch (error) { - console.warn(error); - } - }); + await this.speak(text, getState(target)); } @legacyBlock.askSpeechRecognition() - async askSpeechRecognition(prompt: string, util: BlockUtility) { - await this.speakText(prompt, util); - this.recognizeSpeech(); + async askSpeechRecognition(prompt: string, { target }: BlockUtility) { + await this.speak(prompt, getState(target)); + await this.recognizeSpeech(); } @legacyBlock.getRecognizedSpeech() @@ -151,7 +127,7 @@ export default class TextClassification extends extension(details, "legacySuppor return voiceItems[voiceIndex].value; } - return voiceItems.find(({ value, text }) => value === reported || text === reported)?.value + return voiceItems.find(({ value, text }) => value === reported || text === reported)?.value ?? "SQUEAK"; } } } @@ -159,6 +135,7 @@ export default class TextClassification extends extension(details, "legacySuppor setVoice(voice: Voice, { target }: BlockUtility) { const state = getState(target); state.currentVoice = voice ?? state.currentVoice; + setState(target, state); } @legacyBlock.onHeardSound() @@ -193,9 +170,9 @@ export default class TextClassification extends extension(details, "legacySuppor } async buildCustomDeepModel() { + const indicator = await this.indicate({ msg: "wait .. loading model", type: "warning", }); const identifier = Symbol(); this.currentModelIdentifier = identifier; - const indicator = await this.indicate({ msg: "wait .. loading model", type: "warning", }); const isCurrent = () => this.currentModelIdentifier === identifier; const result = await build(this.labels, this.modelData, isCurrent); @@ -210,19 +187,84 @@ export default class TextClassification extends extension(details, "legacySuppor } } + importClassifier(file: File) { + return new Promise((resolve) => { + if (!file) return resolve(false); + const reader = new FileReader(); + reader.onload = ({ target: { result } }) => { + console.log(result); + try { + const data = JSON.parse(result as string) as Record; + this.modelData = new Map(Object.entries(data)); + this.labels = [...this.modelData.keys()]; + resolve(true); + } catch (err) { + console.error(`Incorrect document form: ${file.name}: ${err}`); + this.modelData = new Map(); + this.labels = []; + resolve(false); + } + }; + reader.readAsText(file); + }); + } + + exportClassifier() { + const serialized = JSON.stringify(Object.fromEntries(this.modelData)); + const data = `text/json;charset=utf-8,${encodeURIComponent(serialized)}`; + const anchor = document.createElement('a'); + anchor.setAttribute("href", "data:" + data); + anchor.setAttribute("download", "classifier-info.json"); + anchor.click(); + } + /** End UI Methods */ /** Begin Private Methods */ + private async speak(text: string, { currentVoice }: State) { + const locale = 'en-US'; + const { audioEngine } = this.runtime; + const { gender, playbackRate } = voices[currentVoice]; + const encoded = encodeURIComponent(text.substring(0, 128)); + const endpoint = getSynthesisURL({ gender, locale, text: encoded }); + + await new Promise(async (resolve) => { + try { + const response = await fetchWithTimeout(endpoint, { timeoutMs: 40000 }); + if (!response.ok) return console.warn(response.statusText); + const buffer = await response.arrayBuffer(); + const soundPlayer = await audioEngine.decodeSoundPlayer({ data: { buffer } }); + this.soundPlayers.set(soundPlayer.id, soundPlayer); + soundPlayer.setPlaybackRate(playbackRate); + const chain = audioEngine.createEffectChain(); + chain.set('volume', 250); + soundPlayer.connect(chain); + soundPlayer.play(); + soundPlayer.on('stop', () => { + this.soundPlayers.delete(soundPlayer.id); + resolve(); + }); + } + catch (error) { + console.warn(error); + } + }); + } + private async getToxicityModel() { + if (this.toxicityModel) return this.toxicityModel; + const msg = await this.indicate({ msg: "Loading toxicity model", type: "warning", }); try { - this.toxicityModel ??= await loadToxicity(0.1, toxicityLabelItems.map(({ value }) => value)); - console.log('loaded Toxicity model'); + this.toxicityModel = await loadToxicity(0.1, toxicityLabelItems.map(({ value }) => value)); + msg.close(); + this.indicateFor({ msg: "Toxicity model loaded!", type: "success", }, 2); + return this.toxicityModel; } catch (error) { - console.log('Failed to load toxicity model', error); + msg.close(); + this.indicateFor({ msg: "Failed to load toxicity model", type: "error", }, 2); } - return this.toxicityModel; } private getLoudness() { @@ -263,9 +305,7 @@ export default class TextClassification extends extension(details, "legacySuppor ? Math.round(filtered[0].results[0].probabilities[returnPositive ? 1 : 0] * 100) : 0; } - catch (error) { - console.log('Failed to classify text', error); - } + catch (error) { console.error('Failed to classify text', error); } } private async getConfidence(text: string) { @@ -280,50 +320,4 @@ export default class TextClassification extends extension(details, "legacySuppor const { label } = await this.customPredictor(newText); return label; } - - /** End Private Methods */ - - private uiEventsTODO() { - /* - // Listen for model editing events emitted by the text modal - this.runtime.on('NEW_EXAMPLES', (examples, label) => { - this.newExamples(examples, label); - }); - this.runtime.on('NEW_LABEL', (label) => { - this.newLabel(label); - }); - this.runtime.on('DELETE_EXAMPLE', (label, exampleNum) => { - this.deleteExample(label, exampleNum); - }); - this.runtime.on('RENAME_LABEL', (oldName, newName) => { - this.renameLabel(oldName, newName); - }); - this.runtime.on('DELETE_LABEL', (label) => { - this.clearAllWithLabel({ LABEL: label }); - }); - this.runtime.on('CLEAR_ALL_LABELS', () => { - if (!this.labelListEmpty && confirm('Are you sure you want to clear all labels?')) { //confirm with alert dialogue before clearing the model - let labels = [...this.labelList]; - for (var i = 0; i < labels.length; i++) { - this.clearAllWithLabel({ LABEL: labels[i] }); - } - //this.clearAll(); this crashed Scratch for some reason - } - }); - - //Listen for model editing events emitted by the classifier modal - this.runtime.on('EXPORT_CLASSIFIER', () => { - this.exportClassifier(); - }); - this.runtime.on('LOAD_CLASSIFIER', () => { - console.log("load"); - this.loadClassifier(); - - }); - - this.runtime.on('DONE', () => { - console.log("DONE"); - this.buildCustomDeepModel(); - });*/ - } } diff --git a/extensions/src/textClassification/model.ts b/extensions/src/textClassification/model.ts index 654c6ca11..c243d23ae 100644 --- a/extensions/src/textClassification/model.ts +++ b/extensions/src/textClassification/model.ts @@ -35,7 +35,7 @@ export const build = async ( ) => { const { length } = labels; - if (length < 2) return { error: "No classes inputted" }; + if (length < 2) return { error: "2 or more classes required" }; const model = sequential(); diff --git a/extensions/src/textClassification/services/synthesis.ts b/extensions/src/textClassification/services/synthesis.ts index 0572e6b30..fd5e76cec 100644 --- a/extensions/src/textClassification/services/synthesis.ts +++ b/extensions/src/textClassification/services/synthesis.ts @@ -1,18 +1,10 @@ import { VoiceInfo } from "../voices"; -/** - * The url of the synthesis server. - * @type {string} - */ -const synthesisURL = new URL('https://synthesis-service.scratch.mit.edu/synth'); - type SearchParams = { locale: string, gender: VoiceInfo["gender"], text: string } -export const getSynthesisURL = (params: SearchParams) => { - for (const key in params) synthesisURL.searchParams.set(key, params[key]); - return synthesisURL.href; -} \ No newline at end of file +export const getSynthesisURL = ({ locale, gender, text }: SearchParams) => + `https://synthesis-service.scratch.mit.edu/synth?locale=${locale}&gender=${gender}&text=${text}` \ No newline at end of file diff --git a/extensions/src/textClassification/services/translation.ts b/extensions/src/textClassification/services/translation.ts index 66abbc4ff..ee3718aa4 100644 --- a/extensions/src/textClassification/services/translation.ts +++ b/extensions/src/textClassification/services/translation.ts @@ -21,7 +21,7 @@ export const getTranslationToEnglish = async (words: string) => { if (cachedTranslations.has(words)) return cachedTranslations.get(words); const endpoint = getTranslationURL({ language: "en", text: encodeURIComponent(words) }); try { - const json: { result: string } = await (await fetchWithTimeout(endpoint, { timeout: 30 })).json(); + const json: { result: string } = await (await fetchWithTimeout(endpoint, { timeoutMs: 30 })).json(); const translated = json.result; cachedTranslations.set(words, translated); return translated; diff --git a/extensions/src/textClassification/state.ts b/extensions/src/textClassification/state.ts index 35f6cc8a3..99f5b4f69 100644 --- a/extensions/src/textClassification/state.ts +++ b/extensions/src/textClassification/state.ts @@ -7,7 +7,7 @@ const stateKey = "Scratch.text2speech"; export const setState = (target: Target, state: State) => target.setCustomState(stateKey, state); -export const hasState = (target: Target) => Boolean(target.getCustomState(stateKey)); +export const hasState = (target: Target) => target.getCustomState(stateKey) !== undefined && target.getCustomState(stateKey) !== null; export const getState = (target: Target): State => { let state: State = target.getCustomState(stateKey); diff --git a/packages/scratch-gui/src/components/classifier-modal/classifier-model-editor.jsx b/packages/scratch-gui/src/components/classifier-modal/classifier-model-editor.jsx deleted file mode 100644 index 64fa7b938..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/classifier-model-editor.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; -import keyMirror from 'keymirror'; - -import Box from '../box/box.jsx'; -import LabelTile from './label-tile.jsx'; -import EditLabelTile from './label-editor.jsx'; -import Dots from './dots.jsx'; - -import styles from './classifier-model-modal.css'; - -const ClassifierModelEditor = props => ( - - - - - - - - - -); - -ClassifierModelEditor.propTypes = { - onCancel: PropTypes.func, - onFileUploader: PropTypes.func, - onAddLabel: PropTypes.func, - onExport: PropTypes.func, - classifierData: PropTypes.object, - activeLabel: PropTypes.string -}; - -export default ClassifierModelEditor; - diff --git a/packages/scratch-gui/src/components/classifier-modal/classifier-model-modal.css b/packages/scratch-gui/src/components/classifier-modal/classifier-model-modal.css deleted file mode 100644 index c38e3c287..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/classifier-model-modal.css +++ /dev/null @@ -1,264 +0,0 @@ -@import "../../css/colors.css"; -@import "../../css/units.css"; - -.modal-content { - width: 600px; -} - -.header { - background-color: $pen-primary; -} - -.body { - background: $ui-white; -} - -.centered-row { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.label-tile-pane { - overflow-y: auto; - width: 100%; - height: 100%; -} - -.label-tile { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - - background-color: $ui-white; - border-radius: 0.25rem; - padding: 10px; - width: 100%; - margin-bottom: 0.5rem; -} - -.label-tile-header { - display: flex; - align-items: center; - height: 2rem; - margin-left: 0.25rem; -} - -.label-tile-footer { - display: flex; - align-items: center; - height: 2rem; - margin-top: 0.5rem; -} - -.label-tile-name { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - font-size: 0.875rem; - margin-right: 1rem; -} - -.label-tile button { - margin:0.25rem; - - padding: 0.3rem 0.5rem; - - border: none; - border-radius: 0.25rem; - - font-weight: 600; - font-size: 0.85rem; - line-height:1rem; - - background: $motion-primary; - border: $motion-primary; - color: white; - cursor: pointer; -} - -.vertical-layout { - align-items: flex-start; - align-self: flex-start; - flex-direction: column; - height: 100%; - width: 100%; -} - -.example-box { - align-items: center; - display: flex; - flex-wrap: wrap; - height: calc(100% - 2rem); - width: calc(100% - 1rem); - margin-left:1.5rem; -} - -.example-viewer-text { - margin: 0.5rem 0.25rem; - height: 1.5rem; - justify-content: flex-start; -} - -.add-example-row { - position: relative; - margin:auto; - - border-width: 1px; - border-style: solid; - border-color: hsla(0, 0%, 0%, 0.15); - border-radius: 0.25rem; -} - -.add-example-row input { - font-size: 0.85rem; - background-color: none; - - border-width: 0px; - border-color: hsla(0, 0%, 0%, 0.15); - border-radius: 0.25rem; -} -.add-example-row button { - background-color: hsla(0, 0%, 0%, 0.35); - margin:0; -} - -.example-viewer-image-container { - display: flex; - overflow-y: auto; - flex-wrap: wrap; - height: calc(100% - 1.5rem); - margin-left: 1rem; -} - -.loaded-examples-box { - height: 50%; - width: 80%; - margin: 0.7rem; - position: relative; - display: flex; - align-items: center; - justify-content: center; - - background-color: $ui-white; - border-radius: 0.25rem; -} - -.example-text { - height: 70%; - max-width:200px; - margin: 0.2rem 0.8rem; - position: relative; - background-color: $motion-light-transparent; - - display: flex; - align-items: center; - - border-radius: 0.5rem; -} - - -.canvas-area { - height: calc(100% - 1.5rem); - margin: 0.25rem; - display: flex; - justify-content: center; -} - -.canvas { - height: 100%; - position: relative; - display: block; - padding: 0.6rem 0.35rem; -} - -.removable { - padding-right: 1.25rem; -} - -.loading-camera-message { - position: absolute; - display: flex; - align-self: center; -} - -.delete-button { - position: absolute; - top: 0.25rem; - right: 0.25rem; - z-index: auto; -} - -.input-field { - height: 1.3rem; - padding: 0 0.75rem; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 0.875rem; - font-weight: bold; - color: hsla(225, 15%, 40%, 1); - border-width: 1px; - border-style: solid; - border-color: hsla(0, 0%, 0%, 0.15); - border-radius: 0.25rem; - outline: none; - cursor: text; - transition: 0.25s ease-out; - box-shadow: none; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; -} - -.activity-area { - height: 300px; - background-color: $motion-light-transparent; - display: flex; - justify-content: center; - align-items: center; - padding: .5rem; -} - -.button-row { - font-weight: bolder; - text-align: center; - display: flex; -} - -.button-row button { - padding: 0.6rem 0.75rem; - border-radius: 0.5rem; - background: $motion-primary; - color: white; - font-weight: 600; - font-size: 0.85rem; - margin: 0.25rem; - border: none; - cursor: pointer; - display: flex; - align-items: center; -} - -.bottom-area { - background-color: $ui-white; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - padding-top: 1rem; - padding-bottom: .75rem; - padding-left: .75rem; - padding-right: .75rem; -} - -.bottom-area .bottom-area-item+.bottom-area-item { - margin-top: 1rem; -} - -.instructions { - text-align: center; - height: 1.5rem; -} \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/classifier-model-modal.jsx b/packages/scratch-gui/src/components/classifier-modal/classifier-model-modal.jsx deleted file mode 100644 index d4afe750f..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/classifier-model-modal.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Box from '../box/box.jsx'; -import Modal from '../../containers/modal.jsx'; - -import ClassifierModelEditor from './classifier-model-editor.jsx'; - -import styles from './classifier-model-modal.css'; - -const ClassifierModelModalComponent = props => ( - - - - - -); - -ClassifierModelModalComponent.propTypes = { - name: PropTypes.node, - onCancel: PropTypes.func.isRequired, - onHelp: PropTypes.func.isRequired, -}; - -export { - ClassifierModelModalComponent as default -}; - diff --git a/packages/scratch-gui/src/components/classifier-modal/dots.jsx b/packages/scratch-gui/src/components/classifier-modal/dots.jsx deleted file mode 100644 index af6f8fcd6..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/dots.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; - -import Box from '../box/box.jsx'; -import styles from './classifier-model-modal.css'; - -const Dots = props => ( - -
- {Array(props.total).fill(0) - .map((_, i) => { - let type = 'inactive'; - if (props.counter === i) type = 'active'; - if (props.success) type = 'success'; - if (props.error) type = 'error'; - return (); - })} -
-
-); - -Dots.propTypes = { - className: PropTypes.string, - counter: PropTypes.number, - error: PropTypes.bool, - success: PropTypes.bool, - total: PropTypes.number -}; - -const Dot = props => ( -
-); - -Dot.propTypes = { - type: PropTypes.string -}; - -export default Dots; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/example-tile.jsx b/packages/scratch-gui/src/components/classifier-modal/example-tile.jsx deleted file mode 100644 index 94ef91fcb..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/example-tile.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import Box from '../box/box.jsx'; -import CloseButton from '../close-button/close-button.jsx'; - -import styles from './classifier-model-modal.css'; - -class ExampleTile extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleDeleteExample' - ]); - } - handleDeleteExample () { - this.props.onDeleteExample(this.props.label, this.props.id); - } - - - render () { - return ( -
- {this.props.closeButton ? - <> - {this.props.text} - - - : -
{this.props.text}
} -
- ); - } -} - -ExampleTile.propTypes = { - label: PropTypes.string, - text: PropTypes.string, - onDeleteExample: PropTypes.func, - closeButton: PropTypes.bool -}; - -export default ExampleTile; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/icon--close.svg b/packages/scratch-gui/src/components/classifier-modal/icon--close.svg deleted file mode 100644 index 73c7b929f..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/icon--close.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Extensions/Connection/Close - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/icon--enter.svg b/packages/scratch-gui/src/components/classifier-modal/icon--enter.svg deleted file mode 100644 index 03b7c0862..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/icon--enter.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - General/Check - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/icon--help.svg b/packages/scratch-gui/src/components/classifier-modal/icon--help.svg deleted file mode 100644 index 2938e8340..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/icon--help.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - help - Created with Sketch. - - - - - \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/label-editor.jsx b/packages/scratch-gui/src/components/classifier-modal/label-editor.jsx deleted file mode 100644 index 381c0e715..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/label-editor.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import keyMirror from 'keymirror'; - -import Box from '../box/box.jsx'; -import ExampleTile from './example-tile.jsx'; - -import styles from './classifier-model-modal.css'; - -class EditLabelTile extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleRenameLabel', - 'handleDeleteLabel', - 'handleNewExample', - 'handleInputForNewExample', - 'handleDoneEditLabel' - ]); - this.state = { - inputText: "" - }; - } - handleDoneEditLabel () { - console.log("Text Model Modal: done editing label " + this.props.labelName); - this.props.onDoneEditLabel(this.props.labelName); - } - handleRenameLabel (input) { //call props.onRenameLabel with the current active label and the value in the input field - if (this.props.labelName !== input.target.value) { - this.props.onRenameLabel(this.props.labelName, input.target.value); - } - } - handleDeleteLabel () { //call props.onDeleteLabel with this label name - this.props.onDeleteLabel(this.props.labelName); - } - handleNewExample () { //call props.onAddExample - let newExample = this.state.inputText; - if (newExample != undefined && newExample != '') { - this.props.onNewExamples(this.props.labelName, [newExample]); - } - } - handleInputForNewExample (input) { //call props.onAddExample - this.setState({ - inputText: input.target.value - }); - - } - - render () { - return ( - - - - - - {" ("+this.props.exampleCount+" examples)"} - - - - - - {this.props.textData[this.props.labelName].map(example => ( - - - - ))} - - - - - - - - - - ); - } -} - -EditLabelTile.propTypes = { - labelName: PropTypes.string, - onRenameLabel: PropTypes.func, - onDeleteExample: PropTypes.func, - onDeleteLabel: PropTypes.func, - onNewExamples: PropTypes.func, - onDoneEditLabel: PropTypes.func, - textData: PropTypes.object, - exampleCount: PropTypes.number -}; - -export default EditLabelTile; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/classifier-modal/label-tile.jsx b/packages/scratch-gui/src/components/classifier-modal/label-tile.jsx deleted file mode 100644 index 463162103..000000000 --- a/packages/scratch-gui/src/components/classifier-modal/label-tile.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import keyMirror from 'keymirror'; - -import Box from '../box/box.jsx'; -import ExampleTile from './example-tile.jsx'; - -import styles from './classifier-model-modal.css'; - -class LabelTile extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleEditLabel' - ]); - this.state = { - inputText: "" - }; - } - handleEditLabel () { - console.log("Text Model Modal: edit label " + this.props.labelName); - this.props.onEditLabel(this.props.labelName); - } - - - render () { - return ( - - - - - {this.props.labelName} - {" ("+this.props.exampleCount+" examples)"} - - - - - {this.props.textData[this.props.labelName].map(example => ( - - - - ))} - - - - ); - } -} - -LabelTile.propTypes = { - labelName: PropTypes.string, - onEditLabel: PropTypes.func, - onNewExamples: PropTypes.func, - textData: PropTypes.object, - exampleCount: PropTypes.number -}; - -export default LabelTile; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index d3cc5ebb4..f2f0b3922 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -2,10 +2,10 @@ import classNames from 'classnames'; import omit from 'lodash.omit'; import PropTypes from 'prop-types'; import React from 'react'; -import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; -import {connect} from 'react-redux'; +import { defineMessages, FormattedMessage, injectIntl, intlShape } from 'react-intl'; +import { connect } from 'react-redux'; import MediaQuery from 'react-responsive'; -import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import tabStyles from 'react-tabs/style/react-tabs.css'; import VM from 'scratch-vm'; import Renderer from 'scratch-render'; @@ -30,11 +30,9 @@ import Alerts from '../../containers/alerts.jsx'; import DragLayer from '../../containers/drag-layer.jsx'; import ConnectionModal from '../../containers/connection-modal.jsx'; import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx'; -import TextModelModal from '../../containers/model-modal.jsx'; -import ClassifierModelModal from '../../containers/classifier-model-modal.jsx' -import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; -import {resolveStageSize} from '../../lib/screen-utils'; +import layout, { STAGE_SIZE_MODES } from '../../lib/layout-constants'; +import { resolveStageSize } from '../../lib/screen-utils'; import styles from './gui.css'; import addExtensionIcon from './icon--extensions.svg'; @@ -182,18 +180,8 @@ const GUIComponent = props => { {tipsLibraryVisible ? ( ) : null} - {textModelModalVisible ? ( - - ) : null} {programmaticModalDetails ? ( - - ) : null} - {classifierModelModalVisible ? ( - + ) : null} {cardsVisible ? ( diff --git a/packages/scratch-gui/src/components/text-model-modal/dots.jsx b/packages/scratch-gui/src/components/text-model-modal/dots.jsx deleted file mode 100644 index 4b8ccd5d0..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/dots.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; - -import Box from '../box/box.jsx'; -import styles from './model-modal.css'; - -const Dots = props => ( - -
- {Array(props.total).fill(0) - .map((_, i) => { - let type = 'inactive'; - if (props.counter === i) type = 'active'; - if (props.success) type = 'success'; - if (props.error) type = 'error'; - return (); - })} -
-
-); - -Dots.propTypes = { - className: PropTypes.string, - counter: PropTypes.number, - error: PropTypes.bool, - success: PropTypes.bool, - total: PropTypes.number -}; - -const Dot = props => ( -
-); - -Dot.propTypes = { - type: PropTypes.string -}; - -export default Dots; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/example-tile.jsx b/packages/scratch-gui/src/components/text-model-modal/example-tile.jsx deleted file mode 100644 index 308500456..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/example-tile.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import Box from '../box/box.jsx'; -import CloseButton from '../close-button/close-button.jsx'; - -import styles from './model-modal.css'; - -class ExampleTile extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleDeleteExample' - ]); - } - handleDeleteExample () { - this.props.onDeleteExample(this.props.label, this.props.id); - } - - - render () { - return ( -
- {this.props.closeButton ? - <> - {this.props.text} - - - : -
{this.props.text}
} -
- ); - } -} - -ExampleTile.propTypes = { - label: PropTypes.string, - text: PropTypes.string, - onDeleteExample: PropTypes.func, - closeButton: PropTypes.bool -}; - -export default ExampleTile; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/icon--close.svg b/packages/scratch-gui/src/components/text-model-modal/icon--close.svg deleted file mode 100644 index 73c7b929f..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/icon--close.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - Extensions/Connection/Close - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/icon--enter.svg b/packages/scratch-gui/src/components/text-model-modal/icon--enter.svg deleted file mode 100644 index 03b7c0862..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/icon--enter.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - General/Check - Created with Sketch. - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/icon--help.svg b/packages/scratch-gui/src/components/text-model-modal/icon--help.svg deleted file mode 100644 index 2938e8340..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/icon--help.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - help - Created with Sketch. - - - - - \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/label-editor.jsx b/packages/scratch-gui/src/components/text-model-modal/label-editor.jsx deleted file mode 100644 index 2866efd73..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/label-editor.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import keyMirror from 'keymirror'; - -import Box from '../box/box.jsx'; -import ExampleTile from './example-tile.jsx'; - -import styles from './model-modal.css'; - -class EditLabelTile extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleRenameLabel', - 'handleDeleteLabel', - 'handleNewExample', - 'handleInputForNewExample', - 'handleDoneEditLabel' - ]); - this.state = { - inputText: "" - }; - } - handleDoneEditLabel () { - console.log("Text Model Modal: done editing label " + this.props.labelName); - this.props.onDoneEditLabel(this.props.labelName); - } - handleRenameLabel (input) { //call props.onRenameLabel with the current active label and the value in the input field - if (this.props.labelName !== input.target.value) { - this.props.onRenameLabel(this.props.labelName, input.target.value); - } - } - handleDeleteLabel () { //call props.onDeleteLabel with this label name - this.props.onDeleteLabel(this.props.labelName); - } - handleNewExample () { //call props.onAddExample - let newExample = this.state.inputText; - this.setState({ - inputText: "" - }); - - if (newExample != undefined && newExample != '') { - this.props.onNewExamples(this.props.labelName, [newExample]); - } - } - handleInputForNewExample (input) { //call props.onAddExample - this.setState({ - inputText: input.target.value - }); - - } - - render () { - return ( - - - - - - {" ("+this.props.exampleCount+" examples)"} - - - - - - {this.props.textData[this.props.labelName].map(example => ( - - - - ))} - - - - - - - - - - ); - } -} - -EditLabelTile.propTypes = { - labelName: PropTypes.string, - onRenameLabel: PropTypes.func, - onDeleteExample: PropTypes.func, - onDeleteLabel: PropTypes.func, - onNewExamples: PropTypes.func, - onDoneEditLabel: PropTypes.func, - textData: PropTypes.object, - exampleCount: PropTypes.number -}; - -export default EditLabelTile; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/label-tile.jsx b/packages/scratch-gui/src/components/text-model-modal/label-tile.jsx deleted file mode 100644 index b5d9cb3bb..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/label-tile.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import keyMirror from 'keymirror'; - -import Box from '../box/box.jsx'; -import ExampleTile from './example-tile.jsx'; - -import styles from './model-modal.css'; - -class LabelTile extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleEditLabel' - ]); - this.state = { - inputText: "" - }; - } - handleEditLabel () { - console.log("Text Model Modal: edit label " + this.props.labelName); - this.props.onEditLabel(this.props.labelName); - } - - - render () { - return ( - - - - - {this.props.labelName} - {" ("+this.props.exampleCount+" examples)"} - - - - - {this.props.textData[this.props.labelName].map(example => ( - - - - ))} - - - - ); - } -} - -LabelTile.propTypes = { - labelName: PropTypes.string, - onEditLabel: PropTypes.func, - onNewExamples: PropTypes.func, - textData: PropTypes.object, - exampleCount: PropTypes.number -}; - -export default LabelTile; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/model-editor.jsx b/packages/scratch-gui/src/components/text-model-modal/model-editor.jsx deleted file mode 100644 index 781d217c6..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/model-editor.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import {FormattedMessage} from 'react-intl'; -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; -import keyMirror from 'keymirror'; - -import Box from '../box/box.jsx'; -import LabelTile from './label-tile.jsx'; -import EditLabelTile from './label-editor.jsx'; -import Dots from './dots.jsx'; - -import styles from './model-modal.css'; - -const ModelEditor = props => ( - - -
- {Object.keys(props.textData).map(label => ( //create column of label tiles - label === props.activeLabel ? - : - ))} -
-
- - - - - - - -
-); - -ModelEditor.propTypes = { - onAddLabel: PropTypes.func, - onCancel: PropTypes.func, - onClearAll: PropTypes.func, - onDeleteLabel: PropTypes.func, - onDeleteExample: PropTypes.func, - onEditLabel: PropTypes.func, - onDoneEditLabel: PropTypes.func, - onRenameLabel: PropTypes.func, - onNewExamples: PropTypes.func, - classifierData: PropTypes.object, - activeLabel: PropTypes.string -}; - -export default ModelEditor; \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/model-modal.css b/packages/scratch-gui/src/components/text-model-modal/model-modal.css deleted file mode 100644 index c38e3c287..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/model-modal.css +++ /dev/null @@ -1,264 +0,0 @@ -@import "../../css/colors.css"; -@import "../../css/units.css"; - -.modal-content { - width: 600px; -} - -.header { - background-color: $pen-primary; -} - -.body { - background: $ui-white; -} - -.centered-row { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.label-tile-pane { - overflow-y: auto; - width: 100%; - height: 100%; -} - -.label-tile { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; - - background-color: $ui-white; - border-radius: 0.25rem; - padding: 10px; - width: 100%; - margin-bottom: 0.5rem; -} - -.label-tile-header { - display: flex; - align-items: center; - height: 2rem; - margin-left: 0.25rem; -} - -.label-tile-footer { - display: flex; - align-items: center; - height: 2rem; - margin-top: 0.5rem; -} - -.label-tile-name { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - font-size: 0.875rem; - margin-right: 1rem; -} - -.label-tile button { - margin:0.25rem; - - padding: 0.3rem 0.5rem; - - border: none; - border-radius: 0.25rem; - - font-weight: 600; - font-size: 0.85rem; - line-height:1rem; - - background: $motion-primary; - border: $motion-primary; - color: white; - cursor: pointer; -} - -.vertical-layout { - align-items: flex-start; - align-self: flex-start; - flex-direction: column; - height: 100%; - width: 100%; -} - -.example-box { - align-items: center; - display: flex; - flex-wrap: wrap; - height: calc(100% - 2rem); - width: calc(100% - 1rem); - margin-left:1.5rem; -} - -.example-viewer-text { - margin: 0.5rem 0.25rem; - height: 1.5rem; - justify-content: flex-start; -} - -.add-example-row { - position: relative; - margin:auto; - - border-width: 1px; - border-style: solid; - border-color: hsla(0, 0%, 0%, 0.15); - border-radius: 0.25rem; -} - -.add-example-row input { - font-size: 0.85rem; - background-color: none; - - border-width: 0px; - border-color: hsla(0, 0%, 0%, 0.15); - border-radius: 0.25rem; -} -.add-example-row button { - background-color: hsla(0, 0%, 0%, 0.35); - margin:0; -} - -.example-viewer-image-container { - display: flex; - overflow-y: auto; - flex-wrap: wrap; - height: calc(100% - 1.5rem); - margin-left: 1rem; -} - -.loaded-examples-box { - height: 50%; - width: 80%; - margin: 0.7rem; - position: relative; - display: flex; - align-items: center; - justify-content: center; - - background-color: $ui-white; - border-radius: 0.25rem; -} - -.example-text { - height: 70%; - max-width:200px; - margin: 0.2rem 0.8rem; - position: relative; - background-color: $motion-light-transparent; - - display: flex; - align-items: center; - - border-radius: 0.5rem; -} - - -.canvas-area { - height: calc(100% - 1.5rem); - margin: 0.25rem; - display: flex; - justify-content: center; -} - -.canvas { - height: 100%; - position: relative; - display: block; - padding: 0.6rem 0.35rem; -} - -.removable { - padding-right: 1.25rem; -} - -.loading-camera-message { - position: absolute; - display: flex; - align-self: center; -} - -.delete-button { - position: absolute; - top: 0.25rem; - right: 0.25rem; - z-index: auto; -} - -.input-field { - height: 1.3rem; - padding: 0 0.75rem; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 0.875rem; - font-weight: bold; - color: hsla(225, 15%, 40%, 1); - border-width: 1px; - border-style: solid; - border-color: hsla(0, 0%, 0%, 0.15); - border-radius: 0.25rem; - outline: none; - cursor: text; - transition: 0.25s ease-out; - box-shadow: none; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; -} - -.activity-area { - height: 300px; - background-color: $motion-light-transparent; - display: flex; - justify-content: center; - align-items: center; - padding: .5rem; -} - -.button-row { - font-weight: bolder; - text-align: center; - display: flex; -} - -.button-row button { - padding: 0.6rem 0.75rem; - border-radius: 0.5rem; - background: $motion-primary; - color: white; - font-weight: 600; - font-size: 0.85rem; - margin: 0.25rem; - border: none; - cursor: pointer; - display: flex; - align-items: center; -} - -.bottom-area { - background-color: $ui-white; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - padding-top: 1rem; - padding-bottom: .75rem; - padding-left: .75rem; - padding-right: .75rem; -} - -.bottom-area .bottom-area-item+.bottom-area-item { - margin-top: 1rem; -} - -.instructions { - text-align: center; - height: 1.5rem; -} \ No newline at end of file diff --git a/packages/scratch-gui/src/components/text-model-modal/model-modal.jsx b/packages/scratch-gui/src/components/text-model-modal/model-modal.jsx deleted file mode 100644 index cd16ded49..000000000 --- a/packages/scratch-gui/src/components/text-model-modal/model-modal.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import Box from '../box/box.jsx'; -import Modal from '../../containers/modal.jsx'; - -import ModelEditor from './model-editor.jsx'; - -import styles from './model-modal.css'; - -const TextModelModalComponent = props => ( - - - - - -); - -TextModelModalComponent.propTypes = { - name: PropTypes.node, - onCancel: PropTypes.func.isRequired, - onHelp: PropTypes.func.isRequired -}; - -export { - TextModelModalComponent as default -}; \ No newline at end of file diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index ee71d75d6..252fd758e 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -14,18 +14,18 @@ import ExtensionLibrary from './extension-library.jsx'; import extensionData from '../lib/libraries/extensions/index.jsx'; import CustomProcedures from './custom-procedures.jsx'; import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; -import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants'; +import { STAGE_DISPLAY_SIZES } from '../lib/layout-constants'; import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import DragConstants from '../lib/drag-constants'; import defineDynamicBlock from '../lib/define-dynamic-block'; -import {connect} from 'react-redux'; -import {updateToolbox} from '../reducers/toolbox'; -import {activateColorPicker} from '../reducers/color-picker'; -import {closeExtensionLibrary, openSoundRecorder, openConnectionModal, openTextModelModal,openClassifierModelModal, openProgrammaticModal} from '../reducers/modals'; -import {activateCustomProcedures, deactivateCustomProcedures} from '../reducers/custom-procedures'; -import {setConnectionModalExtensionId} from '../reducers/connection-modal'; -import {openUIEvent, registerButtonCallbackEvent} from "../../../../extensions/dist/globals"; +import { connect } from 'react-redux'; +import { updateToolbox } from '../reducers/toolbox'; +import { activateColorPicker } from '../reducers/color-picker'; +import { closeExtensionLibrary, openSoundRecorder, openConnectionModal, openTextModelModal, openClassifierModelModal, openProgrammaticModal } from '../reducers/modals'; +import { activateCustomProcedures, deactivateCustomProcedures } from '../reducers/custom-procedures'; +import { setConnectionModalExtensionId } from '../reducers/connection-modal'; +import { openUIEvent, registerButtonCallbackEvent } from "../../../../extensions/dist/globals"; import { activateTab, @@ -46,7 +46,7 @@ const DroppableBlocks = DropAreaHOC([ ])(BlocksComponent); class Blocks extends React.Component { - constructor (props) { + constructor(props) { super(props); this.ScratchBlocks = VMScratchBlocks(props.vm); bindAll(this, [ @@ -86,7 +86,7 @@ class Blocks extends React.Component { this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100); this.toolboxUpdateQueue = []; } - componentDidMount () { + componentDidMount() { this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures; this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); @@ -94,7 +94,7 @@ class Blocks extends React.Component { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options, - {rtl: this.props.isRtl, toolbox: this.props.toolboxXML} + { rtl: this.props.isRtl, toolbox: this.props.toolboxXML } ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -108,12 +108,7 @@ class Blocks extends React.Component { const procButtonCallback = () => { this.ScratchBlocks.Procedures.createProcedureDefCallback_(this.workspace); }; - const textModelEditButtonCallback = () => { - this.props.onOpenTextModelModal(); - }; - const classifierModelEditButtonCallback = () => { - this.props.onOpenClassifierModelModal(); - } + const connectMicrobitRobotCallback = () => { this.props.vm.runtime.emit('CONNECT_MICROBIT_ROBOT'); } @@ -121,10 +116,8 @@ class Blocks extends React.Component { toolboxWorkspace.registerButtonCallback('MAKE_A_VARIABLE', varListButtonCallback('')); toolboxWorkspace.registerButtonCallback('MAKE_A_LIST', varListButtonCallback('list')); toolboxWorkspace.registerButtonCallback('MAKE_A_PROCEDURE', procButtonCallback); - toolboxWorkspace.registerButtonCallback('EDIT_TEXT_MODEL', textModelEditButtonCallback); - toolboxWorkspace.registerButtonCallback('EDIT_TEXT_CLASSIFIER', classifierModelEditButtonCallback); toolboxWorkspace.registerButtonCallback('CONNECT_MICROBIT_ROBOT', connectMicrobitRobotCallback); - + this.props.vm.runtime.on(registerButtonCallbackEvent, (event) => { toolboxWorkspace.registerButtonCallback(event, () => this.props.vm.runtime.emit(event)); }); @@ -155,7 +148,7 @@ class Blocks extends React.Component { this.setLocale(); } } - shouldComponentUpdate (nextProps, nextState) { + shouldComponentUpdate(nextProps, nextState) { return ( this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible || @@ -167,7 +160,7 @@ class Blocks extends React.Component { this.props.stageSize !== nextProps.stageSize ); } - componentDidUpdate (prevProps) { + componentDidUpdate(prevProps) { // If any modals are open, call hideChaff to close z-indexed field editors if (this.props.anyModalVisible && !prevProps.anyModalVisible) { this.ScratchBlocks.hideChaff(); @@ -205,18 +198,18 @@ class Blocks extends React.Component { this.workspace.setVisible(false); } } - componentWillUnmount () { + componentWillUnmount() { this.detachVM(); this.workspace.dispose(); clearTimeout(this.toolboxUpdateTimeout); } - requestToolboxUpdate () { + requestToolboxUpdate() { clearTimeout(this.toolboxUpdateTimeout); this.toolboxUpdateTimeout = setTimeout(() => { this.updateToolbox(); }, 0); } - setLocale () { + setLocale() { this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale); this.props.vm.setLocale(this.props.locale, this.props.messages) .then(() => { @@ -229,7 +222,7 @@ class Blocks extends React.Component { }); } - updateToolbox () { + updateToolbox() { this.toolboxUpdateTimeout = false; const categoryId = this.workspace.toolbox_.getSelectedCategoryId(); @@ -255,7 +248,7 @@ class Blocks extends React.Component { queue.forEach(fn => fn()); } - withToolboxUpdates (fn) { + withToolboxUpdates(fn) { // if there is a queued toolbox update, we need to wait if (this.toolboxUpdateTimeout) { this.toolboxUpdateQueue.push(fn); @@ -264,7 +257,7 @@ class Blocks extends React.Component { } } - attachVM () { + attachVM() { this.workspace.addChangeListener(this.props.vm.blockListener); this.flyoutWorkspace = this.workspace .getFlyout() @@ -283,7 +276,7 @@ class Blocks extends React.Component { this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate); this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); } - detachVM () { + detachVM() { this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn); this.props.vm.removeListener('SCRIPT_GLOW_OFF', this.onScriptGlowOff); this.props.vm.removeListener('BLOCK_GLOW_ON', this.onBlockGlowOn); @@ -297,7 +290,7 @@ class Blocks extends React.Component { this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate); } - updateToolboxBlockValue (id, value) { + updateToolboxBlockValue(id, value) { this.withToolboxUpdates(() => { const block = this.workspace .getFlyout() @@ -309,7 +302,7 @@ class Blocks extends React.Component { }); } - onTargetsUpdate () { + onTargetsUpdate() { if (this.props.vm.editingTarget && this.workspace.getFlyout()) { ['glide', 'move', 'set'].forEach(prefix => { this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString()); @@ -317,7 +310,7 @@ class Blocks extends React.Component { }); } } - onWorkspaceMetricsChange () { + onWorkspaceMetricsChange() { const target = this.props.vm.editingTarget; if (target && target.id) { const workspaceMetrics = Object.assign({}, this.state.workspaceMetrics, { @@ -327,30 +320,30 @@ class Blocks extends React.Component { scale: this.workspace.scale } }); - this.setState({workspaceMetrics}); + this.setState({ workspaceMetrics }); } } - onScriptGlowOn (data) { + onScriptGlowOn(data) { this.workspace.glowStack(data.id, true); } - onScriptGlowOff (data) { + onScriptGlowOff(data) { this.workspace.glowStack(data.id, false); } - onBlockGlowOn (data) { + onBlockGlowOn(data) { this.workspace.glowBlock(data.id, true); } - onBlockGlowOff (data) { + onBlockGlowOff(data) { this.workspace.glowBlock(data.id, false); } - onVisualReport (data) { + onVisualReport(data) { this.workspace.reportValue(data.id, data.value); } - getToolboxXML () { + getToolboxXML() { // Use try/catch because this requires digging pretty deep into the VM // Code inside intentionally ignores several error situations (no stage, etc.) // Because they would get caught by this try/catch try { - let {editingTarget: target, runtime} = this.props.vm; + let { editingTarget: target, runtime } = this.props.vm; const stage = runtime.getTargetForStage(); if (!target) target = stage; // If no editingTarget, use the stage @@ -367,7 +360,7 @@ class Blocks extends React.Component { return null; } } - onWorkspaceUpdate (data) { + onWorkspaceUpdate(data) { // When we change sprites, update the toolbox to have the new sprite's blocks const toolboxXML = this.getToolboxXML(); if (toolboxXML) { @@ -401,7 +394,7 @@ class Blocks extends React.Component { this.workspace.addChangeListener(this.props.vm.blockListener); if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) { - const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id]; + const { scrollX, scrollY, scale } = this.state.workspaceMetrics[this.props.vm.editingTarget.id]; this.workspace.scrollX = scrollX; this.workspace.scrollY = scrollY; this.workspace.scale = scale; @@ -413,7 +406,7 @@ class Blocks extends React.Component { // workspace to be 'undone' here. this.workspace.clearUndo(); } - handleExtensionAdded (categoryInfo) { + handleExtensionAdded(categoryInfo) { const defineBlocks = blockInfoArray => { if (blockInfoArray && blockInfoArray.length > 0) { const staticBlocksJson = []; @@ -454,11 +447,11 @@ class Blocks extends React.Component { this.props.updateToolboxState(toolboxXML); } } - handleBlocksInfoUpdate (categoryInfo) { + handleBlocksInfoUpdate(categoryInfo) { // @todo Later we should replace this to avoid all the warnings from redefining blocks. this.handleExtensionAdded(categoryInfo); } - handleCategorySelected (categoryId) { + handleCategorySelected(categoryId) { const extension = extensionData.find(ext => ext.extensionId === categoryId); if (extension && extension.launchPeripheralConnectionFlow) { this.handleConnectionModalStart(categoryId); @@ -468,11 +461,11 @@ class Blocks extends React.Component { this.workspace.toolbox_.setSelectedCategoryById(categoryId); }); } - setBlocks (blocks) { + setBlocks(blocks) { this.blocks = blocks; } - handlePromptStart (message, defaultValue, callback, optTitle, optVarType) { - const p = {prompt: {callback, message, defaultValue}}; + handlePromptStart(message, defaultValue, callback, optTitle, optVarType) { + const p = { prompt: { callback, message, defaultValue } }; p.prompt.title = optTitle ? optTitle : this.ScratchBlocks.Msg.VARIABLE_MODAL_TITLE; p.prompt.varType = typeof optVarType === 'string' ? @@ -484,16 +477,16 @@ class Blocks extends React.Component { p.prompt.showCloudOption = (optVarType === this.ScratchBlocks.SCALAR_VARIABLE_TYPE) && this.props.canUseCloud; this.setState(p); } - handleConnectionModalStart (extensionId) { - let prgCustomExtensions = ['microbitRobot','teachableMachine']; + handleConnectionModalStart(extensionId) { + let prgCustomExtensions = ['microbitRobot', 'teachableMachine']; if (!prgCustomExtensions.includes(extensionId)) { this.props.onOpenConnectionModal(extensionId); } } - handleStatusButtonUpdate () { + handleStatusButtonUpdate() { this.ScratchBlocks.refreshStatusButtons(this.workspace); } - handleOpenSoundRecorder () { + handleOpenSoundRecorder() { this.props.onOpenSoundRecorder(); } @@ -502,23 +495,23 @@ class Blocks extends React.Component { * and additional potentially conflicting variable names from the VM * to the variable validation prompt callback used in scratch-blocks. */ - handlePromptCallback (input, variableOptions) { + handlePromptCallback(input, variableOptions) { this.state.prompt.callback( input, this.props.vm.runtime.getAllVarNamesOfType(this.state.prompt.varType), variableOptions); this.handlePromptClose(); } - handlePromptClose () { - this.setState({prompt: null}); + handlePromptClose() { + this.setState({ prompt: null }); } - handleCustomProceduresClose (data) { + handleCustomProceduresClose(data) { this.props.onRequestCloseCustomProcedures(data); const ws = this.workspace; ws.refreshToolboxSelection_(); ws.toolbox_.scrollToCategoryById('myBlocks'); } - handleDrop (dragInfo) { + handleDrop(dragInfo) { fetch(dragInfo.payload.bodyUrl) .then(response => response.json()) .then(blocks => this.props.vm.shareBlocksToTarget(blocks, this.props.vm.editingTarget.id)) @@ -527,7 +520,7 @@ class Blocks extends React.Component { this.updateToolbox(); // To show new variables/custom blocks }); } - render () { + render() { /* eslint-disable no-unused-vars */ const { anyModalVisible, diff --git a/packages/scratch-gui/src/containers/classifier-model-modal.jsx b/packages/scratch-gui/src/containers/classifier-model-modal.jsx deleted file mode 100644 index 89328c307..000000000 --- a/packages/scratch-gui/src/containers/classifier-model-modal.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import ClassifierModelModalComponent from '../components/classifier-modal/classifier-model-modal.jsx'; -import VM from 'scratch-vm'; -import {connect} from 'react-redux'; -import {closeClassifierModelModal} from '../reducers/modals'; -import {handleFileUpload} from '../lib/file-uploader.js'; -import {showStandardAlert} from '../reducers/alerts'; - - - -class ClassifierModelModal extends React.Component { - constructor (props) { - console.log("Loading Classifier Model Modal"); - super(props); - bindAll(this, [ - 'handleAddLabel', - 'handleCancel', - 'handleHelp', - 'handleFileUploader', - 'handleFileExport' - ]); - this.state = { - textData: props.vm.runtime.modelData.textData, //when the modal opens, get the model data and the next label number from the vm runtime - nextLabelNumber: props.vm.runtime.modelData.nextLabelNumber, - //classifiedData: props.vm.runtime.this.classifier.getClassifierDataset(), - activeLabel: "" //used by the label and example editors to keep track of the label currently being viewed/edited - }; - } - handleAddLabel () { //when a new label is first created, create a new active label and change to the example editor - console.log("Text Model Modal: add a label"); - // Make a new label - let newLabelName = "Class " + this.state.nextLabelNumber; - this.props.vm.runtime.emit('NEW_LABEL', newLabelName); - this.props.vm.runtime.modelData.nextLabelNumber++; - this.setState({ - nextLabelNumber: this.props.vm.runtime.modelData.nextLabelNumber, - activeLabel: newLabelName - }); - } - handleCancel () { //when modal closed, store the next label number in the runtime for later, then call props.onCancel() to close the modal - console.log("Text Model Modal: handle cancel") - //this.props.vm.runtime.emit('LOAD_CLASSIFIER', classifiedData); - this.setState({ - activeLabel: "" - }); - this.props.vm.runtime.emit('LOAD_CLASSIFIER'); - this.props.onCancel(); - } - handleHelp () { //TODO? - console.log("Text Model Modal: Help requested"); - //window.open(link, '_blank'); - } - handleFileUploader (input) { - console.log("Text Model Modal: Handle File upload"); - } - handleFileExport () { - console.log("Text Model Modal: Handle File export"); - this.props.vm.runtime.emit('EXPORT_CLASSIFIER'); - } - render () { - return ( - - - ); - } -} - -ClassifierModelModal.propTypes = { - onCancel: PropTypes.func.isRequired, - vm: PropTypes.instanceOf(VM).isRequired, -}; - -const mapStateToProps = state => ({ -}); - -const mapDispatchToProps = dispatch => ({ - onCancel: () => { - dispatch(closeClassifierModelModal()); - } -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(ClassifierModelModal); \ No newline at end of file diff --git a/packages/scratch-gui/src/containers/model-modal.jsx b/packages/scratch-gui/src/containers/model-modal.jsx deleted file mode 100644 index f51b9d8f5..000000000 --- a/packages/scratch-gui/src/containers/model-modal.jsx +++ /dev/null @@ -1,142 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import TextModelModalComponent from '../components/text-model-modal/model-modal.jsx'; -import VM from 'scratch-vm'; -import {connect} from 'react-redux'; -import {closeTextModelModal} from '../reducers/modals'; - -class TextModelModal extends React.Component { - constructor (props) { - console.log("Loading Text Model Modal"); - super(props); - bindAll(this, [ - 'handleAddLabel', - 'handleEditLabel', - 'handleDoneEditLabel', - 'handleRenameLabel', - 'handleDeleteLabel', - 'handleNewExamples', - 'handleDeleteExample', - 'handleDeleteLoadedExamples', - 'handleClearAll', - 'handleCancel', - 'handleHelp', - ]); - this.state = { - textData: props.vm.runtime.modelData.textData, //when the modal opens, get the model data and the next label number from the vm runtime - nextLabelNumber: props.vm.runtime.modelData.nextLabelNumber, - //classifiedData: props.vm.runtime.this.classifier.getClassifierDataset(), - activeLabel: "" //used by the label and example editors to keep track of the label currently being viewed/edited - }; - } - handleAddLabel () { //when a new label is first created, create a new active label and change to the example editor - console.log("Text Model Modal: add a label"); - // Make a new label - let newLabelName = "Class " + this.state.nextLabelNumber; - this.props.vm.runtime.emit('NEW_LABEL', newLabelName); - this.props.vm.runtime.modelData.nextLabelNumber++; - this.setState({ - nextLabelNumber: this.props.vm.runtime.modelData.nextLabelNumber, - activeLabel: newLabelName - }); - } - handleEditLabel (labelName) { //change to label editor, set active label based on which label's "edit" button was pressed - console.log("Text Model Modal: edit label " + labelName); - this.setState({ - activeLabel: labelName - }); - } - handleDoneEditLabel (labelName) { //change to label editor, set active label based on which label's "edit" button was pressed - console.log("Text Model Modal: done editing label " + labelName); - this.setState({ - activeLabel: "" - }); - } - handleRenameLabel (labelName, newLabelName) { //rename a label: emit an event so the label changes in the vm, change active label accordingly, and reset model data with the new label name - console.log("Text Model Modal: rename label " + labelName + " to " + newLabelName); - this.props.vm.runtime.emit('RENAME_LABEL', labelName, newLabelName); - this.setState({ - activeLabel: newLabelName - }); - } - handleDeleteLabel (labelName) { //delete a label: emit an event so the label is deleted in the vm, reset model data without the deleted label - console.log("Text Model Modal: delete label " + labelName); - this.props.vm.runtime.emit('DELETE_LABEL', labelName); - this.setState({}); - } - handleNewExamples (labelNum, examples) { //add new examples: emit an event so the example is added in the vm, switch back to label editor, reset model data with the new example - console.log("Text Model Modal: add examples from a new label") - this.props.vm.runtime.emit('NEW_EXAMPLES', examples, labelNum); - this.setState({ }); - } - handleDeleteExample (labelName, exampleNum) { - console.log("Text Model Modal: delete " + exampleNum + " from label " + labelName); - this.props.vm.runtime.emit('DELETE_EXAMPLE', labelName, exampleNum); - this.setState({ }); - } - handleDeleteLoadedExamples () { - console.log("Text Model Modal: delete examples") - this.props.vm.runtime.emit('DELETE_LOADED_EXAMPLES', labelName); - } - handleClearAll () { //clear all labels/examples: emit an event so the data is cleared in the vm, reset model data to be empty - console.log("Text Model Modal: clear all labels") - this.props.vm.runtime.emit('CLEAR_ALL_LABELS'); - this.setState({ }); - } - handleCancel () { //when modal closed, store the next label number in the runtime for later, then call props.onCancel() to close the modal - console.log("Text Model Modal: handle cancel") - this.props.vm.runtime.modelDatanextLabelNumber = this.state.nextLabelNumber; - this.setState({ - activeLabel: "" - }); - this.props.vm.runtime.emit('DONE'); - this.props.onCancel(); - } - handleHelp () { //TODO? - console.log("Text Model Modal: Help requested"); - //window.open(link, '_blank'); - } - render () { - return ( - - ); - } -} - -TextModelModal.propTypes = { - onCancel: PropTypes.func.isRequired, - vm: PropTypes.instanceOf(VM).isRequired -}; - -const mapStateToProps = state => ({ -}); - -const mapDispatchToProps = dispatch => ({ - onCancel: () => { - dispatch(closeTextModelModal()); - } -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(TextModelModal); \ No newline at end of file diff --git a/packages/scratch-gui/src/svelte/Modal.svelte b/packages/scratch-gui/src/svelte/Modal.svelte index 4ad83312d..9f89b8b0c 100644 --- a/packages/scratch-gui/src/svelte/Modal.svelte +++ b/packages/scratch-gui/src/svelte/Modal.svelte @@ -57,6 +57,7 @@ }); onDestroy(() => { + // HACK: This is a hack to ensure a svelte's `onDestroy` callback(s) is called const callbacks = constructed?.["$$"]?.["on_destroy"]; if (!callbacks) return; callbacks.forEach((callback) => callback());