newWebsite2.0
This commit is contained in:
617
ext/scene-manager.js
Normal file
617
ext/scene-manager.js
Normal file
@@ -0,0 +1,617 @@
|
||||
(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 = '<div class="sm-tree-empty">No .sb3 files found in this folder.</div>';
|
||||
} else {
|
||||
for (const [dir, items] of Object.entries(grouped)) {
|
||||
if (dir) treeHtml += `<div class="sm-folder-row">📁 ${this._esc(dir)}/</div>`;
|
||||
for (const item of items) {
|
||||
const name = item.path.split('/').pop().replace(/\.sb3$/i, '');
|
||||
treeHtml += `<div class="sm-file-row" data-path="${this._esc(item.path)}">📄 ${this._esc(name)}.sb3</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
box.innerHTML = `
|
||||
<h2>Browse Projects</h2>
|
||||
<p>Root folder: <code>${this._esc(rootDirectoryHandle.name)}/</code></p>
|
||||
<div class="sm-tree" id="sm-tree">${treeHtml}</div>
|
||||
<p id="sm-sel-label" style="font-size:12.5px;color:#585b70;min-height:18px;margin-bottom:0"></p>
|
||||
<div class="sm-row">
|
||||
<button class="sm-btn sm-btn-ghost" id="sm-cancel-btn">Cancel</button>
|
||||
<button class="sm-btn sm-btn-primary" id="sm-switch-btn" disabled>Switch →</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const tree = box.querySelector('#sm-tree');
|
||||
const switchBtn = box.querySelector('#sm-switch-btn');
|
||||
const selLabel = box.querySelector('#sm-sel-label');
|
||||
|
||||
tree.addEventListener('click', e => {
|
||||
const row = e.target.closest('.sm-file-row');
|
||||
if (!row) return;
|
||||
tree.querySelectorAll('.sm-sel').forEach(el => el.classList.remove('sm-sel'));
|
||||
row.classList.add('sm-sel');
|
||||
selectedPath = row.dataset.path;
|
||||
selLabel.textContent = `Selected: ${selectedPath}`;
|
||||
selLabel.style.color = '#89b4fa';
|
||||
switchBtn.disabled = false;
|
||||
});
|
||||
|
||||
box.querySelector('#sm-cancel-btn').onclick = () => close(null);
|
||||
|
||||
switchBtn.onclick = () => {
|
||||
if (!selectedPath) return;
|
||||
close(selectedPath);
|
||||
};
|
||||
|
||||
}).then(async path => {
|
||||
if (!path) return;
|
||||
await this._loadScene(path, '', /* saveFirst */ true);
|
||||
});
|
||||
}
|
||||
|
||||
async showHelp() {
|
||||
await openModal((box, close) => {
|
||||
box.innerHTML = `
|
||||
<h2>How the Scene Manager works</h2>
|
||||
|
||||
<p>
|
||||
<strong style="color:#cdd6f4">Scene Manager</strong> lets you split your Scratch project
|
||||
into multiple <code>.sb3</code> files and seamlessly switch between them at runtime —
|
||||
just like loading a new level or screen.
|
||||
</p>
|
||||
|
||||
<div class="sm-note">
|
||||
<strong>Scratch Everywhere</strong><br>
|
||||
All <code>.sb3</code> files directly inside the scratch-everywhere folder appear in the SE! menu.<br><br>
|
||||
<strong>Set Root Folder</strong> tells Scene Manager which folder acts as that root folder while
|
||||
you're developing in Scratch/Turbowarp/ScratchBox/Mistwarp or any other SE! compatible editor.
|
||||
</div>
|
||||
|
||||
<hr class="sm-sep">
|
||||
<p><strong style="color:#cdd6f4">Recommended project structure</strong></p>
|
||||
<div class="sm-path">📁 scratch-everywhere/
|
||||
MyGame.sb3 ← launcher (appears in menu)
|
||||
📁 MyGameName-Scenes/
|
||||
Level1.sb3
|
||||
Level2.sb3
|
||||
BossLevel.sb3</div>
|
||||
|
||||
<p>
|
||||
Only <code>MyGame.sb3</code> is visible in the game-selection menu.
|
||||
It uses Scene Manager blocks to load the actual levels from the
|
||||
<code>scenes/</code> subfolder, those files stay private inside the SE! menu.
|
||||
</p>
|
||||
|
||||
<div class="sm-note">
|
||||
Use <strong>subfolder-prefixed paths</strong> in the scene blocks:<br>
|
||||
<code>open scenes/Level1.sb3</code><br>
|
||||
Scene Manager navigates into the subfolder automatically.
|
||||
</div>
|
||||
|
||||
<div class="sm-row">
|
||||
<button class="sm-btn sm-btn-primary" id="sm-ok-btn">Got it!</button>
|
||||
</div>
|
||||
`;
|
||||
box.querySelector('#sm-ok-btn').onclick = () => close(null);
|
||||
});
|
||||
}
|
||||
|
||||
async openSB3(args) {
|
||||
await this._loadScene(args.arg0, '');
|
||||
}
|
||||
|
||||
async openSB3withData(args) {
|
||||
await this._loadScene(args.arg0, args.arg1);
|
||||
}
|
||||
|
||||
receivedData() {
|
||||
return sessionStorage.getItem('scenemanager_data') || '';
|
||||
}
|
||||
|
||||
/** Opens a directory picker. Returns null if cancelled. */
|
||||
async _pickDirectory() {
|
||||
try {
|
||||
return await window.showDirectoryPicker({ id: 'scenemanager-root', mode: 'readwrite', startIn: 'documents' });
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') console.error('[SceneManager] Directory picker failed:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the "folder has been set" confirmation modal. */
|
||||
_showFolderSetModal(name) {
|
||||
openModal((box, close) => {
|
||||
box.innerHTML = `
|
||||
<h2>Root folder set</h2>
|
||||
<p>Scene Manager will now load projects from:</p>
|
||||
<div class="sm-path">📁 ${this._esc(name)}/</div>
|
||||
<div class="sm-note">
|
||||
In <strong>Scratch Everywhere</strong> this corresponds to the
|
||||
<code>scratch-everywhere/</code> 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.
|
||||
</div>
|
||||
<div class="sm-row">
|
||||
<button class="sm-btn sm-btn-primary" id="sm-ok-btn">Got it!</button>
|
||||
</div>
|
||||
`;
|
||||
box.querySelector('#sm-ok-btn').onclick = () => close(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If no root folder is set, prompts the user to select one.
|
||||
* Returns true if a folder is now available, false if cancelled.
|
||||
*/
|
||||
async _requireFolder(action) {
|
||||
return openModal((box, close) => {
|
||||
box.innerHTML = `
|
||||
<h2>Root folder required</h2>
|
||||
<p>
|
||||
<strong style="color:#cdd6f4">${this._esc(action)}</strong> needs to know
|
||||
where your <code>.sb3</code> files are stored.
|
||||
</p>
|
||||
<div class="sm-note">
|
||||
Select your <code>scratch-everywhere/</code> folder (or any folder containing your projects).
|
||||
After selecting, the action will continue automatically.
|
||||
</div>
|
||||
<div class="sm-row">
|
||||
<button class="sm-btn sm-btn-ghost" id="sm-cancel-btn">Cancel</button>
|
||||
<button class="sm-btn sm-btn-primary" id="sm-select-btn">Select folder…</button>
|
||||
</div>
|
||||
`;
|
||||
box.querySelector('#sm-cancel-btn').onclick = () => close(false);
|
||||
box.querySelector('#sm-select-btn').onclick = async () => {
|
||||
const handle = await this._pickDirectory();
|
||||
if (handle) { rootDirectoryHandle = handle; currentFileHandle = null; close(true); }
|
||||
else close(false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Recursively scans a directory for .sb3 files. Returns [{path, handle}]. */
|
||||
async _scanSb3(dirHandle, prefix = '') {
|
||||
const results = [];
|
||||
for await (const [name, handle] of dirHandle.entries()) {
|
||||
const rel = prefix ? `${prefix}/${name}` : name;
|
||||
if (handle.kind === 'file' && name.toLowerCase().endsWith('.sb3')) {
|
||||
results.push({ path: rel, handle });
|
||||
} else if (handle.kind === 'directory') {
|
||||
results.push(...await this._scanSb3(handle, rel));
|
||||
}
|
||||
}
|
||||
return results.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a relative path (e.g. "scenes/Level2" or "scenes/Level2.sb3")
|
||||
* to a FileSystemFileHandle inside rootDirectoryHandle.
|
||||
*/
|
||||
async _getFileHandle(relativePath) {
|
||||
if (!rootDirectoryHandle) {
|
||||
const ok = await this._requireFolder('A scene block tried to load a file');
|
||||
if (!ok) return null;
|
||||
}
|
||||
// Normalise: strip trailing .sb3, then re-add
|
||||
const clean = relativePath.replace(/\.sb3$/i, '');
|
||||
const parts = clean.split('/').filter(Boolean);
|
||||
const fileName = parts[parts.length - 1] + '.sb3';
|
||||
let handle = rootDirectoryHandle;
|
||||
try {
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
handle = await handle.getDirectoryHandle(parts[i]);
|
||||
}
|
||||
return await handle.getFileHandle(fileName);
|
||||
} catch {
|
||||
await this._showError(`File not found: ${clean}.sb3`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Core loading routine used by both the scene blocks and the project browser. */
|
||||
async _loadScene(filePath, dataToPass, fromBrowser = false) {
|
||||
const display = filePath.replace(/\.sb3$/i, '') + '.sb3';
|
||||
|
||||
// Confirm
|
||||
const confirmed = await openModal((box, close) => {
|
||||
const saveRow = currentFileHandle
|
||||
? `<button class="sm-btn sm-btn-primary" id="sm-save-btn">Save current & switch</button>`
|
||||
: `<button class="sm-btn sm-btn-primary" id="sm-switch-btn">Switch</button>`;
|
||||
|
||||
box.innerHTML = `
|
||||
<h2>Switch scene</h2>
|
||||
<p>Load <code>${this._esc(display)}</code>?</p>
|
||||
<div class="sm-note">
|
||||
Unsaved changes in the current project will be lost.
|
||||
${currentFileHandle ? 'You can save first using the button below.' : ''}
|
||||
</div>
|
||||
<div class="sm-row">
|
||||
<button class="sm-btn sm-btn-ghost" id="sm-cancel-btn">Cancel</button>
|
||||
${saveRow}
|
||||
${currentFileHandle ? `<button class="sm-btn sm-btn-warn" id="sm-switch-btn">Switch without saving</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
box.querySelector('#sm-cancel-btn').onclick = () => close('cancel');
|
||||
|
||||
const switchBtn = box.querySelector('#sm-switch-btn');
|
||||
if (switchBtn) switchBtn.onclick = () => close('switch');
|
||||
|
||||
const saveBtn = box.querySelector('#sm-save-btn');
|
||||
if (saveBtn) saveBtn.onclick = () => close('save-and-switch');
|
||||
});
|
||||
|
||||
if (!confirmed || confirmed === 'cancel') return;
|
||||
|
||||
// Optionally save current project back to its file
|
||||
if (confirmed === 'save-and-switch' && currentFileHandle) {
|
||||
try {
|
||||
const blob = await Scratch.vm.saveProjectSb3();
|
||||
const writable = await currentFileHandle.createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
showToast(`Saved: ${currentFileHandle.name}`, 'ok');
|
||||
} catch (e) {
|
||||
console.error('[SceneManager] Save failed:', e);
|
||||
showToast('Save failed — switching anyway.', 'err');
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve file
|
||||
const fh = await this._getFileHandle(filePath);
|
||||
if (!fh) return;
|
||||
|
||||
// Load
|
||||
try {
|
||||
const file = await fh.getFile();
|
||||
const buf = await file.arrayBuffer();
|
||||
sessionStorage.setItem('scenemanager_data', dataToPass);
|
||||
currentFileHandle = fh;
|
||||
await Scratch.vm.loadProject(buf);
|
||||
Scratch.vm.greenFlag();
|
||||
} catch (e) {
|
||||
console.error('[SceneManager] Load failed:', e);
|
||||
await this._showError('Could not load the project. Is it a valid .sb3 file?');
|
||||
}
|
||||
}
|
||||
|
||||
async _showError(msg) {
|
||||
await openModal((box, close) => {
|
||||
box.innerHTML = `
|
||||
<h2>Error</h2>
|
||||
<p>${this._esc(msg)}</p>
|
||||
<div class="sm-row">
|
||||
<button class="sm-btn sm-btn-primary" id="sm-ok-btn">OK</button>
|
||||
</div>
|
||||
`;
|
||||
box.querySelector('#sm-ok-btn').onclick = () => close(null);
|
||||
});
|
||||
}
|
||||
|
||||
_esc(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
}
|
||||
|
||||
Scratch.extensions.register(new SceneManager());
|
||||
|
||||
})(Scratch);
|
||||
Reference in New Issue
Block a user