soiz1's picture
Upload 2891 files
6bcb42f verified
raw
history blame
9.34 kB
export default async function ({ addon, console, msg }) {
let mode = addon.settings.get("fitting");
addon.settings.addEventListener("change", () => {
mode = addon.settings.get("fitting");
});
const createItem = (id, right) => {
const uploadMsg = msg("upload");
const wrapper = Object.assign(document.createElement("div"), { id });
const button = Object.assign(document.createElement("button"), {
className: `${addon.tab.scratchClass("action-menu_button")} ${addon.tab.scratchClass(
"action-menu_more-button"
)} sa-better-img-uploads-btn`,
currentitem: "false",
});
button.dataset.for = `sa-${id}-HD Upload`;
button.dataset.tip = uploadMsg;
const img = Object.assign(document.createElement("img"), {
className: `${addon.tab.scratchClass("action-menu_more-icon")} sa-better-img-uploader`,
draggable: "false",
src: addon.self.getResource("/icon.svg") /* rewritten by pull.js */,
height: "10",
width: "10",
});
button.append(img);
const input = Object.assign(document.createElement("input"), {
accept: ".svg, .png, .bmp, .jpg, .jpeg",
className: `${addon.tab.scratchClass(
"action-menu_file-input" /* TODO: when adding dynamicDisable, ensure compat with drag-drop */
)} sa-better-img-uploads-input`,
multiple: "true",
type: "file",
});
button.append(input);
wrapper.append(button);
const tooltip = Object.assign(document.createElement("div"), {
className: `__react_component_tooltip place-${right ? "left" : "right"} type-dark ${addon.tab.scratchClass(
"action-menu_tooltip"
)} sa-better-img-uploads-tooltip`,
id: `sa-${id}-HD Upload`,
textContent: uploadMsg,
});
tooltip.dataset.id = "tooltip";
wrapper.append(tooltip);
addon.tab.displayNoneWhileDisabled(wrapper);
return [wrapper, button, input, tooltip];
};
while (true) {
//Catch all upload menus as they are created
const spriteSelector = '[class*="sprite-selector_sprite-selector_"] [class*="action-menu_more-buttons_"]';
const stageSelector = '[class*="stage-selector_stage-selector_"] [class*="action-menu_more-buttons_"]';
const costumeSelector = '[data-tabs] > :nth-child(3) [class*="action-menu_more-buttons_"]';
let menu = await addon.tab.waitForElement(`${spriteSelector}, ${stageSelector}, ${costumeSelector}`, {
markAsSeen: true,
reduxCondition: (state) => !state.scratchGui.mode.isPlayerOnly,
reduxEvents: [
"scratch-gui/mode/SET_PLAYER",
"fontsLoaded/SET_FONTS_LOADED",
"scratch-gui/locales/SELECT_LOCALE",
"scratch-gui/navigation/ACTIVATE_TAB",
],
});
let button = menu.parentElement.previousElementSibling.previousElementSibling; //The base button that the popup menu is from
let id = button.getAttribute("aria-label").replace(/\s+/g, "_");
let isRight = //Is it on the right side of the screen?
button.parentElement.classList.contains(addon.tab.scratchClass("sprite-selector_add-button")) ||
button.parentElement.classList.contains(addon.tab.scratchClass("stage-selector_add-button"));
if (isRight) {
id += "_right";
}
const [menuItem, hdButton, input, tooltip] = createItem(id, isRight);
menu.prepend(menuItem);
hdButton.addEventListener("click", (e) => {
// When clicking on the button in the "add backdrop menu", don't switch to the stage before
// a file was selected.
e.stopPropagation();
input.files = new FileList(); //Empty the input to make sure the change event fires even if the same file was uploaded.
input.click();
});
input.addEventListener("change", (e) => {
onchange(e, id);
});
let observer = new MutationObserver(() => doresize(id, menu, menuItem, isRight));
observer.observe(menu, { attributes: true, subtree: true });
function doresize(id, menu, menuItem, isRight) {
let rect = menuItem.getBoundingClientRect();
tooltip.style.top = rect.top + 2 + "px";
tooltip.style[isRight ? "right" : "left"] = isRight
? window.innerWidth - rect.right + rect.width + 10 + "px"
: rect.left + rect.width + "px";
}
}
async function onchange(e, id) {
let iD = id; //Save the id, not sure if this is really necessary?
let el = e.target;
let files = Array.from(el.files);
let processed = new Array();
for (let file of files) {
if (file.type.includes("svg")) {
//The file is already a svg, we should not change it...
processed.push(file);
continue;
}
let blob = await new Promise((resolve) => {
//Get the Blob data url for the image so that we can add it to the svg
let reader = new FileReader();
reader.addEventListener("load", () => resolve(reader.result));
reader.readAsDataURL(file);
});
let i = new Image(); //New image to get the image's size
i.src = blob;
await new Promise((resolve) => {
i.onload = resolve;
});
let dim = { width: i.width, height: i.height };
const originalDim = JSON.parse(JSON.stringify(dim));
if (mode === "fit") {
//Make sure the image fits completely in the stage
dim = getResizedWidthHeight(dim.width, dim.height);
} else if (mode === "fill") {
//Fill the stage with the image
dim.height = (dim.height / dim.width) * 480;
dim.width = 480;
if (dim.height < 360) {
dim.width = (dim.width / dim.height) * 360;
dim.height = 360;
}
if (dim.width < 480) {
dim.height = (dim.height / dim.width) * 480;
dim.width = 480;
}
} //Otherwise just leave the image the same size
function getResizedWidthHeight(oldWidth, oldHeight) {
const STAGE_WIDTH = 479;
const STAGE_HEIGHT = 360;
const STAGE_RATIO = STAGE_WIDTH / STAGE_HEIGHT;
// If both dimensions are smaller than or equal to corresponding stage dimension,
// double both dimensions
if (oldWidth <= STAGE_WIDTH && oldHeight <= STAGE_HEIGHT) {
return { width: oldWidth, height: oldHeight };
}
// If neither dimension is larger than 2x corresponding stage dimension,
// this is an in-between image, return it as is
if (oldWidth <= STAGE_WIDTH && oldHeight <= STAGE_HEIGHT) {
return { width: oldWidth, height: oldHeight };
}
const imageRatio = oldWidth / oldHeight;
// Otherwise, figure out how to resize
if (imageRatio >= STAGE_RATIO) {
// Wide Image
return {
width: STAGE_WIDTH,
height: Math.floor(STAGE_WIDTH / imageRatio),
};
}
// In this case we have either:
// - A wide image, but not with as big a ratio between width and height,
// making it so that fitting the width to double stage size would leave
// the height too big to fit in double the stage height
// - A square image that's still larger than the double at least
// one of the stage dimensions, so pick the smaller of the two dimensions (to fit)
// - A tall image
// In any of these cases, resize the image to fit the height to double the stage height
return {
width: Math.floor(STAGE_HEIGHT * imageRatio),
height: STAGE_HEIGHT,
};
}
processed.push(
new File( //Create the svg file
[
`<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0,0,${
dim.width
},${dim.height}" width="${dim.width}" height="${dim.height}">
<g>
<g
data-paper-data='{"isPaintingLayer":true}'
fill="none"
fill-rule="nonzero"
stroke="none"
stroke-width="0.5"
stroke-linecap="butt"
stroke-linejoin="miter"
stroke-miterlimit="10"
stroke-dasharray=""
stroke-dashoffset="0"
style="mix-blend-mode: normal;"
>
<image
width="${originalDim.width}"
height="${originalDim.height}"
transform="scale(${dim.width / originalDim.width},${dim.height / originalDim.height})"
xlink:href="${blob}"
/>
</g>
</g>
</svg>`,
],
`${file.name.replace(/(.*)\..*/, "$1")}.svg`,
{
type: "image/svg+xml",
}
)
);
}
(el = document.getElementById(iD).nextElementSibling.querySelector("input")).files = new FileList(processed); //Convert processed image array to a FileList, which is not normally constructible.
el.dispatchEvent(new e.constructor(e.type, e)); //Start a new, duplicate, event, but allow scratch to receive it this time.
}
function FileList(arr = []) {
//File list constructor. Does not need the `new` keyword, but it is easier to read
let filelist = new DataTransfer(); //This "creates" a FileList that we can add files to
for (let file of arr) {
filelist.items.add(file);
}
return filelist.files; //Completed FileList
}
}