diff options
Diffstat (limited to 'src/js/main.js')
-rw-r--r-- | src/js/main.js | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..3a3f823 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,222 @@ +'use strict'; + +require('../index.html'); +require('../css/style.css'); + +const { Project } = require('sb-edit'); +const { scriptToScratchblocks } = require('./sb3-gen'); + +// setup step + +const urlInput = document.getElementById('url-input'); +const fileInput = document.getElementById('file-input'); +const generateButton = document.getElementById('generate-button'); +const errorElement = document.getElementById('error-element'); +const downloadIndicator = document.getElementById('download-indicator'); + +function assetURL({md5, ext}) { + return 'https://assets.scratch.mit.edu/' + md5 + '.' + ext; +} + +function isUrlInputValid() { + const re = /^((.*:\/\/scratch.mit.edu\/projects\/[0-9]+\/?)|[0-9]+)(:.*)?$/; + return re.test(urlInput.value); +} + +urlInput.addEventListener('input', () => { + if (urlInput.value === '' || isUrlInputValid()) { + generateButton.classList.add('enabled'); + generateButton.classList.remove('disabled'); + } else { + generateButton.classList.remove('enabled'); + generateButton.classList.add('disabled'); + } +}); + +function clearChildren(el) { + while (el.firstChild) { + el.firstChild.remove(); + } +} + +function showError(msg) { + clearChildren(errorElement); + errorElement.appendChild(document.createTextNode(msg)); +} + +async function submitGenerate(evt) { + try { + if (!isUrlInputValid()) { + return; + } + + const url = urlInput.value; + const id = url.match(/[0-9]+/)[0]; + const result = await fetch('https://projects.scratch.mit.edu/' + id); + + let projectObj; + try { + projectObj = await result.json(); + } catch (error) { /* Scratch 1.4. */ } + if (!(projectObj && projectObj.meta && projectObj.meta.semver && projectObj.meta.semver.startsWith('3.'))) { + showError(`That project hasn't been saved in Scratch 3.0, so it won't work here.\nTry remixing it! That will make an up-to-date version.`); + return; + } + + let assetsDownloaded = 0; + const totalAssets = projectObj.targets.map(t => t.costumes.length + t.sounds.length).reduce((a, b) => a + b, 0); + const project = await Project.fromSb3JSON(projectObj, { + getAsset: async asset => { + const body = await fetch(assetURL(asset)); + const blob = await body.blob(); + assetsDownloaded++; + downloadIndicator.style.width = `${Math.round(assetsDownloaded / totalAssets * 100)}%`; + return blob; + } + }); + downloadIndicator.style.opacity = 0; + presentProject(project); + } catch (error) { + console.error(error); + showError(`Sorry, there was an error loading that project.`); + } +} + +async function submitFile() { + try { + if (!fileInput.files.length) { + return; + } + + const file = fileInput.files[0]; + const project = await Project.fromSb3(file); + presentProject(project); + } catch (error) { + console.error(error); + showError(`Sorry, there was an error loading that project.`); + } +} + +document.getElementById('form').addEventListener('submit', evt => { + evt.preventDefault(); + submitGenerate(); +}); + +function spaceEnterToClick(a) { + a.addEventListener('keydown', evt => { + if (evt.keyCode === 13 || evt.keyCode === 32) { + evt.target.click(); + } + }); +} + +for (const a of document.querySelectorAll('a.button')) { + spaceEnterToClick(a); +} + +document.getElementById('generate-button').addEventListener('click', submitGenerate); +fileInput.addEventListener('input', submitFile); + +if (location.hash.length > 1) { + urlInput.value = decodeURIComponent(location.hash.slice(1)); + submitGenerate(); +} + +// present step + +const targetList = document.getElementById('target-list'); +const scriptArea = document.getElementById('script-area'); + +let project; +let selectedTarget; +const sym = Symbol(); + +function presentTarget(targetName) { + if (!project) { + return; + } + + const target = [project.stage, ...project.sprites].find(t => t.name === targetName); + if (!target) { + return; + } + + if (target === selectedTarget) { + return; + } + + if (selectedTarget) { + if (!selectedTarget[sym]) { + selectedTarget[sym] = {}; + } + Object.assign(selectedTarget[sym], { + scrollLeft: scriptArea.scrollLeft, + scrollTop: scriptArea.scrollTop + }); + } + + selectedTarget = target; + + if (target[sym]) { + Object.assign(scriptArea, { + scrollLeft: target[sym].scrollLeft, + scrollTop: target[sym].scrollTop + }); + } else { + Object.assign(scriptArea, { + scrollLeft: 0, + scrollTop: 0 + }); + } + + for (const el of document.querySelectorAll('.target.selected')) { + el.classList.remove('selected'); + } + document.querySelector(`.target[data-name="${targetName.replace(/"/g, '\\"')}"]`).classList.add('selected'); + + clearChildren(scriptArea); + if (target.scripts.length) { + const offsetX = -target.scripts.reduce((least, s) => Math.min(least, s.x), target.scripts[0].x) + 240; + const offsetY = -target.scripts.reduce((least, s) => Math.min(least, s.y), target.scripts[0].y); + for (const script of target.scripts) { + const sb = scriptToScratchblocks(script, target); + const pre = document.createElement('pre'); + pre.classList.add('blocks'); + pre.appendChild(document.createTextNode(sb)); + pre.style.position = 'absolute'; + pre.style.left = (script.x + offsetX) + 'px'; + pre.style.top = (script.y + offsetY) + 'px'; + scriptArea.appendChild(pre); + } + } + + window.scratchblocks.renderMatching('pre.blocks', { + style: 'scratch3' + }); +} + +function presentProject(p) { + project = p; + + clearChildren(targetList); + for (const target of [project.stage, ...project.sprites]) { + const el = document.createElement('a'); + el.setAttribute('tabindex', 0); + el.classList.add('target'); + const img = document.createElement('img'); + img.src = assetURL(target.costumes[target.costumeNumber]); + el.appendChild(img); + const span = document.createElement('span'); + span.appendChild(document.createTextNode(target.name)); + el.appendChild(span); + el.dataset.name = target.name; + targetList.appendChild(el); + spaceEnterToClick(el); + el.addEventListener('click', () => presentTarget(target.name)); + } + + if (location.hash.includes(':')) { + const targetName = decodeURIComponent(location.hash.slice(location.hash.indexOf(':') + 1)); + presentTarget(targetName); + } +} |