From 140c171742ec4888738341f09cbaaf5e1ffea9a0 Mon Sep 17 00:00:00 2001 From: rgantzos <86856959+rgantzos@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:38:57 -0800 Subject: [PATCH] Fix issues with editor features --- api/content/redux.js | 78 +++++++++++++++++++++++ api/content/vm.js | 32 ++++++++++ api/feature.js | 4 +- api/vm.js | 79 +++++++++++------------- features/dark-paint-editor/script.js | 4 +- features/more-paint-functions/script.js | 48 +++++++------- features/opacity-slider/script.js | 4 +- features/outline-shape-options/script.js | 12 ++-- features/paint-align/script.js | 8 ++- features/rotate-gradient/script.js | 24 +++---- features/watch-later/script.js | 2 +- manifest.json | 8 +++ 12 files changed, 209 insertions(+), 94 deletions(-) create mode 100644 api/content/redux.js create mode 100644 api/content/vm.js diff --git a/api/content/redux.js b/api/content/redux.js new file mode 100644 index 00000000..aea24df6 --- /dev/null +++ b/api/content/redux.js @@ -0,0 +1,78 @@ +// Thank you to WorldLanguages, ErrorGamer2000, and apple502j + +function injectRedux() { + window.__steRedux = {}; + + class ReDucks { + static compose(...composeArgs) { + if (composeArgs.length === 0) return (...args) => args; + return (...args) => { + const composeArgsReverse = composeArgs.slice(0).reverse(); + let result = composeArgsReverse.shift()(...args); + for (const fn of composeArgsReverse) { + result = fn(result); + } + return result; + }; + } + + static applyMiddleware(...middlewares) { + return (createStore) => + (...createStoreArgs) => { + const store = createStore(...createStoreArgs); + let { dispatch } = store; + const api = { + getState: store.getState, + dispatch: (action) => dispatch(action), + }; + const initialized = middlewares.map((middleware) => middleware(api)); + dispatch = ReDucks.compose(...initialized)(store.dispatch); + return Object.assign({}, store, { dispatch }); + }; + } + } + + let newerCompose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; + function compose(...args) { + const steRedux = window.__steRedux; + const reduxTarget = (steRedux.target = new EventTarget()); + steRedux.state = {}; + steRedux.dispatch = () => {}; + + function middleware({ getState, dispatch }) { + steRedux.dispatch = dispatch; + steRedux.state = getState(); + return (next) => (action) => { + const nextReturn = next(action); + const ev = new CustomEvent("statechanged", { + detail: { + prev: steRedux.state, + next: (steRedux.state = getState()), + action, + }, + }); + reduxTarget.dispatchEvent(ev); + return nextReturn; + }; + } + args.splice(1, 0, ReDucks.applyMiddleware(middleware)); + return newerCompose + ? newerCompose.apply(this, args) + : ReDucks.compose.apply(this, args); + } + + try { + Object.defineProperty(window, "__REDUX_DEVTOOLS_EXTENSION_COMPOSE__", { + get: () => compose, + set: (v) => { + newerCompose = v; + }, + }); + } catch (err) { + window.__steRedux = __scratchAddonsRedux; + } +} + +if (!(document.documentElement instanceof SVGElement)) { + immediatelyRunFunctionInMainWorld(injectRedux); +} diff --git a/api/content/vm.js b/api/content/vm.js new file mode 100644 index 00000000..d7975bd9 --- /dev/null +++ b/api/content/vm.js @@ -0,0 +1,32 @@ +// Thank you to mxmou, WorldLanguages, ErrorGamer2000, apple502j, TheColaber, and towerofnix + +function immediatelyRunFunctionInMainWorld(fn) { + if (typeof fn !== "function") throw "Expected function"; + const div = document.createElement("div"); + div.setAttribute("onclick", "(" + fn + ")()"); + document.documentElement.appendChild(div); + div.click(); + div.remove(); +} + +immediatelyRunFunctionInMainWorld(() => { + const oldBind = Function.prototype.bind; + window.__steTraps = new EventTarget() + const onceMap = (__steTraps._onceMap = Object.create(null)); + + Function.prototype.bind = function (...args) { + if (Function.prototype.bind === oldBind) { + return oldBind.apply(this, args); + } else if ( + args[0] && + Object.prototype.hasOwnProperty.call(args[0], "editingTarget") && + Object.prototype.hasOwnProperty.call(args[0], "runtime") + ) { + onceMap.vm = args[0]; + Function.prototype.bind = oldBind; + return oldBind.apply(this, args); + } else { + return oldBind.apply(this, args); + } + }; +}); diff --git a/api/feature.js b/api/feature.js index b3c72521..5f98785c 100644 --- a/api/feature.js +++ b/api/feature.js @@ -93,9 +93,7 @@ class Feature { this.getInternalKey = function(element) { return Object.keys(element).find((key) => key.startsWith("__reactInternalInstance")) || null } - this.redux = document.querySelector("#app")?.[ - Object.keys(app).find((key) => key.startsWith("__reactContainer")) - ].child.stateNode.store + this.redux = window.__steRedux if (finalFeature.version !== 2) { console.warn( `'${finalFeature.file}' does not use Feature v2. It is recommended that you use the newest version.` diff --git a/api/vm.js b/api/vm.js index b5838473..bd087af7 100644 --- a/api/vm.js +++ b/api/vm.js @@ -3,14 +3,7 @@ ScratchTools.Scratch = { blockly: null, }; try { - ScratchTools.Scratch.vm = - window.vm || - (() => { - const app = document.querySelector("#app"); - return app[ - Object.keys(app).find((key) => key.startsWith("__reactContainer")) - ].child.stateNode.store.getState().scratchGui.vm; - })(); + ScratchTools.Scratch.vm = window.vm || window.__steTraps._onceMap.vm; ste.console.log("Able to load Virtual Machine.", "ste-traps"); } catch (err) { ste.console.warn("Unable to load Virtual Machine.", "ste-traps"); @@ -28,11 +21,16 @@ try { ScratchTools.Scratch.scratchSound = function () { try { - return document.querySelector("div.sound-editor_editor-container_iUSW-")[ + let rI = document.querySelector("[class^=sound-editor_editor-container]")[ Object.keys( - document.querySelector("div.sound-editor_editor-container_iUSW-") - ).find((key) => key.startsWith("__reactInternalInstance")) - ].return.return.return.stateNode; + document.querySelector("[class^=sound-editor_editor-container]") + ).find((key) => key.startsWith("__reactFiber")) + ]; + + while (!rI.stateNode?.audioBufferPlayer) { + rI = rI.return; + } + return rI.stateNode; } catch (err) { return null; } @@ -40,10 +38,7 @@ ScratchTools.Scratch.scratchSound = function () { ScratchTools.Scratch.scratchGui = function () { try { - const app = document.querySelector("#app"); - return app[ - Object.keys(app).find((key) => key.startsWith("__reactContainer")) - ].child.stateNode.store.getState().scratchGui; + return window.__steRedux.state.scratchGui; } catch (err) { return null; } @@ -176,46 +171,42 @@ ScratchTools.Scratch.waitForContextMenu = function (info) { }; ScratchTools.Scratch.scratchPaint = function () { - var app = document.querySelector(".paint-editor_mode-selector_28iiQ")||document.querySelector(".paint-editor_mode-selector_O2uhP")||document.querySelector("[class*='paint-editor_mode-selector_']"); - if (app !== null) { - return ( - app[ - Object.keys(app).find((key) => - key.startsWith("__reactInternalInstance") - ) - ].child.stateNode.store?.getState()?.scratchPaint || null - ); - } else { + try { + return __steRedux.state.scratchPaint; + } catch (err) { return null; } }; -ScratchTools.Scratch.getPaper = function () { - let paintElement = document.querySelector( - "[class*='paint-editor_mode-selector']" - ); - let paintState = - paintElement[ - Object.keys(paintElement).find((key) => - key.startsWith("__reactInternalInstance") - ) - ].child; +window.__paperCache = null + +async function getPaper() { + const modeSelector = document.querySelector("[class*='paint-editor_mode-selector']"); + const internalState = modeSelector[Object.keys(modeSelector).find((el) => el.startsWith("__reactFiber"))].child; + let toolState = internalState; let tool; - while (paintState) { - let paintIn = paintState.child?.stateNode; - if (paintIn?.tool) { - tool = paintIn.tool; + while (toolState) { + const toolInstance = toolState.child.child.stateNode; + if (toolInstance.tool) { + tool = toolInstance.tool; break; } - if (paintIn?.blob && paintIn?.blob.tool) { - tool = paintIn.blob.tool; + if (toolInstance.blob && toolInstance.blob.tool) { + tool = toolInstance.blob.tool; break; } - paintState = paintState.sibling; + toolState = toolState.sibling; } if (tool) { - return tool._scope; + const paperScope = tool._scope; + window.__paperCache = paperScope + return paperScope; } + return null +} + +ScratchTools.Scratch.getPaper = async function () { + return await getPaper() }; async function alertForUpdates() { diff --git a/features/dark-paint-editor/script.js b/features/dark-paint-editor/script.js index a466c715..e2e17e99 100644 --- a/features/dark-paint-editor/script.js +++ b/features/dark-paint-editor/script.js @@ -44,8 +44,8 @@ export default async function ({ feature, console, scratchClass }) { } ); - function updateTheme(isDark) { - let paper = feature.traps.getPaper(); + async function updateTheme(isDark) { + let paper = await feature.traps.getPaper(); let backgroundLayer = paper.project.layers.find( (el) => el.data?.["isBackgroundGuideLayer"] diff --git a/features/more-paint-functions/script.js b/features/more-paint-functions/script.js index a753cf89..dd1f3ee4 100644 --- a/features/more-paint-functions/script.js +++ b/features/more-paint-functions/script.js @@ -1,6 +1,6 @@ export default async function ({ feature, console, scratchClass }) { - function unite() { - let paper = feature.traps.getPaper(); + async function unite() { + let paper = await feature.traps.getPaper(); let items = paper.project.selectedItems; if (items.length !== 2) return; @@ -18,8 +18,8 @@ export default async function ({ feature, console, scratchClass }) { paper.tool.onUpdateImage(); } - function subtract() { - let paper = feature.traps.getPaper(); + async function subtract() { + let paper = await feature.traps.getPaper(); let items = paper.project.selectedItems; if (items.length !== 2) return; @@ -37,8 +37,8 @@ export default async function ({ feature, console, scratchClass }) { paper.tool.onUpdateImage(); } - function exclude() { - let paper = feature.traps.getPaper(); + async function exclude() { + let paper = await feature.traps.getPaper(); let items = paper.project.selectedItems; if (items.length !== 2) return; @@ -56,8 +56,8 @@ export default async function ({ feature, console, scratchClass }) { paper.tool.onUpdateImage(); } - function intersect() { - let paper = feature.traps.getPaper(); + async function intersect() { + let paper = await feature.traps.getPaper(); let items = paper.project.selectedItems; if (items.length !== 2) return; @@ -112,23 +112,25 @@ export default async function ({ feature, console, scratchClass }) { } ); - feature.redux.subscribe(function () { - if (document.querySelector(".ste-more-functions")) { - let span = document.querySelector(".ste-more-functions"); - if ( - feature.traps.paint().format === "BITMAP" || - feature.traps.paint().selectedItems?.length < 2 - ) { - document.querySelectorAll(".ste-more-functions").forEach(function (el) { - el.classList.add("button_mod-disabled_1rf31"); - }); - } else { - document.querySelectorAll(".ste-more-functions").forEach(function (el) { - el.classList.remove("button_mod-disabled_1rf31"); - }); + feature.redux.target.addEventListener("statechanged", function(e) { + if (e.detail.action.type.startsWith("scratch-paint/")) { + if (document.querySelector(".ste-more-functions")) { + let span = document.querySelector(".ste-more-functions"); + if ( + feature.traps.paint().format === "BITMAP" || + feature.traps.paint().selectedItems?.length < 2 + ) { + document.querySelectorAll(".ste-more-functions").forEach(function (el) { + el.classList.add("button_mod-disabled_1rf31"); + }); + } else { + document.querySelectorAll(".ste-more-functions").forEach(function (el) { + el.classList.remove("button_mod-disabled_1rf31"); + }); + } } } - }); + }) function makeButton({ name, icon, callback }) { let span = document.createElement("span"); diff --git a/features/opacity-slider/script.js b/features/opacity-slider/script.js index 925109e4..ffaa653d 100644 --- a/features/opacity-slider/script.js +++ b/features/opacity-slider/script.js @@ -58,7 +58,8 @@ export default function ({ feature, console, scratchClass }) { let lastColor; - feature.redux.subscribe(function () { + feature.redux.target.addEventListener("statechanged", function(e) { + if (e.detail.action.type.startsWith("scratch-paint/")) { let slider = document.querySelector(".ste-opacity-background"); let newColor = feature.traps.paint?.()?.selectedItems[0]?.fillColor?._canvasStyle; @@ -66,6 +67,7 @@ export default function ({ feature, console, scratchClass }) { if (newColor === lastColor) return; lastColor = newColor; slider.style.background = `linear-gradient(270deg, ${lastColor} 0%, rgba(0, 0, 0, 0) 100%)`; + } }); function handleSlider(handle, value) { diff --git a/features/outline-shape-options/script.js b/features/outline-shape-options/script.js index f6bcae54..0de58042 100644 --- a/features/outline-shape-options/script.js +++ b/features/outline-shape-options/script.js @@ -4,8 +4,8 @@ export default async function ({ feature, console }) { Join: ["miter", "round", "bevel", "arcs", "miter-clip"], }; - function createSection(type) { - const selectedItems = feature.traps.getPaper().project.selectedItems; + async function createSection(type) { + const selectedItems = (await feature.traps.getPaper()).project.selectedItems; const result = document.createElement("div"); let strokeValue = undefined; @@ -64,13 +64,13 @@ export default async function ({ feature, console }) { ScratchTools.waitForElements( "div[class*='color-picker_swatch-row_']", - function (element) { + async function (element) { if (feature.traps.paint().modals.fillColor) return; - if (feature.traps.getPaper().project.selectedItems.length < 1) return; + if ((await feature.traps.getPaper())?.project.selectedItems.length < 1) return; const dividerLine = document.createElement("div"); dividerLine.classList.add("color-picker_divider_3a3qR"); - const sectionCap = createSection("Cap"); - const sectionJoin = createSection("Join"); + const sectionCap = await createSection("Cap"); + const sectionJoin = await createSection("Join"); element.insertAdjacentElement("afterend", sectionJoin); element.insertAdjacentElement("afterend", sectionCap); diff --git a/features/paint-align/script.js b/features/paint-align/script.js index 4b127091..c3a3fec1 100644 --- a/features/paint-align/script.js +++ b/features/paint-align/script.js @@ -31,7 +31,8 @@ export default async function ({ feature, scratchClass }) { } ); - feature.redux.subscribe(function () { + feature.redux.target.addEventListener("statechanged", function(e) { + if (e.detail.action.type.startsWith("scratch-paint/")) { if (document.querySelector(".ste-align-items")) { let span = document.querySelector(".ste-align-items"); @@ -44,9 +45,10 @@ export default async function ({ feature, scratchClass }) { span.classList.remove("button_mod-disabled_1rf31"); } } + } }); - function centerObjects(stay) { + async function centerObjects(stay) { let items = feature.traps.paint().selectedItems; let allX = []; @@ -83,7 +85,7 @@ export default async function ({ feature, scratchClass }) { } } - feature.traps.getPaper().tool.onUpdateImage(); + (await feature.traps.getPaper()).tool.onUpdateImage(); } function getMidPoint(segments) { diff --git a/features/rotate-gradient/script.js b/features/rotate-gradient/script.js index a26a2ee1..d2d14ba1 100644 --- a/features/rotate-gradient/script.js +++ b/features/rotate-gradient/script.js @@ -53,13 +53,15 @@ export default async function ({ feature, console, scratchClass }) { } ); - feature.redux.subscribe(function() { - if (!document.querySelector("div[class^='paint-editor_editor-container_']")) return; + feature.redux.target.addEventListener("statechanged", function(e) { + if (e.detail.action.type.startsWith("scratch-paint/")) { + if (!document.querySelector("div[class^='paint-editor_editor-container_']")) return; - if (!document.querySelector("div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']") || feature.traps.paint().selectedItems[0]?.fillColor._components[0].radial || feature.traps.paint().selectedItems.length !== 1) { - document.querySelector(".ste-direction-slider")?.remove() + if (!document.querySelector("div[class^='color-picker_gradient-picker-row_'][class*='color-picker_gradient-swatches-row_']") || feature.traps.paint().selectedItems[0]?.fillColor._components[0].radial || feature.traps.paint().selectedItems.length !== 1) { + document.querySelector(".ste-direction-slider")?.remove() + } } -}) + }) const rotateColor = function (amount) { let data = rotatePoints( @@ -135,7 +137,7 @@ export default async function ({ feature, console, scratchClass }) { document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); - function onMouseMove(e) { + async function onMouseMove(e) { if (isDragging) { const offsetX = e.clientX - initialX; let newLeft = handleLeft + offsetX; @@ -143,7 +145,7 @@ export default async function ({ feature, console, scratchClass }) { newLeft = Math.max(0, Math.min(124, newLeft)); rotateColor(Math.floor((newLeft / 124) * 360) - lastRotation) - update() + await update() lastRotation = Math.floor((newLeft / 124) * 360) value.textContent = @@ -168,7 +170,7 @@ export default async function ({ feature, console, scratchClass }) { handle.addEventListener("touchmove", onTouchMove); handle.addEventListener("touchend", onTouchEnd); - function onTouchMove(e) { + async function onTouchMove(e) { if (isDragging) { const offsetX = e.touches[0].clientX - initialX; let newLeft = handleLeft + offsetX; @@ -176,7 +178,7 @@ export default async function ({ feature, console, scratchClass }) { newLeft = Math.max(0, Math.min(124, newLeft)); rotateColor(Math.floor((newLeft / 124) * 360) - lastRotation) - update() + await update() lastRotation = Math.floor((newLeft / 124) * 360) value.textContent = @@ -194,7 +196,7 @@ export default async function ({ feature, console, scratchClass }) { }); } - function update() { - feature.traps.getPaper().tool.onUpdateImage() + async function update() { + (await feature.traps.getPaper()).tool.onUpdateImage() } } diff --git a/features/watch-later/script.js b/features/watch-later/script.js index 86668378..e64f211e 100644 --- a/features/watch-later/script.js +++ b/features/watch-later/script.js @@ -16,7 +16,7 @@ export default function ({ feature, console }) { button.style.display = !feature.redux.getState().preview.projectInfo.is_published ? "none" : null - feature.redux.subscribe(function() { + feature.redux.target.addEventListener("statechanged", function(e) { button.style.display = !feature.redux.getState().preview.projectInfo.is_published ? "none" : null }) diff --git a/manifest.json b/manifest.json index 47dd9f4c..935900a0 100644 --- a/manifest.json +++ b/manifest.json @@ -26,6 +26,14 @@ "128": "/extras/icons/icon128.png" }, "content_scripts": [ + { + "matches": [ + "https://scratch.mit.edu/*" + ], + "run_at": "document_start", + "js": ["api/content/vm.js", "api/content/redux.js"], + "all_frames": true + }, { "matches": [ "https://scratch.mit.edu/*"