Skip to content

Commit 416eb66

Browse files
Addon disabled listener (ScratchAddons#1500)
* add files * add more extensions * final changes * style: format code * merge it * init commit * style: format code * oh yeah * style: format code * just some tweeks * remove load-auto extension * lots of major stuff, still not done tho * style: format code * whoopsie * amazing changes! * style: format code * remove notice * couple of wls * style: format code * more stuff * style: format code * Remove references to late and allAddons, use fireEvent instead of postMessage * style: format code * Revert unnecessary changes by PR * Support disabled event (but not reenabled) on persistent scripts * style: format code * Do not use removed addon.tab.bindEvent * Introduce dynamicUserscriptDisable, remake ordering of addons in "addons" popup tab * style: format code * Add dynamicUserscriptDisable:true to scratchr2 (theme with userscripts) * editor-stage-left: recalculate cached blockly areas after disable/reenable * Add issue number to comment * Rename "dynamicUserscriptDisable" to "dynamicDisable" * Do not inject editor-stage-left CSS if JS is not running (temp fix until ScratchAddons#1672 fix) * Add comment to clarify this won't work very well with dynamic userscript enabling Co-authored-by: TheColaber <[email protected]> Co-authored-by: WorldLanguages <[email protected]> Co-authored-by: WorldLanguages <[email protected]>
1 parent cd60642 commit 416eb66

File tree

15 files changed

+179
-82
lines changed

15 files changed

+179
-82
lines changed

Diff for: addon-api/background/Addon.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default class BackgroundScriptAddon extends Addon {
2828
_kill() {
2929
this.auth.dispose();
3030
this.settings.dispose();
31+
this.self.dispose();
3132
if (this.notifications) this.notifications.dispose();
3233
this._timeouts.forEach((timeoutId) => clearTimeout(timeoutId));
3334
this._intervals.forEach((intervalId) => clearInterval(intervalId));

Diff for: addon-api/common/Addon.js

+2-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Auth from "./Auth.js";
22
import Account from "./Account.js";
3+
import Self from "./Self.js";
34
import fetch from "./fetch.js";
45
import Settings from "../common/Settings.js";
56

@@ -17,20 +18,7 @@ import Settings from "../common/Settings.js";
1718
*/
1819
export default class Addon {
1920
constructor(info) {
20-
this.self = {
21-
id: info.id,
22-
browser: typeof InstallTrigger !== "undefined" ? "firefox" : "chrome",
23-
};
24-
Object.defineProperties(this.self, {
25-
dir: {
26-
enumerable: true,
27-
get: () => `${this._path}addons/${info.id}`,
28-
},
29-
lib: {
30-
enumerable: true,
31-
get: () => `${this._path}libraries`,
32-
},
33-
});
21+
this.self = new Self(this, info);
3422
this.auth = new Auth(this);
3523
this.account = new Account();
3624
this.fetch = fetch;

Diff for: addon-api/common/Self.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Listenable from "./Listenable.js";
2+
3+
/**
4+
* @extends Listenable
5+
*/
6+
export default class Self extends Listenable {
7+
constructor(addonObj, info) {
8+
super();
9+
this._addonId = info.id; // In order to receive fireEvent messages from background
10+
this.id = info.id;
11+
this._addonObj = addonObj;
12+
this.browser = typeof InstallTrigger !== "undefined" ? "firefox" : "chrome";
13+
this.disabled = false;
14+
this.addEventListener("disabled", () => (this.disabled = true));
15+
this.addEventListener("reenabled", () => (this.disabled = false));
16+
}
17+
get dir() {
18+
return `${this._addonObj._path}addons/${this.id}`;
19+
}
20+
21+
get lib() {
22+
return `${this._addonObj._path}libraries`;
23+
}
24+
25+
/**
26+
* @private
27+
*/
28+
get _eventTargetKey() {
29+
return "self";
30+
}
31+
}

Diff for: addon-api/content-script/Addon.js

+2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import Tab from "./Tab.js";
99
export default class UserscriptAddon extends Addon {
1010
constructor(info) {
1111
super(info);
12+
this._addonId = info.id;
1213
this.__path = document.getElementById("scratch-addons").getAttribute("data-path");
1314
this.tab = new Tab(info);
15+
this.self.disabled = false;
1416
}
1517

1618
/**

Diff for: addons/editor-stage-left/addon.json

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
{
22
"name": "Display stage on left side",
33
"description": "Moves the stage to the left side of the editor.",
4-
"info": [
5-
{
6-
"type": "notice",
7-
"text": "Refresh the editor after enabling/disabling this theme.",
8-
"id": "refresheditor"
9-
}
10-
],
114
"credits": [
125
{
136
"name": "NitroCipher/ZenithRogue"
@@ -19,6 +12,7 @@
1912
"matches": ["https://scratch.mit.edu/projects/*"]
2013
}
2114
],
15+
"dynamicDisable": true,
2216
"userstyles": [
2317
{
2418
"url": "stageleft.css",

Diff for: addons/editor-stage-left/fix-share-the-love.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,49 @@
11
export default function ({ addon, global, console }) {
2+
let interval, injected;
3+
4+
addon.self.addEventListener("disabled", () => {
5+
clearInterval(interval);
6+
// TODO: when dynamicEnable is a thing, set it to false for this addon (its default value)
7+
// and make sure "disabled" event is fired after styles are updated, and remove disabled logic
8+
document.querySelector(".scratch-addons-theme[data-addon-id='editor-stage-left']").disabled = true;
9+
Blockly.getMainWorkspace().recordCachedAreas();
10+
});
11+
addon.self.addEventListener("reenabled", () => {
12+
if (!injected) tryInjecting();
13+
document.querySelector(".scratch-addons-theme[data-addon-id='editor-stage-left']").disabled = false;
14+
Blockly.getMainWorkspace().recordCachedAreas();
15+
});
16+
217
const inject = (workspace) => {
18+
injected = true;
319
const originalGetClientRect = workspace.toolbox_.getClientRect;
420
workspace.toolbox_.getClientRect = function () {
521
// we are trying to undo the effect of BIG_NUM in https://github.com/LLK/scratch-blocks/blob/ab26fa2960643fa38fbc7b91ca2956be66055070/core/flyout_vertical.js#L739
622
const rect = originalGetClientRect.call(this);
7-
if (!rect) return rect;
23+
if (!rect || addon.self.disabled) return rect;
824
if (rect.left > 0) return rect;
925
rect.left += 1000000000;
1026
rect.width -= 1000000000;
1127
return rect;
1228
};
1329
};
1430

15-
if (addon.tab.editorMode === "editor") {
16-
const interval = setInterval(() => {
17-
if (Blockly.getMainWorkspace()) {
31+
function tryInjecting() {
32+
if (addon.tab.editorMode === "editor") {
33+
interval = setInterval(() => {
34+
if (Blockly.getMainWorkspace()) {
35+
inject(Blockly.getMainWorkspace());
36+
clearInterval(interval);
37+
}
38+
}, 100);
39+
}
40+
41+
addon.tab.addEventListener("urlChange", () => {
42+
if (addon.tab.editorMode === "editor") {
43+
// Inject even if addon is disabled, will pollute but not change function return value
1844
inject(Blockly.getMainWorkspace());
19-
clearInterval(interval);
2045
}
21-
}, 100);
46+
});
2247
}
23-
addon.tab.addEventListener(
24-
"urlChange",
25-
() => addon.tab.editorMode === "editor" && inject(Blockly.getMainWorkspace())
26-
);
48+
tryInjecting();
2749
}

Diff for: addons/editor-stage-left/stageleft.css

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/* sa-autoupdate-theme-ignore */
2+
/* Userscript handles whether this style should be enabled until dynamicEnable is a thing */
3+
/* If we didn't add autoupdate-ignore, loading the editor with the addon disabled, then
4+
enabling it, would inject the CSS but of course not the JS */
25

36
[class*="stage-header_stage-button-icon"] {
47
transform: scaleX(-1);

Diff for: addons/scratch-messaging/background.js

+8-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default async function ({ addon, global, console, setTimeout, setInterval
44
let lastDateTime;
55
let data;
66
let pendingAuthChange = false;
7+
let addonEnabled = true;
78
const commentLocationPrefixes = {
89
0: "p", // Projects
910
1: "u", // Users
@@ -28,8 +29,7 @@ export default async function ({ addon, global, console, setTimeout, setInterval
2829

2930
async function routine() {
3031
await runCheckMessagesAfter({ checkOld: true }, 0);
31-
// Can't use while(true) because addon might get disabled
32-
while (addon.self) {
32+
while (addonEnabled) {
3333
if (pendingAuthChange) {
3434
pendingAuthChange = false;
3535
resetData();
@@ -101,14 +101,7 @@ export default async function ({ addon, global, console, setTimeout, setInterval
101101
data.ready = true;
102102
}
103103

104-
chrome.runtime.onMessage.addListener(function thisFunction(request, sender, sendResponse) {
105-
// If this addon has been killed, addon.self will throw
106-
try {
107-
addon.self;
108-
} catch (err) {
109-
chrome.runtime.onMessage.removeListener(thisFunction);
110-
return;
111-
}
104+
const messageListener = (request, sender, sendResponse) => {
112105
if (!request.scratchMessaging) return;
113106
const popupRequest = request.scratchMessaging;
114107
if (popupRequest === "getData")
@@ -131,6 +124,11 @@ export default async function ({ addon, global, console, setTimeout, setInterval
131124
.catch((err) => sendResponse(err));
132125
return true;
133126
}
127+
};
128+
chrome.runtime.onMessage.addListener(messageListener);
129+
addon.self.addEventListener("disabled", () => {
130+
chrome.runtime.onMessage.removeListener(messageListener);
131+
addonEnabled = false;
134132
});
135133

136134
async function retrieveComments(resourceType, resourceId, commentIds, page = 1, commentsObj = {}) {

Diff for: addons/scratchr2/addon.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"runAtComplete": false
2323
}
2424
],
25+
"dynamicDisable": true,
2526
"userstyles": [
2627
{
2728
"url": "set-default-var-value.css",

Diff for: background/declare-scratchaddons-object.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ scratchAddons.addonObjects = [];
1111
scratchAddons.eventTargets = {
1212
auth: [],
1313
settings: [],
14+
self: [],
1415
};
1516

1617
// Event target for local background page events

Diff for: background/handle-settings-page.js

+25-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,33 @@ chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
2424
chrome.storage.sync.set({
2525
addonsEnabled: scratchAddons.localState.addonsEnabled,
2626
});
27+
28+
// Fire disabled event for userscripts
29+
// TODO: this might not be an addon being reenabled. We should consider this
30+
// in case we want to provide userscripts with a way to run after the page
31+
// has loaded, dynamically.
32+
chrome.tabs.query({}, (tabs) =>
33+
tabs.forEach(
34+
(tab) =>
35+
(tab.url || (!tab.url && typeof browser !== "undefined")) &&
36+
chrome.tabs.sendMessage(tab.id, {
37+
fireEvent: {
38+
target: "self",
39+
name: newState ? "reenabled" : "disabled",
40+
addonId,
41+
},
42+
})
43+
)
44+
);
45+
2746
if (newState === false) {
47+
// TODO: can there be many addon objects for the same addon?
2848
const addonObjs = scratchAddons.addonObjects.filter((addonObj) => addonObj.self.id === addonId);
29-
if (addonObjs) addonObjs.forEach((addonObj) => addonObj._kill());
49+
if (addonObjs)
50+
addonObjs.forEach((addonObj) => {
51+
addonObj.self.dispatchEvent(new CustomEvent("disabled"));
52+
addonObj._kill();
53+
});
3054
scratchAddons.localEvents.dispatchEvent(new CustomEvent("badgeUpdateNeeded"));
3155
} else {
3256
runPersistentScripts(addonId);

Diff for: content-scripts/cs.js

+14-9
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ window.addEventListener("load", () => {
6161
}
6262
});
6363

64+
// Store all themes that were enabled this session
65+
const sessionEnabledThemes = new Set();
66+
6467
function injectUserstylesAndThemes({ userstyleUrls, themes, isUpdate }) {
6568
document.querySelectorAll(".scratch-addons-theme").forEach((style) => {
6669
if (!style.textContent.startsWith("/* sa-autoupdate-theme-ignore */")) style.remove();
@@ -73,6 +76,7 @@ function injectUserstylesAndThemes({ userstyleUrls, themes, isUpdate }) {
7376
else document.documentElement.appendChild(link);
7477
}
7578
for (const theme of themes) {
79+
sessionEnabledThemes.add(theme.addonId);
7680
for (const styleUrl of theme.styleUrls) {
7781
let css = theme.styles[styleUrl];
7882
// Replace %addon-self-dir% for relative URLs
@@ -131,15 +135,16 @@ function onHeadAvailable({ globalState, l10njson, addonsWithUserscripts, usersty
131135
} else if (typeof request.setMsgCount !== "undefined") {
132136
template.setAttribute("data-msgcount", request.setMsgCount);
133137
} else if (request === "getRunningAddons") {
134-
// We need to send themes that might have been injected dynamically
135-
sendResponse([
136-
...new Set([
137-
...addonsWithUserscripts.map((obj) => obj.addonId),
138-
...Array.from(document.querySelectorAll(".scratch-addons-theme")).map((style) =>
139-
style.getAttribute("data-addon-id")
140-
),
141-
]),
142-
]);
138+
const userscripts = addonsWithUserscripts.map((obj) => obj.addonId);
139+
const activeThemes = Array.from(document.querySelectorAll(".scratch-addons-theme")).map((style) =>
140+
style.getAttribute("data-addon-id")
141+
);
142+
const inactiveThemes = [...sessionEnabledThemes].filter((addonId) => !activeThemes.includes(addonId));
143+
sendResponse({
144+
userscripts,
145+
activeThemes,
146+
inactiveThemes,
147+
});
143148
}
144149
});
145150

Diff for: content-scripts/inject/module.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ scratchAddons.eventTargets = {
2323
auth: [],
2424
settings: [],
2525
tab: [],
26+
self: [],
2627
};
2728
scratchAddons.classNames = { loaded: false };
2829

@@ -92,14 +93,17 @@ const observer = new MutationObserver((mutationsList) => {
9293
removeAttr();
9394
} else if (attrType === "data-fire-event") {
9495
if (attrVal.addonId) {
95-
const settingsEventTarget = scratchAddons.eventTargets.settings.find(
96+
// Addon specific events, like settings change and self disabled
97+
const eventTarget = scratchAddons.eventTargets[attrVal.target].find(
9698
(eventTarget) => eventTarget._addonId === attrVal.addonId
9799
);
98-
if (settingsEventTarget) settingsEventTarget.dispatchEvent(new CustomEvent("change"));
99-
} else
100+
if (eventTarget) eventTarget.dispatchEvent(new CustomEvent(attrVal.name));
101+
} else {
102+
// Global events, like auth change
100103
scratchAddons.eventTargets[attrVal.target].forEach((eventTarget) =>
101104
eventTarget.dispatchEvent(new CustomEvent(attrVal.name))
102105
);
106+
}
103107
removeAttr();
104108
}
105109
}

Diff for: webpages/settings/index.html

+1-3
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ <h1>{{ msg("settings") }}</h1>
129129
</div>
130130

131131
<div class="addons-container">
132-
<div id="running-page" v-show="popupOpenedOnScratchTab && searchInput === ''">
133-
{{ msg("runningOnThisPage") }}
134-
</div>
132+
<div id="running-page" v-show="addonsRunningOnTab && searchInput === ''">{{ msg("runningOnThisPage") }}</div>
135133
<div
136134
class="addon-body"
137135
:class="{ 'addon-body-margin-bottom': addon._marginBottom && searchInput === '' }"

0 commit comments

Comments
 (0)