Scratch JSON Editor

JSONエディター画面を作る

// ==UserScript==
// @name         Scratch JSON Editor
// @namespace    https://scratch.mit.edu/
// @version      2025-09-09
// @description  JSONエディター画面を作る
// @author       You
// @match        https://scratch.mit.edu/projects/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

(async () => {
    'use strict';
    /** @param {() => unknown} listener */
    const onLoaded = listener => {
        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", listener);
        } else {
            listener();
        }
    };
    if (/^\/projects\/\d+\/json\/?/.test(location.pathname)) {
        const id = Number(location.pathname.match(/^\/projects\/(\d+)\/json\/?/)[1]);
        onLoaded(() => {
            /** @type {HTMLDivElement} */
            const boxContent = document.querySelector('.box-content');
            boxContent.innerHTML = 'Loading...';
        });
        const xtoken = await fetch('/session', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
            .then(r => r.json())
            .then(r => r.user?.token);
        const { title, project_token: ptoken }
            = await fetch(`https://api.scratch.mit.edu/projects/${id}`, { headers: { 'X-Token': xtoken } })
                .then(r => r.json());
        onLoaded(() => {
            /** @type {HTMLDivElement} */
            const boxHead = document.querySelector('.box-head');
            boxHead.style.paddingTop = '7px';
            boxHead.innerHTML = `<h2><a href="/projects/${id}/"></a> » JSON Editor</h2>`;
            boxHead.querySelector('a').innerText = title;
        });
        const json = await fetch(`https://projects.scratch.mit.edu/${id}?token=${ptoken}`)
            .then(r => r.text());
        onLoaded(() => {
            /** @type {HTMLDivElement} */
            const boxContent = document.querySelector('.box-content');
            boxContent.innerHTML = '';
            const textarea = document.createElement('textarea');
            const button = document.createElement('button');
            const status = document.createElement('span');
            button.innerText = '保存';
            textarea.value = json;
            Object.assign(textarea.style, {
                margin: '0 auto',
                boxSizing: 'border-box',
                width: 'calc(100% - 40px)',
                height: '30em',
                resize: 'vertical',
                fontFamily: 'monospace',
            });
            Object.assign(button.style, {
                margin: '20px',
            });
            Object.assign(status.style, {
                display: 'inline-block',
                width: '10em',
            });
            boxContent.append(textarea, button, status);
            button.addEventListener('click', () => {
                status.innerText = '';
                fetch(`https://projects.scratch.mit.edu/${id}`, {
                    method: 'PUT',
                    body: textarea.value,
                    credentials: 'include',
                    headers: { 'Content-Type': 'application/json' },
                }).then(r => {
                    status.innerText = String(r.status);
                }).catch(() => {
                    status.innerText = 'Error';
                });
            });
        });
    }
})();