From 4519d2f6b458fc8362aa67773b615ce40aed1c00 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:20:31 -0800 Subject: [PATCH 1/8] Update files.js --- extensions/files.js | 929 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 858 insertions(+), 71 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index f21616f395..c0635b2f3e 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -1,6 +1,7 @@ // Name: Files // ID: files -// Description: Read and download files. +// Description: Read, upload, and download files. +// By: GarboMuffin, SharkPool, Drago Cuven, 0znzw // License: MIT AND MPL-2.0 (function (Scratch) { @@ -10,6 +11,16 @@ throw new Error("files extension must be run unsandboxed"); } + const menuIconURI = +"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMzMuMTY0IiBoZWlnaHQ9IjEzMy4xNjQiIHZpZXdCb3g9IjAgMCAxMzMuMTY0IDEzMy4xNjQiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTMgNjYuNTgyQzMgMzEuNDY3IDMxLjQ2NyAzIDY2LjU4MiAzczYzLjU4MiAyOC40NjcgNjMuNTgyIDYzLjU4Mi0yOC40NjcgNjMuNTgyLTYzLjU4MiA2My41ODJTMyAxMDEuNjk3IDMgNjYuNTgyeiIgZmlsbD0iI2ZjYjEwMyIgc3Ryb2tlPSIjYmY4YjExIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNOTkuODkyIDQ5LjkyN3Y0OS45NjRjMCA0LjU4LTMuNzQ4IDguMzI4LTguMzI4IDguMzI4SDQxLjU1OGMtNC41OCAwLTguMjg1LTMuNzQ4LTguMjg1LTguMzI4bC4wNDEtNjYuNjE4YzAtNC41OCAzLjcwNi04LjMyOCA4LjI4Ni04LjMyOGgzMy4zMXoiIGZpbGw9Im5vbmUiIHN0cm9rZS1vcGFjaXR5PSIuMTQ5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNOTkuODkyIDQ5LjkyN3Y0OS45NjRjMCA0LjU4LTMuNzQ4IDguMzI4LTguMzI4IDguMzI4SDQxLjU1OGMtNC41OCAwLTguMjg1LTMuNzQ4LTguMjg1LTguMzI4bC4wNDEtNjYuNjE4YzAtNC41OCAzLjcwNi04LjMyOCA4LjI4Ni04LjMyOGgzMy4zMXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNNzAuNzIyIDU0LjExNVYzMS4xNjdsMjIuOTQ3IDIyLjk0OHoiIGZpbGw9IiNmY2IxMDMiLz48cGF0aCBkPSJNODQuNjY4IDY5LjkxNGMtLjAyLjA4OC0uMDM2LjE3NS0uMDYyLjI2Mi0uMDc3LjMyNC0uMjM2IDEuMDktLjM5NiAyLjU4N3EuMDAxLjAzOC0uMDA1LjA2MWMuODggMy41MzgtLjYwMiA1LjY5Mi0xLjU4NCA2LjY3NGEyIDIgMCAwIDEtLjIuMTljLS45NjIuODc1LTIuNjQzIDEuOTE4LTUuMTUyIDEuOTE4YTcuNjUgNy42NSAwIDAgMS0zLjQzNS0uNzg2Yy4wMjYgMS40MDMuMDQxIDMuMzM3LjA0MSA2LjAyYTYuOSA2LjkgMCAwIDEgMi40OTQgMS41MzggNy4xIDcuMSAwIDAgMSAyLjIxIDUuMTY3YzAgMi45MS0xLjY3NSA1LjQ0LTQuMzc1IDYuNTk3cS0uMTAzLjA0OC0uMjA1LjA4OGMtLjkwNS4zNDQtMS45MjMuNTE0LTMuMTE2LjUxNC0uNDMyIDAtLjg5NS0uMDItMS4zOTktLjA2N2ExOCAxOCAwIDAgMC0xLjI5NS4wMDVjLS45NzIuMTA4LTIuMzQuMjE2LTQuMTcuMzE5LS4wMzYgMC0uMDcyLjAwNS0uMTE0LjAwNXEtLjM2LjAxNS0uNjk5LjAxNWMtMy4yNzUgMC01LjY2Ni0xLjAzOC03LjExLTMuMDhxLS4wNDYtLjA2LS4wODMtLjEyM2MtMS40Ny0yLjE5NS0xLjUxMi00Ljk3Mi0uMTEzLTcuMjRxLjAxNy0uMDM1LjA0MS0uMDcxYy44OS0xLjM5OSAyLjE5LTIuMzk2IDMuODc3LTIuOTgzLjAyLS45OTIuMDE1LTIuMjM2LS4wMTYtMy43MTJhNy41IDcuNSAwIDAgMS0yLjg5LjU1NWMtMi44NjMgMC01LjIyOC0xLjcwMS02LjA1MS00LjI4OC0uMTctLjUyNC0uMzMtMS4yOS0uNy0zLjAxOGEyIDIgMCAwIDEtLjAzNS0uMTg1bC0uNzYxLTQuMjUyYTcgNyAwIDAgMC0uMTAzLS4zMjRjLS4zOTYtMS4xMjYtLjU4MS0yLjA4My0uNTgxLTMuMDA4IDAtMS4xNDcuMzM0LTIuODggMS45MTgtNC42MDcuNzk3LS44NzQgMi4yNDctMi4wMDUgNC41ODEtMi4zN3EuNDMzLS4wNzEuODc0LS4wNzJoNS42ODdxLjE5MiAwIC4zODUuMDE1YTcyIDcyIDAgMCAwIDUuNzgtLjAwNSA4MyA4MyAwIDAgMCA2LjkzLS41OTZjLjI1OC0uMDgzLjUzNi0uMTY1LjgzNC0uMjM3LjI4My0uMDcyLjU3LS4xMTguODU4LS4xNSAyLjE5LS4yIDQuMjMyLjQzMyA1LjgyIDEuNzkgMS45NyAxLjY5MiAyLjg0IDQuMjUyIDIuMzIgNi44NTQiIGZpbGw9IiNiZjhiMTEiLz48cGF0aCBkPSJNNzkuMTIxIDY4LjgwNnEtLjMxNiAxLjI2Ni0uNTQzIDMuNDM3LS4wOSAxLjA4Ni4wOSAxLjc2NC4zMTYgMS4xMzEtLjA0NCAxLjQ5My0uNDk5LjQ1MS0xLjM1Ny40NTItLjk5NiAwLTEuMzEyLS41OTJBNDEgNDEgMCAwIDEgNzYgNzEuOTQzcS4wOS0xLjk2LjA5LTIuMDUtLjA0NSAwLS4xMzUtLjA5Mi00LjIwNy4xODMtNy45MTUuNTQ1LS4wOS40NTMgMCAxLjQ1LjE4IDEuNDA0LjE4IDEuNjMtLjE4IDEuNDA2LS4xOCA0LjE3LjE4IDEuMTc5LjE4IDEwLjEwNnYzLjMwOXEwIC43Ny4yMjUgMS4wODdoMi43NzdxLjc2Mi0uMDkgMS4yMzIuMzYyLjQ3LjQ1My40NyAxLjA4NSAwIC45OTQtLjk1IDEuNDAzLS41ODguMjI1LTIuMDguMDktLjcyMy0uMDQ1LTIuMTI2IDAtMS4yNjUuMTU3LTQuMDcuMzE2LTIuNDQzLjA5LTIuOTg1LS42NzgtLjM2My0uNTQyIDAtMS4xMy42MzItLjk5NSAzLjMwMi0uOTk1Ljk0OCAwIDEuMTc1LS4xNTguMjI2LS4xNi4yMjYtLjYxVjg5LjAzcS4xMzUtMi4zMDIgMC02Ljg2MS0uMTgtNi41NDQuMTgxLTExLjUwOWwtLjEzNy0uMTM2cS0xLjYzNy4wOS01LjI3NS0uMDktLjQxIDAtMi43NzQuMTgxLjU4NyA0LjExNy43MjMgNi4xOTYgMCAuMjcyLS4wOSAxLjIyMS0uMDQ2LjY3OC0xLjEzMS42NzktLjU4OSAwLS42NzgtLjQwNC0uMDQ2LS4wOS0uNTQzLTIuNDIybC0uNzY5LTQuMzA2cTAtLjE3OS0uMzE2LTEuMTIxLS4yNy0uNzY0LS4yNy0xLjE2Ni0uMDAxLS4zMTUuNDMtLjc4NS40My0uNDcxIDEuMjg5LS42MDZoNS42MDhxMy42NjMuMTM1IDYuNTEyIDBhODggODggMCAwIDAgOC4wNS0uNzI0cS4yNzMtLjEzNS44MTQtLjI3Ljk5NS0uMDkgMS42MjkuNDUyLjYzMy41NC40NTIgMS40NDciIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"; + + const vm = Scratch.vm; + const runtime = vm.runtime; + + const builtInFonts = [ + "Sans Serif", "Serif", "Handwriting", + "Marker", "Curly", "Pixel", "Scratch", "inherit" + ]; const MODE_MODAL = "modal"; const MODE_IMMEDIATELY_SHOW_SELECTOR = "selector"; const MODE_ONLY_SELECTOR = "only-selector"; @@ -18,10 +29,20 @@ MODE_IMMEDIATELY_SHOW_SELECTOR, MODE_ONLY_SELECTOR, ]; - let openFileSelectorMode = MODE_MODAL; + const AS_TEXT = "text", AS_DATA_URL = "url", AS_HEX = "hex"; + const AS_BASE64 = "base64", AS_BUFFER = "arrayBuffer"; - const AS_TEXT = "text"; - const AS_DATA_URL = "url"; + let enableVis = true; + let openFileSelectorMode = MODE_MODAL; + let storedFiles = {}; + let FileName = "", FileSize = "0kb", RawFileSize = "0", fileDate = "", lastData = ""; + let openModals = 0; + let selectorOptions = { + borderColor: "#888", textColor: "#000", outer: "#fff", + sizeFont: 1.5, borderRadius: 16, borderType: "dashed", + font: "inherit", shadow: 0.5, image: "", + textV: "", fontWeight: 40, letterSpacing: "normal" + }; /** * @param {HTMLInputElement} input @@ -40,10 +61,11 @@ /** * @param {string} accept See MODE_ constants above * @param {string} as See AS_ constants above + * @param {boolean} override makes modal use the File API if true * @returns {Promise} format given by as parameter */ - const showFilePrompt = (accept, as) => - new Promise((_resolve) => { + const showFilePrompt = async (accept, as, override) => + new Promise(async (_resolve) => { // We can't reliably show an picker without "user interaction" in all environments, // so we have to show our own UI anyways. We may as well use this to implement some nice features // that native file pickers don't have: @@ -54,14 +76,25 @@ /** @param {string} text */ const callback = (text) => { - _resolve(text); - Scratch.vm.renderer.removeOverlay(outer); - Scratch.vm.runtime.off("PROJECT_STOP_ALL", handleProjectStopped); + let cleansedTxt = text; + if (override === undefined) { + if ([AS_HEX, AS_BASE64].includes(as)) { + let uri = cleansedTxt.split(","); + cleansedTxt = uri.splice(1, uri.length).join(","); + if (as === AS_HEX) cleansedTxt = base64ToHex(cleansedTxt, " "); + } + lastData = cleansedTxt; + } + openModals--; + _resolve(cleansedTxt); + vm.renderer.removeOverlay(outer); + runtime.off("PROJECT_STOP_ALL", handleProjectStopped); document.body.removeEventListener("keydown", handleKeyDown, { capture: true, }); }; + openModals++; let isReadingFile = false; /** @param {File} file */ @@ -73,17 +106,21 @@ const reader = new FileReader(); reader.onload = () => { + FileName = file.name; + FileSize = formatFileSize(file.size); + RawFileSize = file.size; + const rawDate = new Date(file.lastModified); + fileDate = rawDate.toLocaleString(); callback(/** @type {string} */ (reader.result)); }; reader.onerror = () => { console.error("Failed to read file as text", reader.error); callback(""); }; - if (as === AS_TEXT) { - reader.readAsText(file); - } else { - reader.readAsDataURL(file); - } + if (override !== undefined) callback(file); + else if (as === AS_TEXT) reader.readAsText(file); + else if (as === AS_BUFFER) reader.readAsArrayBuffer(file); + else reader.readAsDataURL(file); }; /** @param {KeyboardEvent} e */ @@ -101,9 +138,32 @@ const handleProjectStopped = () => { callback(""); }; - Scratch.vm.runtime.on("PROJECT_STOP_ALL", handleProjectStopped); + runtime.on("PROJECT_STOP_ALL", handleProjectStopped); + + const handleOverride = async () => { + let fileInfo; + if (override === undefined) { + // execute normal behaviour + input.click(); + } else { + try { + if (override === "folder") { + fileInfo = await window.showDirectoryPicker({ + multiple: false, types: [{ accept: { "*/*" : [] }}] + }); + } else { + fileInfo = await window.showOpenFilePicker({ + multiple: false, types: [{ accept: { "*/*" : accept }}] + }); + } + callback(fileInfo); + } catch { + callback("") + } + } + }; - const INITIAL_BORDER_COLOR = "#888"; + const INITIAL_BORDER_COLOR = selectorOptions.borderColor; const DROPPING_BORDER_COLOR = "#03a9fc"; const outer = document.createElement("div"); @@ -113,26 +173,27 @@ outer.style.display = "flex"; outer.style.alignItems = "center"; outer.style.justifyContent = "center"; - outer.style.background = "rgba(0, 0, 0, 0.5)"; - outer.style.color = "black"; outer.style.colorScheme = "light"; - outer.addEventListener("dragover", (e) => { - if (e.dataTransfer.types.includes("Files")) { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - modal.style.borderColor = DROPPING_BORDER_COLOR; - } - }); - outer.addEventListener("dragleave", () => { - modal.style.borderColor = INITIAL_BORDER_COLOR; - }); - outer.addEventListener("drop", (e) => { - const file = e.dataTransfer.files[0]; - if (file) { - e.preventDefault(); - readFile(file); - } - }); + if (override === undefined) { + // the File API cant exactly support dragging files + outer.addEventListener("dragover", (e) => { + if (e.dataTransfer.types.includes("Files")) { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + modal.style.borderColor = DROPPING_BORDER_COLOR; + } + }); + outer.addEventListener("dragleave", () => { + modal.style.borderColor = INITIAL_BORDER_COLOR; + }); + outer.addEventListener("drop", (e) => { + const file = e.dataTransfer.files[0]; + if (file) { + e.preventDefault(); + readFile(file); + } + }); + } outer.addEventListener("click", (e) => { if (e.target === outer) { callback(""); @@ -140,17 +201,17 @@ }); const modal = document.createElement("button"); + modal.id = "tw-files-modal"; modal.style.boxShadow = "0 0 10px -5px currentColor"; modal.style.cursor = "pointer"; - modal.style.font = "inherit"; - modal.style.background = "white"; + modal.style.backgroundSize = "cover"; modal.style.padding = "16px"; - modal.style.borderRadius = "16px"; modal.style.border = `8px dashed ${INITIAL_BORDER_COLOR}`; modal.style.position = "relative"; modal.style.textAlign = "center"; - modal.addEventListener("click", () => { - input.click(); + modal.addEventListener("click", async () => { + // will execute "input.click()" if override is "undefined" + await handleOverride(); }); modal.focus(); outer.appendChild(modal); @@ -158,17 +219,18 @@ const input = document.createElement("input"); input.type = "file"; input.accept = accept; - input.addEventListener("change", (e) => { - // @ts-expect-error - const file = e.target.files[0]; - if (file) { - readFile(file); - } - }); + if (override === undefined) { + input.addEventListener("change", (e) => { + // @ts-expect-error + const file = e.target.files[0]; + if (file) { + readFile(file); + } + }); + } const title = document.createElement("div"); - title.textContent = Scratch.translate("Select or drop file"); - title.style.fontSize = "1.5em"; + title.textContent = override === undefined ? Scratch.translate("Select or drop file") : Scratch.translate("Select file"); title.style.marginBottom = "8px"; modal.appendChild(title); @@ -196,7 +258,7 @@ } if (openFileSelectorMode !== MODE_ONLY_SELECTOR) { - const overlay = Scratch.vm.renderer.addOverlay(outer, "scale"); + const overlay = vm.renderer.addOverlay(outer, "scale"); overlay.container.style.zIndex = "100"; } @@ -204,7 +266,8 @@ openFileSelectorMode === MODE_IMMEDIATELY_SHOW_SELECTOR || openFileSelectorMode === MODE_ONLY_SELECTOR ) { - input.click(); + // will run "input.click()" if override is "undefined" + await handleOverride(); } if (openFileSelectorMode === MODE_ONLY_SELECTOR) { @@ -213,7 +276,37 @@ callback(""); }); } + + updateModalVisuals(); + }); + + const updateModalVisuals = () => { + const allModals = document.querySelectorAll(`button[id="tw-files-modal"]`); + allModals.forEach(modal => { + modal.parentNode.style.background = `rgba(0, 0, 0, ${selectorOptions.shadow})`; + modal.parentNode.style.color = selectorOptions.textColor; + + modal.style.font = selectorOptions.font; + modal.style.fontFamily = selectorOptions.font; + modal.style.background = selectorOptions.image ? selectorOptions.image : selectorOptions.outer; + modal.style.borderRadius = `${selectorOptions.borderRadius}px`; + modal.style.borderStyle = selectorOptions.borderType; + modal.style.borderColor = selectorOptions.borderColor; + + const title = modal.children[0]; + if (selectorOptions.textV) title.textContent = selectorOptions.textV; + title.style.color = selectorOptions.textColor; + title.style.fontSize = `${selectorOptions.sizeFont}em`; + title.style.fontWeight = selectorOptions.fontWeight * 9; + title.style.letterSpacing = `${selectorOptions.letterSpacing}px`; + + const subtitle = modal.children[1]; + subtitle.style.color = selectorOptions.textColor; + subtitle.style.fontSize = `${selectorOptions.sizeFont - 0.5}em`; + subtitle.style.fontWeight = selectorOptions.fontWeight * 9; + subtitle.style.letterSpacing = `${selectorOptions.letterSpacing}px`; }); + }; /** * @param {Blob} blob Data to download @@ -259,15 +352,63 @@ await downloadBlob(blob, file); }; + /** + * @param {number} size + * @returns {string} formatted byte size + */ + const formatFileSize = (size) => { + const units = ["B", "KB", "MB", "GB", "TB"]; + let i = 0; + while (size >= 1024 && i < units.length - 1) { + size /= 1024; + i++; + } + return `${size.toFixed(2)} ${units[i]}`; + }; + + /** + * @param {string} str + * @param {string} delim + * @returns {string} hex split by delimiter + */ + function base64ToHex(str, delim) { + const raw = atob(str); + let result = ""; + for (let i = 0; i < raw.length; i++) { + const hex = raw.charCodeAt(i).toString(16); + result += delim.toString() + (hex.length === 2 ? hex : "0" + hex); + } + return result.toUpperCase(); + } + + /** + * @param {Uint8Array} buffer + * @returns {string} base64 + */ + function bufferToBase64(buffer) { + var binary = ""; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]) + } + return btoa(binary); + } + class Files { + constructor() { + this._showUnsafeOptions = false; + } getInfo() { return { id: "files", name: Scratch.translate("Files"), + menuIconURI, color1: "#fcb103", color2: "#db9a37", color3: "#db8937", blocks: [ + { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Uploading") }, { opcode: "showPicker", blockType: Scratch.BlockType.REPORTER, @@ -279,15 +420,14 @@ opcode: "showPickerExtensions", blockType: Scratch.BlockType.REPORTER, text: Scratch.translate("open a [extension] file"), + hideFromPalette: true, arguments: { extension: { type: Scratch.ArgumentType.STRING, defaultValue: ".txt", }, }, - hideFromPalette: true, }, - { opcode: "showPickerAs", blockType: Scratch.BlockType.REPORTER, @@ -314,9 +454,7 @@ }, }, }, - - "---", - + { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Downloading") }, { opcode: "download", blockType: Scratch.BlockType.COMMAND, @@ -347,9 +485,7 @@ }, }, }, - - "---", - + { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Extra") }, { opcode: "setOpenMode", blockType: Scratch.BlockType.COMMAND, @@ -358,24 +494,252 @@ mode: { type: Scratch.ArgumentType.STRING, defaultValue: MODE_MODAL, - menu: "automaticallyOpen", + menu: "automaticallyOpen" + }, + }, + }, + { + opcode: "fileInfo", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("last opened file [FORMAT]"), + arguments: { + FORMAT: { + type: Scratch.ArgumentType.STRING, + menu: "FILE_INFO", + }, + }, + }, + "---", + { + opcode: "modalOpen", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is modal open?") + }, + { + opcode: "findFileSize", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("[TYPE] file size of [FILE]"), + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "FILE_SIZES" + }, + FILE: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("Hello, world!"), + }, + }, + }, + { blockType: Scratch.BlockType.LABEL, text: "Stored Files" }, + { + opcode: "checkFileAPI", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("is file writing supported?"), + }, + { + opcode: "allStored", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("all stored files"), + }, + { + opcode: "setStoredFile", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("open new stored [FILE] file named [NAME] as [TYPE]"), + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("my-file-1"), + }, + FILE: { + type: Scratch.ArgumentType.STRING, + defaultValue: ".txt", + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "encoding", + }, + }, + }, + { + opcode: "storedFolder", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("open folder and store files as [TYPE] with name [NAME]"), + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "encoding", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("my-folder-1"), + }, + }, + }, + { + opcode: "deleteStoredFile", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("delete file [NAME] from [OPTION]"), + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("my-file-1"), + }, + OPTION: { + type: Scratch.ArgumentType.STRING, + menu: "DELETION", + }, + }, + }, + "---", + { + opcode: "updateFile", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("write [TXT] to stored file [NAME]"), + arguments: { + TXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("new content"), + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("my-file-1"), + }, + }, + }, + { + opcode: "storedInfo", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("[FORMAT] in stored file [NAME]"), + arguments: { + FORMAT: { + type: Scratch.ArgumentType.STRING, + menu: "FILE_INFO", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("my-file-1"), + }, + }, + }, + "---", + { + opcode: "moveStorage", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("migrate files to CST's ZIP Extension"), + }, + { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Visuals") }, + { + func: "toggleVis", + blockType: Scratch.BlockType.BUTTON, + text: Scratch.translate(`${enableVis ? "En" : "Dis"}able Customization`), + }, + { + opcode: "resetStyle", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("reset selector style to default"), + hideFromPalette: enableVis + }, + "---", + { + opcode: "borderColors", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set selector [OPTION] color to [COLOR]"), + hideFromPalette: enableVis, + arguments: { + OPTION: { + type: Scratch.ArgumentType.STRING, + menu: "visualColors", + }, + COLOR: { + type: Scratch.ArgumentType.COLOR, + }, + }, + }, + { + opcode: "visualsSelect", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set selector [OPTION] to [AMT]"), + hideFromPalette: enableVis, + arguments: { + OPTION: { + type: Scratch.ArgumentType.STRING, + menu: "visualOptions", + }, + AMT: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 15, + }, + }, + }, + { + opcode: "imageSet", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set selector image to [IMG]"), + hideFromPalette: enableVis, + arguments: { + IMG: { + type: Scratch.ArgumentType.STRING, + defaultValue: "https://extensions.turbowarp.org/dango.png", + }, + }, + }, + { + opcode: "borderTypeSet", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set selector border type to [TYPE]"), + hideFromPalette: enableVis, + arguments: { + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: "borderTypes", + }, + }, + }, + { + opcode: "fontSet", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set selector font to [FONT]"), + hideFromPalette: enableVis, + arguments: { + FONT: { + type: Scratch.ArgumentType.STRING, + menu: "font", + }, + }, + }, + { + opcode: "textSet", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("set file selector text to [TEXT]"), + hideFromPalette: enableVis, + arguments: { + TEXT: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("Insert File Here"), + }, + }, + }, + { + opcode: "currentX", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("current selector [THING]"), + hideFromPalette: enableVis, + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "modalVisuals", }, }, }, ], menus: { + font: { + acceptReporters: true, + items: "getFonts", + }, encoding: { acceptReporters: true, - items: [ - { - text: Scratch.translate("text"), - value: AS_TEXT, - }, - { - text: "data: URL", - value: AS_DATA_URL, - }, - ], + items: "getEncodings", }, automaticallyOpen: { acceptReporters: true, @@ -395,10 +759,241 @@ }, ], }, + modalVisuals: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("border color"), + value: "border" + }, + { + text: Scratch.translate("text color"), + value: "text" + }, + { + text: Scratch.translate("background color"), + value: "outer" + }, + { + text: Scratch.translate("overlay opacity"), + value: "shadow" + }, + { + text: Scratch.translate("font"), + value: "font" + }, + { + text: Scratch.translate("font size"), + value: "sizeFont" + }, + { + text: Scratch.translate("font thickness"), + value: "fontWeight" + }, + { + text: Scratch.translate("letter spacing"), + value: "letterSpacing" + }, + { + text: Scratch.translate("border radius"), + value: "borderRadius" + }, + { + text: Scratch.translate("border type"), + value: "borderType" + }, + { + text: Scratch.translate("background image"), + value: "image" + }, + { + text: Scratch.translate("text"), + value: "textV" + }, + ], + }, + FILE_SIZES: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("formatted"), + value: "formatted" + }, + { + text: Scratch.translate("unformatted"), + value: "unformatted" + }, + ], + }, + FILE_INFO: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("data"), + value: "data" + }, + { + text: Scratch.translate("name"), + value: "name" + }, + { + text: Scratch.translate("modified date"), + value: "modified date" + }, + { + text: Scratch.translate("size formatted"), + value: "size formatted" + }, + { + text: Scratch.translate("size unformatted"), + value: "size unformatted" + }, + ], + }, + DELETION: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("storage"), + value: "storage" + }, + { + text: Scratch.translate("this device"), + value: "this device" + }, + { + text: Scratch.translate("both"), + value: "both" + }, + ], + }, + visualColors: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("border"), + value: "border" + }, + { + text: Scratch.translate("text"), + value: "text" + }, + { + text: Scratch.translate("background"), + value: "background" + }, + ], + }, + visualOptions: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("font size"), + value: "font size" + }, + { + text: Scratch.translate("font thickness"), + value: "font thickness" + }, + { + text: Scratch.translate("letter spacing"), + value: "letter spacing" + }, + { + text: Scratch.translate("border radius"), + value: "border radius" + }, + { + text: Scratch.translate("overlay opacity"), + value: "overlay opacity" + }, + ], + }, + borderTypes: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("dotted"), + value: "dotted" + }, + { + text: Scratch.translate("dashed"), + value: "dashed" + }, + { + text: Scratch.translate("solid"), + value: "solid" + }, + { + text: Scratch.translate("double"), + value: "double" + }, + { + text: Scratch.translate("groove"), + value: "groove" + }, + { + text: Scratch.translate("ridge"), + value: "ridge" + }, + { + text: Scratch.translate("inset"), + value: "inset" + }, + { + text: Scratch.translate("outset"), + value: "outset" + }, + { + text: Scratch.translate("none"), + value: "none" + }, + ], + }, }, }; } + // Helper Funcs + getFonts() { + const customFonts = runtime.fontManager ? runtime.fontManager.getFonts().map((i) => ({ text: i.name, value: i.family })) : []; + return [ ...builtInFonts, ...customFonts ]; + } + + getEncodings(onlySafe) { + const types = [ + { text: Scratch.translate("text"), value: AS_TEXT }, { text: "data: URL", value: AS_DATA_URL }, + { text: "base64", value: AS_BASE64 }, { text: "hex", value: AS_HEX } + ]; + if (this._showUnsafeOptions) types.push({ text: "arrayBuffer", value: AS_BUFFER }); + return types; + } + + toggleVis() { + enableVis = enableVis ? false : true, vm.extensionManager.refreshBlocks(); + } + + updateStore(name, data, metaData) { + const rawDate = new Date(metaData.lastModified); + storedFiles[name].data = { + size: metaData.size, sizeFormat: formatFileSize(metaData.size), + dateFormat: rawDate.toLocaleString(), data + }; + } + + async encodeData(meta, format) { + const text = await meta.text(); + const buffer = await meta.arrayBuffer(); + if (format === AS_TEXT) return text; + else if (format === AS_BUFFER) return buffer; + const base64 = bufferToBase64(buffer); + if (format === AS_BASE64) return base64; + else if (format === AS_DATA_URL) return `data:${meta.type};charset=utf-8;base64,${base64}`; + else if (format === AS_HEX) return base64ToHex(base64, " "); + return text; + } + + // Block Funcs (Upload & Download) showPicker() { return showFilePrompt("", AS_TEXT); } @@ -444,7 +1039,199 @@ console.warn(`unknown mode`, args.mode); } } + + fileInfo(args) { + if (args.FORMAT === "size formatted") return FileSize; + else if (args.FORMAT === "size unformatted") return RawFileSize; + else if (args.FORMAT === "modified date") return fileDate; + else if (args.FORMAT === "data") return lastData; + return FileName; + } + + modalOpen() { + return openModals !== 0; + } + + // File Writing & Folders + checkFileAPI() { + return "showOpenFilePicker" in window; + } + + allStored() { + return JSON.stringify(Object.keys(storedFiles)); + } + + async setStoredFile(args) { + if (!this.checkFileAPI()) return; + let fileTypes = args.FILE ? args.FILE.split(" ") : []; + try { + const picker = await showFilePrompt(fileTypes, "", "window"); + if (!picker) return; + storedFiles[args.NAME] = { file: picker[0], data: {} }; + const metaData = await picker[0].getFile(); + const encodedData = await this.encodeData(metaData, args.TYPE); + this.updateStore(args.NAME, encodedData, metaData); + } catch(e) { + console.warn(e); + } + } + + async storedFolder(args) { + if (!this.checkFileAPI()) return; + try { + const picker = await showFilePrompt("Folder", "", "folder"); + if (!picker) return; + const entries = picker.entries(); + const folderN = args.NAME ? args.NAME : picker.name; + let thisFile = ""; + while (thisFile !== undefined) { + const outerData = await entries.next(); + thisFile = outerData.value; + if (thisFile !== undefined) { + const innerData = thisFile[1]; + const name = `${folderN}/${innerData.name}`; + storedFiles[name] = { file: innerData, data: {} }; + const metaData = await innerData.getFile(); + const encodedData = await this.encodeData(metaData, args.TYPE); + this.updateStore(name, encodedData, metaData); + } + } + } catch(e) { + console.warn(e); + } + } + + deleteStoredFile(args) { + if (args.OPTION === "this device" || args.OPTION === "both") { + if (!this.checkFileAPI() || storedFiles[args.NAME] === undefined) return; + storedFiles[args.NAME].file.remove(); + } + if (args.OPTION === "storage" || args.OPTION === "both") delete storedFiles[args.NAME] + } + + async updateFile(args) { + if (!this.checkFileAPI() || storedFiles[args.NAME] === undefined) return; + try { + const writable = await storedFiles[args.NAME].file.createWritable(); + await writable.write(args.TXT); + await writable.close(); + this.updateStore(args.NAME, args.TXT, { lastModified: Date.now(), size: args.TXT.length }); + } catch (e) { + console.warn(e); + } + } + + storedInfo(args) { + const fileInfo = storedFiles[args.NAME]; + if (fileInfo === undefined) return ""; + else if (args.FORMAT === "size formatted") return fileInfo.data.sizeFormat; + else if (args.FORMAT === "size unformatted") return fileInfo.data.size; + else if (args.FORMAT === "modified date") return fileInfo.data.dateFormat; + else if (args.FORMAT === "data") return fileInfo.data.data; + return fileInfo.file.name; + } + + moveStorage() { + const ext = runtime.ext_cst1229zip; + if (ext === undefined) return; + ext.createEmptyAs({ NAME: "filesExpanded_storedFiles" }); + if (ext.zipError) return; + const zip = ext.zips["filesExpanded_storedFiles"]; + for (const [name, file] of Object.entries(storedFiles)) { + zip.file(name, file.data.data); + } + } + + // Extra + findFileSize(args) { + const size = Scratch.Cast.toString(args.FILE).length; // bytes + return args.TYPE === "formatted" ? formatFileSize(size) : size; + } + + // Visuals + resetStyle() { + selectorOptions = { + borderColor: "#888", textColor: "#000", outer: "#fff", + sizeFont: 1.5, borderRadius: 16, borderType: "dashed", + font: "inherit", shadow: 0.5, image: "", + textV: "", fontWeight: 40, letterSpacing: "normal" + }; + updateModalVisuals(); + } + + borderColors(args) { + switch (args.OPTION) { + case "text": + selectorOptions.textColor = args.COLOR; + break; + case "background": + selectorOptions.outer = args.COLOR; + selectorOptions.image = ""; + break; + default: + selectorOptions.borderColor = args.COLOR; + } + updateModalVisuals(); + } + + visualsSelect(args) { + const amtIn = Scratch.Cast.toNumber(args.AMT); + switch (args.OPTION) { + case "font size": + selectorOptions.sizeFont = amtIn / 10; + break; + case "font thickness": + selectorOptions.fontWeight = amtIn; + break; + case "letter spacing": + selectorOptions.letterSpacing = amtIn; + break; + case "border radius": + selectorOptions.borderRadius = amtIn; + break; + case "overlay opacity": + selectorOptions.shadow = Scratch.Cast.toNumber(amtIn) / 100; + break; + default: + selectorOptions.border = amtIn; + } + updateModalVisuals(); + } + + borderTypeSet(args) { + selectorOptions.borderType = args.TYPE; + updateModalVisuals(); + } + + fontSet(args) { + selectorOptions.font = args.FONT; + updateModalVisuals(); + } + + currentX(args) { + if (args.THING === "shadow" || args.THING === "sizeFont") { + const multiplier = args.THING === "shadow" ? 100 : 10; + return selectorOptions[args.THING] * multiplier; + } + return selectorOptions[args.THING]; + } + + imageSet(args) { + Scratch.canFetch(encodeURI(args.IMG)).then(canFetch => { + if (canFetch) { + selectorOptions.image = `url(${encodeURI(args.IMG)})`; + updateModalVisuals(); + } else { + console.warn("Cannot fetch content from the URL."); + } + }); + } + + textSet(args) { + selectorOptions.textV = args.TEXT; + updateModalVisuals(); + } } - Scratch.extensions.register(new Files()); + Scratch.extensions.register(runtime.ext_Files = new Files()); })(Scratch); From 76090aa46cfaa3cfa43c6050a525e261fcbd3ecb Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:28:39 -0800 Subject: [PATCH 2/8] Update files.js --- extensions/files.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index c0635b2f3e..1fa6df6fc3 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -64,8 +64,8 @@ * @param {boolean} override makes modal use the File API if true * @returns {Promise} format given by as parameter */ - const showFilePrompt = async (accept, as, override) => - new Promise(async (_resolve) => { + const showFilePrompt = (accept, as, override) => + new Promise((_resolve) => { // We can't reliably show an picker without "user interaction" in all environments, // so we have to show our own UI anyways. We may as well use this to implement some nice features // that native file pickers don't have: @@ -267,7 +267,7 @@ openFileSelectorMode === MODE_ONLY_SELECTOR ) { // will run "input.click()" if override is "undefined" - await handleOverride(); + handleOverride(); } if (openFileSelectorMode === MODE_ONLY_SELECTOR) { From e6dbf45575a766c9d8124a975ce7a373d3397938 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:47:06 -0800 Subject: [PATCH 3/8] Update files.js --- extensions/files.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/files.js b/extensions/files.js index 1fa6df6fc3..8370ffa47d 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -1,7 +1,10 @@ // Name: Files // ID: files // Description: Read, upload, and download files. -// By: GarboMuffin, SharkPool, Drago Cuven, 0znzw +// By: GarboMuffin +// By: SharkPool +// By: Drago Cuven +// By: 0znzw // License: MIT AND MPL-2.0 (function (Scratch) { From 6d2312a83259ddbf8b208cbd8b8f3b98e7c13376 Mon Sep 17 00:00:00 2001 From: "DangoCat[bot]" Date: Thu, 2 Jan 2025 07:03:20 +0000 Subject: [PATCH 4/8] [Automated] Format code --- extensions/files.js | 247 ++++++++++++++++++++++++++++---------------- 1 file changed, 159 insertions(+), 88 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index 8370ffa47d..eb64ade593 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -15,14 +15,20 @@ } const menuIconURI = -"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMzMuMTY0IiBoZWlnaHQ9IjEzMy4xNjQiIHZpZXdCb3g9IjAgMCAxMzMuMTY0IDEzMy4xNjQiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTMgNjYuNTgyQzMgMzEuNDY3IDMxLjQ2NyAzIDY2LjU4MiAzczYzLjU4MiAyOC40NjcgNjMuNTgyIDYzLjU4Mi0yOC40NjcgNjMuNTgyLTYzLjU4MiA2My41ODJTMyAxMDEuNjk3IDMgNjYuNTgyeiIgZmlsbD0iI2ZjYjEwMyIgc3Ryb2tlPSIjYmY4YjExIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNOTkuODkyIDQ5LjkyN3Y0OS45NjRjMCA0LjU4LTMuNzQ4IDguMzI4LTguMzI4IDguMzI4SDQxLjU1OGMtNC41OCAwLTguMjg1LTMuNzQ4LTguMjg1LTguMzI4bC4wNDEtNjYuNjE4YzAtNC41OCAzLjcwNi04LjMyOCA4LjI4Ni04LjMyOGgzMy4zMXoiIGZpbGw9Im5vbmUiIHN0cm9rZS1vcGFjaXR5PSIuMTQ5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNOTkuODkyIDQ5LjkyN3Y0OS45NjRjMCA0LjU4LTMuNzQ4IDguMzI4LTguMzI4IDguMzI4SDQxLjU1OGMtNC41OCAwLTguMjg1LTMuNzQ4LTguMjg1LTguMzI4bC4wNDEtNjYuNjE4YzAtNC41OCAzLjcwNi04LjMyOCA4LjI4Ni04LjMyOGgzMy4zMXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNNzAuNzIyIDU0LjExNVYzMS4xNjdsMjIuOTQ3IDIyLjk0OHoiIGZpbGw9IiNmY2IxMDMiLz48cGF0aCBkPSJNODQuNjY4IDY5LjkxNGMtLjAyLjA4OC0uMDM2LjE3NS0uMDYyLjI2Mi0uMDc3LjMyNC0uMjM2IDEuMDktLjM5NiAyLjU4N3EuMDAxLjAzOC0uMDA1LjA2MWMuODggMy41MzgtLjYwMiA1LjY5Mi0xLjU4NCA2LjY3NGEyIDIgMCAwIDEtLjIuMTljLS45NjIuODc1LTIuNjQzIDEuOTE4LTUuMTUyIDEuOTE4YTcuNjUgNy42NSAwIDAgMS0zLjQzNS0uNzg2Yy4wMjYgMS40MDMuMDQxIDMuMzM3LjA0MSA2LjAyYTYuOSA2LjkgMCAwIDEgMi40OTQgMS41MzggNy4xIDcuMSAwIDAgMSAyLjIxIDUuMTY3YzAgMi45MS0xLjY3NSA1LjQ0LTQuMzc1IDYuNTk3cS0uMTAzLjA0OC0uMjA1LjA4OGMtLjkwNS4zNDQtMS45MjMuNTE0LTMuMTE2LjUxNC0uNDMyIDAtLjg5NS0uMDItMS4zOTktLjA2N2ExOCAxOCAwIDAgMC0xLjI5NS4wMDVjLS45NzIuMTA4LTIuMzQuMjE2LTQuMTcuMzE5LS4wMzYgMC0uMDcyLjAwNS0uMTE0LjAwNXEtLjM2LjAxNS0uNjk5LjAxNWMtMy4yNzUgMC01LjY2Ni0xLjAzOC03LjExLTMuMDhxLS4wNDYtLjA2LS4wODMtLjEyM2MtMS40Ny0yLjE5NS0xLjUxMi00Ljk3Mi0uMTEzLTcuMjRxLjAxNy0uMDM1LjA0MS0uMDcxYy44OS0xLjM5OSAyLjE5LTIuMzk2IDMuODc3LTIuOTgzLjAyLS45OTIuMDE1LTIuMjM2LS4wMTYtMy43MTJhNy41IDcuNSAwIDAgMS0yLjg5LjU1NWMtMi44NjMgMC01LjIyOC0xLjcwMS02LjA1MS00LjI4OC0uMTctLjUyNC0uMzMtMS4yOS0uNy0zLjAxOGEyIDIgMCAwIDEtLjAzNS0uMTg1bC0uNzYxLTQuMjUyYTcgNyAwIDAgMC0uMTAzLS4zMjRjLS4zOTYtMS4xMjYtLjU4MS0yLjA4My0uNTgxLTMuMDA4IDAtMS4xNDcuMzM0LTIuODggMS45MTgtNC42MDcuNzk3LS44NzQgMi4yNDctMi4wMDUgNC41ODEtMi4zN3EuNDMzLS4wNzEuODc0LS4wNzJoNS42ODdxLjE5MiAwIC4zODUuMDE1YTcyIDcyIDAgMCAwIDUuNzgtLjAwNSA4MyA4MyAwIDAgMCA2LjkzLS41OTZjLjI1OC0uMDgzLjUzNi0uMTY1LjgzNC0uMjM3LjI4My0uMDcyLjU3LS4xMTguODU4LS4xNSAyLjE5LS4yIDQuMjMyLjQzMyA1LjgyIDEuNzkgMS45NyAxLjY5MiAyLjg0IDQuMjUyIDIuMzIgNi44NTQiIGZpbGw9IiNiZjhiMTEiLz48cGF0aCBkPSJNNzkuMTIxIDY4LjgwNnEtLjMxNiAxLjI2Ni0uNTQzIDMuNDM3LS4wOSAxLjA4Ni4wOSAxLjc2NC4zMTYgMS4xMzEtLjA0NCAxLjQ5My0uNDk5LjQ1MS0xLjM1Ny40NTItLjk5NiAwLTEuMzEyLS41OTJBNDEgNDEgMCAwIDEgNzYgNzEuOTQzcS4wOS0xLjk2LjA5LTIuMDUtLjA0NSAwLS4xMzUtLjA5Mi00LjIwNy4xODMtNy45MTUuNTQ1LS4wOS40NTMgMCAxLjQ1LjE4IDEuNDA0LjE4IDEuNjMtLjE4IDEuNDA2LS4xOCA0LjE3LjE4IDEuMTc5LjE4IDEwLjEwNnYzLjMwOXEwIC43Ny4yMjUgMS4wODdoMi43NzdxLjc2Mi0uMDkgMS4yMzIuMzYyLjQ3LjQ1My40NyAxLjA4NSAwIC45OTQtLjk1IDEuNDAzLS41ODguMjI1LTIuMDguMDktLjcyMy0uMDQ1LTIuMTI2IDAtMS4yNjUuMTU3LTQuMDcuMzE2LTIuNDQzLjA5LTIuOTg1LS42NzgtLjM2My0uNTQyIDAtMS4xMy42MzItLjk5NSAzLjMwMi0uOTk1Ljk0OCAwIDEuMTc1LS4xNTguMjI2LS4xNi4yMjYtLjYxVjg5LjAzcS4xMzUtMi4zMDIgMC02Ljg2MS0uMTgtNi41NDQuMTgxLTExLjUwOWwtLjEzNy0uMTM2cS0xLjYzNy4wOS01LjI3NS0uMDktLjQxIDAtMi43NzQuMTgxLjU4NyA0LjExNy43MjMgNi4xOTYgMCAuMjcyLS4wOSAxLjIyMS0uMDQ2LjY3OC0xLjEzMS42NzktLjU4OSAwLS42NzgtLjQwNC0uMDQ2LS4wOS0uNTQzLTIuNDIybC0uNzY5LTQuMzA2cTAtLjE3OS0uMzE2LTEuMTIxLS4yNy0uNzY0LS4yNy0xLjE2Ni0uMDAxLS4zMTUuNDMtLjc4NS40My0uNDcxIDEuMjg5LS42MDZoNS42MDhxMy42NjMuMTM1IDYuNTEyIDBhODggODggMCAwIDAgOC4wNS0uNzI0cS4yNzMtLjEzNS44MTQtLjI3Ljk5NS0uMDkgMS42MjkuNDUyLjYzMy41NC40NTIgMS40NDciIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"; + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMzMuMTY0IiBoZWlnaHQ9IjEzMy4xNjQiIHZpZXdCb3g9IjAgMCAxMzMuMTY0IDEzMy4xNjQiPjxnIHN0cm9rZS1taXRlcmxpbWl0PSIxMCI+PHBhdGggZD0iTTMgNjYuNTgyQzMgMzEuNDY3IDMxLjQ2NyAzIDY2LjU4MiAzczYzLjU4MiAyOC40NjcgNjMuNTgyIDYzLjU4Mi0yOC40NjcgNjMuNTgyLTYzLjU4MiA2My41ODJTMyAxMDEuNjk3IDMgNjYuNTgyeiIgZmlsbD0iI2ZjYjEwMyIgc3Ryb2tlPSIjYmY4YjExIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNOTkuODkyIDQ5LjkyN3Y0OS45NjRjMCA0LjU4LTMuNzQ4IDguMzI4LTguMzI4IDguMzI4SDQxLjU1OGMtNC41OCAwLTguMjg1LTMuNzQ4LTguMjg1LTguMzI4bC4wNDEtNjYuNjE4YzAtNC41OCAzLjcwNi04LjMyOCA4LjI4Ni04LjMyOGgzMy4zMXoiIGZpbGw9Im5vbmUiIHN0cm9rZS1vcGFjaXR5PSIuMTQ5IiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNOTkuODkyIDQ5LjkyN3Y0OS45NjRjMCA0LjU4LTMuNzQ4IDguMzI4LTguMzI4IDguMzI4SDQxLjU1OGMtNC41OCAwLTguMjg1LTMuNzQ4LTguMjg1LTguMzI4bC4wNDEtNjYuNjE4YzAtNC41OCAzLjcwNi04LjMyOCA4LjI4Ni04LjMyOGgzMy4zMXoiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNNzAuNzIyIDU0LjExNVYzMS4xNjdsMjIuOTQ3IDIyLjk0OHoiIGZpbGw9IiNmY2IxMDMiLz48cGF0aCBkPSJNODQuNjY4IDY5LjkxNGMtLjAyLjA4OC0uMDM2LjE3NS0uMDYyLjI2Mi0uMDc3LjMyNC0uMjM2IDEuMDktLjM5NiAyLjU4N3EuMDAxLjAzOC0uMDA1LjA2MWMuODggMy41MzgtLjYwMiA1LjY5Mi0xLjU4NCA2LjY3NGEyIDIgMCAwIDEtLjIuMTljLS45NjIuODc1LTIuNjQzIDEuOTE4LTUuMTUyIDEuOTE4YTcuNjUgNy42NSAwIDAgMS0zLjQzNS0uNzg2Yy4wMjYgMS40MDMuMDQxIDMuMzM3LjA0MSA2LjAyYTYuOSA2LjkgMCAwIDEgMi40OTQgMS41MzggNy4xIDcuMSAwIDAgMSAyLjIxIDUuMTY3YzAgMi45MS0xLjY3NSA1LjQ0LTQuMzc1IDYuNTk3cS0uMTAzLjA0OC0uMjA1LjA4OGMtLjkwNS4zNDQtMS45MjMuNTE0LTMuMTE2LjUxNC0uNDMyIDAtLjg5NS0uMDItMS4zOTktLjA2N2ExOCAxOCAwIDAgMC0xLjI5NS4wMDVjLS45NzIuMTA4LTIuMzQuMjE2LTQuMTcuMzE5LS4wMzYgMC0uMDcyLjAwNS0uMTE0LjAwNXEtLjM2LjAxNS0uNjk5LjAxNWMtMy4yNzUgMC01LjY2Ni0xLjAzOC03LjExLTMuMDhxLS4wNDYtLjA2LS4wODMtLjEyM2MtMS40Ny0yLjE5NS0xLjUxMi00Ljk3Mi0uMTEzLTcuMjRxLjAxNy0uMDM1LjA0MS0uMDcxYy44OS0xLjM5OSAyLjE5LTIuMzk2IDMuODc3LTIuOTgzLjAyLS45OTIuMDE1LTIuMjM2LS4wMTYtMy43MTJhNy41IDcuNSAwIDAgMS0yLjg5LjU1NWMtMi44NjMgMC01LjIyOC0xLjcwMS02LjA1MS00LjI4OC0uMTctLjUyNC0uMzMtMS4yOS0uNy0zLjAxOGEyIDIgMCAwIDEtLjAzNS0uMTg1bC0uNzYxLTQuMjUyYTcgNyAwIDAgMC0uMTAzLS4zMjRjLS4zOTYtMS4xMjYtLjU4MS0yLjA4My0uNTgxLTMuMDA4IDAtMS4xNDcuMzM0LTIuODggMS45MTgtNC42MDcuNzk3LS44NzQgMi4yNDctMi4wMDUgNC41ODEtMi4zN3EuNDMzLS4wNzEuODc0LS4wNzJoNS42ODdxLjE5MiAwIC4zODUuMDE1YTcyIDcyIDAgMCAwIDUuNzgtLjAwNSA4MyA4MyAwIDAgMCA2LjkzLS41OTZjLjI1OC0uMDgzLjUzNi0uMTY1LjgzNC0uMjM3LjI4My0uMDcyLjU3LS4xMTguODU4LS4xNSAyLjE5LS4yIDQuMjMyLjQzMyA1LjgyIDEuNzkgMS45NyAxLjY5MiAyLjg0IDQuMjUyIDIuMzIgNi44NTQiIGZpbGw9IiNiZjhiMTEiLz48cGF0aCBkPSJNNzkuMTIxIDY4LjgwNnEtLjMxNiAxLjI2Ni0uNTQzIDMuNDM3LS4wOSAxLjA4Ni4wOSAxLjc2NC4zMTYgMS4xMzEtLjA0NCAxLjQ5My0uNDk5LjQ1MS0xLjM1Ny40NTItLjk5NiAwLTEuMzEyLS41OTJBNDEgNDEgMCAwIDEgNzYgNzEuOTQzcS4wOS0xLjk2LjA5LTIuMDUtLjA0NSAwLS4xMzUtLjA5Mi00LjIwNy4xODMtNy45MTUuNTQ1LS4wOS40NTMgMCAxLjQ1LjE4IDEuNDA0LjE4IDEuNjMtLjE4IDEuNDA2LS4xOCA0LjE3LjE4IDEuMTc5LjE4IDEwLjEwNnYzLjMwOXEwIC43Ny4yMjUgMS4wODdoMi43NzdxLjc2Mi0uMDkgMS4yMzIuMzYyLjQ3LjQ1My40NyAxLjA4NSAwIC45OTQtLjk1IDEuNDAzLS41ODguMjI1LTIuMDguMDktLjcyMy0uMDQ1LTIuMTI2IDAtMS4yNjUuMTU3LTQuMDcuMzE2LTIuNDQzLjA5LTIuOTg1LS42NzgtLjM2My0uNTQyIDAtMS4xMy42MzItLjk5NSAzLjMwMi0uOTk1Ljk0OCAwIDEuMTc1LS4xNTguMjI2LS4xNi4yMjYtLjYxVjg5LjAzcS4xMzUtMi4zMDIgMC02Ljg2MS0uMTgtNi41NDQuMTgxLTExLjUwOWwtLjEzNy0uMTM2cS0xLjYzNy4wOS01LjI3NS0uMDktLjQxIDAtMi43NzQuMTgxLjU4NyA0LjExNy43MjMgNi4xOTYgMCAuMjcyLS4wOSAxLjIyMS0uMDQ2LjY3OC0xLjEzMS42NzktLjU4OSAwLS42NzgtLjQwNC0uMDQ2LS4wOS0uNTQzLTIuNDIybC0uNzY5LTQuMzA2cTAtLjE3OS0uMzE2LTEuMTIxLS4yNy0uNzY0LS4yNy0xLjE2Ni0uMDAxLS4zMTUuNDMtLjc4NS40My0uNDcxIDEuMjg5LS42MDZoNS42MDhxMy42NjMuMTM1IDYuNTEyIDBhODggODggMCAwIDAgOC4wNS0uNzI0cS4yNzMtLjEzNS44MTQtLjI3Ljk5NS0uMDkgMS42MjkuNDUyLjYzMy41NC40NTIgMS40NDciIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+"; const vm = Scratch.vm; const runtime = vm.runtime; const builtInFonts = [ - "Sans Serif", "Serif", "Handwriting", - "Marker", "Curly", "Pixel", "Scratch", "inherit" + "Sans Serif", + "Serif", + "Handwriting", + "Marker", + "Curly", + "Pixel", + "Scratch", + "inherit", ]; const MODE_MODAL = "modal"; const MODE_IMMEDIATELY_SHOW_SELECTOR = "selector"; @@ -32,19 +38,34 @@ MODE_IMMEDIATELY_SHOW_SELECTOR, MODE_ONLY_SELECTOR, ]; - const AS_TEXT = "text", AS_DATA_URL = "url", AS_HEX = "hex"; - const AS_BASE64 = "base64", AS_BUFFER = "arrayBuffer"; + const AS_TEXT = "text", + AS_DATA_URL = "url", + AS_HEX = "hex"; + const AS_BASE64 = "base64", + AS_BUFFER = "arrayBuffer"; let enableVis = true; let openFileSelectorMode = MODE_MODAL; let storedFiles = {}; - let FileName = "", FileSize = "0kb", RawFileSize = "0", fileDate = "", lastData = ""; + let FileName = "", + FileSize = "0kb", + RawFileSize = "0", + fileDate = "", + lastData = ""; let openModals = 0; let selectorOptions = { - borderColor: "#888", textColor: "#000", outer: "#fff", - sizeFont: 1.5, borderRadius: 16, borderType: "dashed", - font: "inherit", shadow: 0.5, image: "", - textV: "", fontWeight: 40, letterSpacing: "normal" + borderColor: "#888", + textColor: "#000", + outer: "#fff", + sizeFont: 1.5, + borderRadius: 16, + borderType: "dashed", + font: "inherit", + shadow: 0.5, + image: "", + textV: "", + fontWeight: 40, + letterSpacing: "normal", }; /** @@ -152,16 +173,18 @@ try { if (override === "folder") { fileInfo = await window.showDirectoryPicker({ - multiple: false, types: [{ accept: { "*/*" : [] }}] + multiple: false, + types: [{ accept: { "*/*": [] } }], }); } else { fileInfo = await window.showOpenFilePicker({ - multiple: false, types: [{ accept: { "*/*" : accept }}] + multiple: false, + types: [{ accept: { "*/*": accept } }], }); } callback(fileInfo); } catch { - callback("") + callback(""); } } }; @@ -233,7 +256,10 @@ } const title = document.createElement("div"); - title.textContent = override === undefined ? Scratch.translate("Select or drop file") : Scratch.translate("Select file"); + title.textContent = + override === undefined + ? Scratch.translate("Select or drop file") + : Scratch.translate("Select file"); title.style.marginBottom = "8px"; modal.appendChild(title); @@ -285,13 +311,15 @@ const updateModalVisuals = () => { const allModals = document.querySelectorAll(`button[id="tw-files-modal"]`); - allModals.forEach(modal => { + allModals.forEach((modal) => { modal.parentNode.style.background = `rgba(0, 0, 0, ${selectorOptions.shadow})`; modal.parentNode.style.color = selectorOptions.textColor; modal.style.font = selectorOptions.font; modal.style.fontFamily = selectorOptions.font; - modal.style.background = selectorOptions.image ? selectorOptions.image : selectorOptions.outer; + modal.style.background = selectorOptions.image + ? selectorOptions.image + : selectorOptions.outer; modal.style.borderRadius = `${selectorOptions.borderRadius}px`; modal.style.borderStyle = selectorOptions.borderType; modal.style.borderColor = selectorOptions.borderColor; @@ -393,7 +421,7 @@ var bytes = new Uint8Array(buffer); var len = bytes.byteLength; for (var i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]) + binary += String.fromCharCode(bytes[i]); } return btoa(binary); } @@ -411,7 +439,10 @@ color2: "#db9a37", color3: "#db8937", blocks: [ - { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Uploading") }, + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Uploading"), + }, { opcode: "showPicker", blockType: Scratch.BlockType.REPORTER, @@ -457,7 +488,10 @@ }, }, }, - { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Downloading") }, + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Downloading"), + }, { opcode: "download", blockType: Scratch.BlockType.COMMAND, @@ -488,7 +522,10 @@ }, }, }, - { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Extra") }, + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Extra"), + }, { opcode: "setOpenMode", blockType: Scratch.BlockType.COMMAND, @@ -497,7 +534,7 @@ mode: { type: Scratch.ArgumentType.STRING, defaultValue: MODE_MODAL, - menu: "automaticallyOpen" + menu: "automaticallyOpen", }, }, }, @@ -516,7 +553,7 @@ { opcode: "modalOpen", blockType: Scratch.BlockType.BOOLEAN, - text: Scratch.translate("is modal open?") + text: Scratch.translate("is modal open?"), }, { opcode: "findFileSize", @@ -525,7 +562,7 @@ arguments: { TYPE: { type: Scratch.ArgumentType.STRING, - menu: "FILE_SIZES" + menu: "FILE_SIZES", }, FILE: { type: Scratch.ArgumentType.STRING, @@ -547,7 +584,9 @@ { opcode: "setStoredFile", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate("open new stored [FILE] file named [NAME] as [TYPE]"), + text: Scratch.translate( + "open new stored [FILE] file named [NAME] as [TYPE]" + ), arguments: { NAME: { type: Scratch.ArgumentType.STRING, @@ -566,7 +605,9 @@ { opcode: "storedFolder", blockType: Scratch.BlockType.COMMAND, - text: Scratch.translate("open folder and store files as [TYPE] with name [NAME]"), + text: Scratch.translate( + "open folder and store files as [TYPE] with name [NAME]" + ), arguments: { TYPE: { type: Scratch.ArgumentType.STRING, @@ -630,17 +671,22 @@ blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("migrate files to CST's ZIP Extension"), }, - { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Visuals") }, + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Visuals"), + }, { func: "toggleVis", blockType: Scratch.BlockType.BUTTON, - text: Scratch.translate(`${enableVis ? "En" : "Dis"}able Customization`), + text: Scratch.translate( + `${enableVis ? "En" : "Dis"}able Customization` + ), }, { opcode: "resetStyle", blockType: Scratch.BlockType.COMMAND, text: Scratch.translate("reset selector style to default"), - hideFromPalette: enableVis + hideFromPalette: enableVis, }, "---", { @@ -767,51 +813,51 @@ items: [ { text: Scratch.translate("border color"), - value: "border" + value: "border", }, { text: Scratch.translate("text color"), - value: "text" + value: "text", }, { text: Scratch.translate("background color"), - value: "outer" + value: "outer", }, { text: Scratch.translate("overlay opacity"), - value: "shadow" + value: "shadow", }, { text: Scratch.translate("font"), - value: "font" + value: "font", }, { text: Scratch.translate("font size"), - value: "sizeFont" + value: "sizeFont", }, { text: Scratch.translate("font thickness"), - value: "fontWeight" + value: "fontWeight", }, { text: Scratch.translate("letter spacing"), - value: "letterSpacing" + value: "letterSpacing", }, { text: Scratch.translate("border radius"), - value: "borderRadius" + value: "borderRadius", }, { text: Scratch.translate("border type"), - value: "borderType" + value: "borderType", }, { text: Scratch.translate("background image"), - value: "image" + value: "image", }, { text: Scratch.translate("text"), - value: "textV" + value: "textV", }, ], }, @@ -820,11 +866,11 @@ items: [ { text: Scratch.translate("formatted"), - value: "formatted" + value: "formatted", }, { text: Scratch.translate("unformatted"), - value: "unformatted" + value: "unformatted", }, ], }, @@ -833,23 +879,23 @@ items: [ { text: Scratch.translate("data"), - value: "data" + value: "data", }, { text: Scratch.translate("name"), - value: "name" + value: "name", }, { text: Scratch.translate("modified date"), - value: "modified date" + value: "modified date", }, { text: Scratch.translate("size formatted"), - value: "size formatted" + value: "size formatted", }, { text: Scratch.translate("size unformatted"), - value: "size unformatted" + value: "size unformatted", }, ], }, @@ -858,15 +904,15 @@ items: [ { text: Scratch.translate("storage"), - value: "storage" + value: "storage", }, { text: Scratch.translate("this device"), - value: "this device" + value: "this device", }, { text: Scratch.translate("both"), - value: "both" + value: "both", }, ], }, @@ -875,15 +921,15 @@ items: [ { text: Scratch.translate("border"), - value: "border" + value: "border", }, { text: Scratch.translate("text"), - value: "text" + value: "text", }, { text: Scratch.translate("background"), - value: "background" + value: "background", }, ], }, @@ -892,23 +938,23 @@ items: [ { text: Scratch.translate("font size"), - value: "font size" + value: "font size", }, { text: Scratch.translate("font thickness"), - value: "font thickness" + value: "font thickness", }, { text: Scratch.translate("letter spacing"), - value: "letter spacing" + value: "letter spacing", }, { text: Scratch.translate("border radius"), - value: "border radius" + value: "border radius", }, { text: Scratch.translate("overlay opacity"), - value: "overlay opacity" + value: "overlay opacity", }, ], }, @@ -917,39 +963,39 @@ items: [ { text: Scratch.translate("dotted"), - value: "dotted" + value: "dotted", }, { text: Scratch.translate("dashed"), - value: "dashed" + value: "dashed", }, { text: Scratch.translate("solid"), - value: "solid" + value: "solid", }, { text: Scratch.translate("double"), - value: "double" + value: "double", }, { text: Scratch.translate("groove"), - value: "groove" + value: "groove", }, { text: Scratch.translate("ridge"), - value: "ridge" + value: "ridge", }, { text: Scratch.translate("inset"), - value: "inset" + value: "inset", }, { text: Scratch.translate("outset"), - value: "outset" + value: "outset", }, { text: Scratch.translate("none"), - value: "none" + value: "none", }, ], }, @@ -959,28 +1005,38 @@ // Helper Funcs getFonts() { - const customFonts = runtime.fontManager ? runtime.fontManager.getFonts().map((i) => ({ text: i.name, value: i.family })) : []; - return [ ...builtInFonts, ...customFonts ]; + const customFonts = runtime.fontManager + ? runtime.fontManager + .getFonts() + .map((i) => ({ text: i.name, value: i.family })) + : []; + return [...builtInFonts, ...customFonts]; } getEncodings(onlySafe) { const types = [ - { text: Scratch.translate("text"), value: AS_TEXT }, { text: "data: URL", value: AS_DATA_URL }, - { text: "base64", value: AS_BASE64 }, { text: "hex", value: AS_HEX } + { text: Scratch.translate("text"), value: AS_TEXT }, + { text: "data: URL", value: AS_DATA_URL }, + { text: "base64", value: AS_BASE64 }, + { text: "hex", value: AS_HEX }, ]; - if (this._showUnsafeOptions) types.push({ text: "arrayBuffer", value: AS_BUFFER }); + if (this._showUnsafeOptions) + types.push({ text: "arrayBuffer", value: AS_BUFFER }); return types; } toggleVis() { - enableVis = enableVis ? false : true, vm.extensionManager.refreshBlocks(); + (enableVis = enableVis ? false : true), + vm.extensionManager.refreshBlocks(); } updateStore(name, data, metaData) { const rawDate = new Date(metaData.lastModified); storedFiles[name].data = { - size: metaData.size, sizeFormat: formatFileSize(metaData.size), - dateFormat: rawDate.toLocaleString(), data + size: metaData.size, + sizeFormat: formatFileSize(metaData.size), + dateFormat: rawDate.toLocaleString(), + data, }; } @@ -991,7 +1047,8 @@ else if (format === AS_BUFFER) return buffer; const base64 = bufferToBase64(buffer); if (format === AS_BASE64) return base64; - else if (format === AS_DATA_URL) return `data:${meta.type};charset=utf-8;base64,${base64}`; + else if (format === AS_DATA_URL) + return `data:${meta.type};charset=utf-8;base64,${base64}`; else if (format === AS_HEX) return base64ToHex(base64, " "); return text; } @@ -1074,7 +1131,7 @@ const metaData = await picker[0].getFile(); const encodedData = await this.encodeData(metaData, args.TYPE); this.updateStore(args.NAME, encodedData, metaData); - } catch(e) { + } catch (e) { console.warn(e); } } @@ -1099,17 +1156,19 @@ this.updateStore(name, encodedData, metaData); } } - } catch(e) { + } catch (e) { console.warn(e); } } deleteStoredFile(args) { if (args.OPTION === "this device" || args.OPTION === "both") { - if (!this.checkFileAPI() || storedFiles[args.NAME] === undefined) return; + if (!this.checkFileAPI() || storedFiles[args.NAME] === undefined) + return; storedFiles[args.NAME].file.remove(); } - if (args.OPTION === "storage" || args.OPTION === "both") delete storedFiles[args.NAME] + if (args.OPTION === "storage" || args.OPTION === "both") + delete storedFiles[args.NAME]; } async updateFile(args) { @@ -1118,7 +1177,10 @@ const writable = await storedFiles[args.NAME].file.createWritable(); await writable.write(args.TXT); await writable.close(); - this.updateStore(args.NAME, args.TXT, { lastModified: Date.now(), size: args.TXT.length }); + this.updateStore(args.NAME, args.TXT, { + lastModified: Date.now(), + size: args.TXT.length, + }); } catch (e) { console.warn(e); } @@ -1127,7 +1189,8 @@ storedInfo(args) { const fileInfo = storedFiles[args.NAME]; if (fileInfo === undefined) return ""; - else if (args.FORMAT === "size formatted") return fileInfo.data.sizeFormat; + else if (args.FORMAT === "size formatted") + return fileInfo.data.sizeFormat; else if (args.FORMAT === "size unformatted") return fileInfo.data.size; else if (args.FORMAT === "modified date") return fileInfo.data.dateFormat; else if (args.FORMAT === "data") return fileInfo.data.data; @@ -1154,10 +1217,18 @@ // Visuals resetStyle() { selectorOptions = { - borderColor: "#888", textColor: "#000", outer: "#fff", - sizeFont: 1.5, borderRadius: 16, borderType: "dashed", - font: "inherit", shadow: 0.5, image: "", - textV: "", fontWeight: 40, letterSpacing: "normal" + borderColor: "#888", + textColor: "#000", + outer: "#fff", + sizeFont: 1.5, + borderRadius: 16, + borderType: "dashed", + font: "inherit", + shadow: 0.5, + image: "", + textV: "", + fontWeight: 40, + letterSpacing: "normal", }; updateModalVisuals(); } @@ -1220,7 +1291,7 @@ } imageSet(args) { - Scratch.canFetch(encodeURI(args.IMG)).then(canFetch => { + Scratch.canFetch(encodeURI(args.IMG)).then((canFetch) => { if (canFetch) { selectorOptions.image = `url(${encodeURI(args.IMG)})`; updateModalVisuals(); @@ -1236,5 +1307,5 @@ } } - Scratch.extensions.register(runtime.ext_Files = new Files()); + Scratch.extensions.register((runtime.ext_Files = new Files())); })(Scratch); From 521e7096b0a7a03fdca2fb6399535fb886165765 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:08:11 -0800 Subject: [PATCH 5/8] files.js -- Add update file with URL block --- extensions/files.js | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index eb64ade593..15d57e07e4 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -46,7 +46,7 @@ let enableVis = true; let openFileSelectorMode = MODE_MODAL; - let storedFiles = {}; + let storedFiles = Object.create(null); let FileName = "", FileSize = "0kb", RawFileSize = "0", @@ -650,6 +650,21 @@ }, }, }, + { + opcode: "updateFileURL", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate("write URL [URL] to stored file [NAME]"), + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==", + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: Scratch.translate("my-file-1"), + }, + }, + }, { opcode: "storedInfo", blockType: Scratch.BlockType.REPORTER, @@ -1179,7 +1194,27 @@ await writable.close(); this.updateStore(args.NAME, args.TXT, { lastModified: Date.now(), - size: args.TXT.length, + size: Scratch.Cast.toString(args.TXT).length, + }); + } catch (e) { + console.warn(e); + } + } + + async updateFileURL(args) { + if (!this.checkFileAPI() || storedFiles[args.NAME] === undefined) return; + try { + const url = Scratch.Cast.toString(args.URL); + const res = await Scratch.fetch(url); + if (!res.ok) return; + const blob = await res.blob(); + + const writable = await storedFiles[args.NAME].file.createWritable(); + await writable.write(blob); + await writable.close(); + this.updateStore(args.NAME, url, { + lastModified: Date.now(), + size: url.length, }); } catch (e) { console.warn(e); From ace85d50a299d39f48115a922fcfc896b9404f67 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:10:20 -0800 Subject: [PATCH 6/8] lint --- extensions/files.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/files.js b/extensions/files.js index 15d57e07e4..6fd2efff1d 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -570,7 +570,7 @@ }, }, }, - { blockType: Scratch.BlockType.LABEL, text: "Stored Files" }, + { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Stored Files") }, { opcode: "checkFileAPI", blockType: Scratch.BlockType.BOOLEAN, From a37b24065a3142a114a5177de724e344ea1d4808 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:44:43 -0700 Subject: [PATCH 7/8] files.js -- fix opening folders within folders --- extensions/files.js | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index 6fd2efff1d..44950c9ba1 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -1156,21 +1156,29 @@ try { const picker = await showFilePrompt("Folder", "", "folder"); if (!picker) return; - const entries = picker.entries(); - const folderN = args.NAME ? args.NAME : picker.name; - let thisFile = ""; - while (thisFile !== undefined) { - const outerData = await entries.next(); - thisFile = outerData.value; - if (thisFile !== undefined) { - const innerData = thisFile[1]; - const name = `${folderN}/${innerData.name}`; - storedFiles[name] = { file: innerData, data: {} }; - const metaData = await innerData.getFile(); - const encodedData = await this.encodeData(metaData, args.TYPE); - this.updateStore(name, encodedData, metaData); + + const createExtFile = async (folderN, data) => { + const name = `${folderN}/${data.name}`; + storedFiles[name] = { file: data, data: {} }; + const metaData = await data.getFile(); + const encodedData = await this.encodeData(metaData, args.TYPE); + this.updateStore(name, encodedData, metaData); + }; + const digThroughFolder = async (folderCtx) => { + const entries = folderCtx.entries(); + const folderN = args.NAME ? args.NAME : folderCtx.name; + let thisFile = ""; + while (thisFile !== undefined) { + const outerData = await entries.next(); + thisFile = outerData.value; + if (thisFile !== undefined) { + const innerData = thisFile[1]; + if (innerData.kind === "directory") await digThroughFolder(innerData); + else await createExtFile(folderN, innerData); + } } - } + }; + await digThroughFolder(picker); } catch (e) { console.warn(e); } From d60ce9ce15a528c04f67d09afadc79e523fdadb7 Mon Sep 17 00:00:00 2001 From: "DangoCat[bot]" Date: Wed, 16 Apr 2025 03:46:59 +0000 Subject: [PATCH 8/8] [Automated] Format code --- extensions/files.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/extensions/files.js b/extensions/files.js index 44950c9ba1..8cf8d31703 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -570,7 +570,10 @@ }, }, }, - { blockType: Scratch.BlockType.LABEL, text: Scratch.translate("Stored Files") }, + { + blockType: Scratch.BlockType.LABEL, + text: Scratch.translate("Stored Files"), + }, { opcode: "checkFileAPI", blockType: Scratch.BlockType.BOOLEAN, @@ -1173,7 +1176,8 @@ thisFile = outerData.value; if (thisFile !== undefined) { const innerData = thisFile[1]; - if (innerData.kind === "directory") await digThroughFolder(innerData); + if (innerData.kind === "directory") + await digThroughFolder(innerData); else await createExtFile(folderN, innerData); } } @@ -1216,7 +1220,7 @@ const res = await Scratch.fetch(url); if (!res.ok) return; const blob = await res.blob(); - + const writable = await storedFiles[args.NAME].file.createWritable(); await writable.write(blob); await writable.close();