├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bugreport.md │ └── feature.md └── workflows │ ├── codeql-analysis.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── Contributor License Agreement ├── LICENSE ├── README.md ├── TRADEMARK ├── docs ├── project_state_diagram.svg └── project_state_example.png ├── gen-meta.js ├── package.json ├── prune-gh-pages.sh ├── renovate.json5 ├── src ├── .eslintrc.js ├── components │ ├── about-modal │ │ ├── about-modal.css │ │ ├── about-modal.jsx │ │ ├── clipcc3_logo.svg │ │ ├── discord.svg │ │ ├── github.svg │ │ ├── qq.svg │ │ └── telegram.svg │ ├── action-menu │ │ ├── action-menu.css │ │ ├── action-menu.jsx │ │ ├── icon--backdrop.svg │ │ ├── icon--file-upload.svg │ │ ├── icon--paint.svg │ │ ├── icon--search.svg │ │ ├── icon--sprite.svg │ │ └── icon--surprise.svg │ ├── alerts │ │ ├── alert.css │ │ ├── alert.jsx │ │ ├── alerts.css │ │ ├── alerts.jsx │ │ ├── inline-message.css │ │ └── inline-message.jsx │ ├── asset-panel │ │ ├── asset-panel.css │ │ ├── asset-panel.jsx │ │ ├── icon--add-backdrop-lib.svg │ │ ├── icon--add-blank-costume.svg │ │ ├── icon--add-costume-lib.svg │ │ ├── icon--add-sound-lib.svg │ │ ├── icon--add-sound-record.svg │ │ ├── icon--sound-rtl.svg │ │ ├── icon--sound.svg │ │ ├── selector.css │ │ ├── selector.jsx │ │ └── sortable-asset.jsx │ ├── audio-trimmer │ │ ├── audio-selector.jsx │ │ ├── audio-trimmer.css │ │ ├── audio-trimmer.jsx │ │ ├── icon--handle.svg │ │ ├── playhead.jsx │ │ └── selection-handle.jsx │ ├── backpack │ │ ├── backpack.css │ │ └── backpack.jsx │ ├── blocks │ │ ├── blocks.css │ │ └── blocks.jsx │ ├── box │ │ ├── box.css │ │ └── box.jsx │ ├── browser-modal │ │ ├── browser-modal.css │ │ ├── browser-modal.jsx │ │ └── unsupported-browser.svg │ ├── button │ │ ├── button.css │ │ └── button.jsx │ ├── cards │ │ ├── card.css │ │ ├── cards.jsx │ │ ├── icon--close.svg │ │ ├── icon--expand.svg │ │ ├── icon--next.svg │ │ ├── icon--prev.svg │ │ └── icon--shrink.svg │ ├── checkbox │ │ ├── check.svg │ │ ├── checkbox.css │ │ └── checkbox.jsx │ ├── close-button │ │ ├── close-button.css │ │ ├── close-button.jsx │ │ ├── icon--close-orange.svg │ │ └── icon--close.svg │ ├── coming-soon │ │ ├── aww-cat.png │ │ ├── coming-soon.css │ │ ├── coming-soon.jsx │ │ └── cool-cat.png │ ├── connection-modal │ │ ├── auto-scanning-step.jsx │ │ ├── connected-step.jsx │ │ ├── connecting-step.jsx │ │ ├── connection-modal.css │ │ ├── connection-modal.jsx │ │ ├── dots.jsx │ │ ├── error-step.jsx │ │ ├── icons │ │ │ ├── back.svg │ │ │ ├── bluetooth-white.svg │ │ │ ├── bluetooth.svg │ │ │ ├── cancel.svg │ │ │ ├── close.svg │ │ │ ├── help.svg │ │ │ ├── refresh.svg │ │ │ ├── scratchlink.svg │ │ │ └── searching.png │ │ ├── peripheral-tile.jsx │ │ ├── scanning-step.jsx │ │ └── unavailable-step.jsx │ ├── context-menu │ │ ├── context-menu.css │ │ └── context-menu.jsx │ ├── contributor-modal │ │ ├── contributor-list.js │ │ ├── contributor-modal.css │ │ └── contributor-modal.jsx │ ├── controls │ │ ├── controls.css │ │ └── controls.jsx │ ├── crash-message │ │ ├── crash-message.css │ │ ├── crash-message.jsx │ │ └── reload.svg │ ├── custom-procedures │ │ ├── custom-procedures.css │ │ ├── custom-procedures.jsx │ │ ├── icon--boolean-input.svg │ │ ├── icon--label.svg │ │ └── icon--text-input.svg │ ├── delete-button │ │ ├── delete-button.css │ │ ├── delete-button.jsx │ │ └── icon--delete.svg │ ├── direction-picker │ │ ├── dial.css │ │ ├── dial.jsx │ │ ├── direction-picker.css │ │ ├── direction-picker.jsx │ │ ├── icon--all-around.svg │ │ ├── icon--dial.svg │ │ ├── icon--dont-rotate.svg │ │ ├── icon--handle.svg │ │ └── icon--left-right.svg │ ├── divider │ │ ├── divider.css │ │ └── divider.jsx │ ├── drag-layer │ │ ├── drag-layer.css │ │ └── drag-layer.jsx │ ├── filter │ │ ├── filter.css │ │ ├── filter.jsx │ │ ├── icon--filter.svg │ │ └── icon--x.svg │ ├── forms │ │ ├── buffered-input-hoc.jsx │ │ ├── input.css │ │ ├── input.jsx │ │ ├── label.css │ │ └── label.jsx │ ├── green-flag │ │ ├── green-flag.css │ │ ├── green-flag.jsx │ │ └── icon--green-flag.svg │ ├── gui │ │ ├── gui.css │ │ ├── gui.jsx │ │ ├── icon--code.svg │ │ ├── icon--costumes.svg │ │ ├── icon--extensions.svg │ │ └── icon--sounds.svg │ ├── icon-button │ │ ├── icon-button.css │ │ └── icon-button.jsx │ ├── language-selector │ │ ├── language-icon.svg │ │ ├── language-selector.css │ │ └── language-selector.jsx │ ├── library-item │ │ ├── bluetooth.svg │ │ ├── internet-connection.svg │ │ ├── lib-icon--sound-rtl.svg │ │ ├── lib-icon--sound.svg │ │ ├── library-item.css │ │ └── library-item.jsx │ ├── library │ │ ├── library.css │ │ └── library.jsx │ ├── load-error-modal │ │ ├── load-error-modal.css │ │ └── load-error-modal.jsx │ ├── loader │ │ ├── blocks-loader.jsx │ │ ├── bottom-block.svg │ │ ├── loader.css │ │ ├── loader.jsx │ │ ├── middle-block.svg │ │ └── top-block.svg │ ├── loupe │ │ ├── loupe.css │ │ └── loupe.jsx │ ├── menu-bar │ │ ├── account-nav.css │ │ ├── account-nav.jsx │ │ ├── author-info.css │ │ ├── author-info.jsx │ │ ├── clipcc3_logo_white.svg │ │ ├── community-button.css │ │ ├── community-button.jsx │ │ ├── dropdown-caret.svg │ │ ├── icon--about.svg │ │ ├── icon--mystuff.png │ │ ├── icon--profile.png │ │ ├── icon--remix.svg │ │ ├── icon--see-community.svg │ │ ├── login-dropdown.css │ │ ├── login-dropdown.jsx │ │ ├── menu-bar-menu.jsx │ │ ├── menu-bar.css │ │ ├── menu-bar.jsx │ │ ├── project-title-input.css │ │ ├── project-title-input.jsx │ │ ├── save-status.css │ │ ├── save-status.jsx │ │ ├── scratch-logo.svg │ │ ├── share-button.css │ │ ├── share-button.jsx │ │ ├── user-avatar.css │ │ └── user-avatar.jsx │ ├── menu │ │ ├── menu.css │ │ └── menu.jsx │ ├── message-box-modal │ │ ├── message-box-modal.css │ │ └── message-box-modal.jsx │ ├── meter │ │ ├── meter.css │ │ └── meter.jsx │ ├── mic-indicator │ │ ├── mic-indicator.css │ │ ├── mic-indicator.jsx │ │ └── mic-indicator.svg │ ├── modal │ │ ├── modal.css │ │ └── modal.jsx │ ├── monitor-list │ │ ├── monitor-list.css │ │ └── monitor-list.jsx │ ├── monitor │ │ ├── default-monitor.jsx │ │ ├── large-monitor.jsx │ │ ├── list-monitor-scroller.jsx │ │ ├── list-monitor.jsx │ │ ├── monitor.css │ │ ├── monitor.jsx │ │ └── slider-monitor.jsx │ ├── play-button │ │ ├── icon--play.svg │ │ ├── icon--stop.svg │ │ ├── play-button.css │ │ └── play-button.jsx │ ├── prompt │ │ ├── icon--dropdown-caret.svg │ │ ├── prompt.css │ │ └── prompt.jsx │ ├── question │ │ ├── icon--enter.svg │ │ ├── question.css │ │ └── question.jsx │ ├── record-modal │ │ ├── icon--back.svg │ │ ├── icon--play.svg │ │ ├── icon--stop-playback.svg │ │ ├── icon--stop-recording.svg │ │ ├── playback-step.jsx │ │ ├── record-modal.css │ │ ├── record-modal.jsx │ │ └── recording-step.jsx │ ├── select │ │ ├── chevron-down.svg │ │ ├── select.css │ │ └── select.jsx │ ├── settings-modal │ │ ├── settings-modal.css │ │ └── settings-modal.jsx │ ├── slider-prompt │ │ ├── slider-prompt.css │ │ └── slider-prompt.jsx │ ├── sound-editor │ │ ├── icon--copy-to-new.svg │ │ ├── icon--copy.svg │ │ ├── icon--delete.svg │ │ ├── icon--echo.svg │ │ ├── icon--fade-in.svg │ │ ├── icon--fade-out.svg │ │ ├── icon--faster.svg │ │ ├── icon--louder.svg │ │ ├── icon--mute.svg │ │ ├── icon--paste.svg │ │ ├── icon--play.svg │ │ ├── icon--redo.svg │ │ ├── icon--reverse.svg │ │ ├── icon--robot.svg │ │ ├── icon--slower.svg │ │ ├── icon--softer.svg │ │ ├── icon--stop.svg │ │ ├── icon--trim-confirm.svg │ │ ├── icon--trim.svg │ │ ├── icon--undo.svg │ │ ├── sound-editor.css │ │ └── sound-editor.jsx │ ├── spinner │ │ ├── spinner.css │ │ └── spinner.jsx │ ├── sprite-info │ │ ├── icon--draggable-off.svg │ │ ├── icon--draggable-on.svg │ │ ├── icon--hide.svg │ │ ├── icon--show.svg │ │ ├── icon--x.svg │ │ ├── icon--y.svg │ │ ├── sprite-info.css │ │ └── sprite-info.jsx │ ├── sprite-selector-item │ │ ├── sprite-selector-item.css │ │ └── sprite-selector-item.jsx │ ├── sprite-selector │ │ ├── sprite-list.jsx │ │ ├── sprite-selector.css │ │ └── sprite-selector.jsx │ ├── stage-header │ │ ├── icon--fullscreen.svg │ │ ├── icon--large-stage.svg │ │ ├── icon--small-stage.svg │ │ ├── icon--unfullscreen.svg │ │ ├── stage-header.css │ │ └── stage-header.jsx │ ├── stage-selector │ │ ├── stage-selector.css │ │ └── stage-selector.jsx │ ├── stage-wrapper │ │ ├── stage-wrapper.css │ │ └── stage-wrapper.jsx │ ├── stage │ │ ├── stage.css │ │ └── stage.jsx │ ├── stop-all │ │ ├── icon--stop-all.svg │ │ ├── stop-all.css │ │ └── stop-all.jsx │ ├── switch │ │ ├── switch.css │ │ └── switch.jsx │ ├── tag-button │ │ ├── tag-button.css │ │ └── tag-button.jsx │ ├── target-pane │ │ ├── target-pane.css │ │ └── target-pane.jsx │ ├── telemetry-modal │ │ ├── telemetry-modal-header.png │ │ ├── telemetry-modal.css │ │ └── telemetry-modal.jsx │ ├── text-switch │ │ ├── text-switch.css │ │ └── text-switch.jsx │ ├── turbo-mode │ │ ├── icon--turbo.svg │ │ ├── turbo-mode.css │ │ └── turbo-mode.jsx │ ├── watermark │ │ ├── watermark.css │ │ └── watermark.jsx │ ├── waveform │ │ ├── waveform.css │ │ └── waveform.jsx │ └── webgl-modal │ │ ├── unsupported.png │ │ ├── webgl-modal.css │ │ └── webgl-modal.jsx ├── containers │ ├── account-nav.jsx │ ├── alert.jsx │ ├── alerts.jsx │ ├── audio-selector.jsx │ ├── audio-trimmer.jsx │ ├── auto-scanning-step.jsx │ ├── backdrop-library.jsx │ ├── backpack.jsx │ ├── blocks.jsx │ ├── cards.jsx │ ├── connection-modal.jsx │ ├── controls.jsx │ ├── costume-library.jsx │ ├── costume-tab.jsx │ ├── custom-procedures.jsx │ ├── deletion-restorer.jsx │ ├── direction-picker.jsx │ ├── dom-element-renderer.jsx │ ├── drag-layer.jsx │ ├── error-boundary.jsx │ ├── extension-library.jsx │ ├── green-flag-overlay.jsx │ ├── gui.jsx │ ├── inline-messages.jsx │ ├── language-selector.jsx │ ├── library-item.jsx │ ├── list-monitor.jsx │ ├── load-error-modal.jsx │ ├── menu-bar-hoc.jsx │ ├── menu-item.jsx │ ├── menu.jsx │ ├── modal.jsx │ ├── monitor-list.jsx │ ├── monitor.jsx │ ├── paint-editor-wrapper.jsx │ ├── play-button.jsx │ ├── playback-step.jsx │ ├── project-watcher.jsx │ ├── prompt.jsx │ ├── question.jsx │ ├── record-modal.jsx │ ├── recording-step.jsx │ ├── sb-file-uploader.jsx │ ├── sb3-downloader.jsx │ ├── scanning-step.jsx │ ├── settings-modal.jsx │ ├── slider-monitor.jsx │ ├── slider-prompt.jsx │ ├── sound-editor.jsx │ ├── sound-library.jsx │ ├── sound-tab.jsx │ ├── sprite-info.jsx │ ├── sprite-library.jsx │ ├── sprite-selector-item.jsx │ ├── stage-header.jsx │ ├── stage-selector.jsx │ ├── stage-wrapper.jsx │ ├── stage.jsx │ ├── tag-button.jsx │ ├── target-highlight.jsx │ ├── target-pane.jsx │ ├── tips-library.jsx │ ├── turbo-mode.jsx │ ├── watermark.jsx │ └── webgl-modal.jsx ├── css │ ├── colors.css │ ├── scrollbar.css │ ├── typography.css │ ├── units.css │ └── z-index.css ├── examples │ └── extensions │ │ ├── .eslintrc.js │ │ └── example-extension.js ├── index.js ├── lib │ ├── alerts │ │ └── index.jsx │ ├── analytics.js │ ├── app-state-hoc.jsx │ ├── assets │ │ ├── icon--back.svg │ │ ├── icon--help.svg │ │ ├── icon--success.svg │ │ ├── icon--tutorials.svg │ │ └── placeholder.svg │ ├── audio │ │ ├── audio-buffer-player.js │ │ ├── audio-effects.js │ │ ├── audio-recorder.js │ │ ├── audio-util.js │ │ ├── effects │ │ │ ├── echo-effect.js │ │ │ ├── fade-effect.js │ │ │ ├── mute-effect.js │ │ │ ├── robot-effect.js │ │ │ └── volume-effect.js │ │ └── shared-audio-context.js │ ├── backpack-api.js │ ├── backpack │ │ ├── block-to-image.js │ │ ├── code-payload.js │ │ ├── costume-payload.js │ │ ├── jpeg-thumbnail.js │ │ ├── sound-payload.js │ │ ├── sound-thumbnail.jpg │ │ └── sprite-payload.js │ ├── blocks.js │ ├── bmp-converter.js │ ├── cloud-manager-hoc.jsx │ ├── cloud-provider.js │ ├── collect-metadata.js │ ├── connected-intl-provider.jsx │ ├── data-uri-to-blob.js │ ├── default-project │ │ ├── 8503e5b283cf0a746478e000a67c7e6f.svg │ │ ├── ade71c65863ef2b939bf573ab9cb0049.svg │ │ ├── cd21514d0531fdffb22204e0ec5ed84a.svg │ │ ├── fd8543abeeba255072da239223d2d342.wav │ │ ├── index.js │ │ └── project-data.js │ ├── define-dynamic-block.js │ ├── detect-locale.js │ ├── download-blob.js │ ├── drag-constants.js │ ├── drag-recognizer.js │ ├── drag-utils.js │ ├── drop-area-hoc.jsx │ ├── empty-assets.js │ ├── error-boundary-hoc.jsx │ ├── extension-api.js │ ├── extension-manager.js │ ├── file-uploader.js │ ├── font-loader-hoc.jsx │ ├── get-costume-url.js │ ├── gif-decoder.js │ ├── hash-parser-hoc.jsx │ ├── import-csv.js │ ├── isScratchDesktop.js │ ├── l10n.js │ ├── layout-constants.js │ ├── lazy-blocks-hoc.jsx │ ├── lazy-blocks.js │ ├── libraries │ │ ├── .eslintrc.js │ │ ├── async-load-libraries.js │ │ ├── backdrop-tags.js │ │ ├── backdrops.json │ │ ├── costumes.json │ │ ├── decks │ │ │ ├── en-steps.js │ │ │ ├── index.jsx │ │ │ ├── thumbnails │ │ │ │ └── getting-started-asl.png │ │ │ ├── translate-image.js │ │ │ └── translate-video.js │ │ ├── extensions │ │ │ ├── HTTPIO │ │ │ │ ├── HTTPIO.png │ │ │ │ ├── HTTPIO_icon.svg │ │ │ │ └── clipcc.httpio-small.svg │ │ │ ├── JSON │ │ │ │ ├── JSON.png │ │ │ │ ├── JSON_icon.svg │ │ │ │ └── ccjson-small.svg │ │ │ ├── boost │ │ │ │ ├── boost-button-illustration.svg │ │ │ │ ├── boost-illustration.svg │ │ │ │ ├── boost-small.svg │ │ │ │ └── boost.png │ │ │ ├── clipcc │ │ │ │ ├── CCUnknownExtension.jpg │ │ │ │ └── CCUnknownExtension.svg │ │ │ ├── ev3 │ │ │ │ ├── ev3-hub-illustration.svg │ │ │ │ ├── ev3-small.svg │ │ │ │ └── ev3.png │ │ │ ├── gdxfor │ │ │ │ ├── gdxfor-illustration.svg │ │ │ │ ├── gdxfor-small.svg │ │ │ │ └── gdxfor.png │ │ │ ├── index.jsx │ │ │ ├── libra │ │ │ │ ├── Libra-small.svg │ │ │ │ └── Libra.png │ │ │ ├── makeymakey │ │ │ │ ├── makeymakey-small.svg │ │ │ │ └── makeymakey.png │ │ │ ├── microbit │ │ │ │ ├── microbit-illustration.svg │ │ │ │ ├── microbit-small.svg │ │ │ │ └── microbit.png │ │ │ ├── music │ │ │ │ ├── music-small.svg │ │ │ │ └── music.png │ │ │ ├── pen │ │ │ │ ├── pen-old.png │ │ │ │ ├── pen-small.svg │ │ │ │ └── pen.png │ │ │ ├── speech2text │ │ │ │ └── speech.png │ │ │ ├── text2speech │ │ │ │ ├── text2speech-old.png │ │ │ │ ├── text2speech-small.svg │ │ │ │ └── text2speech.png │ │ │ ├── translate │ │ │ │ ├── translate-small.png │ │ │ │ └── translate.png │ │ │ ├── upload │ │ │ │ └── upload.png │ │ │ ├── videoSensing │ │ │ │ ├── video-sensing-small.svg │ │ │ │ └── video-sensing.png │ │ │ └── wedo2 │ │ │ │ ├── wedo-button-illustration.svg │ │ │ │ ├── wedo-illustration.svg │ │ │ │ ├── wedo-small.svg │ │ │ │ └── wedo.png │ │ ├── sound-tags.js │ │ ├── sounds.json │ │ ├── sprite-tags.js │ │ ├── sprites.json │ │ ├── tag-messages.js │ │ └── tutorial-tags.js │ ├── locale-utils.js │ ├── localization-hoc.jsx │ ├── log.js │ ├── make-toolbox-xml.js │ ├── math.js │ ├── monitor-adapter.js │ ├── opcode-labels.js │ ├── project-fetcher-hoc.jsx │ ├── project-saver-hoc.jsx │ ├── query-parser-hoc.jsx │ ├── randomize-sprite-position.js │ ├── save-project-to-server.js │ ├── sb-file-uploader-hoc.jsx │ ├── screen-utils.js │ ├── shared-messages.js │ ├── sortable-hoc.jsx │ ├── storage.js │ ├── supported-browser.js │ ├── tablet-full-screen.js │ ├── throttled-property-hoc.jsx │ ├── titled-hoc.jsx │ ├── touch-utils.js │ ├── tutorial-from-url.js │ ├── variable-utils.js │ ├── video │ │ ├── camera.js │ │ └── video-provider.js │ ├── vm-listener-hoc.jsx │ └── vm-manager-hoc.jsx ├── playground │ ├── blocks-only.css │ ├── blocks-only.jsx │ ├── compatibility-testing.jsx │ ├── index.css │ ├── index.ejs │ ├── index.jsx │ ├── player.css │ ├── player.jsx │ └── render-gui.jsx ├── reducers │ ├── alerts.js │ ├── asset-drag.js │ ├── block-drag.js │ ├── cards.js │ ├── color-picker.js │ ├── connection-modal.js │ ├── custom-procedures.js │ ├── editor-tab.js │ ├── extension-settings.js │ ├── extension.js │ ├── fonts-loaded.js │ ├── gui.js │ ├── hovered-target.js │ ├── load-error.js │ ├── locales.js │ ├── menus.js │ ├── mic-indicator.js │ ├── modals.js │ ├── mode.js │ ├── monitor-layout.js │ ├── monitors.js │ ├── project-changed.js │ ├── project-state.js │ ├── project-title.js │ ├── restore-deletion.js │ ├── settings.js │ ├── stage-size.js │ ├── targets.js │ ├── timeout.js │ ├── toolbox.js │ ├── vm-status.js │ ├── vm.js │ └── workspace-metrics.js └── test.js ├── static ├── bottom-block.svg ├── clipcc_logo144.png ├── clipcc_logo48.png ├── clipcc_logo72.png ├── clipcc_logo96.png ├── favicon.ico ├── manifest.webmanifest ├── middle-block.svg ├── social.jpg ├── sw.js └── top-block.svg ├── test ├── .eslintrc.js ├── __mocks__ │ ├── audio-buffer-player.js │ ├── audio-effects.js │ ├── editor-msgs-mock.js │ ├── fileMock.js │ └── styleMock.js ├── fixtures │ ├── 100-100.svg │ ├── bmpfile.bmp │ ├── corrupt-bmp.sb3 │ ├── corrupt-bmp.sprite3 │ ├── corrupt-from-scratch3.svg │ ├── corrupt-svg.sb2 │ ├── corrupt-svg.sb3 │ ├── corrupt-svg.sprite3 │ ├── corrupted-svg.sprite2 │ ├── gh-3582-png.png │ ├── missing-bmp.sb3 │ ├── missing-bmp.sprite3 │ ├── missing-sprite-svg.sb3 │ ├── missing-svg.sb2 │ ├── missing-svg.sprite2 │ ├── missing-svg.sprite3 │ ├── movie.wav │ ├── paddleball.gif │ ├── project1.sb3 │ ├── scratch2-corrupted.svg │ └── sneaker.wav ├── helpers │ ├── enzyme-setup.js │ ├── intl-helpers.jsx │ └── selenium-helper.js ├── integration │ ├── backdrops.test.js │ ├── backpack.test.js │ ├── blocks.test.js │ ├── connection-modal.test.js │ ├── costumes.test.js │ ├── examples.test.js │ ├── how-tos.test.js │ ├── localization.test.js │ ├── menu-bar.test.js │ ├── project-loading.test.js │ ├── project-state.test.js │ ├── sb-file-uploader-hoc.test.js │ ├── sounds.test.js │ ├── sprites.test.js │ ├── stage-size.test.js │ └── tutorials-shortcut.test.js ├── smoke │ └── browser.test.js └── unit │ ├── components │ ├── __snapshots__ │ │ ├── button.test.jsx.snap │ │ ├── icon-button.test.jsx.snap │ │ ├── sound-editor.test.jsx.snap │ │ └── sprite-selector-item.test.jsx.snap │ ├── button.test.jsx │ ├── cards.test.jsx │ ├── controls.test.jsx │ ├── icon-button.test.jsx │ ├── menu-bar.test.jsx │ ├── monitor-list.test.jsx │ ├── sound-editor.test.jsx │ └── sprite-selector-item.test.jsx │ ├── containers │ ├── menu-bar-hoc.test.jsx │ ├── save-status.test.jsx │ ├── slider-prompt.test.jsx │ ├── sound-editor.test.jsx │ └── sprite-selector-item.test.jsx │ ├── reducers │ ├── alerts-reducer.test.js │ ├── mode-reducer.test.js │ ├── monitor-layout-reducer.test.js │ ├── project-state-reducer.test.js │ └── workspace-metrics-reducer.test.js │ └── util │ ├── audio-context.test.js │ ├── audio-effects.test.js │ ├── audio-util.test.js │ ├── cloud-manager-hoc.test.jsx │ ├── cloud-provider.test.js │ ├── code-payload.test.js │ ├── default-project.test.js │ ├── define-dynamic-block.test.js │ ├── detect-locale.test.js │ ├── drag-recognizer.test.js │ ├── drag-utils.test.js │ ├── get-costume-url.test.js │ ├── hash-project-loader-hoc.test.jsx │ ├── opcode-labels.test.js │ ├── project-fetcher-hoc.test.jsx │ ├── project-saver-hoc.test.jsx │ ├── sb-file-uploader-hoc.test.jsx │ ├── throttled-property-hoc.test.jsx │ ├── translate-video.test.js │ ├── tutorial-from-url.test.js │ ├── vm-listener-hoc.test.jsx │ └── vm-manager-hoc.test.jsx ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-syntax-dynamic-import", 4 | "@babel/plugin-transform-async-to-generator", 5 | "@babel/plugin-proposal-object-rest-spread", 6 | ["react-intl", { 7 | "messagesDir": "./translations/messages/" 8 | }]], 9 | "presets": [ 10 | ["@babel/preset-env"], 11 | "@babel/preset-react" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | Safari >= 12 2 | iOS >= 12 3 | chrome >= 72 4 | firefox >= 70 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | 10 | [*.{js,html}] 11 | indent_style = space 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | build/* 3 | dist/* 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['scratch', 'scratch/node'] 3 | }; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly specify line endings for as many files as possible. 5 | # People who (for example) rsync between Windows and Linux need this. 6 | 7 | # File types which we know are binary 8 | 9 | # Treat SVG files as binary so that their contents don't change due to line 10 | # endings. The contents of SVGs must not change from the way they're stored 11 | # on assets.scratch.mit.edu so that MD5 calculations don't change. 12 | *.svg binary 13 | 14 | # Prefer LF for most file types 15 | *.css text eol=lf 16 | *.frag text eol=lf 17 | *.htm text eol=lf 18 | *.html text eol=lf 19 | *.iml text eol=lf 20 | *.js text eol=lf 21 | *.js.map text eol=lf 22 | *.json text eol=lf 23 | *.json5 text eol=lf 24 | *.jsx text eol=lf 25 | *.md text eol=lf 26 | *.vert text eol=lf 27 | *.xml text eol=lf 28 | *.yml text eol=lf 29 | 30 | # Prefer LF for these files 31 | .editorconfig text eol=lf 32 | .eslintrc text eol=lf 33 | .gitattributes text eol=lf 34 | .gitignore text eol=lf 35 | .gitmodules text eol=lf 36 | LICENSE text eol=lf 37 | Makefile text eol=lf 38 | README text eol=lf 39 | TRADEMARK text eol=lf 40 | 41 | # Use CRLF for Windows-specific file types 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bugreport.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 漏洞反馈 3 | about: 反馈在使用cc过程中遇到的问题 4 | --- 5 | 6 | 7 | 8 | 9 | ### 描述一下bug 10 | 11 | ## 请叙述bug触发步骤 12 | 13 | ## 预期的行为 14 | 15 | ## 截图或视频 16 | 17 | ## ClipCC版本 18 | 19 | ## 附加内容 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 新功能 3 | about: 让你cc有更多你想要的功能 4 | --- 5 | 6 | 7 | 8 | 9 | ### 描述一下你想要的功能 10 | 11 | ### 你为什么想要实现它?(可选) -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Publish Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: 14 19 | - run: yarn 20 | - run: yarn run build:dist 21 | env: 22 | NODE_ENV: production 23 | - name: Upload results 24 | uses: actions/upload-artifact@v2 25 | with: 26 | name: dist 27 | path: dist 28 | 29 | publish-npm: 30 | needs: build 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v2 35 | with: 36 | node-version: 14 37 | registry-url: https://registry.npmjs.org/ 38 | always-auth: true 39 | - name: Download results 40 | uses: actions/download-artifact@v2 41 | with: 42 | name: dist 43 | path: dist 44 | - run: yarn publish 45 | env: 46 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS 2 | .DS_Store 3 | 4 | # NPM 5 | /node_modules 6 | npm-* 7 | yarn-error.log 8 | 9 | # Testing 10 | /.nyc_output 11 | /coverage 12 | 13 | # Build 14 | /build 15 | /dist 16 | /.opt-in 17 | 18 | # Generated translation files 19 | /translations 20 | /locale 21 | 22 | /src/lib/app-info.js 23 | 24 | .vscode 25 | static/sw.build.js 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Mac OS 2 | .DS_Store 3 | 4 | # NPM 5 | /node_modules 6 | npm-* 7 | 8 | # Double copies of all the static assets and tutorial gifs 9 | /src 10 | 11 | # Testing 12 | /.nyc_output 13 | /coverage 14 | /test 15 | 16 | # Build 17 | /.opt-in 18 | /build 19 | 20 | # generated translation files 21 | /translations 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | 5 | before_install: 6 | - npm install yarn -g 7 | 8 | install: 9 | - yarn install 10 | 11 | script: 12 | - yarn run build 13 | -------------------------------------------------------------------------------- /TRADEMARK: -------------------------------------------------------------------------------- 1 | The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT). Marks may not be used to endorse or promote products derived from this software without specific prior written permission. 2 | -------------------------------------------------------------------------------- /docs/project_state_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/docs/project_state_example.png -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | 4 | "extends": [ 5 | "github>LLK/scratch-renovate-config:conservative" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | root: true, 4 | extends: ['scratch', 'scratch/es6', 'scratch/react', 'plugin:import/errors'], 5 | env: { 6 | browser: true 7 | }, 8 | globals: { 9 | process: true 10 | }, 11 | rules: { 12 | 'import/no-mutable-exports': 'error', 13 | 'import/no-commonjs': 'error', 14 | 'import/no-amd': 'error', 15 | 'import/no-nodejs-modules': 'error', 16 | 'react/jsx-no-literals': 'error', 17 | 'no-confusing-arrow': ['error', { 18 | allowParens: true 19 | }], 20 | 'camelcase': [2, { 21 | properties: 'never', // This is from the base `scratch` config 22 | allow: ['^UNSAFE_'] // Allow until migrated to new lifecycle methods 23 | }] 24 | }, 25 | settings: { 26 | 'react': { 27 | version: 'detect' 28 | }, 29 | 'import/resolver': { 30 | webpack: { 31 | config: path.resolve(__dirname, '../webpack.config.js') 32 | } 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/about-modal/about-modal.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .modal-content { 5 | width: 450px; 6 | } 7 | 8 | .body { 9 | background: $ui-white; 10 | padding: 1.5rem 2.25rem; 11 | } 12 | 13 | [theme='dark'] .body { 14 | background: $ui-primary-dark; 15 | color: $text-primary-dark; 16 | } 17 | 18 | .logo { 19 | display: block; 20 | margin: 0px auto; 21 | width: 100%; 22 | max-width: 200px; 23 | } 24 | 25 | .contact { 26 | display: flex; 27 | justify-content: space-around; 28 | padding: 0 5rem; 29 | } 30 | 31 | .contact > a > img { 32 | width: 2rem; 33 | } 34 | 35 | [theme='dark'] .logo { 36 | filter: invert(100%) brightness(10000%); 37 | } -------------------------------------------------------------------------------- /src/components/about-modal/github.svg: -------------------------------------------------------------------------------- 1 | Github 2 | -------------------------------------------------------------------------------- /src/components/alerts/alerts.css: -------------------------------------------------------------------------------- 1 | .alerts-inner-container { 2 | min-width: 200px; 3 | max-width: 548px; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/alerts/inline-message.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .inline-message { 5 | color: $ui-white; 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | display: flex; 8 | justify-content: flex-end; 9 | align-items: center; 10 | font-size: .8125rem; 11 | } 12 | 13 | .success { 14 | color: $ui-white-dim; 15 | } 16 | 17 | .info { 18 | color: $ui-white; 19 | } 20 | 21 | .warn { 22 | color: $error-light; 23 | } 24 | 25 | .spinner { 26 | margin-right: $space; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/alerts/inline-message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | import Spinner from '../spinner/spinner.jsx'; 6 | import {AlertLevels} from '../../lib/alerts/index.jsx'; 7 | 8 | import styles from './inline-message.css'; 9 | 10 | const InlineMessageComponent = ({ 11 | content, 12 | iconSpinner, 13 | level 14 | }) => ( 15 |
18 | {/* TODO: implement Rtl handling */} 19 | {iconSpinner && ( 20 | 25 | )} 26 | {content} 27 |
28 | ); 29 | 30 | InlineMessageComponent.propTypes = { 31 | content: PropTypes.element, 32 | iconSpinner: PropTypes.bool, 33 | level: PropTypes.string 34 | }; 35 | 36 | InlineMessageComponent.defaultProps = { 37 | level: AlertLevels.INFO 38 | }; 39 | 40 | export default InlineMessageComponent; 41 | -------------------------------------------------------------------------------- /src/components/asset-panel/asset-panel.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | @import "../../css/colors.css"; 3 | 4 | .wrapper { 5 | display: flex; 6 | flex-grow: 1; 7 | border: 1px solid $ui-black-transparent; 8 | background: white; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | font-size: 0.85rem; 11 | } 12 | 13 | [theme='dark'] .wrapper { 14 | background: $motion-primary-dark; 15 | color: $text-primary-dark; 16 | } 17 | 18 | [dir="ltr"] .wrapper { 19 | border-top-right-radius: $space; 20 | border-bottom-right-radius: $space; 21 | } 22 | 23 | [dir="rtl"] .wrapper { 24 | border-top-left-radius: $space; 25 | border-bottom-left-radius: $space; 26 | } 27 | 28 | .detail-area { 29 | display: flex; 30 | flex-grow: 1; 31 | flex-shrink: 1; 32 | overflow: visible; 33 | } 34 | 35 | [dir="ltr"] .detail-area { 36 | border-left: 1px solid $ui-black-transparent; 37 | } 38 | 39 | [dir="rtl"] .detail-area { 40 | border-right: 1px solid $ui-black-transparent; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/asset-panel/asset-panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Box from '../box/box.jsx'; 4 | import Selector from './selector.jsx'; 5 | import styles from './asset-panel.css'; 6 | 7 | const AssetPanel = props => ( 8 | 9 | 13 | 14 | {props.children} 15 | 16 | 17 | ); 18 | 19 | AssetPanel.propTypes = { 20 | ...Selector.propTypes 21 | }; 22 | 23 | export default AssetPanel; 24 | -------------------------------------------------------------------------------- /src/components/asset-panel/sortable-asset.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import bindAll from 'lodash.bindall'; 5 | 6 | class SortableAsset extends React.Component { 7 | constructor (props) { 8 | super(props); 9 | bindAll(this, [ 10 | 'setRef' 11 | ]); 12 | this.ref = React.createRef(); 13 | } 14 | componentDidMount () { 15 | this.props.onAddSortable(this.ref); 16 | } 17 | componentWillUnmount () { 18 | this.props.onRemoveSortable(this.ref); 19 | } 20 | setRef (ref) { 21 | this.ref = ref; 22 | } 23 | render () { 24 | return ( 25 |
32 | {this.props.children} 33 |
34 | ); 35 | } 36 | } 37 | 38 | SortableAsset.propTypes = { 39 | children: PropTypes.node.isRequired, 40 | className: PropTypes.string, 41 | index: PropTypes.number.isRequired, 42 | onAddSortable: PropTypes.func.isRequired, 43 | onRemoveSortable: PropTypes.func.isRequired 44 | }; 45 | 46 | export default SortableAsset; 47 | -------------------------------------------------------------------------------- /src/components/audio-trimmer/icon--handle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bottom Left Handle 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/audio-trimmer/playhead.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './audio-trimmer.css'; 5 | 6 | const Playhead = props => ( 7 |
8 |
14 |
15 | ); 16 | 17 | Playhead.propTypes = { 18 | playbackPosition: PropTypes.number 19 | }; 20 | 21 | export default Playhead; 22 | -------------------------------------------------------------------------------- /src/components/audio-trimmer/selection-handle.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import Box from '../box/box.jsx'; 5 | import styles from './audio-trimmer.css'; 6 | import handleIcon from './icon--handle.svg'; 7 | 8 | const SelectionHandle = props => ( 9 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | 23 | SelectionHandle.propTypes = { 24 | handleStyle: PropTypes.string, 25 | onMouseDown: PropTypes.func 26 | }; 27 | 28 | export default SelectionHandle; 29 | -------------------------------------------------------------------------------- /src/components/blocks/blocks.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import classNames from 'classnames'; 3 | import React from 'react'; 4 | import Box from '../box/box.jsx'; 5 | import styles from './blocks.css'; 6 | 7 | const BlocksComponent = props => { 8 | const { 9 | containerRef, 10 | dragOver, 11 | ...componentProps 12 | } = props; 13 | return ( 14 | 21 | ); 22 | }; 23 | BlocksComponent.propTypes = { 24 | containerRef: PropTypes.func, 25 | dragOver: PropTypes.bool 26 | }; 27 | export default BlocksComponent; 28 | -------------------------------------------------------------------------------- /src/components/box/box.css: -------------------------------------------------------------------------------- 1 | @import "../../css/scrollbar.css"; 2 | 3 | .box { 4 | } 5 | -------------------------------------------------------------------------------- /src/components/button/button.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | 3 | .outlined-button { 4 | cursor: pointer; 5 | border-radius: $form-radius; 6 | font-weight: bold; 7 | display: flex; 8 | flex-direction: row; 9 | align-items: center; 10 | padding-left: .75rem; 11 | padding-right: .75rem; 12 | user-select: none; 13 | } 14 | 15 | .icon { 16 | height: 1.5rem; 17 | } 18 | 19 | [dir="ltr"] .icon { 20 | margin-right: .5rem; 21 | } 22 | 23 | [dir="rtl"] .icon { 24 | margin-left: .5rem; 25 | } 26 | 27 | .content { 28 | white-space: nowrap; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/button/button.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import styles from './button.css'; 6 | 7 | const ButtonComponent = ({ 8 | className, 9 | disabled, 10 | iconClassName, 11 | iconSrc, 12 | onClick, 13 | children, 14 | ...props 15 | }) => { 16 | 17 | if (disabled) { 18 | onClick = function () {}; 19 | } 20 | 21 | const icon = iconSrc && ( 22 | 27 | ); 28 | 29 | return ( 30 | 39 | {icon} 40 |
{children}
41 |
42 | ); 43 | }; 44 | 45 | ButtonComponent.propTypes = { 46 | children: PropTypes.node, 47 | className: PropTypes.string, 48 | disabled: PropTypes.bool, 49 | iconClassName: PropTypes.string, 50 | iconSrc: PropTypes.string, 51 | onClick: PropTypes.func 52 | }; 53 | 54 | export default ButtonComponent; 55 | -------------------------------------------------------------------------------- /src/components/cards/icon--next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Next 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/cards/icon--prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Previous 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/checkbox/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/checkbox/checkbox.css: -------------------------------------------------------------------------------- 1 | @import '../../css/colors.css'; 2 | 3 | .label { 4 | padding: 0.5rem 1rem; 5 | display: flex; 6 | align-items: center; 7 | border-radius: 0.3rem; 8 | } 9 | 10 | .label span { 11 | line-height: 32px; 12 | } 13 | 14 | .disabled { 15 | color: darkgrey; 16 | } 17 | 18 | [theme='dark'] .disabled { 19 | color: rgb(213 213 213)!important; 20 | } 21 | 22 | .checkbox { 23 | margin-right: 0.5rem; 24 | background-color: $ui-white; 25 | background-size: 100%; 26 | background-origin: content-box; 27 | background-repeat: no-repeat; 28 | appearance: none; 29 | width: 18px; 30 | height: 18px; 31 | border: 1px solid rgb(92, 92, 92); 32 | border-radius: 4px; 33 | outline: none; 34 | padding: 2px; 35 | transition-duration: .2s; 36 | } 37 | 38 | [theme='dark'] .label{ 39 | color: text-primary-dark; 40 | } 41 | 42 | .checkbox:checked { 43 | background-image: url('./check.svg'); 44 | background-color: $motion-primary; 45 | border-color: $motion-primary; 46 | } 47 | 48 | .checkbox:disabled { 49 | border-color: darkgrey; 50 | } 51 | 52 | [theme='dark'] .checkbox:disabled { 53 | background-color: darkgrey; 54 | } 55 | 56 | .checkbox:disabled:checked { 57 | background-color: darkgrey; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/checkbox/checkbox.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './checkbox.css'; 6 | 7 | const CheckboxComponent = ({ 8 | disabled, 9 | checked, 10 | onChange, 11 | children, 12 | ...props 13 | }) => ( 14 | 27 | ); 28 | 29 | CheckboxComponent.propTypes = { 30 | children: PropTypes.node, 31 | className: PropTypes.string, 32 | disabled: PropTypes.bool, 33 | checked: PropTypes.bool, 34 | onChange: PropTypes.func 35 | }; 36 | 37 | export default CheckboxComponent; 38 | -------------------------------------------------------------------------------- /src/components/close-button/icon--close.svg: -------------------------------------------------------------------------------- 1 | icon--add -------------------------------------------------------------------------------- /src/components/coming-soon/aww-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/coming-soon/aww-cat.png -------------------------------------------------------------------------------- /src/components/coming-soon/cool-cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/coming-soon/cool-cat.png -------------------------------------------------------------------------------- /src/components/connection-modal/icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | back 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/connection-modal/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cancel 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/connection-modal/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | close 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/connection-modal/icons/searching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/connection-modal/icons/searching.png -------------------------------------------------------------------------------- /src/components/context-menu/context-menu.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | @import "../../css/z-index.css"; 4 | 5 | .context-menu { 6 | min-width: 130px; 7 | padding: 5px 0; /* The white strip at the top and bottom of the menu */ 8 | margin: 2px 0 0; /* To keep the menu below the cursor comfortably */ 9 | font-size: 0.85rem; 10 | text-align: left; 11 | background-color: $ui-white; 12 | border: 1px solid $ui-black-transparent; 13 | border-radius: calc($space / 2); 14 | box-shadow: 0px 0px 5px 1px $ui-black-transparent; 15 | pointer-events: none; 16 | transition: opacity 0.2s ease; 17 | z-index: $z-index-context-menu; 18 | } 19 | 20 | [theme='dark'] .context-menu { 21 | background-color: $ui-primary-dark; 22 | color: $text-primary-dark; 23 | } 24 | 25 | .menu-item { 26 | padding: 8px 12px; 27 | white-space: nowrap; 28 | cursor: pointer; 29 | transition: 0.1s ease; 30 | } 31 | 32 | .menu-item:hover { 33 | background: $motion-primary; 34 | color: white; 35 | } 36 | 37 | .menu-item-bordered { 38 | border-top: 1px solid $ui-black-transparent; 39 | } 40 | 41 | .menu-item-danger:hover { 42 | background: $error-primary; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/context-menu/context-menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ContextMenu, MenuItem} from 'react-contextmenu'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './context-menu.css'; 6 | 7 | const StyledContextMenu = props => ( 8 | 12 | ); 13 | 14 | const StyledMenuItem = props => ( 15 | 19 | ); 20 | 21 | const BorderedMenuItem = props => ( 22 | 26 | ); 27 | 28 | const DangerousMenuItem = props => ( 29 | 33 | ); 34 | 35 | 36 | export { 37 | BorderedMenuItem, 38 | DangerousMenuItem, 39 | StyledContextMenu as ContextMenu, 40 | StyledMenuItem as MenuItem 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/contributor-modal/contributor-list.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable linebreak-style */ 2 | export const contributorList = { 3 | alexcui: 'AlexCui', 4 | sinangentoo: 'SinanGentoo', 5 | frank782: 'frank_782', 6 | hydrostic: 'Hydrostic', 7 | solstice23: 'solstice23', 8 | e4361: 'e4361', 9 | sparrowhe: 'SparrowHe', 10 | stevexmh: 'SteveXMH', 11 | waterblock79: 'waterblock79', 12 | jasonxu: 'JasonXu', 13 | zerlight: 'Zerlight', 14 | soilzhu: 'SoilZhu', 15 | bleshi: 'BleShi', 16 | xiaoji4093: 'xiaoji4093', 17 | hotover: 'Hotover', 18 | lmlanmei: 'LanmeiCN', 19 | yuan3old: 'Yuan3old', 20 | someoneyoung: 'Someone_Young', 21 | jasonjia: 'JasonJia', 22 | unknown: 'Unknown contributor', 23 | lyricepic: 'LyricEpic', 24 | cyarice: 'Cya_Rice', 25 | afadian: 'Afadian_c5qE', 26 | mbzzw: 'mbzzw', 27 | editmr: 'Edit Mr.' 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/contributor-modal/contributor-modal.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .modal-content { 5 | width: 600px; 6 | } 7 | 8 | .body { 9 | background: $ui-white; 10 | padding: 1.5rem 2.25rem; 11 | overflow-y: auto; 12 | height: 70vh; 13 | } 14 | 15 | [theme='dark'] .body { 16 | background: $ui-primary-dark; 17 | color: text-primary-dark; 18 | } 19 | 20 | .logo { 21 | display: block; 22 | margin: 0px auto; 23 | width: 100%; 24 | max-width: 200px; 25 | } 26 | 27 | [theme='dark'] .logo { 28 | filter: invert(100%) brightness(10000%); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/controls/controls.css: -------------------------------------------------------------------------------- 1 | .controls-container { 2 | display: flex; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/crash-message/crash-message.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | @import "../../css/typography.css"; 4 | 5 | .crash-wrapper { 6 | background-color: $motion-primary; 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | .body { 14 | width: 35%; 15 | color: white; 16 | text-align: center; 17 | } 18 | 19 | .reloadButton { 20 | border: 1px solid $motion-primary; 21 | border-radius: 0.25rem; 22 | padding: 0.5rem 2rem; 23 | background: white; 24 | color: $motion-primary; 25 | font-weight: bold; 26 | font-size: 0.875rem; 27 | cursor: pointer; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/custom-procedures/icon--boolean-input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | R1_ C.Procedure Editble Inputs 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/custom-procedures/icon--text-input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | R1_ C.Procedure Editble Inputs 10 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/delete-button/delete-button.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | /* wrapper to allow for touch slop if we decide to add it */ 5 | .delete-button { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | user-select: none; 10 | cursor: pointer; 11 | transition: all 0.15s ease-out; 12 | } 13 | 14 | .delete-button-visible { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | overflow: hidden; /* Mask the icon animation */ 19 | width: 1.75rem; 20 | height: 1.75rem; 21 | box-shadow: 0px 0px 0px 2px $motion-transparent; 22 | background-color: $motion-primary; 23 | color: $ui-white; 24 | border-radius: 50%; 25 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 26 | user-select: none; 27 | cursor: pointer; 28 | transition: all 0.15s ease-out; 29 | } 30 | 31 | .delete-icon { 32 | position: relative; 33 | margin: 0.25rem; 34 | user-select: none; 35 | transform-origin: 50%; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/delete-button/delete-button.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './delete-button.css'; 6 | import deleteIcon from './icon--delete.svg'; 7 | 8 | const DeleteButton = props => ( 9 |
19 |
20 | 24 |
25 |
26 | 27 | ); 28 | 29 | DeleteButton.propTypes = { 30 | className: PropTypes.string, 31 | onClick: PropTypes.func.isRequired, 32 | tabIndex: PropTypes.number 33 | }; 34 | 35 | DeleteButton.defaultProps = { 36 | tabIndex: 0 37 | }; 38 | 39 | export default DeleteButton; 40 | -------------------------------------------------------------------------------- /src/components/direction-picker/dial.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .container { 4 | padding: 1rem; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | user-select: none; 9 | } 10 | 11 | .dial-container { 12 | position: relative; 13 | } 14 | 15 | .dial-face, .dial-handle, .gauge { 16 | position: absolute; 17 | top: 0; 18 | left: 0; 19 | overflow: visible; 20 | } 21 | 22 | .dial-face { 23 | width: 100%; 24 | } 25 | 26 | $dial-size: 40px; 27 | 28 | .dial-handle { 29 | cursor: pointer; 30 | width: $dial-size; 31 | height: $dial-size; 32 | /* Use margin to make positioning via top/left easier */ 33 | margin-left: calc($dial-size / -2); 34 | margin-top: calc($dial-size / -2); 35 | } 36 | 37 | .gauge-path { 38 | fill: $motion-transparent; 39 | stroke: $motion-primary; 40 | stroke-width: 1px; 41 | } 42 | -------------------------------------------------------------------------------- /src/components/direction-picker/direction-picker.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/z-index.css"; 3 | 4 | .popover { 5 | z-index: $z-index-direction-picker; 6 | } 7 | 8 | .button-row { 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: center; 12 | 13 | } 14 | 15 | .icon-button { 16 | margin: 0.25rem; 17 | border: none; 18 | background: none; 19 | outline: none; 20 | cursor: pointer; 21 | user-select: none; 22 | } 23 | 24 | .icon-button:active > img { 25 | width: 20px; 26 | height: 20px; 27 | transform: scale(1.15); 28 | } 29 | 30 | .icon-button > img { 31 | transition: transform 0.1s; 32 | filter: grayscale(100%); 33 | } 34 | 35 | .icon-button.active > img { 36 | filter: none; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/direction-picker/icon--all-around.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | all-around-active 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/direction-picker/icon--handle.svg: -------------------------------------------------------------------------------- 1 | 01_Dial Elements -------------------------------------------------------------------------------- /src/components/divider/divider.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .divider { 4 | border-right: 1px dashed $ui-black-transparent; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/divider/divider.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import styles from './divider.css'; 6 | 7 | const Divider = ({className}) => ( 8 |
9 | ); 10 | 11 | Divider.propTypes = { 12 | className: PropTypes.string 13 | }; 14 | 15 | export default Divider; 16 | -------------------------------------------------------------------------------- /src/components/drag-layer/drag-layer.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | @import "../../css/colors.css"; 3 | @import "../../css/z-index.css"; 4 | 5 | .drag-layer { 6 | position: fixed; 7 | pointer-events: none; 8 | z-index: $z-index-drag-layer; 9 | left: 0; 10 | top: 0; 11 | width: 100%; 12 | height: 100%; 13 | direction: ltr; 14 | } 15 | 16 | .image-wrapper { 17 | /* Absolute allows wrapper to snuggly fit image */ 18 | position: absolute; 19 | } 20 | 21 | .image { 22 | max-width: 80px; 23 | max-height: 80px; 24 | min-width: 50px; 25 | min-height: 50px; 26 | 27 | /* Center the dragging image on the given position */ 28 | margin-left: -50%; 29 | margin-top: -50%; 30 | 31 | padding: 0.25rem; 32 | border: 2px solid $motion-primary; 33 | background: $ui-white; 34 | border-radius: 0.5rem; 35 | 36 | /* Use the same drop shadow as stage dragging */ 37 | box-shadow: 5px 5px 5px $ui-black-transparent; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/drag-layer/drag-layer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './drag-layer.css'; 4 | 5 | /* eslint no-confusing-arrow: ["error", {"allowParens": true}] */ 6 | const DragLayer = ({dragging, img, currentOffset}) => (dragging ? ( 7 |
8 |
14 | 18 |
19 |
20 | ) : null); 21 | 22 | DragLayer.propTypes = { 23 | currentOffset: PropTypes.shape({ 24 | x: PropTypes.number.isRequired, 25 | y: PropTypes.number.isRequired 26 | }), 27 | dragging: PropTypes.bool.isRequired, 28 | img: PropTypes.string 29 | }; 30 | 31 | export default DragLayer; 32 | -------------------------------------------------------------------------------- /src/components/filter/icon--filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/filter/icon--x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/forms/input.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | @import "../../css/colors.css"; 3 | 4 | .input-form { 5 | height: 2rem; 6 | padding: 0 0.75rem; 7 | 8 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | font-size: 0.625rem; 10 | font-weight: bold; 11 | color: $text-primary; 12 | 13 | border-width: 1px; 14 | border-style: solid; 15 | border-color: $ui-black-transparent; 16 | border-radius: 0.75rem; 17 | 18 | outline: none; 19 | cursor: text; 20 | transition: 0.25s ease-out; /* @todo: standardize with var */ 21 | box-shadow: none; 22 | 23 | /* 24 | For truncating overflowing text gracefully 25 | Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text 26 | @todo: move this out into a mixin or a helper component 27 | */ 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | min-width: 0; 32 | } 33 | 34 | [theme='dark'] .input-form { 35 | background-color: $ui-tertiary-dark; 36 | color: white; 37 | } 38 | 39 | .input-form:hover { 40 | border-color: $motion-primary; 41 | } 42 | 43 | .input-form:focus { 44 | border-color: $motion-primary; 45 | box-shadow: 0 0 0 0.25rem $motion-transparent; 46 | } 47 | 48 | .input-small { 49 | width: 3rem; 50 | padding: 0 0.5rem; 51 | text-overflow: clip; 52 | text-align: center; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/forms/input.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './input.css'; 6 | 7 | 8 | const Input = props => { 9 | const {small, ...componentProps} = props; 10 | return ( 11 | 21 | ); 22 | }; 23 | 24 | Input.propTypes = { 25 | className: PropTypes.string, 26 | small: PropTypes.bool 27 | }; 28 | 29 | Input.defaultProps = { 30 | small: false 31 | }; 32 | 33 | export default Input; 34 | -------------------------------------------------------------------------------- /src/components/forms/label.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | @import "../../css/colors.css"; 3 | 4 | .input-group { 5 | display: inline-flex; 6 | flex-direction: row; 7 | align-items: center; 8 | } 9 | 10 | .input-group-column { 11 | display: inline-flex; 12 | flex-direction: column; 13 | align-items: flex-start; 14 | } 15 | 16 | .input-group-column span { 17 | margin-bottom: .25rem; 18 | } 19 | 20 | .input-label, .input-label-secondary { 21 | font-size: 0.625rem; 22 | user-select: none; 23 | cursor: default; 24 | 25 | white-space: nowrap; 26 | } 27 | 28 | [dir="ltr"] .input-label, [dir="ltr"] .input-label-secondary { 29 | margin-right: .5rem; 30 | } 31 | 32 | [dir="rtl"] .input-label, [dir="rtl"] .input-label-secondary { 33 | margin-left: .5rem; 34 | } 35 | 36 | .input-label { 37 | font-weight: bold; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/forms/label.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import styles from './label.css'; 5 | 6 | const Label = props => ( 7 | 13 | ); 14 | 15 | Label.propTypes = { 16 | above: PropTypes.bool, 17 | children: PropTypes.node, 18 | secondary: PropTypes.bool, 19 | text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired 20 | }; 21 | 22 | Label.defaultProps = { 23 | above: false, 24 | secondary: false 25 | }; 26 | 27 | export default Label; 28 | -------------------------------------------------------------------------------- /src/components/green-flag/green-flag.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .green-flag { 4 | width: 2rem; 5 | height: 2rem; 6 | padding: 0.375rem; 7 | border-radius: 0.25rem; 8 | user-select: none; 9 | user-drag: none; 10 | cursor: pointer; 11 | } 12 | 13 | .green-flag:hover { 14 | background-color: $motion-light-transparent; 15 | } 16 | 17 | .green-flag.is-active { 18 | background-color: $motion-transparent; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/green-flag/green-flag.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import greenFlagIcon from './icon--green-flag.svg'; 6 | import styles from './green-flag.css'; 7 | 8 | const GreenFlagComponent = function (props) { 9 | const { 10 | active, 11 | className, 12 | onClick, 13 | title, 14 | ...componentProps 15 | } = props; 16 | return ( 17 | 31 | ); 32 | }; 33 | GreenFlagComponent.propTypes = { 34 | active: PropTypes.bool, 35 | className: PropTypes.string, 36 | onClick: PropTypes.func.isRequired, 37 | title: PropTypes.string 38 | }; 39 | GreenFlagComponent.defaultProps = { 40 | active: false, 41 | title: 'Go' 42 | }; 43 | export default GreenFlagComponent; 44 | -------------------------------------------------------------------------------- /src/components/green-flag/icon--green-flag.svg: -------------------------------------------------------------------------------- 1 | icon--green-flag -------------------------------------------------------------------------------- /src/components/icon-button/icon-button.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | cursor: pointer; 8 | font-size: 0.75rem; 9 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color: $motion-primary; 11 | border-radius: 0.5rem; 12 | } 13 | 14 | .container + .container { 15 | margin-top: 1.25rem; 16 | } 17 | 18 | .title { 19 | margin-top: 0.5rem; 20 | text-align: center; 21 | } 22 | 23 | .disabled { 24 | opacity: 0.5; 25 | pointer-events: none; 26 | } 27 | 28 | .container:active { 29 | background-color: $motion-light-transparent; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/icon-button/icon-button.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './icon-button.css'; 5 | 6 | const IconButton = ({ 7 | img, 8 | disabled, 9 | className, 10 | title, 11 | onClick 12 | }) => ( 13 |
22 | 27 |
28 | {title} 29 |
30 |
31 | ); 32 | 33 | IconButton.propTypes = { 34 | className: PropTypes.string, 35 | disabled: PropTypes.bool, 36 | img: PropTypes.string, 37 | onClick: PropTypes.func.isRequired, 38 | title: PropTypes.node.isRequired 39 | }; 40 | 41 | export default IconButton; 42 | -------------------------------------------------------------------------------- /src/components/language-selector/language-selector.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | /* Position the language select over the language icon, and make it transparent */ 5 | .language-select { 6 | position: absolute; 7 | width: $language-selector-width; 8 | height: $menu-bar-height; 9 | opacity: 0; 10 | user-select: none; 11 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 12 | font-size: .875rem; 13 | color: $text-primary; 14 | background: $ui-white; 15 | } 16 | 17 | [dir="ltr"] .language-select { 18 | right: 0; 19 | } 20 | 21 | [dir="rtl"] .language-select { 22 | left: 0; 23 | } 24 | 25 | .language-select option { 26 | opacity: 1; 27 | } 28 | 29 | .language-select:focus { 30 | border: 1px solid white; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/language-selector/language-selector.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import locales from 'clipcc-l10n'; 5 | import styles from './language-selector.css'; 6 | 7 | // supported languages to exclude from the menu, but allow as a URL option 8 | const ignore = []; 9 | 10 | const LanguageSelector = ({currentLocale, label, onChange}) => ( 11 | 30 | ); 31 | 32 | LanguageSelector.propTypes = { 33 | currentLocale: PropTypes.string, 34 | label: PropTypes.string, 35 | onChange: PropTypes.func 36 | }; 37 | 38 | export default LanguageSelector; 39 | -------------------------------------------------------------------------------- /src/components/loader/blocks-loader.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import styles from './loader.css'; 5 | 6 | import topBlock from './top-block.svg'; 7 | import middleBlock from './middle-block.svg'; 8 | import bottomBlock from './bottom-block.svg'; 9 | 10 | export default function BlocksLoaderComponent () { 11 | return ( 12 |
15 |
16 |
17 | 21 | 25 | 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/loupe/loupe.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .color-picker { 4 | position: absolute; 5 | border-radius: 100%; 6 | border: 4px solid $ui-black-transparent; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/menu-bar/account-nav.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .user-info { 5 | display: inline-flex; 6 | flex-wrap: nowrap; 7 | justify-content: center; 8 | align-items: center; 9 | align-content: center; 10 | padding: 0 0.95rem; 11 | max-width: 260px; 12 | height: $menu-bar-height; 13 | overflow: hidden; 14 | text-decoration: none; 15 | text-overflow: ellipsis; 16 | white-space: nowrap; 17 | color: $ui-white; 18 | font-size: .75rem; 19 | font-weight: normal; 20 | } 21 | 22 | [dir="ltr"] .user-info .avatar { 23 | margin-right: calc($space * .8125); 24 | } 25 | 26 | [dir="rtl"] .user-info .avatar { 27 | margin-left: calc($space * .8125); 28 | } 29 | 30 | .user-info .avatar { 31 | width: 2rem; 32 | height: 2rem; 33 | } 34 | 35 | [dir="ltr"] .user-info .dropdown-caret-position { 36 | margin-left: calc($space * .8125); 37 | } 38 | 39 | [dir="rtl"] .user-info .dropdown-caret-position { 40 | margin-right: calc($space * .8125); 41 | } 42 | 43 | .user-info .dropdown-caret-position { 44 | display: inline-block; 45 | padding-bottom: .0625rem; 46 | vertical-align: middle; 47 | } 48 | 49 | .user-info .profile-name { 50 | font-size: .75rem; 51 | line-height: .9375rem; 52 | font-weight: bold; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/menu-bar/author-info.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .author-info { 5 | color: $ui-white; 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | display: flex; 8 | justify-content: flex-start; 9 | align-items: center; 10 | cursor: default; 11 | } 12 | 13 | .avatar { 14 | margin-right: .5625rem; 15 | } 16 | 17 | .project-title { 18 | max-width: $menu-bar-item-max-width; 19 | display: block; 20 | overflow: hidden; 21 | font-size: $menu-bar-large-font-size; 22 | font-weight: bold; 23 | text-overflow: ellipsis; 24 | white-space: nowrap; 25 | } 26 | 27 | .username-line { 28 | max-width: $menu-bar-item-max-width; 29 | font-size: $menu-bar-standard-font-size; 30 | display: block; 31 | overflow: hidden; 32 | font-weight: normal; 33 | text-overflow: ellipsis; 34 | white-space: nowrap; 35 | } 36 | 37 | .username { 38 | font-weight: bold; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/menu-bar/community-button.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .community-button { 4 | box-shadow: 0 0 0 1px $ui-black-transparent; 5 | } 6 | 7 | .community-button-icon { 8 | height: 1.25rem; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/menu-bar/community-button.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {FormattedMessage} from 'react-intl'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import Button from '../button/button.jsx'; 6 | 7 | import communityIcon from './icon--see-community.svg'; 8 | import styles from './community-button.css'; 9 | 10 | const CommunityButton = ({ 11 | className, 12 | onClick 13 | }) => ( 14 | 29 | ); 30 | 31 | CommunityButton.propTypes = { 32 | className: PropTypes.string, 33 | onClick: PropTypes.func 34 | }; 35 | 36 | CommunityButton.defaultProps = { 37 | onClick: () => {} 38 | }; 39 | 40 | export default CommunityButton; 41 | -------------------------------------------------------------------------------- /src/components/menu-bar/dropdown-caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dropdown-caret 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/menu-bar/icon--about.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/menu-bar/icon--mystuff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/menu-bar/icon--mystuff.png -------------------------------------------------------------------------------- /src/components/menu-bar/icon--profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/menu-bar/icon--profile.png -------------------------------------------------------------------------------- /src/components/menu-bar/login-dropdown.css: -------------------------------------------------------------------------------- 1 | 2 | .login { 3 | padding: .625rem; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/menu-bar/menu-bar-menu.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Menu from '../../containers/menu.jsx'; 4 | 5 | const MenuBarMenu = ({ 6 | children, 7 | className, 8 | onRequestClose, 9 | open, 10 | place = 'right' 11 | }) => ( 12 |
13 | 18 | {children} 19 | 20 |
21 | ); 22 | 23 | MenuBarMenu.propTypes = { 24 | children: PropTypes.node, 25 | className: PropTypes.string, 26 | onRequestClose: PropTypes.func, 27 | open: PropTypes.bool, 28 | place: PropTypes.oneOf(['left', 'right']) 29 | }; 30 | 31 | export default MenuBarMenu; 32 | -------------------------------------------------------------------------------- /src/components/menu-bar/project-title-input.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | @import "../../css/z-index.css"; 4 | 5 | /* 6 | If project-title-input.jsx is part of a menu bar say menu-bar.jsx, it can have additional css classes that 7 | can set a width for example or what it should do in a flex box (eg. grow). 8 | */ 9 | 10 | .title-field { 11 | border: 1px dashed $ui-black-transparent; 12 | border-radius: $form-radius; 13 | -webkit-border-radius: $form-radius; 14 | -moz-border-radius: $form-radius; 15 | background-color: $ui-white-transparent; 16 | background-clip: padding-box; 17 | -webkit-background-clip: padding-box; 18 | height: auto; 19 | padding: .5rem; 20 | } 21 | 22 | .title-field { 23 | color: $ui-white; 24 | font-weight: bold; 25 | font-size: .8rem; 26 | } 27 | 28 | .title-field::placeholder { 29 | color: $ui-white; 30 | font-weight: normal; 31 | font-size: .8rem; 32 | font-style: italic; 33 | } 34 | 35 | .title-field:hover { 36 | background-color: hsla(0, 100%, 100%, 0.5); 37 | } 38 | 39 | .title-field:focus { 40 | outline:none; 41 | border: 1px solid $ui-transparent; 42 | -webkit-box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent; 43 | box-shadow: 0 0 0 calc($space * .5) $ui-white-transparent; 44 | background-color: $ui-white; 45 | color: $text-primary; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/menu-bar/save-status.css: -------------------------------------------------------------------------------- 1 | .save-now { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/menu-bar/share-button.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .share-button { 4 | background: $data-primary; 5 | } 6 | 7 | .share-button-is-shared { 8 | background: $ui-black-transparent; 9 | cursor: default; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/menu-bar/share-button.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import {FormattedMessage} from 'react-intl'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import Button from '../button/button.jsx'; 6 | 7 | import styles from './share-button.css'; 8 | 9 | const ShareButton = ({ 10 | className, 11 | isShared, 12 | onClick 13 | }) => ( 14 | 36 | ); 37 | 38 | ShareButton.propTypes = { 39 | className: PropTypes.string, 40 | isShared: PropTypes.bool, 41 | onClick: PropTypes.func 42 | }; 43 | 44 | ShareButton.defaultProps = { 45 | onClick: () => {} 46 | }; 47 | 48 | export default ShareButton; 49 | -------------------------------------------------------------------------------- /src/components/menu-bar/user-avatar.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .user-thumbnail { 5 | width: $menu-bar-button-size; 6 | height: $menu-bar-button-size; 7 | border-radius: $form-radius; 8 | vertical-align: middle; 9 | box-shadow: 0 0 0 1px $ui-black-transparent; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/menu-bar/user-avatar.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import styles from './user-avatar.css'; 6 | 7 | const UserAvatar = ({ 8 | className, 9 | imageUrl 10 | }) => ( 11 | 18 | ); 19 | 20 | UserAvatar.propTypes = { 21 | className: PropTypes.string, 22 | imageUrl: PropTypes.string 23 | }; 24 | 25 | export default UserAvatar; 26 | -------------------------------------------------------------------------------- /src/components/menu/menu.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .menu { 4 | position: absolute; 5 | border: 1px solid $ui-black-transparent; 6 | border-radius: 0 0 8px 8px; 7 | background-color: $motion-primary; 8 | padding: 0; 9 | margin: 0; 10 | min-width: 186px; 11 | max-width: 260px; 12 | overflow: visible; 13 | color: $ui-white; 14 | box-shadow: 0 8px 8px 0 $ui-black-transparent; 15 | } 16 | 17 | [theme='dark'] .menu { 18 | background-color: $motion-primary-dark; 19 | } 20 | 21 | .menu.left { 22 | right: 0; 23 | } 24 | 25 | .menu.right { 26 | left: 0; 27 | } 28 | 29 | .menu-item { 30 | display: block; 31 | line-height: 34px; 32 | white-space: nowrap; 33 | padding: 0 10px; 34 | font-size: .75rem; 35 | margin: 0; 36 | font-weight: bold; 37 | transition: 0.25s ease-out; /* @todo: standardize with var */ 38 | } 39 | 40 | .menu-item.active, 41 | .menu-item:hover { 42 | background-color: $ui-black-transparent; 43 | } 44 | 45 | [theme='dark'] .menu-item.active, 46 | [theme='dark'] .menu-item:hover { 47 | background-color: $ui-white-transparent; 48 | } 49 | 50 | .menu-item.hoverable { 51 | cursor: pointer; 52 | } 53 | 54 | .menu-section { 55 | border-top: 1px solid $ui-black-transparent; 56 | } 57 | -------------------------------------------------------------------------------- /src/components/meter/meter.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .green { 4 | fill: rgb(171, 220, 170); 5 | stroke: rgb(174, 211, 168); 6 | } 7 | 8 | .yellow { 9 | fill: rgb(251, 219, 130); 10 | stroke: rgb(239, 212, 134); 11 | } 12 | 13 | .red { 14 | fill: rgb(251, 194, 142); 15 | stroke: rgb(235, 189, 142); 16 | } 17 | 18 | .mask-container { 19 | position: relative; 20 | } 21 | 22 | .mask { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | height: 100%; 27 | width: 100%; 28 | transform-origin: top; 29 | will-change: transform; 30 | background: $ui-primary; 31 | opacity: 0.75; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/mic-indicator/mic-indicator.css: -------------------------------------------------------------------------------- 1 | @keyframes popIn { 2 | from {transform: scale(0.5)} 3 | to {transform: scale(1)} 4 | } 5 | 6 | .mic-img { 7 | margin: 10px; 8 | transform-origin: center; 9 | animation: popIn 0.1s ease-in-out; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/mic-indicator/mic-indicator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './mic-indicator.css'; 4 | import micIcon from './mic-indicator.svg'; 5 | import {stageSizeToTransform} from '../../lib/screen-utils'; 6 | 7 | const MicIndicatorComponent = props => ( 8 |
12 | 16 |
17 | ); 18 | 19 | MicIndicatorComponent.propTypes = { 20 | className: PropTypes.string, 21 | stageSize: PropTypes.shape({ 22 | width: PropTypes.number, 23 | height: PropTypes.number, 24 | widthDefault: PropTypes.number, 25 | heightDefault: PropTypes.number 26 | }).isRequired 27 | }; 28 | 29 | export default MicIndicatorComponent; 30 | -------------------------------------------------------------------------------- /src/components/monitor-list/monitor-list.css: -------------------------------------------------------------------------------- 1 | .monitor-list { 2 | /* Width/height are set by the component, margin: auto centers in fullscreen */ 3 | margin: auto; 4 | pointer-events: none; 5 | overflow: hidden; 6 | } 7 | 8 | .monitor-list-scaler { 9 | /* Scaling for monitors should happen from the top left */ 10 | transform-origin: left top; 11 | } 12 | 13 | ::-ms-clear { display: none; } 14 | -------------------------------------------------------------------------------- /src/components/monitor/default-monitor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './monitor.css'; 4 | 5 | const DefaultMonitor = ({categoryColor, label, value}) => ( 6 |
7 |
8 |
9 | {label} 10 |
11 |
15 | {value} 16 |
17 |
18 |
19 | ); 20 | 21 | DefaultMonitor.propTypes = { 22 | categoryColor: PropTypes.string.isRequired, 23 | label: PropTypes.string.isRequired, 24 | value: PropTypes.oneOfType([ 25 | PropTypes.string, 26 | PropTypes.number 27 | ]) 28 | }; 29 | 30 | export default DefaultMonitor; 31 | -------------------------------------------------------------------------------- /src/components/monitor/large-monitor.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styles from './monitor.css'; 4 | 5 | const LargeMonitor = ({categoryColor, value}) => ( 6 |
7 |
11 | {value} 12 |
13 |
14 | ); 15 | 16 | LargeMonitor.propTypes = { 17 | categoryColor: PropTypes.string, 18 | value: PropTypes.oneOfType([ 19 | PropTypes.string, 20 | PropTypes.number 21 | ]) 22 | }; 23 | 24 | export default LargeMonitor; 25 | -------------------------------------------------------------------------------- /src/components/play-button/play-button.css: -------------------------------------------------------------------------------- 1 | 2 | @import "../../css/colors.css"; 3 | @import "../../css/units.css"; 4 | 5 | .play-button { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | overflow: hidden; /* Mask the icon animation */ 11 | width: 2.5rem; 12 | height: 2.5rem; 13 | background-color: $sound-primary; 14 | color: $ui-white; 15 | border-radius: 50%; 16 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 17 | user-select: none; 18 | cursor: pointer; 19 | transition: all 0.15s ease-out; 20 | } 21 | 22 | .play-button { 23 | position: absolute; 24 | top: .5rem; 25 | z-index: auto; 26 | } 27 | 28 | .play-button:focus { 29 | outline: none; 30 | } 31 | 32 | .play-icon { 33 | width: 50%; 34 | } 35 | 36 | [dir="ltr"] .play-button { 37 | right: .5rem; 38 | } 39 | 40 | [dir="rtl"] .play-button { 41 | left: .5rem; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/prompt/icon--dropdown-caret.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dropdown-caret-gray 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/question/icon--enter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | General/Check 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/select/chevron-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/sound-editor/icon--faster.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon--faster 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/sound-editor/icon--slower.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icon--slower 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/sound-editor/icon--stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | stop-playback 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/spinner/spinner.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import styles from './spinner.css'; 6 | 7 | const SpinnerComponent = function (props) { 8 | const { 9 | className, 10 | level, 11 | small, 12 | large 13 | } = props; 14 | return ( 15 |
26 | ); 27 | }; 28 | SpinnerComponent.propTypes = { 29 | className: PropTypes.string, 30 | large: PropTypes.bool, 31 | level: PropTypes.string, 32 | small: PropTypes.bool 33 | }; 34 | SpinnerComponent.defaultProps = { 35 | className: '', 36 | large: false, 37 | level: 'info', 38 | small: false 39 | }; 40 | export default SpinnerComponent; 41 | -------------------------------------------------------------------------------- /src/components/sprite-info/icon--draggable-off.svg: -------------------------------------------------------------------------------- 1 | draggable-off -------------------------------------------------------------------------------- /src/components/sprite-info/icon--draggable-on.svg: -------------------------------------------------------------------------------- 1 | icon--draggable-on -------------------------------------------------------------------------------- /src/components/sprite-info/icon--x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | x-icon_V2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/sprite-info/icon--y.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | y icon V2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/stage-header/icon--large-stage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Large Stage (active) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/stage-header/icon--small-stage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Small Stage (inactive) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/stage-wrapper/stage-wrapper.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | @import "../../css/colors.css"; 3 | @import "../../css/z-index.css"; 4 | 5 | .stage-wrapper * { 6 | box-sizing: border-box; 7 | } 8 | 9 | .stage-canvas-wrapper { 10 | /* Hides negative space between edge of rounded corners + container, when selected */ 11 | user-select: none; 12 | } 13 | 14 | .stage-wrapper.full-screen { 15 | position: fixed; 16 | top: $stage-menu-height; 17 | left: 0; 18 | right: 0; 19 | bottom: 0; 20 | z-index: $z-index-stage-wrapper-overlay; 21 | background-color: $ui-white; 22 | /* spacing between stage and control bar (on the top), or between 23 | stage and window edges (on left/right/bottom) */ 24 | padding: $stage-full-screen-stage-padding; 25 | 26 | /* this centers the stage */ 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/stop-all/icon--stop-all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/stop-all/stop-all.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .stop-all { 4 | width: 2rem; 5 | height: 2rem; 6 | padding: 0.375rem; 7 | border-radius: 0.25rem; 8 | user-select: none; 9 | cursor: pointer; 10 | } 11 | 12 | .stop-all:hover { 13 | background-color: $motion-light-transparent; 14 | } 15 | 16 | .stop-all { 17 | opacity: 0.5; 18 | } 19 | 20 | .stop-all.is-active { 21 | opacity: 1; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/stop-all/stop-all.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import stopAllIcon from './icon--stop-all.svg'; 6 | import styles from './stop-all.css'; 7 | 8 | const StopAllComponent = function (props) { 9 | const { 10 | active, 11 | className, 12 | onClick, 13 | title, 14 | ...componentProps 15 | } = props; 16 | return ( 17 | 31 | ); 32 | }; 33 | 34 | StopAllComponent.propTypes = { 35 | active: PropTypes.bool, 36 | className: PropTypes.string, 37 | onClick: PropTypes.func.isRequired, 38 | title: PropTypes.string 39 | }; 40 | 41 | StopAllComponent.defaultProps = { 42 | active: false, 43 | title: 'Stop' 44 | }; 45 | 46 | export default StopAllComponent; 47 | -------------------------------------------------------------------------------- /src/components/tag-button/tag-button.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .tag-button { 5 | padding: .625rem 1rem; 6 | background: $motion-primary; 7 | border-radius: 1.375rem; 8 | color: $ui-white; 9 | height: $library-filter-bar-height; 10 | transition: 0.25s ease-out; /* @todo: standardize with var */ 11 | } 12 | 13 | .tag-button-icon { 14 | max-width: 1rem; 15 | max-height: 1rem; 16 | } 17 | 18 | .active { 19 | background: $data-primary; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/tag-button/tag-button.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import {FormattedMessage} from 'react-intl'; 5 | 6 | import Button from '../button/button.jsx'; 7 | 8 | import styles from './tag-button.css'; 9 | 10 | const TagButtonComponent = ({ 11 | active, 12 | iconClassName, 13 | className, 14 | tag, // eslint-disable-line no-unused-vars 15 | intlLabel, 16 | ...props 17 | }) => ( 18 | 33 | ); 34 | 35 | TagButtonComponent.propTypes = { 36 | ...Button.propTypes, 37 | active: PropTypes.bool, 38 | intlLabel: PropTypes.shape({ 39 | defaultMessage: PropTypes.string, 40 | description: PropTypes.string, 41 | id: PropTypes.string 42 | }).isRequired, 43 | tag: PropTypes.string.isRequired 44 | }; 45 | 46 | TagButtonComponent.defaultProps = { 47 | active: false 48 | }; 49 | 50 | export default TagButtonComponent; 51 | -------------------------------------------------------------------------------- /src/components/target-pane/target-pane.css: -------------------------------------------------------------------------------- 1 | @import "../../css/units.css"; 2 | 3 | .target-pane.scratch2 { 4 | flex-direction: row-reverse; 5 | } 6 | 7 | .target-pane { 8 | /* Makes columns for the sprite library selector + and the stage selector */ 9 | display: flex; 10 | flex-direction: row; 11 | flex-grow: 1; 12 | } 13 | 14 | [dir="ltr"] .stage-selector-wrapper.scratch2 { 15 | margin-left: 0; 16 | margin-right: $space; 17 | } 18 | 19 | .stage-selector-wrapper { 20 | display: flex; 21 | flex-basis: 72px; 22 | flex-shrink: 0; 23 | } 24 | 25 | [dir="ltr"] .stage-selector-wrapper { 26 | margin-left: calc($space / 2); 27 | } 28 | 29 | [dir="rtl"] .stage-selector-wrapper { 30 | margin-right: calc($space / 2); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/telemetry-modal/telemetry-modal-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/telemetry-modal/telemetry-modal-header.png -------------------------------------------------------------------------------- /src/components/text-switch/text-switch.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | @import "../../css/units.css"; 3 | 4 | .text-switch { 5 | display: flex; 6 | align-content: center; 7 | align-items: center; 8 | } 9 | 10 | .switch { 11 | cursor: pointer; 12 | border-color: $motion-tertiary; 13 | padding: $space; 14 | border-top-style: solid; 15 | border-bottom-style: solid; 16 | border-width: 1px; 17 | border-color: $motion-tertiary; 18 | border-right-style: solid; 19 | background-color: $ui-white; 20 | color: $motion-primary; 21 | text-align: center; 22 | transition: 0.25s ease-out; /* @todo: standardize with var */ 23 | } 24 | 25 | [theme='dark'] .switch { 26 | background-color: $ui-primary-dark; 27 | color: $text-primary-dark; 28 | } 29 | 30 | .switch:first-child { 31 | border-color: $motion-tertiary; 32 | border-style: solid; 33 | border-radius: $form-radius 0 0 $form-radius; 34 | } 35 | 36 | .switch:last-child { 37 | border-right-style: solid; 38 | border-radius: 0 $form-radius $form-radius 0; 39 | } 40 | 41 | .switch.active { 42 | background-color: $motion-primary; 43 | color: $ui-white; 44 | } 45 | 46 | .switch img { 47 | image-rendering: crisp-edges; 48 | filter: invert(50%) sepia(41%) saturate(1318%) hue-rotate(188deg) brightness(103%) contrast(101%); 49 | } 50 | 51 | .switch.active img { 52 | filter: none; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/components/text-switch/text-switch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | 5 | import styles from './text-switch.css'; 6 | 7 | const TextSwitch = props => ( 8 |
9 | {props.options.map(option => ( 10 | props.onChange(option.id)} 19 | > 20 | {option.text} 21 | 22 | ))} 23 |
24 | ); 25 | 26 | TextSwitch.propTypes = { 27 | options: PropTypes.arrayOf(PropTypes.shape({ 28 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 29 | text: PropTypes.string 30 | })).isRequired, 31 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 32 | onChange: PropTypes.func.isRequired 33 | }; 34 | 35 | export default TextSwitch; 36 | -------------------------------------------------------------------------------- /src/components/turbo-mode/icon--turbo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | turbo-bolt-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/turbo-mode/turbo-mode.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .turbo-container { 4 | display: flex; 5 | align-items: center; 6 | padding: 0.25rem; 7 | user-select: none; 8 | } 9 | 10 | .turbo-icon { 11 | margin: 0.25rem; 12 | } 13 | 14 | .turbo-label { 15 | font-size: 0.625rem; 16 | font-weight: bold; 17 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 18 | color: $control-primary; 19 | white-space: nowrap; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/turbo-mode/turbo-mode.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {FormattedMessage} from 'react-intl'; 3 | 4 | import turboIcon from './icon--turbo.svg'; 5 | 6 | import styles from './turbo-mode.css'; 7 | 8 | const TurboMode = () => ( 9 |
10 | 14 |
15 | 20 |
21 |
22 | ); 23 | 24 | export default TurboMode; 25 | -------------------------------------------------------------------------------- /src/components/watermark/watermark.css: -------------------------------------------------------------------------------- 1 | 2 | .sprite-image { 3 | margin: auto; 4 | user-select: none; 5 | max-width: 48px; 6 | max-height: 48px; 7 | opacity: 0.35; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/watermark/watermark.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | import styles from './watermark.css'; 5 | 6 | const Watermark = props => ( 7 | 11 | ); 12 | 13 | Watermark.propTypes = { 14 | costumeURL: PropTypes.string 15 | }; 16 | 17 | export default Watermark; 18 | -------------------------------------------------------------------------------- /src/components/waveform/waveform.css: -------------------------------------------------------------------------------- 1 | @import "../../css/colors.css"; 2 | 3 | .container { 4 | width: 100%; 5 | } 6 | .waveform-path { 7 | /* 8 | This color is lighter than sound-primary, but 9 | cannot use alpha because of overlapping elements. 10 | */ 11 | fill: hsl(300, 54%, 72%); 12 | stroke: $sound-tertiary; 13 | } 14 | .baseline { 15 | stroke: $sound-tertiary; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/webgl-modal/unsupported.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/components/webgl-modal/unsupported.png -------------------------------------------------------------------------------- /src/containers/alerts.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | 5 | import { 6 | closeAlert, 7 | filterPopupAlerts 8 | } from '../reducers/alerts'; 9 | 10 | import AlertsComponent from '../components/alerts/alerts.jsx'; 11 | 12 | const Alerts = ({ 13 | alertsList, 14 | className, 15 | onCloseAlert 16 | }) => ( 17 | 23 | ); 24 | 25 | Alerts.propTypes = { 26 | alertsList: PropTypes.arrayOf(PropTypes.object), 27 | className: PropTypes.string, 28 | onCloseAlert: PropTypes.func 29 | }; 30 | 31 | const mapStateToProps = state => ({ 32 | alertsList: state.scratchGui.alerts.alertsList 33 | }); 34 | 35 | const mapDispatchToProps = dispatch => ({ 36 | onCloseAlert: index => dispatch(closeAlert(index)) 37 | }); 38 | 39 | export default connect( 40 | mapStateToProps, 41 | mapDispatchToProps 42 | )(Alerts); 43 | -------------------------------------------------------------------------------- /src/containers/drag-layer.jsx: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux'; 2 | import DragLayer from '../components/drag-layer/drag-layer.jsx'; 3 | 4 | const mapStateToProps = state => ({ 5 | dragging: state.scratchGui.assetDrag.dragging, 6 | currentOffset: state.scratchGui.assetDrag.currentOffset, 7 | img: state.scratchGui.assetDrag.img 8 | }); 9 | 10 | export default connect(mapStateToProps)(DragLayer); 11 | -------------------------------------------------------------------------------- /src/containers/load-error-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from 'react-redux'; 4 | 5 | import LoadErrorModalComponent from '../components/load-error-modal/load-error-modal.jsx'; 6 | 7 | const LoadErrorModal = ({ 8 | data, 9 | onRequestClose 10 | }) => ( 11 | 15 | ); 16 | 17 | LoadErrorModal.propTypes = { 18 | onRequestClose: PropTypes.func, 19 | data: PropTypes.shape({ 20 | errorId: PropTypes.string, 21 | detail: PropTypes.string, 22 | missingExtensions: PropTypes.arrayOf(PropTypes.shape({ 23 | id: PropTypes.string, 24 | version: PropTypes.string 25 | })) 26 | }) 27 | }; 28 | 29 | const mapStateToProps = state => ({ 30 | data: state.scratchGui.loadError 31 | }); 32 | 33 | export default connect( 34 | mapStateToProps 35 | )(LoadErrorModal); 36 | -------------------------------------------------------------------------------- /src/containers/menu-item.jsx: -------------------------------------------------------------------------------- 1 | import bindAll from 'lodash.bindall'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import {MenuItem as MenuItemComponent} from '../components/menu/menu.jsx'; 6 | 7 | class MenuItem extends React.Component { 8 | constructor (props) { 9 | super(props); 10 | bindAll(this, [ 11 | 'navigateToHref' 12 | ]); 13 | } 14 | navigateToHref () { 15 | if (this.props.href) window.location.href = this.props.href; 16 | } 17 | render () { 18 | const { 19 | children, 20 | className, 21 | onClick 22 | } = this.props; 23 | const clickAction = onClick ? onClick : this.navigateToHref; 24 | return ( 25 | 29 | {children} 30 | 31 | ); 32 | } 33 | } 34 | 35 | MenuItem.propTypes = { 36 | children: PropTypes.node, 37 | className: PropTypes.string, 38 | // can take an onClick prop, or take an href and build an onClick handler 39 | href: PropTypes.string, 40 | onClick: PropTypes.func 41 | }; 42 | 43 | export default MenuItem; 44 | -------------------------------------------------------------------------------- /src/containers/question.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import bindAll from 'lodash.bindall'; 4 | import QuestionComponent from '../components/question/question.jsx'; 5 | 6 | class Question extends React.Component { 7 | constructor (props) { 8 | super(props); 9 | bindAll(this, [ 10 | 'handleChange', 11 | 'handleKeyPress', 12 | 'handleSubmit' 13 | ]); 14 | this.state = { 15 | answer: '' 16 | }; 17 | } 18 | handleChange (e) { 19 | this.setState({answer: e.target.value}); 20 | } 21 | handleKeyPress (event) { 22 | if (event.key === 'Enter') this.handleSubmit(); 23 | } 24 | handleSubmit () { 25 | this.props.onQuestionAnswered(this.state.answer); 26 | } 27 | render () { 28 | return ( 29 | 36 | ); 37 | } 38 | } 39 | 40 | Question.propTypes = { 41 | onQuestionAnswered: PropTypes.func.isRequired, 42 | question: PropTypes.string 43 | }; 44 | 45 | export default Question; 46 | -------------------------------------------------------------------------------- /src/containers/stage-wrapper.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import VM from 'clipcc-vm'; 4 | import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants.js'; 5 | import StageWrapperComponent from '../components/stage-wrapper/stage-wrapper.jsx'; 6 | 7 | const StageWrapper = props => ; 8 | 9 | StageWrapper.propTypes = { 10 | isRendererSupported: PropTypes.bool.isRequired, 11 | layoutStyle: PropTypes.string, 12 | stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, 13 | vm: PropTypes.instanceOf(VM).isRequired 14 | }; 15 | 16 | export default StageWrapper; 17 | -------------------------------------------------------------------------------- /src/containers/tag-button.jsx: -------------------------------------------------------------------------------- 1 | import bindAll from 'lodash.bindall'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | 5 | import TagButtonComponent from '../components/tag-button/tag-button.jsx'; 6 | 7 | class TagButton extends React.Component { 8 | constructor (props) { 9 | super(props); 10 | bindAll(this, [ 11 | 'handleClick' 12 | ]); 13 | } 14 | handleClick () { 15 | this.props.onClick(this.props.tag); 16 | } 17 | render () { 18 | return ( 19 | 23 | ); 24 | } 25 | } 26 | 27 | TagButton.propTypes = { 28 | ...TagButtonComponent.propTypes, 29 | onClick: PropTypes.func 30 | }; 31 | 32 | export default TagButton; 33 | -------------------------------------------------------------------------------- /src/containers/webgl-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import WebGlModalComponent from '../components/webgl-modal/webgl-modal.jsx'; 5 | 6 | class WebGlModal extends React.Component { 7 | handleCancel () { 8 | window.history.back(); 9 | } 10 | render () { 11 | return ( 12 | 16 | ); 17 | } 18 | } 19 | 20 | WebGlModal.propTypes = { 21 | isRtl: PropTypes.bool 22 | }; 23 | 24 | export default WebGlModal; 25 | -------------------------------------------------------------------------------- /src/css/scrollbar.css: -------------------------------------------------------------------------------- 1 | @import "colors.css"; 2 | 3 | $scrollbar-thickness: 11px; 4 | $scrollbar-radius: 6px; 5 | $scrollbar-padding: 2.5px; 6 | $scrollbar-color: #bbb; 7 | $scrollbar-color-dark: #333; 8 | $scrollbar-color-hover: #aaa; 9 | 10 | /* for webkit */ 11 | .scrollbar::-webkit-scrollbar { 12 | width: $scrollbar-thickness; 13 | height: $scrollbar-thickness; 14 | } 15 | 16 | .scrollbar::-webkit-scrollbar-thumb { 17 | border-radius: $scrollbar-radius; 18 | background-color: $scrollbar-color; 19 | border: solid white $scrollbar-padding; 20 | } 21 | 22 | [theme='dark'] .scrollbar::-webkit-scrollbar-thumb { 23 | border-color: $scrollbar-color-dark; 24 | } 25 | 26 | .scrollbar::-webkit-scrollbar-thumb:hover { 27 | background-color: $scrollbar-color-hover; 28 | } 29 | 30 | /* for firefox */ 31 | .scrollbar { 32 | scrollbar-color: $scrollbar-color; 33 | scrollbar-width: thin; 34 | } 35 | 36 | [theme='dark'] .scrollbar { 37 | scrollbar-color: $scrollbar-color-dark; 38 | } 39 | -------------------------------------------------------------------------------- /src/css/typography.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | } 4 | 5 | h2 { 6 | font-size: 1.5rem; 7 | font-weight: bold; 8 | } 9 | 10 | p { 11 | font-size: 1rem; 12 | line-height: 1.5em; 13 | } 14 | -------------------------------------------------------------------------------- /src/css/units.css: -------------------------------------------------------------------------------- 1 | /* make sure to keep these in sync with other constants, 2 | e.g. STAGE_DIMENSION_DEFAULTS in lib/screen-utils.js */ 3 | 4 | $space: 0.5rem; 5 | 6 | $sprites-per-row: 5; 7 | 8 | $menu-bar-height: 3rem; 9 | $language-selector-width: 3rem; 10 | $sprite-info-height: 6rem; 11 | $stage-menu-height: 2.75rem; 12 | 13 | $library-header-height: 3.125rem; 14 | $library-filter-bar-height: 2.5rem; 15 | 16 | $stage-standard-border-width: 0.0625rem; 17 | $stage-full-screen-border-width: 0.1875rem; 18 | $stage-full-screen-stage-padding: 0.1875rem; 19 | 20 | $form-radius: calc($space / 2); 21 | 22 | /* layout contants from `layout-constants.js` */ 23 | $full-size: 1095px; 24 | $full-size-paint: 1249px; 25 | 26 | $menu-bar-standard-font-size: 0.75rem; 27 | $menu-bar-large-font-size: 0.875rem; 28 | $menu-bar-button-size: 2rem; 29 | 30 | $menu-bar-item-max-width: 12rem; 31 | -------------------------------------------------------------------------------- /src/examples/extensions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['scratch'], // no ES6 3 | env: { 4 | worker: true 5 | }, 6 | globals: { 7 | Scratch: true 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import GUI from './containers/gui.jsx'; 2 | import AppStateHOC from './lib/app-state-hoc.jsx'; 3 | import TitledHOC from './lib/titled-hoc.jsx'; 4 | import GuiReducer, {guiInitialState, guiMiddleware, initEmbedded, initFullScreen, initPlayer} from './reducers/gui'; 5 | import LocalesReducer, {localesInitialState, initLocale} from './reducers/locales'; 6 | import {ScratchPaintReducer} from 'clipcc-paint'; 7 | import {setFullScreen, setPlayer} from './reducers/mode'; 8 | import {remixProject} from './reducers/project-state'; 9 | import {loadExtensionFromFile} from './lib/extension-manager.js'; 10 | import {setAppElement} from 'react-modal'; 11 | import totallyNormalStrings from './lib/l10n.js'; 12 | 13 | const guiReducers = { 14 | locales: LocalesReducer, 15 | scratchGui: GuiReducer, 16 | scratchPaint: ScratchPaintReducer 17 | }; 18 | 19 | export { 20 | GUI as default, 21 | AppStateHOC, 22 | TitledHOC, 23 | setAppElement, 24 | guiReducers, 25 | guiInitialState, 26 | guiMiddleware, 27 | initEmbedded, 28 | initPlayer, 29 | initFullScreen, 30 | initLocale, 31 | localesInitialState, 32 | remixProject, 33 | setFullScreen, 34 | setPlayer, 35 | loadExtensionFromFile, 36 | totallyNormalStrings 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/analytics.js: -------------------------------------------------------------------------------- 1 | import GoogleAnalytics from 'react-ga'; 2 | 3 | import log from './log'; 4 | 5 | const GA_ID = false; 6 | if (GA_ID) { 7 | GoogleAnalytics.initialize(GA_ID, { 8 | debug: (process.env.NODE_ENV !== 'production'), 9 | titleCase: true, 10 | sampleRate: (process.env.NODE_ENV === 'production') ? 100 : 0, 11 | forceSSL: true 12 | }); 13 | } else { 14 | log.info('Disabling GA because GA_ID is not set.'); 15 | window.ga = () => { 16 | // The `react-ga` module calls this function to implement all Google Analytics calls. Providing an empty 17 | // function effectively disables `react-ga`. This is similar to the `testModeAPI` feature of `react-ga` except 18 | // that `testModeAPI` logs the arguments of every call into an array. That's nice for testing purposes but 19 | // would look like a memory leak in a live program. 20 | }; 21 | } 22 | 23 | export default GoogleAnalytics; 24 | -------------------------------------------------------------------------------- /src/lib/assets/icon--back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | back-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/assets/icon--success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Alerts/Check 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/assets/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/audio/effects/fade-effect.js: -------------------------------------------------------------------------------- 1 | class FadeEffect { 2 | constructor (audioContext, fadeIn, startSeconds, endSeconds) { 3 | this.audioContext = audioContext; 4 | 5 | this.input = this.audioContext.createGain(); 6 | this.output = this.audioContext.createGain(); 7 | 8 | this.gain = this.audioContext.createGain(); 9 | 10 | this.gain.gain.setValueAtTime(1, 0); 11 | 12 | if (fadeIn) { 13 | this.gain.gain.setValueAtTime(0, startSeconds); 14 | this.gain.gain.linearRampToValueAtTime(1, endSeconds); 15 | } else { 16 | this.gain.gain.setValueAtTime(1, startSeconds); 17 | this.gain.gain.linearRampToValueAtTime(0, endSeconds); 18 | } 19 | 20 | this.gain.gain.setValueAtTime(1, endSeconds); 21 | 22 | this.input.connect(this.gain); 23 | this.gain.connect(this.output); 24 | } 25 | } 26 | 27 | export default FadeEffect; 28 | -------------------------------------------------------------------------------- /src/lib/audio/effects/mute-effect.js: -------------------------------------------------------------------------------- 1 | class MuteEffect { 2 | constructor (audioContext, startSeconds, endSeconds) { 3 | this.audioContext = audioContext; 4 | 5 | this.input = this.audioContext.createGain(); 6 | this.output = this.audioContext.createGain(); 7 | 8 | this.gain = this.audioContext.createGain(); 9 | 10 | // Smoothly ramp the gain down before the start time, and up after the end time. 11 | this.rampLength = 0.001; 12 | this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength)); 13 | this.gain.gain.linearRampToValueAtTime(0, startSeconds); 14 | this.gain.gain.setValueAtTime(0, endSeconds); 15 | this.gain.gain.linearRampToValueAtTime(1.0, endSeconds + this.rampLength); 16 | 17 | this.input.connect(this.gain); 18 | this.gain.connect(this.output); 19 | } 20 | } 21 | 22 | export default MuteEffect; 23 | -------------------------------------------------------------------------------- /src/lib/audio/effects/volume-effect.js: -------------------------------------------------------------------------------- 1 | class VolumeEffect { 2 | constructor (audioContext, volume, startSeconds, endSeconds) { 3 | this.audioContext = audioContext; 4 | 5 | this.input = this.audioContext.createGain(); 6 | this.output = this.audioContext.createGain(); 7 | 8 | this.gain = this.audioContext.createGain(); 9 | 10 | // Smoothly ramp the gain up before the start time, and down after the end time. 11 | this.rampLength = 0.01; 12 | this.gain.gain.setValueAtTime(1.0, Math.max(0, startSeconds - this.rampLength)); 13 | this.gain.gain.exponentialRampToValueAtTime(volume, startSeconds); 14 | this.gain.gain.setValueAtTime(volume, endSeconds); 15 | this.gain.gain.exponentialRampToValueAtTime(1.0, endSeconds + this.rampLength); 16 | 17 | this.input.connect(this.gain); 18 | this.gain.connect(this.output); 19 | } 20 | } 21 | 22 | export default VolumeEffect; 23 | -------------------------------------------------------------------------------- /src/lib/audio/shared-audio-context.js: -------------------------------------------------------------------------------- 1 | import StartAudioContext from 'startaudiocontext'; 2 | import bowser from 'bowser'; 3 | 4 | let AUDIO_CONTEXT; 5 | 6 | if (!bowser.msie) { 7 | /** 8 | * AudioContext can be initialized only when user interaction event happens 9 | */ 10 | const event = 11 | typeof document.ontouchstart === 'undefined' ? 12 | 'mousedown' : 13 | 'touchstart'; 14 | const initAudioContext = () => { 15 | document.removeEventListener(event, initAudioContext); 16 | AUDIO_CONTEXT = new (window.AudioContext || 17 | window.webkitAudioContext)(); 18 | StartAudioContext(AUDIO_CONTEXT); 19 | }; 20 | document.addEventListener(event, initAudioContext, { capture: true }); 21 | } 22 | 23 | /** 24 | * Wrap browser AudioContext because we shouldn't create more than one 25 | * @return {AudioContext} The singleton AudioContext 26 | */ 27 | export default function () { 28 | return AUDIO_CONTEXT; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/backpack/code-payload.js: -------------------------------------------------------------------------------- 1 | import blockToImage from './block-to-image'; 2 | import jpegThumbnail from './jpeg-thumbnail'; 3 | import {Base64} from 'js-base64'; 4 | 5 | const codePayload = ({blockObjects, topBlockId}) => { 6 | const payload = { 7 | type: 'script', // Needs to match backpack-server type name 8 | name: 'code', // All code currently gets the same name 9 | mime: 'application/json', 10 | // Backpack expects a base64 encoded string to store. Cannot use btoa because 11 | // the code can contain characters outside the 0-255 code-point range supported by btoa 12 | body: Base64.encode(JSON.stringify(blockObjects)) // Base64 encode the json 13 | }; 14 | 15 | return blockToImage(topBlockId) 16 | .then(jpegThumbnail) 17 | .then(thumbnail => { 18 | payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); 19 | return payload; 20 | }); 21 | }; 22 | 23 | export default codePayload; 24 | -------------------------------------------------------------------------------- /src/lib/backpack/sound-payload.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import soundThumbnail from '!base64-loader!./sound-thumbnail.jpg'; 3 | 4 | const soundPayload = sound => { 5 | const assetDataUrl = sound.asset.encodeDataURI(); 6 | const assetDataFormat = sound.dataFormat; 7 | const payload = { 8 | type: 'sound', 9 | name: sound.name, 10 | thumbnail: soundThumbnail, 11 | // Params to be filled in below 12 | mime: '', 13 | body: '' 14 | }; 15 | 16 | switch (assetDataFormat) { 17 | case 'wav': 18 | payload.mime = 'audio/x-wav'; 19 | payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', ''); 20 | break; 21 | case 'mp3': 22 | payload.mime = 'audio/mp3'; 23 | // TODO scratch-storage should be fixed so that encodeDataURI does not 24 | // always prepend the wave format header; Once that is fixed, the following 25 | // line will have to change. 26 | payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', ''); 27 | break; 28 | default: 29 | alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line 30 | } 31 | 32 | // Return a promise to make it consistent with other payload constructors like costume-payload 33 | return new Promise(resolve => resolve(payload)); 34 | }; 35 | 36 | export default soundPayload; 37 | -------------------------------------------------------------------------------- /src/lib/backpack/sound-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/backpack/sound-thumbnail.jpg -------------------------------------------------------------------------------- /src/lib/backpack/sprite-payload.js: -------------------------------------------------------------------------------- 1 | import jpegThumbnail from './jpeg-thumbnail'; 2 | 3 | const spritePayload = (id, vm) => { 4 | const target = vm.runtime.getTargetById(id); 5 | if (!target) return null; 6 | 7 | return vm.exportSprite( 8 | id, 9 | 'base64' 10 | ).then(zippedSprite => { 11 | const payload = { 12 | type: 'sprite', 13 | name: target.sprite.name, 14 | mime: 'application/zip', 15 | body: zippedSprite, 16 | // Filled in below 17 | thumbnail: '' 18 | }; 19 | 20 | const costumeDataUrl = target.sprite.costumes[target.currentCostume].asset.encodeDataURI(); 21 | 22 | return jpegThumbnail(costumeDataUrl).then(thumbnail => { 23 | payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', ''); 24 | return payload; 25 | }); 26 | }); 27 | }; 28 | 29 | export default spritePayload; 30 | -------------------------------------------------------------------------------- /src/lib/bmp-converter.js: -------------------------------------------------------------------------------- 1 | export default bmpImage => new Promise(resolve => { 2 | // If the input is an ArrayBuffer, we need to convert it to a `Blob` and give it a URL so we can use it as an 3 | // `src`. If it's a data URI, we can use it as-is. 4 | const imageUrl = bmpImage instanceof String ? 5 | bmpImage : 6 | window.URL.createObjectURL(new Blob([bmpImage], {type: 'image/bmp'})); 7 | 8 | const canvas = document.createElement('canvas'); 9 | const ctx = canvas.getContext('2d'); 10 | 11 | const image = document.createElement('img'); 12 | 13 | image.addEventListener('load', () => { 14 | canvas.width = image.naturalWidth; 15 | canvas.height = image.naturalHeight; 16 | ctx.drawImage(image, 0, 0); 17 | 18 | const dataUrl = canvas.toDataURL('image/png'); 19 | 20 | // Revoke URL. If a blob URL was generated earlier, this allows the blob to be GC'd and prevents a memory leak. 21 | window.URL.revokeObjectURL(imageUrl); 22 | 23 | resolve(dataUrl); 24 | }); 25 | 26 | image.setAttribute('src', imageUrl); 27 | }); 28 | -------------------------------------------------------------------------------- /src/lib/connected-intl-provider.jsx: -------------------------------------------------------------------------------- 1 | import {IntlProvider as ReactIntlProvider} from 'react-intl'; 2 | import {connect} from 'react-redux'; 3 | 4 | const mapStateToProps = state => ({ 5 | key: state.locales.locale, 6 | locale: state.locales.locale, 7 | messages: state.locales.messages 8 | }); 9 | 10 | export default connect(mapStateToProps)(ReactIntlProvider); 11 | -------------------------------------------------------------------------------- /src/lib/data-uri-to-blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility to convert data URIs to blobs 3 | * Adapted from https://stackoverflow.com/questions/12168909/blob-from-dataurl 4 | * @param {string} dataURI the data uri to blobify 5 | * @return {Blob} a blob representing the data uri 6 | */ 7 | export default function dataURItoBlob (dataURI) { 8 | const byteString = atob(dataURI.split(',')[1]); 9 | const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; 10 | const arrayBuffer = new ArrayBuffer(byteString.length); 11 | const uintArray = new Uint8Array(arrayBuffer); 12 | for (let i = 0; i < byteString.length; i++) { 13 | uintArray[i] = byteString.charCodeAt(i); 14 | } 15 | const blob = new Blob([arrayBuffer], {type: mimeString}); 16 | return blob; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/default-project/cd21514d0531fdffb22204e0ec5ed84a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/lib/default-project/fd8543abeeba255072da239223d2d342.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/default-project/fd8543abeeba255072da239223d2d342.wav -------------------------------------------------------------------------------- /src/lib/download-blob.js: -------------------------------------------------------------------------------- 1 | export default (filename, blob) => { 2 | const downloadLink = document.createElement('a'); 3 | document.body.appendChild(downloadLink); 4 | 5 | // Use special ms version if available to get it working on Edge. 6 | if (navigator.msSaveOrOpenBlob) { 7 | navigator.msSaveOrOpenBlob(blob, filename); 8 | return; 9 | } 10 | 11 | if ('download' in HTMLAnchorElement.prototype) { 12 | const url = window.URL.createObjectURL(blob); 13 | downloadLink.href = url; 14 | downloadLink.download = filename; 15 | downloadLink.type = blob.type; 16 | downloadLink.click(); 17 | // remove the link after a timeout to prevent a crash on iOS 13 Safari 18 | window.setTimeout(() => { 19 | document.body.removeChild(downloadLink); 20 | window.URL.revokeObjectURL(url); 21 | }, 1000); 22 | } else { 23 | // iOS 12 Safari, open a new page and set href to data-uri 24 | let popup = window.open('', '_blank'); 25 | const reader = new FileReader(); 26 | reader.onloadend = function () { 27 | popup.location.href = reader.result; 28 | popup = null; 29 | }; 30 | reader.readAsDataURL(blob); 31 | } 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/drag-constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SOUND: 'SOUND', 3 | COSTUME: 'COSTUME', 4 | SPRITE: 'SPRITE', 5 | CODE: 'CODE', 6 | 7 | BACKPACK_SOUND: 'BACKPACK_SOUND', 8 | BACKPACK_COSTUME: 'BACKPACK_COSTUME', 9 | BACKPACK_SPRITE: 'BACKPACK_SPRITE', 10 | BACKPACK_CODE: 'BACKPACK_CODE' 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/error-boundary-hoc.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ErrorBoundary from '../containers/error-boundary.jsx'; 3 | 4 | /* 5 | * Higher Order Component to provide error boundary for wrapped component. 6 | * A curried function, call like errorHOC()(). 7 | * @param {string} action - Label for GA tracking of errors. 8 | * @returns {function} a function that accepts a component to wrap. 9 | */ 10 | const ErrorBoundaryHOC = function (action){ 11 | /** 12 | * The function to be called with a React component to wrap it. 13 | * @param {React.Component} WrappedComponent - Component to wrap with an error boundary. 14 | * @returns {React.Component} the component wrapped with an error boundary. 15 | */ 16 | return function (WrappedComponent) { 17 | const ErrorBoundaryWrapper = props => ( 18 | 19 | 20 | 21 | ); 22 | return ErrorBoundaryWrapper; 23 | }; 24 | }; 25 | 26 | export default ErrorBoundaryHOC; 27 | -------------------------------------------------------------------------------- /src/lib/extension-api.js: -------------------------------------------------------------------------------- 1 | class ExtensionAPI { 2 | constructor (gui) { 3 | this.gui = gui; 4 | } 5 | 6 | isDesktop () { 7 | return this.gui.props.isScratchDesktop; 8 | } 9 | 10 | isEditorLoading () { 11 | return this.gui.props.isLoading; 12 | } 13 | 14 | getSettings (id) { 15 | return this.gui.props.settings[id]; 16 | } 17 | } 18 | 19 | export default ExtensionAPI; 20 | -------------------------------------------------------------------------------- /src/lib/get-costume-url.js: -------------------------------------------------------------------------------- 1 | import storage from './storage'; 2 | import {inlineSvgFonts} from 'scratch-svg-renderer'; 3 | 4 | // Contains 'font-family', but doesn't only contain 'font-family="none"' 5 | const HAS_FONT_REGEXP = 'font-family(?!="none")'; 6 | 7 | const getCostumeUrl = (function () { 8 | let cachedAssetId; 9 | let cachedUrl; 10 | 11 | return function (asset) { 12 | 13 | if (cachedAssetId === asset.assetId) { 14 | return cachedUrl; 15 | } 16 | 17 | cachedAssetId = asset.assetId; 18 | 19 | // If the SVG refers to fonts, they must be inlined in order to display correctly in the img tag. 20 | // Avoid parsing the SVG when possible, since it's expensive. 21 | if (asset.assetType === storage.AssetType.ImageVector) { 22 | const svgString = asset.decodeText(); 23 | if (svgString.match(HAS_FONT_REGEXP)) { 24 | const svgText = inlineSvgFonts(svgString); 25 | cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; 26 | } else { 27 | cachedUrl = asset.encodeDataURI(); 28 | } 29 | } else { 30 | cachedUrl = asset.encodeDataURI(); 31 | } 32 | 33 | return cachedUrl; 34 | }; 35 | }()); 36 | 37 | export { 38 | getCostumeUrl as default, 39 | HAS_FONT_REGEXP 40 | }; 41 | -------------------------------------------------------------------------------- /src/lib/import-csv.js: -------------------------------------------------------------------------------- 1 | import Papa from 'papaparse'; 2 | 3 | export default () => new Promise((resolve, reject) => { 4 | const fileInput = document.createElement('input'); 5 | fileInput.setAttribute('type', 'file'); 6 | fileInput.setAttribute('accept', '.csv, .tsv, .txt'); // parser auto-detects delimiter 7 | fileInput.onchange = e => { 8 | const file = e.target.files[0]; 9 | Papa.parse(file, { 10 | header: false, 11 | complete: results => { 12 | document.body.removeChild(fileInput); 13 | resolve(results.data); 14 | }, 15 | error: err => { 16 | document.body.removeChild(fileInput); 17 | reject(err); 18 | } 19 | }); 20 | }; 21 | document.body.appendChild(fileInput); 22 | fileInput.click(); 23 | }); 24 | -------------------------------------------------------------------------------- /src/lib/isScratchDesktop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal stored state. Not valid until after at least one call to `setIsScratchDesktop()`. 3 | * @type {boolean} 4 | */ 5 | let _isScratchDesktop; // undefined = not ready yet 6 | 7 | /** 8 | * Tell the `isScratchDesktop()` whether or not the GUI is running under Scratch Desktop. 9 | * @param {boolean} value - the new value which `isScratchDesktop()` should return in the future. 10 | */ 11 | const setIsScratchDesktop = function (value) { 12 | _isScratchDesktop = value; 13 | }; 14 | 15 | /** 16 | * @returns {boolean} - true if it seems like the GUI is running under Scratch Desktop; false otherwise. 17 | * If `setIsScratchDesktop()` has not yet been called, this can return `undefined`. 18 | */ 19 | const isScratchDesktop = function () { 20 | return _isScratchDesktop; 21 | }; 22 | 23 | /** 24 | * @returns {boolean} - false if it seems like the GUI is running under Scratch Desktop; true otherwise. 25 | */ 26 | const notScratchDesktop = function () { 27 | return !isScratchDesktop(); 28 | }; 29 | 30 | export default isScratchDesktop; 31 | export { 32 | isScratchDesktop, 33 | notScratchDesktop, 34 | setIsScratchDesktop 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/l10n.js: -------------------------------------------------------------------------------- 1 | import {defineMessages} from 'react-intl'; 2 | 3 | export default defineMessages({ 4 | 'mode': { 5 | defaultMessage: 'Mode', 6 | description: 'Mode menu item in the menu bar', 7 | id: 'gui.menuBar.modeMenu' 8 | }, 9 | 'normal': { 10 | defaultMessage: 'Normal mode', 11 | description: 'April fools: resets editor to not have any pranks', 12 | id: 'gui.menuBar.normalMode' 13 | }, 14 | 'cat': { 15 | defaultMessage: 'Caturday mode', 16 | description: 'April fools: Cat blocks mode', 17 | id: 'gui.menuBar.caturdayMode' 18 | }, 19 | '90s': { 20 | defaultMessage: '90s mode', 21 | description: 'April fools: Makes the editor look like the 1990s', 22 | id: 'gui.menuBar.1990sMode' 23 | }, 24 | 'old': { 25 | defaultMessage: 'Old timey mode', 26 | description: 'April fools: Makes the editor look like an old movie projector', // eslint-disable-line max-len 27 | id: 'gui.menuBar.oldTimeyMode' 28 | }, 29 | 'prehistoric': { 30 | defaultMessage: 'Prehistoric mode', 31 | description: 'April fools: Makes the editor look like inside a cave', 32 | id: 'gui.menuBar.prehistoricMode' 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/lazy-blocks.js: -------------------------------------------------------------------------------- 1 | import blocks from './blocks.js'; 2 | /* eslint-disable linebreak-style */ 3 | // 异步加载 clipcc-block 4 | let BlocksComponent = null; 5 | 6 | const loaded = () => !!BlocksComponent; 7 | 8 | const get = () => { 9 | if (!loaded()) return Error('blocks not loaded'); 10 | return BlocksComponent; 11 | }; 12 | 13 | const load = (vm, callback) => { 14 | if (BlocksComponent) return Promise.resolve(BlocksComponent); 15 | return import(/* webpackChunkName: "ccblocks" */'clipcc-block').then(data => { 16 | BlocksComponent = data.default; 17 | callback(blocks(vm)); 18 | return BlocksComponent; 19 | }); 20 | }; 21 | 22 | export default { 23 | get, 24 | load, 25 | loaded 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/libraries/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // These manifest files use duplicate imports to make things easier to follow 4 | // by providing clear parallel structure. Turn the error off for this folder. 5 | 'no-duplicate-imports': 0 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/libraries/async-load-libraries.js: -------------------------------------------------------------------------------- 1 | const asyncLibrary = callback => { 2 | let data = null; 3 | return () => { 4 | if (data) return data; 5 | return callback() 6 | .then(mod => (data = mod.default)); 7 | }; 8 | }; 9 | 10 | export const getBackdropLibrary = asyncLibrary( 11 | () => import('./backdrops.json') 12 | ); 13 | export const getCostumeLibrary = asyncLibrary( 14 | () => import('./costumes.json') 15 | ); 16 | export const getSoundLibrary = asyncLibrary( 17 | () => import('./sounds.json') 18 | ); 19 | export const getSpriteLibrary = asyncLibrary( 20 | () => import('./sprites.json') 21 | ); 22 | -------------------------------------------------------------------------------- /src/lib/libraries/backdrop-tags.js: -------------------------------------------------------------------------------- 1 | import messages from './tag-messages.js'; 2 | export default [ 3 | {tag: 'fantasy', intlLabel: messages.fantasy}, 4 | {tag: 'music', intlLabel: messages.music}, 5 | {tag: 'sports', intlLabel: messages.sports}, 6 | {tag: 'outdoors', intlLabel: messages.outdoors}, 7 | {tag: 'indoors', intlLabel: messages.indoors}, 8 | {tag: 'space', intlLabel: messages.space}, 9 | {tag: 'underwater', intlLabel: messages.underwater}, 10 | {tag: 'patterns', intlLabel: messages.patterns} 11 | ]; 12 | -------------------------------------------------------------------------------- /src/lib/libraries/decks/en-steps.js: -------------------------------------------------------------------------------- 1 | const enImages = {}; 2 | export {enImages}; 3 | -------------------------------------------------------------------------------- /src/lib/libraries/decks/index.jsx: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /src/lib/libraries/decks/thumbnails/getting-started-asl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/decks/thumbnails/getting-started-asl.png -------------------------------------------------------------------------------- /src/lib/libraries/decks/translate-image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Utility functions for handling tutorial images in multiple languages 4 | */ 5 | 6 | import {enImages as defaultImages} from './en-steps.js'; 7 | 8 | let savedImages = {}; 9 | let savedLocale = ''; 10 | 11 | const translations = {}; 12 | 13 | const loadImageData = locale => { 14 | if (translations.hasOwnProperty(locale)) { 15 | translations[locale]() 16 | .then(newImages => { 17 | savedImages = newImages; 18 | savedLocale = locale; 19 | }); 20 | } 21 | }; 22 | 23 | /** 24 | * Return image data for tutorials based on locale (default: en) 25 | * @param {string} imageId key in the images object, or id string. 26 | * @param {string} locale requested locale 27 | * @return {string} image 28 | */ 29 | const translateImage = (imageId, locale) => { 30 | if (locale !== savedLocale || !savedImages.hasOwnProperty(imageId)) { 31 | return defaultImages[imageId]; 32 | } 33 | return savedImages[imageId]; 34 | }; 35 | 36 | export { 37 | loadImageData, 38 | translateImage 39 | }; 40 | -------------------------------------------------------------------------------- /src/lib/libraries/extensions/HTTPIO/HTTPIO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/HTTPIO/HTTPIO.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/HTTPIO/HTTPIO_icon.svg: -------------------------------------------------------------------------------- 1 | HTTPIO_icon -------------------------------------------------------------------------------- /src/lib/libraries/extensions/HTTPIO/clipcc.httpio-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/libraries/extensions/JSON/JSON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/JSON/JSON.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/JSON/JSON_icon.svg: -------------------------------------------------------------------------------- 1 | JSON_icon -------------------------------------------------------------------------------- /src/lib/libraries/extensions/JSON/ccjson-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/lib/libraries/extensions/boost/boost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/boost/boost.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/clipcc/CCUnknownExtension.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/clipcc/CCUnknownExtension.jpg -------------------------------------------------------------------------------- /src/lib/libraries/extensions/ev3/ev3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/ev3/ev3.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/gdxfor/gdxfor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/gdxfor/gdxfor.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/libra/Libra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/libra/Libra.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/makeymakey/makeymakey-small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/libraries/extensions/makeymakey/makeymakey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/makeymakey/makeymakey.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/microbit/microbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/microbit/microbit.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/music/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/music/music.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/pen/pen-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/pen/pen-old.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/pen/pen-small.svg: -------------------------------------------------------------------------------- 1 | pen-icon -------------------------------------------------------------------------------- /src/lib/libraries/extensions/pen/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/pen/pen.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/speech2text/speech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/speech2text/speech.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/text2speech/text2speech-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/text2speech/text2speech-old.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/text2speech/text2speech.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/text2speech/text2speech.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/translate/translate-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/translate/translate-small.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/translate/translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/translate/translate.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/upload/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/upload/upload.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/videoSensing/video-sensing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/videoSensing/video-sensing.png -------------------------------------------------------------------------------- /src/lib/libraries/extensions/wedo2/wedo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/lib/libraries/extensions/wedo2/wedo.png -------------------------------------------------------------------------------- /src/lib/libraries/sound-tags.js: -------------------------------------------------------------------------------- 1 | import messages from './tag-messages.js'; 2 | export default [ 3 | {tag: 'animals', intlLabel: messages.animals}, 4 | {tag: 'effects', intlLabel: messages.effects}, 5 | {tag: 'loops', intlLabel: messages.loops}, 6 | {tag: 'notes', intlLabel: messages.notes}, 7 | {tag: 'percussion', intlLabel: messages.percussion}, 8 | {tag: 'space', intlLabel: messages.space}, 9 | {tag: 'sports', intlLabel: messages.sports}, 10 | {tag: 'voice', intlLabel: messages.voice}, 11 | {tag: 'wacky', intlLabel: messages.wacky} 12 | ]; 13 | -------------------------------------------------------------------------------- /src/lib/libraries/sprite-tags.js: -------------------------------------------------------------------------------- 1 | import messages from './tag-messages.js'; 2 | export default [ 3 | {tag: 'animals', intlLabel: messages.animals}, 4 | {tag: 'people', intlLabel: messages.people}, 5 | {tag: 'fantasy', intlLabel: messages.fantasy}, 6 | {tag: 'dance', intlLabel: messages.dance}, 7 | {tag: 'music', intlLabel: messages.music}, 8 | {tag: 'sports', intlLabel: messages.sports}, 9 | {tag: 'food', intlLabel: messages.food}, 10 | {tag: 'fashion', intlLabel: messages.fashion}, 11 | {tag: 'letters', intlLabel: messages.letters} 12 | ]; 13 | -------------------------------------------------------------------------------- /src/lib/libraries/tutorial-tags.js: -------------------------------------------------------------------------------- 1 | import messages from './tag-messages.js'; 2 | export default [ 3 | {tag: 'animation', intlLabel: messages.animation}, 4 | {tag: 'art', intlLabel: messages.art}, 5 | {tag: 'music', intlLabel: messages.music}, 6 | {tag: 'games', intlLabel: messages.games}, 7 | {tag: 'stories', intlLabel: messages.stories} 8 | ]; 9 | -------------------------------------------------------------------------------- /src/lib/locale-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Utility functions related to localization specific to the GUI 4 | */ 5 | 6 | const wideLocales = [ 7 | 'ab', 8 | 'ca', 9 | 'de', 10 | 'el', 11 | 'it', 12 | 'ja', 13 | 'ja-Hira', 14 | 'ko', 15 | 'hu', 16 | 'ru', 17 | 'vi' 18 | ]; 19 | 20 | /** 21 | * Identify the languages where translations are too long to fit in fixed width parts of the gui. 22 | * @param {string} locale The current locale. 23 | * @return {bool} true if translations in this language are too long 24 | */ 25 | 26 | const isWideLocale = locale => ( 27 | wideLocales.indexOf(locale) !== -1 28 | ); 29 | 30 | export { 31 | wideLocales, 32 | isWideLocale 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/log.js: -------------------------------------------------------------------------------- 1 | import minilog from 'minilog'; 2 | minilog.enable(); 3 | 4 | export default minilog('gui'); 5 | -------------------------------------------------------------------------------- /src/lib/math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adjust a number to specific precision. 3 | * @param {function} func Adjustment function. 4 | * @param {number} value The number. 5 | * @param {number} precision The count of numbers after point. 6 | * @returns {number} The adjusted value. 7 | */ 8 | const adjustNumber = (func, value, precision) => { 9 | if (typeof precision === 'undefined' || +precision === 0) { 10 | return func(value); 11 | } 12 | value = +value; 13 | precision = +precision; 14 | if (isNaN(value) || !(typeof precision === 'number' && precision % 1 === 0)) { 15 | return NaN; 16 | } 17 | value = value.toString().split('e'); 18 | value = func(+(`${value[0]}e${value[1] ? (+value[1] + precision) : +precision}`)); 19 | value = value.toString().split('e'); 20 | return +(`${value[0]}e${value[1] ? (+value[1] - precision) : -precision}`); 21 | }; 22 | 23 | const round10 = (value, precision) => adjustNumber(Math.round, value, precision); 24 | const floor10 = (value, precision) => adjustNumber(Math.floor, value, precision); 25 | const ceil10 = (value, precision) => adjustNumber(Math.ceil, value, precision); 26 | 27 | export { 28 | round10, floor10, ceil10 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/randomize-sprite-position.js: -------------------------------------------------------------------------------- 1 | const randomizeSpritePosition = spriteObject => { 2 | // https://github.com/LLK/scratch-flash/blob/689f3c79a7e8b2e98f5be80056d877f303a8d8ba/src/Scratch.as#L1385 3 | spriteObject.x = Math.floor((200 * Math.random()) - 100); 4 | spriteObject.y = Math.floor((100 * Math.random()) - 50); 5 | }; 6 | 7 | export default randomizeSpritePosition; 8 | -------------------------------------------------------------------------------- /src/lib/supported-browser.js: -------------------------------------------------------------------------------- 1 | import bowser from 'bowser'; 2 | 3 | const minVersions = { 4 | chrome: '72', 5 | msedge: '19', 6 | firefox: '70', 7 | safari: '12' 8 | }; 9 | 10 | /** 11 | * Helper function to determine if the browser is supported at all. 12 | * @returns {boolean} False if the platform is definitely not supported. 13 | */ 14 | const supportedBrowser = () => { 15 | if (bowser.msie) { 16 | return false; 17 | } 18 | return true; 19 | }; 20 | 21 | /** 22 | * Helper function to determine if the browser meets the minimum recommended version 23 | * @returns {boolean} False if the browser isn't a recommended browser, or doesn't 24 | * meet the minimum version for recommended browsers. 25 | * NOTE: uses strict_mode==true so that any browser not listed in the minVersions 26 | * always returns false 27 | */ 28 | 29 | const recommendedBrowser = () => 30 | !bowser.isUnsupportedBrowser(minVersions, true) || 31 | window.navigator.userAgent.toLowerCase().indexOf('googlebot') !== -1; 32 | 33 | export { 34 | supportedBrowser as default, 35 | recommendedBrowser 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/tablet-full-screen.js: -------------------------------------------------------------------------------- 1 | import bowser from 'bowser'; 2 | 3 | /** 4 | * Helper method to request full screen in the browser when on a tablet. 5 | */ 6 | export default function () { 7 | if (bowser.tablet) { 8 | if ((bowser.webkit || bowser.blink) && document.documentElement.webkitRequestFullScreen) { 9 | document.documentElement.webkitRequestFullScreen(); 10 | } 11 | if (bowser.gecko && document.documentElement.mozRequestFullScreen) { 12 | document.documentElement.mozRequestFullScreen(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/touch-utils.js: -------------------------------------------------------------------------------- 1 | const getEventXY = e => { 2 | if (e.touches && e.touches[0]) { 3 | return {x: e.touches[0].clientX, y: e.touches[0].clientY}; 4 | } else if (e.changedTouches && e.changedTouches[0]) { 5 | return {x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY}; 6 | } 7 | return {x: e.clientX, y: e.clientY}; 8 | }; 9 | 10 | export { 11 | getEventXY 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/variable-utils.js: -------------------------------------------------------------------------------- 1 | // Utility functions for updating variables in the VM 2 | // TODO (VM#1145) these should be moved to top-level VM API 3 | const getVariable = (vm, targetId, variableId) => { 4 | const target = targetId ? 5 | vm.runtime.getTargetById(targetId) : 6 | vm.runtime.getTargetForStage(); 7 | return target.variables[variableId]; 8 | }; 9 | 10 | const getVariableValue = (vm, targetId, variableId) => { 11 | const variable = getVariable(vm, targetId, variableId); 12 | // If array, return a new copy for mutating, ensuring that updates stay immutable. 13 | if (variable.value instanceof Array) return variable.value.slice(); 14 | return variable.value; 15 | }; 16 | 17 | const setVariableValue = (vm, targetId, variableId, value) => { 18 | getVariable(vm, targetId, variableId).value = value; 19 | }; 20 | 21 | export { 22 | getVariable, 23 | getVariableValue, 24 | setVariableValue 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/video/camera.js: -------------------------------------------------------------------------------- 1 | import getUserMedia from 'get-user-media-promise'; 2 | 3 | // Single Setup For All Video Streams used by the GUI 4 | // While VideoProvider uses a private _singleSetup 5 | // property to ensure that each instance of a VideoProvider 6 | // use the same setup, this ensures that all instances 7 | // of VideoProviders use a single stream. This way, closing a camera modal 8 | // does not affect the video on the stage, and a program running and disabling 9 | // video on the stage will not affect the camera modal's video. 10 | const requestStack = []; 11 | const requestVideoStream = videoDesc => { 12 | let streamPromise; 13 | if (requestStack.length === 0) { 14 | streamPromise = getUserMedia({ 15 | audio: false, 16 | video: videoDesc 17 | }); 18 | requestStack.push(streamPromise); 19 | } else if (requestStack.length > 0) { 20 | streamPromise = requestStack[0]; 21 | requestStack.push(true); 22 | } 23 | return streamPromise; 24 | }; 25 | 26 | const requestDisableVideo = () => { 27 | requestStack.pop(); 28 | if (requestStack.length > 0) return false; 29 | return true; 30 | }; 31 | 32 | export { 33 | requestVideoStream, 34 | requestDisableVideo 35 | }; 36 | -------------------------------------------------------------------------------- /src/playground/blocks-only.css: -------------------------------------------------------------------------------- 1 | .controls { 2 | position: absolute; 3 | z-index: 2; 4 | top: 10px; 5 | right: 15px; 6 | } 7 | -------------------------------------------------------------------------------- /src/playground/blocks-only.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {connect} from 'react-redux'; 4 | 5 | import Controls from '../containers/controls.jsx'; 6 | import Blocks from '../containers/blocks.jsx'; 7 | import GUI from '../containers/gui.jsx'; 8 | import HashParserHOC from '../lib/hash-parser-hoc.jsx'; 9 | import AppStateHOC from '../lib/app-state-hoc.jsx'; 10 | 11 | import styles from './blocks-only.css'; 12 | 13 | const mapStateToProps = state => ({vm: state.scratchGui.vm}); 14 | 15 | const VMBlocks = connect(mapStateToProps)(Blocks); 16 | const VMControls = connect(mapStateToProps)(Controls); 17 | 18 | const BlocksOnly = props => ( 19 | 20 | 26 | 27 | 28 | ); 29 | 30 | const App = AppStateHOC(HashParserHOC(BlocksOnly)); 31 | 32 | const appTarget = document.createElement('div'); 33 | document.body.appendChild(appTarget); 34 | 35 | ReactDOM.render(, appTarget); 36 | -------------------------------------------------------------------------------- /src/playground/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .app { 4 | /* probably unecessary, transitional until layout is refactored */ 5 | width: 100%; 6 | height: 100%; 7 | margin: 0; 8 | 9 | /* Setting min height/width makes the UI scroll below those sizes */ 10 | min-width: 1024px; 11 | min-height: 640px; /* Min height to fit sprite/backdrop button */ 12 | } 13 | 14 | /* @todo: move globally? Safe / side FX, for blocks particularly? */ 15 | * { box-sizing: border-box; } 16 | -------------------------------------------------------------------------------- /src/playground/player.css: -------------------------------------------------------------------------------- 1 | .stage-only { 2 | width: calc(480px + 1rem); 3 | } 4 | 5 | .editor { 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | .stage-only * { 14 | box-sizing: border-box; 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/asset-drag.js: -------------------------------------------------------------------------------- 1 | const DRAG_UPDATE = 'clipcc-gui/asset-drag/DRAG_UPDATE'; 2 | 3 | const initialState = { 4 | dragging: false, 5 | currentOffset: null, 6 | img: null 7 | }; 8 | 9 | const reducer = function (state, action) { 10 | if (typeof state === 'undefined') state = initialState; 11 | 12 | switch (action.type) { 13 | case DRAG_UPDATE: 14 | return Object.assign({}, state, action.state); 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | const updateAssetDrag = function (state) { 21 | return { 22 | type: DRAG_UPDATE, 23 | state: state 24 | }; 25 | }; 26 | 27 | export { 28 | reducer as default, 29 | initialState as assetDragInitialState, 30 | updateAssetDrag 31 | }; 32 | -------------------------------------------------------------------------------- /src/reducers/block-drag.js: -------------------------------------------------------------------------------- 1 | const BLOCK_DRAG_UPDATE = 'clipcc-gui/block-drag/BLOCK_DRAG_UPDATE'; 2 | 3 | const initialState = false; 4 | 5 | const reducer = function (state, action) { 6 | if (typeof state === 'undefined') state = initialState; 7 | switch (action.type) { 8 | case BLOCK_DRAG_UPDATE: 9 | return action.areBlocksOverGui; 10 | default: 11 | return state; 12 | } 13 | }; 14 | 15 | const updateBlockDrag = function (areBlocksOverGui) { 16 | return { 17 | type: BLOCK_DRAG_UPDATE, 18 | areBlocksOverGui: areBlocksOverGui, 19 | meta: { 20 | throttle: 30 21 | } 22 | }; 23 | }; 24 | 25 | export { 26 | reducer as default, 27 | initialState as blockDragInitialState, 28 | updateBlockDrag 29 | }; 30 | -------------------------------------------------------------------------------- /src/reducers/connection-modal.js: -------------------------------------------------------------------------------- 1 | const SET_ID = 'clipcc-gui/connection-modal/setId'; 2 | 3 | const initialState = { 4 | extensionId: null 5 | }; 6 | 7 | const reducer = function (state, action) { 8 | if (typeof state === 'undefined') state = initialState; 9 | switch (action.type) { 10 | case SET_ID: 11 | return Object.assign({}, state, { 12 | extensionId: action.extensionId 13 | }); 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | const setConnectionModalExtensionId = function (extensionId) { 20 | return { 21 | type: SET_ID, 22 | extensionId: extensionId 23 | }; 24 | }; 25 | 26 | export { 27 | reducer as default, 28 | initialState as connectionModalInitialState, 29 | setConnectionModalExtensionId 30 | }; 31 | -------------------------------------------------------------------------------- /src/reducers/editor-tab.js: -------------------------------------------------------------------------------- 1 | const ACTIVATE_TAB = 'clipcc-gui/navigation/ACTIVATE_TAB'; 2 | 3 | // Constants use numbers to make it easier to work with react-tabs 4 | const BLOCKS_TAB_INDEX = 0; 5 | const COSTUMES_TAB_INDEX = 1; 6 | const SOUNDS_TAB_INDEX = 2; 7 | 8 | const initialState = { 9 | activeTabIndex: BLOCKS_TAB_INDEX 10 | }; 11 | 12 | const reducer = function (state, action) { 13 | if (typeof state === 'undefined') state = initialState; 14 | switch (action.type) { 15 | case ACTIVATE_TAB: 16 | return Object.assign({}, state, { 17 | activeTabIndex: action.activeTabIndex 18 | }); 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | const activateTab = function (tab) { 25 | return { 26 | type: ACTIVATE_TAB, 27 | activeTabIndex: tab 28 | }; 29 | }; 30 | 31 | export { 32 | reducer as default, 33 | initialState as editorTabInitialState, 34 | activateTab, 35 | BLOCKS_TAB_INDEX, 36 | COSTUMES_TAB_INDEX, 37 | SOUNDS_TAB_INDEX 38 | }; 39 | -------------------------------------------------------------------------------- /src/reducers/extension-settings.js: -------------------------------------------------------------------------------- 1 | const NEW_EXTENSION = 'clipcc-gui/extension-settings/NEW_EXTENSION'; 2 | 3 | const initialState = {}; 4 | 5 | const reducer = function (state, action) { 6 | if (typeof state === 'undefined') state = initialState; 7 | switch (action.type) { 8 | case NEW_EXTENSION: 9 | state[action.id] = action.options; 10 | return Object.assign({}, state); 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | const newExtensionSettings = (id, options) => ({ 17 | type: NEW_EXTENSION, 18 | id, 19 | options 20 | }); 21 | 22 | export { 23 | reducer as default, 24 | initialState as extensionSettingsInitialState, 25 | newExtensionSettings 26 | }; 27 | -------------------------------------------------------------------------------- /src/reducers/fonts-loaded.js: -------------------------------------------------------------------------------- 1 | const SET_FONTS_LOADED = 'fontsLoaded/SET_FONTS_LOADED'; 2 | 3 | const initialState = false; 4 | 5 | const reducer = function (state, action) { 6 | if (typeof state === 'undefined') state = initialState; 7 | switch (action.type) { 8 | case SET_FONTS_LOADED: 9 | return action.loaded; 10 | default: 11 | return state; 12 | } 13 | }; 14 | const setFontsLoaded = () => ({ 15 | type: SET_FONTS_LOADED, 16 | loaded: true 17 | }); 18 | 19 | export { 20 | reducer as default, 21 | initialState as fontsLoadedInitialState, 22 | setFontsLoaded 23 | }; 24 | -------------------------------------------------------------------------------- /src/reducers/hovered-target.js: -------------------------------------------------------------------------------- 1 | const SET_HOVERED_SPRITE = 'clipcc-gui/hovered-target/SET_HOVERED_SPRITE'; 2 | const SET_RECEIVED_BLOCKS = 'clipcc-gui/hovered-target/SET_RECEIVED_BLOCKS'; 3 | 4 | const initialState = { 5 | sprite: null, 6 | receivedBlocks: false 7 | }; 8 | 9 | const reducer = function (state, action) { 10 | if (typeof state === 'undefined') state = initialState; 11 | switch (action.type) { 12 | case SET_HOVERED_SPRITE: 13 | return { 14 | sprite: action.spriteId, 15 | receivedBlocks: false 16 | }; 17 | case SET_RECEIVED_BLOCKS: 18 | return { 19 | sprite: state.sprite, 20 | receivedBlocks: action.receivedBlocks 21 | }; 22 | default: 23 | return state; 24 | } 25 | }; 26 | 27 | const setHoveredSprite = function (spriteId) { 28 | return { 29 | type: SET_HOVERED_SPRITE, 30 | spriteId: spriteId, 31 | meta: { 32 | throttle: 30 33 | } 34 | }; 35 | }; 36 | 37 | const setReceivedBlocks = function (receivedBlocks) { 38 | return { 39 | type: SET_RECEIVED_BLOCKS, 40 | receivedBlocks: receivedBlocks 41 | }; 42 | }; 43 | 44 | export { 45 | reducer as default, 46 | initialState as hoveredTargetInitialState, 47 | setHoveredSprite, 48 | setReceivedBlocks 49 | }; 50 | -------------------------------------------------------------------------------- /src/reducers/load-error.js: -------------------------------------------------------------------------------- 1 | const SET_LOAD_ERROR = 'clipcc-gui/load-error/SET_LOAD_ERROR'; 2 | 3 | const initialState = { 4 | errorId: '', 5 | detail: '', 6 | missingExtensions: [] 7 | }; 8 | 9 | const reducer = function (state, action) { 10 | if (typeof state === 'undefined') state = initialState; 11 | if (action.type === SET_LOAD_ERROR) { 12 | return Object.assign({}, initialState, action.data); 13 | } 14 | return state; 15 | }; 16 | 17 | const setLoadError = function (data) { 18 | return { 19 | type: SET_LOAD_ERROR, 20 | data: data 21 | }; 22 | }; 23 | 24 | export { 25 | reducer as default, 26 | initialState as loadErrorInitialState, 27 | setLoadError 28 | }; 29 | -------------------------------------------------------------------------------- /src/reducers/mic-indicator.js: -------------------------------------------------------------------------------- 1 | const UPDATE = 'clipcc-gui/mic-indicator/UPDATE'; 2 | 3 | const initialState = false; 4 | 5 | const reducer = function (state, action) { 6 | if (typeof state === 'undefined') state = initialState; 7 | switch (action.type) { 8 | case UPDATE: 9 | return action.visible; 10 | default: 11 | return state; 12 | } 13 | }; 14 | 15 | const updateMicIndicator = function (visible) { 16 | return { 17 | type: UPDATE, 18 | visible: visible 19 | }; 20 | }; 21 | 22 | export { 23 | reducer as default, 24 | initialState as micIndicatorInitialState, 25 | updateMicIndicator 26 | }; 27 | -------------------------------------------------------------------------------- /src/reducers/monitors.js: -------------------------------------------------------------------------------- 1 | const UPDATE_MONITORS = 'clipcc-gui/monitors/UPDATE_MONITORS'; 2 | import {OrderedMap} from 'immutable'; 3 | 4 | const initialState = OrderedMap(); 5 | 6 | const reducer = function (state, action) { 7 | if (typeof state === 'undefined') state = initialState; 8 | switch (action.type) { 9 | case UPDATE_MONITORS: 10 | return action.monitors; 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | const updateMonitors = function (monitors) { 17 | return { 18 | type: UPDATE_MONITORS, 19 | monitors: monitors, 20 | meta: { 21 | throttle: 30 22 | } 23 | }; 24 | }; 25 | 26 | export { 27 | reducer as default, 28 | initialState as monitorsInitialState, 29 | updateMonitors 30 | }; 31 | -------------------------------------------------------------------------------- /src/reducers/project-changed.js: -------------------------------------------------------------------------------- 1 | const SET_PROJECT_CHANGED = 'clipcc-gui/project-changed/SET_PROJECT_CHANGED'; 2 | 3 | const initialState = false; 4 | 5 | const reducer = function (state, action) { 6 | if (typeof state === 'undefined') state = initialState; 7 | switch (action.type) { 8 | case SET_PROJECT_CHANGED: 9 | return action.changed; 10 | default: 11 | return state; 12 | } 13 | }; 14 | const setProjectChanged = () => ({ 15 | type: SET_PROJECT_CHANGED, 16 | changed: true 17 | }); 18 | const setProjectUnchanged = () => ({ 19 | type: SET_PROJECT_CHANGED, 20 | changed: false 21 | }); 22 | 23 | export { 24 | reducer as default, 25 | initialState as projectChangedInitialState, 26 | setProjectChanged, 27 | setProjectUnchanged 28 | }; 29 | -------------------------------------------------------------------------------- /src/reducers/project-title.js: -------------------------------------------------------------------------------- 1 | const SET_PROJECT_TITLE = 'projectTitle/SET_PROJECT_TITLE'; 2 | 3 | // we are initializing to a blank string instead of an actual title, 4 | // because it would be hard to localize here 5 | const initialState = ''; 6 | 7 | const reducer = function (state, action) { 8 | if (typeof state === 'undefined') state = initialState; 9 | switch (action.type) { 10 | case SET_PROJECT_TITLE: 11 | return action.title; 12 | default: 13 | return state; 14 | } 15 | }; 16 | const setProjectTitle = title => ({ 17 | type: SET_PROJECT_TITLE, 18 | title: title 19 | }); 20 | 21 | export { 22 | reducer as default, 23 | initialState as projectTitleInitialState, 24 | setProjectTitle 25 | }; 26 | -------------------------------------------------------------------------------- /src/reducers/restore-deletion.js: -------------------------------------------------------------------------------- 1 | const RESTORE_UPDATE = 'clipcc-gui/restore-deletion/RESTORE_UPDATE'; 2 | 3 | const initialState = { 4 | restoreFun: null, 5 | deletedItem: '' 6 | }; 7 | 8 | const reducer = function (state, action) { 9 | if (typeof state === 'undefined') state = initialState; 10 | 11 | switch (action.type) { 12 | case RESTORE_UPDATE: 13 | return Object.assign({}, state, action.state); 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | const setRestore = function (state) { 20 | return { 21 | type: RESTORE_UPDATE, 22 | state: { 23 | restoreFun: state.restoreFun, 24 | deletedItem: state.deletedItem 25 | } 26 | }; 27 | }; 28 | 29 | export { 30 | reducer as default, 31 | initialState as restoreDeletionInitialState, 32 | setRestore 33 | }; 34 | -------------------------------------------------------------------------------- /src/reducers/stage-size.js: -------------------------------------------------------------------------------- 1 | import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants.js'; 2 | 3 | const SET_STAGE_SIZE = 'clipcc-gui/StageSize/SET_STAGE_SIZE'; 4 | 5 | const initialState = { 6 | stageSize: STAGE_DISPLAY_SIZES.large 7 | }; 8 | 9 | const reducer = function (state, action) { 10 | if (typeof state === 'undefined') state = initialState; 11 | switch (action.type) { 12 | case SET_STAGE_SIZE: 13 | return { 14 | stageSize: action.stageSize 15 | }; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | const setStageSize = function (stageSize) { 22 | return { 23 | type: SET_STAGE_SIZE, 24 | stageSize: stageSize 25 | }; 26 | }; 27 | 28 | export { 29 | reducer as default, 30 | initialState as stageSizeInitialState, 31 | setStageSize 32 | }; 33 | -------------------------------------------------------------------------------- /src/reducers/timeout.js: -------------------------------------------------------------------------------- 1 | const SET_AUTOSAVE_TIMEOUT_ID = 'timeout/SET_AUTOSAVE_TIMEOUT_ID'; 2 | 3 | const initialState = { 4 | autoSaveTimeoutId: null 5 | }; 6 | 7 | const reducer = function (state, action) { 8 | if (typeof state === 'undefined') state = initialState; 9 | switch (action.type) { 10 | case SET_AUTOSAVE_TIMEOUT_ID: 11 | return Object.assign({}, state, { 12 | autoSaveTimeoutId: action.id 13 | }); 14 | default: 15 | return state; 16 | } 17 | }; 18 | const setAutoSaveTimeoutId = id => ({ 19 | type: SET_AUTOSAVE_TIMEOUT_ID, 20 | id 21 | }); 22 | 23 | export { 24 | reducer as default, 25 | initialState as timeoutInitialState, 26 | setAutoSaveTimeoutId 27 | }; 28 | -------------------------------------------------------------------------------- /src/reducers/toolbox.js: -------------------------------------------------------------------------------- 1 | const UPDATE_TOOLBOX = 'clipcc-gui/toolbox/UPDATE_TOOLBOX'; 2 | import makeToolboxXML from '../lib/make-toolbox-xml'; 3 | 4 | const initialState = { 5 | toolboxXML: makeToolboxXML(true) 6 | }; 7 | 8 | const reducer = function (state, action) { 9 | if (typeof state === 'undefined') state = initialState; 10 | switch (action.type) { 11 | case UPDATE_TOOLBOX: 12 | return Object.assign({}, state, { 13 | toolboxXML: action.toolboxXML 14 | }); 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | const updateToolbox = function (toolboxXML) { 21 | return { 22 | type: UPDATE_TOOLBOX, 23 | toolboxXML: toolboxXML 24 | }; 25 | }; 26 | 27 | export { 28 | reducer as default, 29 | initialState as toolboxInitialState, 30 | updateToolbox 31 | }; 32 | -------------------------------------------------------------------------------- /src/reducers/vm.js: -------------------------------------------------------------------------------- 1 | import VM from 'clipcc-vm'; 2 | import storage from '../lib/storage'; 3 | import {extensionManager} from 'clipcc-extension'; 4 | import {appVersion} from '../lib/app-info'; 5 | 6 | const SET_VM = 'clipcc-gui/vm/SET_VM'; 7 | const defaultVM = new VM({appVersion, extensionManager}); 8 | defaultVM.attachStorage(storage); 9 | console.log(`%cClipCC ${appVersion}`, 'font-size:32px;'); 10 | const initialState = defaultVM; 11 | 12 | const reducer = function (state, action) { 13 | if (typeof state === 'undefined') state = initialState; 14 | switch (action.type) { 15 | case SET_VM: 16 | return action.vm; 17 | default: 18 | return state; 19 | } 20 | }; 21 | const setVM = function (vm) { 22 | return { 23 | type: SET_VM, 24 | vm: vm 25 | }; 26 | }; 27 | 28 | export { 29 | reducer as default, 30 | initialState as vmInitialState, 31 | setVM 32 | }; 33 | -------------------------------------------------------------------------------- /src/reducers/workspace-metrics.js: -------------------------------------------------------------------------------- 1 | const UPDATE_METRICS = 'scratch-gui/workspace-metrics/UPDATE_METRICS'; 2 | 3 | const initialState = { 4 | targets: {} 5 | }; 6 | 7 | const reducer = function (state, action) { 8 | if (typeof state === 'undefined') state = initialState; 9 | 10 | switch (action.type) { 11 | case UPDATE_METRICS: 12 | return Object.assign({}, state, { 13 | targets: Object.assign({}, state.targets, { 14 | [action.targetID]: { 15 | scrollX: action.scrollX, 16 | scrollY: action.scrollY, 17 | scale: action.scale 18 | } 19 | }) 20 | }); 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | const updateMetrics = function (metrics) { 27 | return { 28 | type: UPDATE_METRICS, 29 | ...metrics 30 | }; 31 | }; 32 | 33 | export { 34 | reducer as default, 35 | initialState as workspaceMetricsInitialState, 36 | updateMetrics 37 | }; 38 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/src/test.js -------------------------------------------------------------------------------- /static/clipcc_logo144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/static/clipcc_logo144.png -------------------------------------------------------------------------------- /static/clipcc_logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/static/clipcc_logo48.png -------------------------------------------------------------------------------- /static/clipcc_logo72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/static/clipcc_logo72.png -------------------------------------------------------------------------------- /static/clipcc_logo96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/static/clipcc_logo96.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/static/favicon.ico -------------------------------------------------------------------------------- /static/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ClipCC", 3 | "short_name": "ClipCC", 4 | "id": "com.codingclip.clipcc3", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#4D97FF", 8 | "description": "A powerful modified Scratch editor.", 9 | "icons": [ 10 | { 11 | "src": "static/clipcc_logo48.png", 12 | "sizes": "48x48", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "static/clipcc_logo72.png", 17 | "sizes": "72x72", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "static/clipcc_logo96.png", 22 | "sizes": "96x96", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "static/clipcc_logo144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /static/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/static/social.jpg -------------------------------------------------------------------------------- /static/sw.js: -------------------------------------------------------------------------------- 1 | const appId = 'com.codingclip.clipcc3'; 2 | const appVer = 'gen@appVer'; // Don't modify, generated by gen-meta.js 3 | 4 | const cacheName = appId + '@' + appVer; 5 | const cacheList = global.serviceWorkerOption.assets; 6 | 7 | self.addEventListener('install', event => { 8 | console.log('[Service Worker] Event: install'); 9 | event.waitUntil( 10 | caches.open(cacheName).then(cache => { 11 | return cache.addAll(cacheList); 12 | }) 13 | ); 14 | }); 15 | 16 | self.addEventListener('fetch', event => { 17 | console.log('[Service Worker] Event: fetch'); 18 | event.respondWith( 19 | caches.match(event.request).then(response => { 20 | if (response) { 21 | console.log('[Service Workder] Debug: ', response); 22 | return response; 23 | } 24 | return fetch(event.request); 25 | }) 26 | ); 27 | }); 28 | 29 | self.addEventListener('activate', event => { 30 | console.log('[Service Workder] Event: activate'); 31 | const cacheWhitelist = [cacheName]; 32 | event.waitUntil( 33 | caches.keys().then(cacheNames => Promise.all( 34 | cacheNames.map(cacheName => { 35 | if (cacheWhitelist.indexOf(cacheName) === -1) { 36 | return caches.delete(cacheName); 37 | } 38 | return ''; 39 | }) 40 | )) 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['scratch/react', 'scratch/es6', 'plugin:jest/recommended'], 3 | env: { 4 | browser: true, 5 | jest: true 6 | }, 7 | plugins: ['jest'], 8 | rules: { 9 | 'react/prop-types': 0 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/__mocks__/audio-buffer-player.js: -------------------------------------------------------------------------------- 1 | export default class MockAudioBufferPlayer { 2 | constructor (samples, sampleRate) { 3 | this.samples = samples; 4 | this.sampleRate = sampleRate; 5 | this.buffer = { 6 | getChannelData: jest.fn(() => samples), 7 | sampleRate: sampleRate 8 | }; 9 | this.play = jest.fn((trimStart, trimEnd, onUpdate) => { 10 | this.onUpdate = onUpdate; 11 | }); 12 | this.stop = jest.fn(); 13 | MockAudioBufferPlayer.instance = this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/__mocks__/audio-effects.js: -------------------------------------------------------------------------------- 1 | export default class MockAudioEffects { 2 | static get effectTypes () { // @todo can this be imported from the real file? 3 | return { 4 | ROBOT: 'robot', 5 | REVERSE: 'reverse', 6 | LOUDER: 'higher', 7 | SOFTER: 'lower', 8 | FASTER: 'faster', 9 | SLOWER: 'slower', 10 | ECHO: 'echo' 11 | }; 12 | } 13 | constructor (buffer, name) { 14 | this.buffer = buffer; 15 | this.name = name; 16 | this.process = jest.fn(done => { 17 | this._finishProcessing = renderedBuffer => { 18 | done(renderedBuffer, 0, 1); 19 | return new Promise(resolve => setTimeout(resolve)); 20 | }; 21 | }); 22 | MockAudioEffects.instance = this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/__mocks__/editor-msgs-mock.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en: {} 3 | }; 4 | -------------------------------------------------------------------------------- /test/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/fileMock.js 2 | 3 | module.exports = 'test-file-stub'; 4 | -------------------------------------------------------------------------------- /test/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/styleMock.js 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/fixtures/bmpfile.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/bmpfile.bmp -------------------------------------------------------------------------------- /test/fixtures/corrupt-bmp.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/corrupt-bmp.sb3 -------------------------------------------------------------------------------- /test/fixtures/corrupt-bmp.sprite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/corrupt-bmp.sprite3 -------------------------------------------------------------------------------- /test/fixtures/corrupt-svg.sb2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/corrupt-svg.sb2 -------------------------------------------------------------------------------- /test/fixtures/corrupt-svg.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/corrupt-svg.sb3 -------------------------------------------------------------------------------- /test/fixtures/corrupt-svg.sprite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/corrupt-svg.sprite3 -------------------------------------------------------------------------------- /test/fixtures/corrupted-svg.sprite2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/corrupted-svg.sprite2 -------------------------------------------------------------------------------- /test/fixtures/gh-3582-png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/gh-3582-png.png -------------------------------------------------------------------------------- /test/fixtures/missing-bmp.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/missing-bmp.sb3 -------------------------------------------------------------------------------- /test/fixtures/missing-bmp.sprite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/missing-bmp.sprite3 -------------------------------------------------------------------------------- /test/fixtures/missing-sprite-svg.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/missing-sprite-svg.sb3 -------------------------------------------------------------------------------- /test/fixtures/missing-svg.sb2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/missing-svg.sb2 -------------------------------------------------------------------------------- /test/fixtures/missing-svg.sprite2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/missing-svg.sprite2 -------------------------------------------------------------------------------- /test/fixtures/missing-svg.sprite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/missing-svg.sprite3 -------------------------------------------------------------------------------- /test/fixtures/movie.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/movie.wav -------------------------------------------------------------------------------- /test/fixtures/paddleball.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/paddleball.gif -------------------------------------------------------------------------------- /test/fixtures/project1.sb3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/project1.sb3 -------------------------------------------------------------------------------- /test/fixtures/sneaker.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clipteam/clipcc-gui/675ef136990992515588f3d8e37b00090c698968/test/fixtures/sneaker.wav -------------------------------------------------------------------------------- /test/helpers/enzyme-setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({adapter: new Adapter()}); 5 | -------------------------------------------------------------------------------- /test/helpers/intl-helpers.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Helpers for using enzyme and react-test-renderer with react-intl 3 | * Directly from https://github.com/yahoo/react-intl/wiki/Testing-with-React-Intl 4 | */ 5 | import React from 'react'; 6 | import renderer from 'react-test-renderer'; 7 | import {IntlProvider, intlShape} from 'react-intl'; 8 | import {mount, shallow} from 'enzyme'; 9 | 10 | const intlProvider = new IntlProvider({locale: 'en'}, {}); 11 | const {intl} = intlProvider.getChildContext(); 12 | 13 | const nodeWithIntlProp = node => React.cloneElement(node, {intl}); 14 | 15 | const shallowWithIntl = (node, {context} = {}) => shallow( 16 | nodeWithIntlProp(node), 17 | { 18 | context: Object.assign({}, context, {intl}) 19 | } 20 | ); 21 | 22 | const mountWithIntl = (node, {context, childContextTypes} = {}) => mount( 23 | nodeWithIntlProp(node), 24 | { 25 | context: Object.assign({}, context, {intl}), 26 | childContextTypes: Object.assign({}, {intl: intlShape}, childContextTypes) 27 | } 28 | ); 29 | 30 | // react-test-renderer component for use with snapshot testing 31 | const componentWithIntl = (children, props = {locale: 'en'}) => renderer.create( 32 | {children} 33 | ); 34 | 35 | export { 36 | componentWithIntl, 37 | shallowWithIntl, 38 | mountWithIntl 39 | }; 40 | -------------------------------------------------------------------------------- /test/integration/how-tos.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import SeleniumHelper from '../helpers/selenium-helper'; 3 | 4 | const { 5 | clickText, 6 | clickXpath, 7 | findByXpath, 8 | getDriver, 9 | getLogs, 10 | loadUri 11 | } = new SeleniumHelper(); 12 | 13 | const uri = path.resolve(__dirname, '../../build/index.html'); 14 | 15 | let driver; 16 | 17 | describe('Working with the how-to library', () => { 18 | beforeAll(() => { 19 | driver = getDriver(); 20 | }); 21 | 22 | afterAll(async () => { 23 | await driver.quit(); 24 | }); 25 | 26 | test('Choosing a how-to', async () => { 27 | await loadUri(uri); 28 | await clickText('Costumes'); 29 | await clickXpath('//*[@aria-label="Tutorials"]'); 30 | await clickText('Getting Started'); // Modal should close 31 | // Make sure YouTube video on first card appears 32 | await findByXpath('//div[contains(@class, "step-video")]'); 33 | const logs = await getLogs(); 34 | await expect(logs).toEqual([]); 35 | }); 36 | 37 | // @todo navigating cards, etc. 38 | }); 39 | -------------------------------------------------------------------------------- /test/integration/stage-size.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import SeleniumHelper from '../helpers/selenium-helper'; 3 | 4 | const { 5 | clickText, 6 | clickXpath, 7 | rightClickText, 8 | getDriver, 9 | getLogs, 10 | loadUri, 11 | scope 12 | } = new SeleniumHelper(); 13 | 14 | const uri = path.resolve(__dirname, '../../build/index.html'); 15 | 16 | let driver; 17 | 18 | describe('Loading scratch gui', () => { 19 | beforeAll(() => { 20 | driver = getDriver(); 21 | }); 22 | 23 | afterAll(async () => { 24 | await driver.quit(); 25 | }); 26 | 27 | test('Switching small/large stage after highlighting and deleting sprite', async () => { 28 | await loadUri(uri); 29 | 30 | // Highlight the sprite 31 | await clickText('Sprite1', scope.spriteTile); 32 | 33 | // Delete it 34 | await rightClickText('Sprite1', scope.spriteTile); 35 | await clickText('delete', scope.spriteTile); 36 | 37 | // Go to small stage mode 38 | await clickXpath('//img[@alt="Switch to small stage"]'); 39 | 40 | // Confirm app still working 41 | await clickXpath('//img[@alt="Switch to large stage"]'); 42 | 43 | const logs = await getLogs(); 44 | await expect(logs).toEqual([]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/integration/tutorials-shortcut.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import SeleniumHelper from '../helpers/selenium-helper'; 3 | 4 | const { 5 | clickText, 6 | findByXpath, 7 | getDriver, 8 | loadUri 9 | } = new SeleniumHelper(); 10 | 11 | const uri = path.resolve(__dirname, '../../build/index.html?tutorial=all'); 12 | const uriPrefix = path.resolve(__dirname, '../../build/index.html?tutorial='); 13 | 14 | let driver; 15 | 16 | describe('Working with shortcut to Tutorials library', () => { 17 | beforeAll(() => { 18 | driver = getDriver(); 19 | }); 20 | 21 | afterAll(async () => { 22 | await driver.quit(); 23 | }); 24 | 25 | test('opens with the Tutorial Library showing', async () => { 26 | await loadUri(uri); 27 | // make sure there is a tutorial visible that doesn't have a shortcut 28 | await clickText('Make It Spin'); 29 | await findByXpath('//div[contains(@class, "step-video")]'); 30 | }); 31 | 32 | test('can open hidden tutorials', async () => { 33 | await loadUri(`${uriPrefix}whatsnew`); 34 | // should open the tutorial video immediately 35 | await findByXpath('//div[contains(@class, "step-video")]'); 36 | }); 37 | // @todo navigating cards, etc. 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/components/__snapshots__/button.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ButtonComponent matches snapshot 1`] = ` 4 | 9 |
12 | 13 | `; 14 | -------------------------------------------------------------------------------- /test/unit/components/__snapshots__/icon-button.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`IconButtonComponent matches snapshot 1`] = ` 4 |
9 | 14 |
17 |
18 | Text 19 |
20 |
21 |
22 | `; 23 | -------------------------------------------------------------------------------- /test/unit/components/button.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | import ButtonComponent from '../../../src/components/button/button'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | describe('ButtonComponent', () => { 7 | test('matches snapshot', () => { 8 | const onClick = jest.fn(); 9 | const component = renderer.create( 10 | 11 | ); 12 | expect(component.toJSON()).toMatchSnapshot(); 13 | }); 14 | 15 | test('triggers callback when clicked', () => { 16 | const onClick = jest.fn(); 17 | const componentShallowWrapper = shallow( 18 | 19 | ); 20 | componentShallowWrapper.simulate('click'); 21 | expect(onClick).toHaveBeenCalled(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/unit/components/icon-button.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {shallow} from 'enzyme'; 3 | import IconButton from '../../../src/components/icon-button/icon-button'; 4 | import renderer from 'react-test-renderer'; 5 | 6 | describe('IconButtonComponent', () => { 7 | test('matches snapshot', () => { 8 | const onClick = jest.fn(); 9 | const title =
Text
; 10 | const imgSrc = 'imgSrc'; 11 | const className = 'custom-class-name'; 12 | const component = renderer.create( 13 | 19 | ); 20 | expect(component.toJSON()).toMatchSnapshot(); 21 | }); 22 | 23 | test('triggers callback when clicked', () => { 24 | const onClick = jest.fn(); 25 | const title =
Text
; 26 | const imgSrc = 'imgSrc'; 27 | const componentShallowWrapper = shallow( 28 | 33 | ); 34 | componentShallowWrapper.simulate('click'); 35 | expect(onClick).toHaveBeenCalled(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/reducers/workspace-metrics-reducer.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import workspaceMetricsReducer, {updateMetrics} from '../../../src/reducers/workspace-metrics'; 3 | 4 | test('initialState', () => { 5 | let defaultState; 6 | /* workspaceMetricsReducer(state, action) */ 7 | expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toBeDefined(); 8 | expect(workspaceMetricsReducer(defaultState, {type: 'anything'})).toEqual({targets: {}}); 9 | }); 10 | 11 | test('updateMetrics action creator', () => { 12 | let defaultState; 13 | const action = updateMetrics({ 14 | targetID: 'abcde', 15 | scrollX: 225, 16 | scrollY: 315, 17 | scale: 1.25 18 | }); 19 | const resultState = workspaceMetricsReducer(defaultState, action); 20 | expect(Object.keys(resultState.targets).length).toBe(1); 21 | expect(resultState.targets.abcde).toBeDefined(); 22 | expect(resultState.targets.abcde.scrollX).toBe(225); 23 | expect(resultState.targets.abcde.scrollY).toBe(315); 24 | expect(resultState.targets.abcde.scale).toBe(1.25); 25 | }); 26 | -------------------------------------------------------------------------------- /test/unit/util/audio-context.test.js: -------------------------------------------------------------------------------- 1 | import 'web-audio-test-api'; 2 | import SharedAudioContext from '../../../src/lib/audio/shared-audio-context'; 3 | 4 | describe('Shared Audio Context', () => { 5 | const audioContext = new AudioContext(); 6 | 7 | test('returns empty object without user gesture', () => { 8 | const sharedAudioContext = new SharedAudioContext(); 9 | expect(sharedAudioContext).toMatchObject({}); 10 | }); 11 | 12 | test('returns AudioContext when mousedown is triggered', () => { 13 | const sharedAudioContext = new SharedAudioContext(); 14 | const event = new Event('mousedown'); 15 | document.dispatchEvent(event); 16 | expect(sharedAudioContext).toMatchObject(audioContext); 17 | }); 18 | 19 | test('returns AudioContext when touchstart is triggered', () => { 20 | const sharedAudioContext = new SharedAudioContext(); 21 | const event = new Event('touchstart'); 22 | document.dispatchEvent(event); 23 | expect(sharedAudioContext).toMatchObject(audioContext); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/unit/util/code-payload.test.js: -------------------------------------------------------------------------------- 1 | jest.mock('../../../src/lib/backpack/block-to-image', () => () => Promise.resolve('block-image')); 2 | jest.mock('../../../src/lib/backpack/jpeg-thumbnail', () => () => Promise.resolve('thumbnail')); 3 | 4 | import codePayload from '../../../src/lib/backpack/code-payload'; 5 | import {Base64} from 'js-base64'; 6 | 7 | describe('codePayload', () => { 8 | test('base64 encodes the blocks as json', () => { 9 | const blocks = '☁︎❤️🐻'; 10 | const payload = codePayload({ 11 | blockObjects: blocks 12 | }); 13 | return payload.then(p => { 14 | expect( 15 | JSON.parse(Base64.decode(p.body)) 16 | ).toEqual(blocks); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/util/default-project.test.js: -------------------------------------------------------------------------------- 1 | import defaultProjectGenerator from '../../../src/lib/default-project/index.js'; 2 | 3 | describe('defaultProject', () => { 4 | // This test ensures that the assets referenced in the default project JSON 5 | // do not get out of sync with the raw assets that are included alongside. 6 | // see https://github.com/LLK/scratch-gui/issues/4844 7 | test('assets referenced by the project are included', () => { 8 | const translatorFn = () => ''; 9 | const defaultProject = defaultProjectGenerator(translatorFn); 10 | const includedAssetIds = defaultProject.map(obj => obj.id); 11 | const projectData = JSON.parse(defaultProject[0].data); 12 | projectData.targets.forEach(target => { 13 | target.costumes.forEach(costume => { 14 | expect(includedAssetIds.includes(costume.assetId)).toBe(true); 15 | }); 16 | target.sounds.forEach(sound => { 17 | expect(includedAssetIds.includes(sound.assetId)).toBe(true); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/util/get-costume-url.test.js: -------------------------------------------------------------------------------- 1 | import {HAS_FONT_REGEXP} from '../../../src/lib/get-costume-url'; 2 | 3 | describe('SVG Font Parsing', () => { 4 | test('Has font regexp works', () => { 5 | expect('font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); 6 | expect('font-family="none" font-family="Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); 7 | expect('font-family = "Sans Serif"'.match(HAS_FONT_REGEXP)).toBeTruthy(); 8 | 9 | expect('font-family="none"'.match(HAS_FONT_REGEXP)).toBeFalsy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/unit/util/opcode-labels.test.js: -------------------------------------------------------------------------------- 1 | import opcodeLabels from '../../../src/lib/opcode-labels'; 2 | 3 | describe('Opcode Labels', () => { 4 | test('day of week label', () => { 5 | const labelFun = opcodeLabels.getLabel('sensing_current').labelFn; 6 | expect(labelFun({CURRENTMENU: 'dayofweek'})).toBe('day of week'); 7 | expect(labelFun({CURRENTMENU: 'DAYOFWEEK'})).toBe('day of week'); 8 | }); 9 | 10 | test('unspecified opcodes default to extension category and opcode as label', () => { 11 | const labelInfo = opcodeLabels.getLabel('music_getTempo'); 12 | expect(labelInfo.label).toBe('music_getTempo'); 13 | expect(labelInfo.category).toBe('extension'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/unit/util/translate-video.test.js: -------------------------------------------------------------------------------- 1 | import {translateVideo} from '../../../src/lib/libraries/decks/translate-video.js'; 2 | 3 | describe('translateVideo', () => { 4 | test('returns the id if it is not found', () => { 5 | expect(translateVideo('not-a-key', 'en')).toEqual('not-a-key'); 6 | }); 7 | 8 | test('returns the expected id for Japanese', () => { 9 | expect(translateVideo('intro-move-sayhello', 'ja')).toEqual('v2c2f3y2sc'); 10 | }); 11 | 12 | test('returns the expected id for English', () => { 13 | expect(translateVideo('intro-move-sayhello', 'en')).toEqual('rpjvs3v9gj'); 14 | }); 15 | 16 | test('returns the English id for non-existent locales', () => { 17 | expect(translateVideo('intro-move-sayhello', 'yum')).toEqual('rpjvs3v9gj'); 18 | }); 19 | }); 20 | --------------------------------------------------------------------------------