Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion addons-l10n/en/pause.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"pause/pause": "Pause",
"pause/play": "Resume"
"pause/play": "Resume",
"pause/cannot-pause": "This project cannot be paused"
}
90 changes: 90 additions & 0 deletions addons/pause/check-online.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
let hasOnlineFeatures;

/** Guesses whether the project has cloud variable features */
const scanForOnlineFeatures = (vm, console, msg, element) => {
// Well, only if it actually has cloud variables :P
if (!vm.runtime.hasCloudData()) {
hasOnlineFeatures = false;
return;
}

const allBlocks = vm.runtime.targets.flatMap((target) => Object.values(target.blocks._blocks));
// Map the names and values of cloud variables to an object
const cloudVariables = {};
const globalVariables = vm.runtime.getTargetForStage().variables;
for (let id of Object.keys(globalVariables)) {
if (globalVariables[id].isCloud) {
cloudVariables[globalVariables[id].name] = globalVariables[id].value;
}
}

const names = Object.keys(cloudVariables);
const values = Object.values(cloudVariables);

const varsHaveInvalidNumbers = values.some((i) => String(Number(i)) !== i);
Copy link
Member

@CST1229 CST1229 Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this condition might trigger false positives if a cloud variable's value is an actual number instead of a numeral string? (because a string is never strictly equal to a number). not sure if it's intentional or not though

Copy link
Member Author

@DNin01 DNin01 Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the strict inequality sign is on the outside of the String() constructor?

string(number) !== string

And this function takes in a string as an argument, so it's a string-to-string comparison. …That is, unless Cloud variable value messages aren't always a string. I think they are, but I'll have to check that.

Copy link
Member

@CST1229 CST1229 Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, unless Cloud variable value messages aren't always a string

if a project sets a cloud variable to a number itself (not receiving a change) then it is a number

const areVarNamesSequential = names.some((i) => i.endsWith("2")) && names.some((i) => i.endsWith("3"));
const usesUsernameBlock = allBlocks.some((block) => block.opcode === "sensing_username");

if (varsHaveInvalidNumbers && areVarNamesSequential && usesUsernameBlock) {
// It's definitely online
hasOnlineFeatures = true;
disablePauseButton(console, msg, element);
} else if (varsHaveInvalidNumbers || areVarNamesSequential || usesUsernameBlock) {
// It might be online, we'll analyze during runtime
threshold = 5;
console.log("This Cloud project has the following characteristics:", {
varsHaveInvalidNumbers,
areVarNamesSequential,
usesUsernameBlock,
});
}
};

let interactions = 0;
let threshold = 10;
let timeout;
/** Tracks incoming and outgoing data for cloud variables */
const onDataTransferred = (value) => {
const hasInvalidNumbers = String(Number(value)) !== value;
if (hasInvalidNumbers) interactions++;
clearTimeout(timeout);
if (interactions < threshold) {
// Reset counter after 1.2s
timeout = setTimeout(() => (interactions = 0), 1200);
} else {
hasOnlineFeatures = true;
}
};

function disablePauseButton(console, msg, pauseBtnElement) {
console.log("Online features detected - pause disabled.");
pauseBtnElement.classList.add("disabled");
pauseBtnElement.title = msg("cannot-pause");
}

export const getHasOnlineFeatures = () => hasOnlineFeatures;
export const checkForOnlineFeatures = async (addon, console, msg, element) => {
await addon.tab.redux.waitForState((state) => state.scratchGui.projectState.loadingState.startsWith("SHOWING"));
scanForOnlineFeatures(addon.tab.traps.vm, console, msg, element);
if (hasOnlineFeatures === undefined) {
// Watch for cloud variable updates
const originalSend = addon.tab.traps.vm.runtime.ioDevices.cloud.provider.connection.send;
addon.tab.traps.vm.runtime.ioDevices.cloud.provider.connection.send = function (data) {
originalSend.call(this, data);
const json = JSON.parse(data);
if (!hasOnlineFeatures && json.name) {
onDataTransferred(json.value);
if (hasOnlineFeatures) disablePauseButton(console, msg, element);
}
};
const originalOnMessage = addon.tab.traps.vm.runtime.ioDevices.cloud.provider.connection.onmessage;
addon.tab.traps.vm.runtime.ioDevices.cloud.provider.connection.onmessage = function (message) {
originalOnMessage.call(this, message);
const json = JSON.parse(message.data.split("\n")[0]);
if (!hasOnlineFeatures && json.name) {
onDataTransferred(json.value);
if (hasOnlineFeatures) disablePauseButton(console, msg, element);
}
};
}
};
4 changes: 4 additions & 0 deletions addons/pause/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
.pause-btn:hover {
background-color: hsla(260, 60%, 60%, 0.15);
}

.pause-btn.disabled {
opacity: 0.5;
}
20 changes: 13 additions & 7 deletions addons/pause/userscript.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { isPaused, setPaused, onPauseChanged, setup } from "../debugger/module.js";
import { checkForOnlineFeatures, getHasOnlineFeatures } from "./check-online.js";

export default async function ({ addon, console, msg }) {
setup(addon);

const img = document.createElement("img");
img.className = "pause-btn";
img.draggable = false;
img.title = msg("pause");

const togglePause = () => {
if (getHasOnlineFeatures() && !isPaused()) return;
setPaused(!isPaused());
};
const setSrc = () => {
img.src = addon.self.dir + (isPaused() ? "/play.svg" : "/pause.svg");
img.title = isPaused() ? msg("play") : msg("pause");
};
img.addEventListener("click", () => setPaused(!isPaused()));

const img = document.createElement("img");
img.className = "pause-btn";
img.draggable = false;
img.title = msg("pause");
img.addEventListener("click", togglePause);
addon.tab.displayNoneWhileDisabled(img);
addon.self.addEventListener("disabled", () => setPaused(false));
setSrc();
onPauseChanged(setSrc);
checkForOnlineFeatures(addon, console, msg, img);

document.addEventListener(
"keydown",
Expand All @@ -28,7 +34,7 @@ export default async function ({ addon, console, msg }) {
if (e.altKey && (e.key.toLowerCase() === "x" || e.keyCode === 88) && !addon.self.disabled) {
e.preventDefault();
e.stopImmediatePropagation();
setPaused(!isPaused());
togglePause();
}
},
{ capture: true }
Expand Down
Loading