From ac2af72d182b1d1fcbb45bde01102bd128b1985a Mon Sep 17 00:00:00 2001 From: rgantzos <86856959+rgantzos@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:27:04 -0700 Subject: [PATCH] Thumbnail setter --- api/modals.js | 8 + features/features.json | 5 + features/upload-thumbnail/data.json | 17 +++ features/upload-thumbnail/script.js | 153 ++++++++++++++++++++ features/upload-thumbnail/style.css | 24 +++ features/upload-thumbnail/thumbnail-btn.svg | 1 + 6 files changed, 208 insertions(+) create mode 100644 features/upload-thumbnail/data.json create mode 100644 features/upload-thumbnail/script.js create mode 100644 features/upload-thumbnail/style.css create mode 100644 features/upload-thumbnail/thumbnail-btn.svg diff --git a/api/modals.js b/api/modals.js index 67ec7161..acf885d6 100644 --- a/api/modals.js +++ b/api/modals.js @@ -32,6 +32,8 @@ ScratchTools.modals = { var code = document.createElement("code"); code.textContent = component.content; modal.appendChild(code); + } else if (component.type === "html") { + modal.appendChild(component.content); } }); @@ -45,5 +47,11 @@ ScratchTools.modals = { div.appendChild(modal); modal.prepend(orangeBar); document.body.appendChild(div); + + return { + close: function () { + div.remove(); + }, + }; }, }; diff --git a/features/features.json b/features/features.json index 1a6014eb..9e7bf259 100644 --- a/features/features.json +++ b/features/features.json @@ -1,4 +1,9 @@ [ + { + "version": 2, + "id": "upload-thumbnail", + "versionAdded": "v4.0.0" + }, { "version": 2, "id": "paint-align", diff --git a/features/upload-thumbnail/data.json b/features/upload-thumbnail/data.json new file mode 100644 index 00000000..1d78f181 --- /dev/null +++ b/features/upload-thumbnail/data.json @@ -0,0 +1,17 @@ +{ + "title": "Set Thumbnail", + "description": "Allows you to upload an image or GIF as a project thumbnail, or set the thumbnail to the current stage.", + "credits": [ + { + "username": "Sanjang_Beta", + "url": "https://scratch.mit.edu/users/Sanjang_Beta/" + }, + { "username": "rgantzos", "url": "https://scratch.mit.edu/users/rgantzos/" } + ], + "type": ["Website"], + "tags": ["New", "Recommended"], + "scripts": [{ "file": "script.js", "runOn": "/projects/*" }], + "styles": [{ "file": "style.css", "runOn": "/projects/*" }], + "resources": [{ "name": "thumbnail-btn", "path": "/thumbnail-btn.svg" }], + "dynamic": true +} diff --git a/features/upload-thumbnail/script.js b/features/upload-thumbnail/script.js new file mode 100644 index 00000000..b8a83d89 --- /dev/null +++ b/features/upload-thumbnail/script.js @@ -0,0 +1,153 @@ +export default async function ({ feature, console }) { + ScratchTools.waitForElements( + ".preview .inner .flex-row.action-buttons", + async function (row) { + if (feature.redux.getState()?.preview.projectInfo.author.username !== feature.redux.getState()?.session?.session?.user?.username) return; + + if (row.querySelector(".ste-thumbnail")) return; + let button = document.createElement("button"); + button.className = "button action-button ste-thumbnail"; + button.textContent = "Set Thumbnail"; + feature.self.hideOnDisable(button); + + let input = document.createElement("input"); + input.className = "ste-thumbnail-input"; + input.style.display = "none"; + input.type = "file"; + input.accept = "image/*"; + input.addEventListener("input", onThumbInput); + document.body.appendChild(input); + + function onThumbInput() { + if (input.files?.[0]) { + setThumbnail(input.files[0]); + } + } + + button.addEventListener("click", async function () { + let upload = document.createElement("button"); + upload.textContent = "Upload Image or GIF"; + upload.style.marginRight = ".5rem"; + upload.addEventListener("click", function () { + input.click(); + }); + + async function getStage() { + return new Promise((resolve) => { + feature.traps.vm.postIOData("video", { + forceTransparentPreview: true, + }); + feature.traps.vm.renderer.requestSnapshot((dataURL) => { + feature.traps.vm.postIOData("video", { + forceTransparentPreview: false, + }); + resolve(dataURL); + }); + }); + } + + let useStage = document.createElement("button"); + useStage.textContent = "Use Stage"; + useStage.className = "ste-thumbnail-stage"; + useStage.addEventListener("click", async function () { + function dataURLtoBlob(dataurl) { + let arr = dataurl.split(","); + let mime = arr[0].match(/:(.*?);/)[1]; + let bstr = atob(arr[1]); + let n = bstr.length; + let u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + return new Blob([u8arr], { type: mime }); + } + + let url = await getStage() + console.log(url) + let blob = dataURLtoBlob(url); + + let file = new File([blob], "image.png", { type: "image/png" }); + + let dataTransfer = new DataTransfer(); + dataTransfer.items.add(file); + + input.files = dataTransfer.files; + + onThumbInput(); + }); + + if (!feature.traps.gui().vmStatus.started) { + useStage.setAttribute("disabled", ""); + } + + let modal = ScratchTools.modals.create({ + title: "Set Thumbnail", + description: + "You can set the thumbnail to an image you upload or you can set it to what is currently on the stage. The project needs to have been started already in order to upload from the stage.", + components: [ + { + type: "html", + content: upload, + }, + { + type: "html", + content: useStage, + }, + { + type: "html", + content: document.createElement("br"), + }, + ], + }); + + useStage.addEventListener("click", function () { + modal.close(); + }); + + upload.addEventListener("click", function () { + modal.close(); + }); + }); + row.appendChild(button); + } + ); + + async function setThumbnail(file) { + let options = { + body: file, + headers: { + accept: "*/*", + "content-type": file.type, + "x-csrftoken": feature.auth.csrf(), + "x-requested-with": "XMLHttpRequest", + }, + referrer: window.location.href, + referrerPolicy: "strict-origin-when-cross-origin", + method: "POST", + mode: "cors", + credentials: "include", + }; + + let response = await fetch( + `https://scratch.mit.edu/internalapi/project/thumbnail/${ + window.location.pathname.split("/")[2] + }/set/`, + options + ); + + if (response.ok) { + ScratchTools.modals.create({ + title: "Successfully Set Thumbnail", + description: "This project's thumbnail has been updated.", + components: [], + }); + } else { + ScratchTools.modals.create({ + title: "Failed to Set Thumbnail", + description: "This project's thumbnail was not able to be updated.", + components: [], + }); + } + } + } + \ No newline at end of file diff --git a/features/upload-thumbnail/style.css b/features/upload-thumbnail/style.css new file mode 100644 index 00000000..d3d9ce8a --- /dev/null +++ b/features/upload-thumbnail/style.css @@ -0,0 +1,24 @@ +.ste-thumbnail::before { + background-image: var(--scratchtoolsresource-thumbnail-btn); + display: inline-block; + margin-right: 0.25rem; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + width: 0.875rem; + height: 0.875rem; + vertical-align: bottom; + content: ""; + transform: scale(1.3); +} + + +.ste-thumbnail-stage:disabled { + opacity: .5; + cursor: not-allowed !important; + background: #b5b5b5 !important; +} + +.ste-thumbnail-stage:disabled:hover { + top: 0px !important; +} \ No newline at end of file diff --git a/features/upload-thumbnail/thumbnail-btn.svg b/features/upload-thumbnail/thumbnail-btn.svg new file mode 100644 index 00000000..270f1ecb --- /dev/null +++ b/features/upload-thumbnail/thumbnail-btn.svg @@ -0,0 +1 @@ + \ No newline at end of file