(function (Scratch) { 'use strict'; if (!Scratch.extensions.unsandboxed) { throw new Error('SceneManager must run unsandboxed!'); } let rootDirectoryHandle = null; let currentFileHandle = null; const styleEl = document.createElement('style'); styleEl.textContent = ` /* ── Overlay & Box ───────────────────────────────────────────── */ .sm-ov { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 99999; animation: smFade .14s ease; } @keyframes smFade { from { opacity:0 } to { opacity:1 } } .sm-box { background: #1e1e2e; border: 1px solid #383860; border-radius: 14px; padding: 26px 28px 22px; max-width: 540px; width: calc(100% - 40px); color: #cdd6f4; box-shadow: 0 28px 70px rgba(0,0,0,.55); animation: smSlide .18s ease; max-height: 90vh; overflow-y: auto; } @keyframes smSlide { from { transform:translateY(-10px);opacity:0 } to { transform:none;opacity:1 } } /* ── Typography ──────────────────────────────────────────────── */ .sm-box h2 { margin: 0 0 14px; font: 600 17px/1.3 system-ui, sans-serif; color: #89b4fa; display: flex; align-items: center; gap: 8px; } .sm-box p { margin: 0 0 12px; font: 400 13.5px/1.65 system-ui, sans-serif; color: #bac2de; } .sm-box p:last-child { margin-bottom: 0; } .sm-box code { background: #313244; border-radius: 4px; padding: 1px 5px; font-size: 12px; font-family: monospace; color: #a6e3a1; } /* ── Note / Info block ───────────────────────────────────────── */ .sm-note { background: #252538; border-left: 3px solid #89b4fa; border-radius: 0 8px 8px 0; padding: 10px 14px; font: 400 13px/1.6 system-ui, sans-serif; color: #bac2de; margin: 12px 0; } .sm-note strong { color: #cdd6f4; font-weight: 600; } /* ── Path display ────────────────────────────────────────────── */ .sm-path { background: #181825; border: 1px solid #45475a; border-radius: 7px; padding: 9px 13px; font: 400 12.5px/1.5 monospace; color: #a6e3a1; margin: 12px 0; word-break: break-all; white-space: pre-wrap; } /* ── Buttons ─────────────────────────────────────────────────── */ .sm-row { display: flex; gap: 10px; justify-content: flex-end; margin-top: 22px; flex-wrap: wrap; } .sm-btn { padding: 8px 20px; border-radius: 7px; border: none; cursor: pointer; font: 600 13px system-ui, sans-serif; transition: filter .12s, transform .1s; white-space: nowrap; } .sm-btn:hover { filter: brightness(1.13); } .sm-btn:active { transform: scale(.97); } .sm-btn-primary { background: #89b4fa; color: #1e1e2e; } .sm-btn-ghost { background: #313244; color: #cdd6f4; } .sm-btn-warn { background: #f38ba8; color: #1e1e2e; } .sm-btn:disabled { opacity: .4; cursor: default; filter: none; } /* ── Project tree ────────────────────────────────────────────── */ .sm-tree { background: #181825; border: 1px solid #45475a; border-radius: 8px; max-height: 300px; overflow-y: auto; margin: 12px 0; } .sm-tree-empty { padding: 28px 16px; text-align: center; color: #585b70; font: italic 13px system-ui, sans-serif; } .sm-folder-row { padding: 7px 14px; background: #1e1e2e; border-bottom: 1px solid #2a2a42; font: 600 12.5px system-ui, sans-serif; color: #fab387; display: flex; align-items: center; gap: 7px; user-select: none; } .sm-file-row { padding: 8px 14px 8px 30px; border-bottom: 1px solid #252538; font: 400 13px system-ui, sans-serif; color: #cdd6f4; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: background .1s; } .sm-file-row:last-child { border-bottom: none; } .sm-file-row:hover { background: #313244; } .sm-file-row.sm-sel { background: #3a3a5c; color: #89b4fa; } /* ── Toast ───────────────────────────────────────────────────── */ .sm-toast { position: fixed; bottom: 24px; right: 24px; background: #313244; border: 1px solid #45475a; border-radius: 8px; padding: 10px 16px; font: 400 13px system-ui, sans-serif; color: #cdd6f4; z-index: 100000; opacity: 0; transform: translateY(8px); transition: opacity .22s ease, transform .22s ease; max-width: 320px; box-shadow: 0 8px 24px rgba(0,0,0,.4); } .sm-toast.sm-toast-in { opacity: 1; transform: none; } .sm-toast.sm-ok { border-left: 3px solid #a6e3a1; } .sm-toast.sm-err { border-left: 3px solid #f38ba8; } /* ── Divider ─────────────────────────────────────────────────── */ .sm-sep { border: none; border-top: 1px solid #313244; margin: 14px 0; } /* ── Loading spinner ─────────────────────────────────────────── */ .sm-spin { display: inline-block; width: 14px; height: 14px; border: 2px solid #45475a; border-top-color: #89b4fa; border-radius: 50%; animation: smSpin .7s linear infinite; } @keyframes smSpin { to { transform: rotate(360deg); } } `; document.head.appendChild(styleEl); /** Opens a modal. `buildFn(box, close)` populates .sm-box and calls close(value) to resolve. */ function openModal(buildFn) { return new Promise(resolve => { const overlay = document.createElement('div'); overlay.className = 'sm-ov'; const box = document.createElement('div'); box.className = 'sm-box'; overlay.appendChild(box); document.body.appendChild(overlay); let closed = false; const close = val => { if (closed) return; closed = true; document.removeEventListener('keydown', onKey); overlay.remove(); resolve(val); }; const onKey = e => { if (e.key === 'Escape') close(null); }; document.addEventListener('keydown', onKey); overlay.addEventListener('click', e => { if (e.target === overlay) close(null); }); buildFn(box, close); }); } function showToast(msg, type = 'ok') { const t = document.createElement('div'); t.className = `sm-toast sm-${type}`; t.textContent = msg; document.body.appendChild(t); requestAnimationFrame(() => requestAnimationFrame(() => t.classList.add('sm-toast-in'))); setTimeout(() => { t.classList.remove('sm-toast-in'); t.addEventListener('transitionend', () => t.remove(), { once: true }); }, 2800); } class SceneManager { getInfo() { return { id: 'sceneManager', name: 'Scene Manager', color1: '#4A90E2', color2: '#357ABD', color3: '#285A8F', blocks: [ // ── Folder management ── { blockType: Scratch.BlockType.LABEL, text: 'Root Folder' }, { opcode: 'selectFolder', blockType: Scratch.BlockType.BUTTON, text: 'Set Root Folder', func: 'selectFolder' }, { opcode: 'browseProjects', blockType: Scratch.BlockType.BUTTON, text: 'Browse & Switch Project', func: 'browseProjects' }, { opcode: 'showHelp', blockType: Scratch.BlockType.BUTTON, text: 'How does this work?', func: 'showHelp' }, // ── Scene blocks ── { blockType: Scratch.BlockType.LABEL, text: 'Scene Blocks' }, { opcode: 'openSB3', blockType: Scratch.BlockType.COMMAND, text: 'open [arg0].sb3', arguments: { arg0: { type: Scratch.ArgumentType.STRING, defaultValue: 'MyGame/Level2' } } }, { opcode: 'openSB3withData', blockType: Scratch.BlockType.COMMAND, text: 'open [arg0].sb3 with data [arg1]', arguments: { arg0: { type: Scratch.ArgumentType.STRING, defaultValue: 'MyGame/Level2' }, arg1: { type: Scratch.ArgumentType.STRING, defaultValue: 'score=100' } } }, { opcode: 'receivedData', blockType: Scratch.BlockType.REPORTER, text: 'received data' }, { blockType: Scratch.BlockType.LABEL, text: 'SE Support: Coming in v1.0' } ] }; } async selectFolder() { const handle = await this._pickDirectory(); if (!handle) return; rootDirectoryHandle = handle; currentFileHandle = null; this._showFolderSetModal(handle.name); } async browseProjects() { if (!rootDirectoryHandle) { const ok = await this._requireFolder('Open the project browser'); if (!ok) return; } const files = await this._scanSb3(rootDirectoryHandle); await openModal((box, close) => { let selectedPath = null; // Build tree HTML const grouped = {}; for (const f of files) { const parts = f.path.split('/'); const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : ''; (grouped[dir] = grouped[dir] || []).push(f); } let treeHtml = ''; if (files.length === 0) { treeHtml = '
Root folder: ${this._esc(rootDirectoryHandle.name)}/
Scene Manager lets you split your Scratch project
into multiple .sb3 files and seamlessly switch between them at runtime —
just like loading a new level or screen.
.sb3 files directly inside the scratch-everywhere folder appear in the SE! menu.Recommended project structure
Only MyGame.sb3 is visible in the game-selection menu.
It uses Scene Manager blocks to load the actual levels from the
scenes/ subfolder, those files stay private inside the SE! menu.
open scenes/Level1.sb3Scene Manager will now load projects from:
scratch-everywhere/ folder, where all your .sb3 files are located.
Put your launcher project in the root and keep scene files in a subfolder.
You theoretically don't have to put all your scenes inside a subfolder, but it is recommended.
${this._esc(action)} needs to know
where your .sb3 files are stored.
scratch-everywhere/ folder (or any folder containing your projects).
After selecting, the action will continue automatically.
Load ${this._esc(display)}?
${this._esc(msg)}