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 }