« get me outta code hell

scratchblocks-generator-3 - scratchblocks generator for projects made in 3.0
about summary refs log tree commit diff
path: root/src/js/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/main.js')
-rw-r--r--src/js/main.js222
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);
+    }
+}