Skip to content

Commit ac2af72

Browse files
committed
Thumbnail setter
1 parent 9db919a commit ac2af72

File tree

6 files changed

+208
-0
lines changed

6 files changed

+208
-0
lines changed

api/modals.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ ScratchTools.modals = {
3232
var code = document.createElement("code");
3333
code.textContent = component.content;
3434
modal.appendChild(code);
35+
} else if (component.type === "html") {
36+
modal.appendChild(component.content);
3537
}
3638
});
3739

@@ -45,5 +47,11 @@ ScratchTools.modals = {
4547
div.appendChild(modal);
4648
modal.prepend(orangeBar);
4749
document.body.appendChild(div);
50+
51+
return {
52+
close: function () {
53+
div.remove();
54+
},
55+
};
4856
},
4957
};

features/features.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[
2+
{
3+
"version": 2,
4+
"id": "upload-thumbnail",
5+
"versionAdded": "v4.0.0"
6+
},
27
{
38
"version": 2,
49
"id": "paint-align",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"title": "Set Thumbnail",
3+
"description": "Allows you to upload an image or GIF as a project thumbnail, or set the thumbnail to the current stage.",
4+
"credits": [
5+
{
6+
"username": "Sanjang_Beta",
7+
"url": "https://scratch.mit.edu/users/Sanjang_Beta/"
8+
},
9+
{ "username": "rgantzos", "url": "https://scratch.mit.edu/users/rgantzos/" }
10+
],
11+
"type": ["Website"],
12+
"tags": ["New", "Recommended"],
13+
"scripts": [{ "file": "script.js", "runOn": "/projects/*" }],
14+
"styles": [{ "file": "style.css", "runOn": "/projects/*" }],
15+
"resources": [{ "name": "thumbnail-btn", "path": "/thumbnail-btn.svg" }],
16+
"dynamic": true
17+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
export default async function ({ feature, console }) {
2+
ScratchTools.waitForElements(
3+
".preview .inner .flex-row.action-buttons",
4+
async function (row) {
5+
if (feature.redux.getState()?.preview.projectInfo.author.username !== feature.redux.getState()?.session?.session?.user?.username) return;
6+
7+
if (row.querySelector(".ste-thumbnail")) return;
8+
let button = document.createElement("button");
9+
button.className = "button action-button ste-thumbnail";
10+
button.textContent = "Set Thumbnail";
11+
feature.self.hideOnDisable(button);
12+
13+
let input = document.createElement("input");
14+
input.className = "ste-thumbnail-input";
15+
input.style.display = "none";
16+
input.type = "file";
17+
input.accept = "image/*";
18+
input.addEventListener("input", onThumbInput);
19+
document.body.appendChild(input);
20+
21+
function onThumbInput() {
22+
if (input.files?.[0]) {
23+
setThumbnail(input.files[0]);
24+
}
25+
}
26+
27+
button.addEventListener("click", async function () {
28+
let upload = document.createElement("button");
29+
upload.textContent = "Upload Image or GIF";
30+
upload.style.marginRight = ".5rem";
31+
upload.addEventListener("click", function () {
32+
input.click();
33+
});
34+
35+
async function getStage() {
36+
return new Promise((resolve) => {
37+
feature.traps.vm.postIOData("video", {
38+
forceTransparentPreview: true,
39+
});
40+
feature.traps.vm.renderer.requestSnapshot((dataURL) => {
41+
feature.traps.vm.postIOData("video", {
42+
forceTransparentPreview: false,
43+
});
44+
resolve(dataURL);
45+
});
46+
});
47+
}
48+
49+
let useStage = document.createElement("button");
50+
useStage.textContent = "Use Stage";
51+
useStage.className = "ste-thumbnail-stage";
52+
useStage.addEventListener("click", async function () {
53+
function dataURLtoBlob(dataurl) {
54+
let arr = dataurl.split(",");
55+
let mime = arr[0].match(/:(.*?);/)[1];
56+
let bstr = atob(arr[1]);
57+
let n = bstr.length;
58+
let u8arr = new Uint8Array(n);
59+
while (n--) {
60+
u8arr[n] = bstr.charCodeAt(n);
61+
}
62+
return new Blob([u8arr], { type: mime });
63+
}
64+
65+
let url = await getStage()
66+
console.log(url)
67+
let blob = dataURLtoBlob(url);
68+
69+
let file = new File([blob], "image.png", { type: "image/png" });
70+
71+
let dataTransfer = new DataTransfer();
72+
dataTransfer.items.add(file);
73+
74+
input.files = dataTransfer.files;
75+
76+
onThumbInput();
77+
});
78+
79+
if (!feature.traps.gui().vmStatus.started) {
80+
useStage.setAttribute("disabled", "");
81+
}
82+
83+
let modal = ScratchTools.modals.create({
84+
title: "Set Thumbnail",
85+
description:
86+
"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.",
87+
components: [
88+
{
89+
type: "html",
90+
content: upload,
91+
},
92+
{
93+
type: "html",
94+
content: useStage,
95+
},
96+
{
97+
type: "html",
98+
content: document.createElement("br"),
99+
},
100+
],
101+
});
102+
103+
useStage.addEventListener("click", function () {
104+
modal.close();
105+
});
106+
107+
upload.addEventListener("click", function () {
108+
modal.close();
109+
});
110+
});
111+
row.appendChild(button);
112+
}
113+
);
114+
115+
async function setThumbnail(file) {
116+
let options = {
117+
body: file,
118+
headers: {
119+
accept: "*/*",
120+
"content-type": file.type,
121+
"x-csrftoken": feature.auth.csrf(),
122+
"x-requested-with": "XMLHttpRequest",
123+
},
124+
referrer: window.location.href,
125+
referrerPolicy: "strict-origin-when-cross-origin",
126+
method: "POST",
127+
mode: "cors",
128+
credentials: "include",
129+
};
130+
131+
let response = await fetch(
132+
`https://scratch.mit.edu/internalapi/project/thumbnail/${
133+
window.location.pathname.split("/")[2]
134+
}/set/`,
135+
options
136+
);
137+
138+
if (response.ok) {
139+
ScratchTools.modals.create({
140+
title: "Successfully Set Thumbnail",
141+
description: "This project's thumbnail has been updated.",
142+
components: [],
143+
});
144+
} else {
145+
ScratchTools.modals.create({
146+
title: "Failed to Set Thumbnail",
147+
description: "This project's thumbnail was not able to be updated.",
148+
components: [],
149+
});
150+
}
151+
}
152+
}
153+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.ste-thumbnail::before {
2+
background-image: var(--scratchtoolsresource-thumbnail-btn);
3+
display: inline-block;
4+
margin-right: 0.25rem;
5+
background-repeat: no-repeat;
6+
background-position: center center;
7+
background-size: contain;
8+
width: 0.875rem;
9+
height: 0.875rem;
10+
vertical-align: bottom;
11+
content: "";
12+
transform: scale(1.3);
13+
}
14+
15+
16+
.ste-thumbnail-stage:disabled {
17+
opacity: .5;
18+
cursor: not-allowed !important;
19+
background: #b5b5b5 !important;
20+
}
21+
22+
.ste-thumbnail-stage:disabled:hover {
23+
top: 0px !important;
24+
}
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)