diff --git a/api/feature.js b/api/feature.js
index 26fe2fb1..8eb4b689 100644
--- a/api/feature.js
+++ b/api/feature.js
@@ -6,6 +6,9 @@ class Feature {
finalFeature = el;
}
});
+ this.requestPermissions = async function(...permissions) {
+ return await ScratchTools.sendMessage("request-perms", permissions)
+ }
this.data = finalFeature;
this.msg = function (string) {
return this.data.localesData[`${this.data.id}/`+string] || `ScratchTools.${this.data.id}.${string}`;
diff --git a/api/main.js b/api/main.js
index 9ae24062..f45ee597 100644
--- a/api/main.js
+++ b/api/main.js
@@ -101,6 +101,23 @@ if (
ScratchTools.type = "Website";
}
+ScratchTools.MESSAGES = []
+ScratchTools.sendMessage = function(id, content) {
+ let uuid = UUID()
+ chrome.runtime.sendMessage(ScratchTools.id, { message: id, content, source: "message-api", uuid });
+ return new Promise((resolve, reject) => {
+ ScratchTools.MESSAGES.push({ message: id, source: "message-api", uuid, resolve });
+ });
+}
+
+function UUID() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {
+ const random = Math.random() * 16 | 0;
+ const value = char === 'x' ? random : (random & 0x3 | 0x8);
+ return value.toString(16);
+ });
+}
+
var storagePromises = [];
ScratchTools.storage = {
get: async function (key) {
diff --git a/api/update/changelogs/forum.json b/api/update/changelogs/forum.json
new file mode 100644
index 00000000..9a9b0818
--- /dev/null
+++ b/api/update/changelogs/forum.json
@@ -0,0 +1,7 @@
+{
+ "active": false,
+ "title": "Forum Changes for {{ version }}",
+ "description": "",
+ "changes": []
+ }
+
\ No newline at end of file
diff --git a/api/update/changelogs/project.json b/api/update/changelogs/project.json
new file mode 100644
index 00000000..ca32136d
--- /dev/null
+++ b/api/update/changelogs/project.json
@@ -0,0 +1,63 @@
+{
+ "active": true,
+ "title": "Project Page Changes for {{ version }}",
+ "description": "ScratchTools is introducing many new project page and editor features that will revolutionize the way that you use Scratch. You can enable them on the settings page.",
+ "changes": [
+ {
+ "icon": "gradient.svg",
+ "slogan": "Rotate gradient colors in the paint editor"
+ },
+ {
+ "icon": "align.svg",
+ "slogan": "Align shapes and objects in the paint editor"
+ },
+ {
+ "icon": "align.svg",
+ "slogan": "Quickly center text in your project instructions"
+ },
+ {
+ "icon": "extend.svg",
+ "slogan": "Extend C-blocks around code when dragging them"
+ },
+ {
+ "icon": "filesize.svg",
+ "slogan": "View file sizes for any asset"
+ },
+ {
+ "icon": "font.svg",
+ "slogan": "Use dozens of extra fonts in the paint editor"
+ },
+ {
+ "icon": "nocloud.svg",
+ "slogan": "Temporarily disable cloud variables in projects"
+ },
+ {
+ "icon": "opacity.svg",
+ "slogan": "Customize the opacity of stage variable monitors"
+ },
+ {
+ "icon": "outline.svg",
+ "slogan": "Customize and round lines and shape outlines"
+ },
+ {
+ "icon": "reaction.svg",
+ "slogan": "Add and view emoji reactions on any project"
+ },
+ {
+ "icon": "record.svg",
+ "slogan": "Record and save videos of your project stage"
+ },
+ {
+ "icon": "shapes.svg",
+ "slogan": "Unite, subtract, intersect, and exclude shapes"
+ },
+ {
+ "icon": "thumbnail.svg",
+ "slogan": "Upload custom project thumbnails"
+ },
+ {
+ "icon": "upload.svg",
+ "slogan": "Upload WEBP images as costumes and sprites"
+ }
+ ]
+}
diff --git a/api/update/changelogs/website.json b/api/update/changelogs/website.json
new file mode 100644
index 00000000..8e4cd84c
--- /dev/null
+++ b/api/update/changelogs/website.json
@@ -0,0 +1,28 @@
+{
+ "active": true,
+ "title": "Website Changes for {{ version }}",
+ "description": "ScratchTools is introducing many new features for the Scratch website. You can enable them on the settings page.",
+ "changes": [
+ {
+ "icon": "change.svg",
+ "slogan": "Customize what tag is used by default on the explore page"
+ },
+ {
+ "icon": "gift.svg",
+ "slogan": "A ScratchTools selection of featured projects"
+ },
+ {
+ "icon": "filter.svg",
+ "slogan": "Filter through studio, explore, and searched projects"
+ },
+ {
+ "icon": "date.svg",
+ "slogan": "See when a studio was created"
+ },
+ {
+ "icon": "countdown.svg",
+ "slogan": "View how many replies are left in a thread"
+ }
+ ]
+ }
+
\ No newline at end of file
diff --git a/api/update/icons/align.svg b/api/update/icons/align.svg
new file mode 100644
index 00000000..0f181f48
--- /dev/null
+++ b/api/update/icons/align.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/change.svg b/api/update/icons/change.svg
new file mode 100644
index 00000000..5040be17
--- /dev/null
+++ b/api/update/icons/change.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/countdown.svg b/api/update/icons/countdown.svg
new file mode 100644
index 00000000..b2483d3d
--- /dev/null
+++ b/api/update/icons/countdown.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/date.svg b/api/update/icons/date.svg
new file mode 100644
index 00000000..98123090
--- /dev/null
+++ b/api/update/icons/date.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/extend.svg b/api/update/icons/extend.svg
new file mode 100644
index 00000000..59355358
--- /dev/null
+++ b/api/update/icons/extend.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/filesize.svg b/api/update/icons/filesize.svg
new file mode 100644
index 00000000..b3887810
--- /dev/null
+++ b/api/update/icons/filesize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/filter.svg b/api/update/icons/filter.svg
new file mode 100644
index 00000000..add0849e
--- /dev/null
+++ b/api/update/icons/filter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/font.svg b/api/update/icons/font.svg
new file mode 100644
index 00000000..9886d27c
--- /dev/null
+++ b/api/update/icons/font.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/gift.svg b/api/update/icons/gift.svg
new file mode 100644
index 00000000..ada83f3c
--- /dev/null
+++ b/api/update/icons/gift.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/gradient.svg b/api/update/icons/gradient.svg
new file mode 100644
index 00000000..b60fe149
--- /dev/null
+++ b/api/update/icons/gradient.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/logo.svg b/api/update/icons/logo.svg
new file mode 100644
index 00000000..e5f60834
--- /dev/null
+++ b/api/update/icons/logo.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/api/update/icons/nocloud.svg b/api/update/icons/nocloud.svg
new file mode 100644
index 00000000..b550d987
--- /dev/null
+++ b/api/update/icons/nocloud.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/opacity.svg b/api/update/icons/opacity.svg
new file mode 100644
index 00000000..4f91c12a
--- /dev/null
+++ b/api/update/icons/opacity.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/outline.svg b/api/update/icons/outline.svg
new file mode 100644
index 00000000..76653e2b
--- /dev/null
+++ b/api/update/icons/outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/reaction.svg b/api/update/icons/reaction.svg
new file mode 100644
index 00000000..939ef3e1
--- /dev/null
+++ b/api/update/icons/reaction.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/record.svg b/api/update/icons/record.svg
new file mode 100644
index 00000000..19a6067a
--- /dev/null
+++ b/api/update/icons/record.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/shapes.svg b/api/update/icons/shapes.svg
new file mode 100644
index 00000000..b0546afa
--- /dev/null
+++ b/api/update/icons/shapes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/thumbnail.svg b/api/update/icons/thumbnail.svg
new file mode 100644
index 00000000..ba07ba78
--- /dev/null
+++ b/api/update/icons/thumbnail.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/upload.svg b/api/update/icons/upload.svg
new file mode 100644
index 00000000..ebd22086
--- /dev/null
+++ b/api/update/icons/upload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/icons/variable.svg b/api/update/icons/variable.svg
new file mode 100644
index 00000000..73a6a993
--- /dev/null
+++ b/api/update/icons/variable.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/api/update/index.js b/api/update/index.js
new file mode 100644
index 00000000..7c235ca0
--- /dev/null
+++ b/api/update/index.js
@@ -0,0 +1,146 @@
+function getPageType() {
+ let url = new URL(window.location.href);
+ if (document.querySelector("#page-404")) {
+ return null;
+ } else if (url.pathname.startsWith("/projects/")) {
+ return "project";
+ } else if (url.pathname.startsWith("/discuss/")) {
+ return "forum";
+ } else {
+ return "website";
+ }
+}
+
+async function checkUpdate() {
+ let version = chrome.runtime.getManifest().version;
+ let { updateScreens } = await chrome.storage.sync.get("updateScreens");
+
+ if (updateScreens) {
+ let page = getPageType();
+
+ if (page && updateScreens[page] !== version) {
+ updateScreens[page] = version;
+ await chrome.storage.sync.set({
+ updateScreens,
+ });
+ let update = await (
+ await fetch(
+ chrome.runtime.getURL(`/api/update/changelogs/${page}.json`)
+ )
+ ).json();
+ if (update.active) {
+ makeScreen(update);
+ }
+ }
+ } else {
+ await chrome.storage.sync.set({
+ updateScreens: {
+ website: "0",
+ project: "0",
+ forum: "0",
+ },
+ });
+ checkUpdate()
+ }
+}
+checkUpdate();
+
+function makeScreen(update) {
+ console.log(update);
+ if (document.querySelector(".ste-update-bg")) return;
+ let background = Object.assign(document.createElement("div"), {
+ className: "ste-update-bg",
+ });
+
+ let div = Object.assign(document.createElement("div"), {
+ className: "ste-update-box",
+ });
+
+ let topRow = document.createElement("div");
+ topRow.append(
+ Object.assign(document.createElement("img"), {
+ src: chrome.runtime.getURL("/api/update/icons/logo.svg"),
+ })
+ );
+ div.appendChild(topRow);
+
+ let h2 = document.createElement("h2");
+ buildText(h2, update.title);
+ div.appendChild(h2);
+
+ let p = document.createElement("p");
+ p.textContent = update.description;
+ div.appendChild(p);
+
+ let b = document.createElement("b");
+ b.textContent = " Here's what's new:";
+ p.appendChild(b);
+
+ let rows = Object.assign(document.createElement("div"), {
+ className: "rows",
+ });
+
+ for (var i in update.changes) {
+ let row = document.createElement("div");
+ let img = document.createElement("img");
+ img.src = chrome.runtime.getURL(
+ `/api/update/icons/${update.changes[i].icon}`
+ );
+ let slogan = document.createElement("p");
+ slogan.textContent = update.changes[i].slogan;
+ row.append(img, slogan);
+ rows.appendChild(row);
+ }
+
+ div.appendChild(rows);
+
+ let viewMore = document.createElement("div");
+ viewMore.className = "view-more";
+ let viewMoreSpan = viewMore.appendChild(
+ Object.assign(document.createElement("span"), {
+ textContent: "View All",
+ })
+ );
+ div.appendChild(viewMore);
+
+ viewMoreSpan.addEventListener("click", function () {
+ viewMore.remove();
+ rows.style.maxHeight = "none";
+ });
+
+ let button = document.createElement("button");
+ button.textContent = "Continue";
+ button.addEventListener("click", function () {
+ background.remove();
+ div.remove();
+ });
+ background.addEventListener("click", function () {
+ div.remove();
+ background.remove();
+ });
+ div.appendChild(button);
+
+ document.body.appendChild(div);
+ document.body.appendChild(background);
+}
+
+function buildText(element, title) {
+ let blocks = title.split("{{ version }}");
+ console.log(blocks);
+
+ for (var i in blocks) {
+ let span = document.createElement("span");
+ span.textContent = blocks[i];
+ element.appendChild(span);
+
+ if (Number(i) !== Number(blocks.length - 1)) {
+ console.log(i);
+ let version = document.createElement("span");
+ version.textContent = "v" + chrome.runtime.getManifest().version;
+ version.className = "color";
+ element.appendChild(version);
+ }
+ }
+
+ return;
+}
diff --git a/api/update/style.css b/api/update/style.css
new file mode 100644
index 00000000..2654759b
--- /dev/null
+++ b/api/update/style.css
@@ -0,0 +1,138 @@
+@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
+
+.ste-update-bg {
+ display: block;
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ z-index: 2147483646;
+ width: 100vw;
+ height: 100vh;
+ background: #00000080;
+}
+
+.ste-update-box {
+ all: unset; /* Reset inherited styles */
+ box-sizing: border-box;
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translateX(-50%) translateY(-50%);
+ background: white;
+ padding: 32px;
+ border-radius: 12.8px;
+ z-index: 2147483647;
+ width: calc(40 * 16px);
+ max-width: calc(100% - calc(8 * 16px));
+ max-height: calc(100% - calc(8 * 16px));
+ overflow-y: auto;
+ font-size: 16px;
+ line-height: 20px;
+}
+
+.ste-update-box > div:first-child img {
+ height: calc(2 * 16px);
+ float: right;
+}
+
+.ste-update-box * {
+ font-family: "Inter", sans-serif !important;
+ text-shadow: none !important;
+}
+
+.ste-update-box h2 {
+ position: relative;
+ top: calc(.4 * -16px);
+ margin-bottom: 20px;
+ color: black;
+ font-size: calc(2 * 16px);
+}
+
+.ste-update-box .rows div {
+ display: flex;
+ vertical-align: middle;
+ margin-bottom: calc(2 * 16px);
+ align-items: center;
+ break-inside: avoid
+}
+
+.ste-update-box h2 span.color {
+ background: -webkit-linear-gradient(#ff8c2d, #ffb740);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.ste-update-box .rows div p {
+ color: black;
+ margin: 0px;
+ font-weight: 600;
+ font-size: calc(1 * 16px);
+ margin-left: calc(.8 * 16px);
+}
+
+.ste-update-box > p {
+ color: black;
+ opacity: .6;
+ margin-top: 0px;
+ margin-bottom: calc(1 * 16px);
+ position: relative;
+ top: calc(1 * -16px);
+}
+
+.ste-update-box > p > b {
+ font-weight: 600;
+}
+
+.ste-update-box .rows img {
+ height: calc(2 * 16px);
+}
+
+.ste-update-box .rows {
+ column-count: 2;
+ column-gap: calc(2 * 16px);
+ margin-bottom: calc(2 * 16px);
+ max-height: calc(14 * 16px);
+ overflow-y: hidden;
+}
+
+.ste-update-box > button {
+ background: linear-gradient(0.25turn, #ff8c2d, #ffb740);
+ color: white;
+ width: 100%;
+ outline: none;
+ border: 0px;
+ padding: calc(.8 * 16px);
+ border-radius: calc(.5 * 16px);
+ cursor: pointer;
+ font-weight: 600;
+ transition: background .3s, opacity .3s;
+ line-height: 24px;
+ height: 48px;
+ font-size: 16px;
+}
+
+.ste-update-box > button:hover {
+ background: linear-gradient(0.25turn, #ffb740, #ff8c2d);
+ opacity: .7;
+}
+
+.ste-update-box .view-more {
+ height: calc(5 * 16px);
+ width: 100%;
+ background: linear-gradient(360deg, white, transparent);
+ position: relative;
+ top: calc(-8 * 16px);
+ margin-bottom: calc(-4 * 16px);
+}
+
+.ste-update-box .view-more span {
+ position: absolute;
+ left: 50%;
+ top: calc(50% + calc(3 * 16px));
+ transform: translateX(-50%) translateY(-50%);
+ font-size: calc(1 * 16px);
+ font-weight: 600;
+ color: black;
+ opacity: .6;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/build/index.js b/build/index.js
new file mode 100644
index 00000000..152e7566
--- /dev/null
+++ b/build/index.js
@@ -0,0 +1 @@
+require("./write-permissions")
\ No newline at end of file
diff --git a/build/write-permissions.js b/build/write-permissions.js
new file mode 100644
index 00000000..b1136f1a
--- /dev/null
+++ b/build/write-permissions.js
@@ -0,0 +1,26 @@
+const fs = require("fs")
+let features = JSON.parse(fs.readFileSync("./features/features.json"))
+let manifest = JSON.parse(fs.readFileSync("./manifest.json"))
+let permissions = ["https://api.scratch.mit.edu"]
+
+for (var i in features) {
+ if (features[i].version === 2) {
+ let feature = JSON.parse(fs.readFileSync(`./features/${features[i].id}/data.json`))
+ if (feature.permissions) {
+ permissions.push(...feature.permissions.filter((perm) => !permissions.includes(perm)))
+ }
+ }
+}
+
+manifest.optional_permissions = permissions.filter((perm) => !checkUrl(perm))
+manifest.optional_host_permissions = permissions.filter((perm) => checkUrl(perm))
+fs.writeFileSync("./manifest.json", JSON.stringify(manifest, null, 2), 'utf8');
+
+function checkUrl(perm) {
+ try {
+ new URL(perm);
+ return true;
+ } catch (_) {
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/extras/background.js b/extras/background.js
index 61eefa65..1e404847 100644
--- a/extras/background.js
+++ b/extras/background.js
@@ -692,7 +692,28 @@ chrome.runtime.onMessageExternal.addListener(async function (
});
}
if (msg === "returnToTab") {
- await chrome.tabs.update(sender.tab.id, {active: true})
+ await chrome.tabs.update(sender.tab.id, { active: true });
+ }
+ if (msg.source === "message-api") {
+ if (msg.message?.startsWith("request-perms")) {
+ let perms = msg.content;
+
+ chrome.permissions.request({ permissions: perms }, async (granted) => {
+ let isComplete = !!granted;
+
+ await chrome.scripting.executeScript({
+ args: [isComplete, msg.uuid],
+ target: { tabId: sender.tab.id },
+ func: sendPermsResponse,
+ world: "MAIN",
+ });
+ function sendPermsResponse(completed, uuid) {
+ ScratchTools.MESSAGES.find((el) => el.uuid === uuid).resolve(
+ completed
+ );
+ }
+ });
+ }
}
if (typeof msg === "object") {
if (msg.message === "storageSet") {
diff --git a/manifest.json b/manifest.json
index 66e10786..ca65517a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -32,7 +32,11 @@
],
"run_at": "document_start",
"js": [
- "extras/inject-styles.js"
+ "extras/inject-styles.js",
+ "api/update/index.js"
+ ],
+ "css": [
+ "api/update/style.css"
],
"all_frames": true
}