diff --git a/static/SuperStorage.js b/static/SuperStorage.js index 2b3de9244c3..30994933a93 100644 --- a/static/SuperStorage.js +++ b/static/SuperStorage.js @@ -1,17 +1,121 @@ - (function(Scratch) { +// SuperStorage (v2) by pooiod7 + +(function(Scratch) { 'use strict'; if (!Scratch.extensions.unsandboxed) { throw new Error('This extension must run unsandboxed'); } - class StorageV2 { + if (!Scratch.download) { + Scratch.download = function(url, filename) { + return new Promise((resolve, reject) => { + if (Scratch.vm.runtime.isPackaged || !typeof scaffolding === "undefined") { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + resolve(); + } else { + const modal = document.createElement('div'); + modal.style.position = 'fixed'; + modal.style.top = '0'; + modal.style.left = '0'; + modal.style.width = '100%'; + modal.style.height = '100%'; + modal.style.backgroundColor = 'rgba(0, 0, 0, 0.2)'; + modal.style.display = 'flex'; + modal.style.alignItems = 'center'; + modal.style.justifyContent = 'center'; + modal.style.zIndex = '9999'; + + const modalContent = document.createElement('div'); + modalContent.style.backgroundColor = '#fff'; + modalContent.style.padding = '40px'; + modalContent.style.borderRadius = '8px'; + modalContent.style.textAlign = 'center'; + modalContent.style.width = '500px'; + + const message = document.createElement('p'); + message.innerHTML = `This project wants to download "${filename}" to your computer.
This file has not been reviewed for malicious intent.`; + + const buttonsContainer = document.createElement('div'); + buttonsContainer.style.marginTop = '30px'; + + const acceptButton = document.createElement('button'); + acceptButton.textContent = 'Accept Download'; + acceptButton.style.marginRight = '20px'; + acceptButton.style.backgroundColor = '#4CAF50'; + acceptButton.style.color = 'white'; + acceptButton.style.border = 'none'; + acceptButton.style.padding = '15px 30px'; + acceptButton.style.fontSize = '16px'; + acceptButton.style.cursor = 'pointer'; + acceptButton.style.borderRadius = '8px'; + acceptButton.style.transition = 'background-color 0.3s'; + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Reject Download'; + cancelButton.style.backgroundColor = '#f44336'; + cancelButton.style.color = 'white'; + cancelButton.style.border = 'none'; + cancelButton.style.padding = '15px 30px'; + cancelButton.style.fontSize = '16px'; + cancelButton.style.cursor = 'pointer'; + cancelButton.style.borderRadius = '8px'; + cancelButton.style.transition = 'background-color 0.3s'; + + buttonsContainer.appendChild(acceptButton); + buttonsContainer.appendChild(cancelButton); + + modalContent.appendChild(message); + modalContent.appendChild(buttonsContainer); + modal.appendChild(modalContent); + document.body.appendChild(modal); + + acceptButton.addEventListener('mouseover', () => { + acceptButton.style.backgroundColor = '#45a049'; + }); + acceptButton.addEventListener('mouseout', () => { + acceptButton.style.backgroundColor = '#4CAF50'; + }); + cancelButton.addEventListener('mouseover', () => { + cancelButton.style.backgroundColor = '#e53935'; + }); + cancelButton.addEventListener('mouseout', () => { + cancelButton.style.backgroundColor = '#f44336'; + }); + + acceptButton.addEventListener('click', () => { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + document.body.removeChild(modal); + resolve(); + }); + + cancelButton.addEventListener('click', () => { + document.body.removeChild(modal); + reject(); + }); + } + }); + } + } + + class SuperStorage { constructor() { this.currentServer = "https://storage-ext.penguinmod.com/"; this.useGlobal = true; this.waitingForResponse = false; this.serverFailedResponse = false; this.serverError = ""; + this.prefix = "SuperStorage_"; } getInfo() { @@ -20,90 +124,237 @@ name: 'Super Storage', color1: '#31b3d4', color2: '#179fc2', - docsURI: 'https://pooiod7.neocities.org/markdown/#/projects/scratch/extensions/other/markdown/SuperStorage', + docsURI: 'https://p7scratchextensions.pages.dev/docs/#/SuperStorage', blocks: [ { blockType: Scratch.BlockType.LABEL, text: "Local Storage" }, + { opcode: 'getValue', - text: 'get local [KEY]', + text: 'Get local [KEY]', disableMonitor: true, blockType: Scratch.BlockType.REPORTER, arguments: { - KEY: { type: Scratch.ArgumentType.STRING, defaultValue: "key" } + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: "File1" + } } }, + { opcode: 'setValue', - text: 'set local [KEY] to [VALUE]', + text: 'Set local [KEY] to [VALUE]', blockType: Scratch.BlockType.COMMAND, arguments: { - KEY: { type: Scratch.ArgumentType.STRING, defaultValue: "key" }, - VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: "value" } + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: "File1" + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Hello, World!" + } } }, + { opcode: 'deleteValue', - text: 'delete local [KEY]', + text: 'Delete local [KEY]', blockType: Scratch.BlockType.COMMAND, - arguments: { KEY: { type: Scratch.ArgumentType.STRING, defaultValue: "key" } } + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: "File1" + } + } }, + { opcode: 'getKeys', - text: 'get all local stored names', + text: 'Get all local stored names', disableMonitor: true, blockType: Scratch.BlockType.REPORTER }, + + { blockType: Scratch.BlockType.LABEL, text: "Server Storage" }, + { - blockType: Scratch.BlockType.LABEL, - text: "Server Storage" + opcode: 'canUseOnlineStorage', + text: 'Can connect to server?', + disableMonitor: true, + blockType: Scratch.BlockType.BOOLEAN }, + { opcode: 'waitingForConnection', - text: 'waiting for server to respond?', + text: 'Waiting for server to respond?', disableMonitor: true, blockType: Scratch.BlockType.BOOLEAN }, + { opcode: 'connectionFailed', - text: 'server failed to respond?', + text: 'Server failed to respond?', disableMonitor: true, blockType: Scratch.BlockType.BOOLEAN }, { opcode: 'serverErrorOutput', - text: 'server error', + text: 'Server error', disableMonitor: false, blockType: Scratch.BlockType.REPORTER }, + "---", + { opcode: 'getServerValue', - text: 'get server [KEY]', + text: 'Get server [KEY]', disableMonitor: true, blockType: Scratch.BlockType.REPORTER, - arguments: { KEY: { type: Scratch.ArgumentType.STRING, defaultValue: "key" } } + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: "File1" + } + } }, + { opcode: 'setServerValue', - text: 'set server [KEY] to [VALUE]', + text: 'Set server [KEY] to [VALUE]', blockType: Scratch.BlockType.COMMAND, arguments: { - KEY: { type: Scratch.ArgumentType.STRING, defaultValue: "key" }, - VALUE: { type: Scratch.ArgumentType.STRING, defaultValue: "value" } + KEY: { + type: Scratch.ArgumentType.STRING, defaultValue: "File1" + }, + VALUE: { + type: Scratch.ArgumentType.STRING, defaultValue: "Hello, World!" + } } }, + { opcode: 'deleteServerValue', - text: 'delete server [KEY]', + text: 'Delete server [KEY]', blockType: Scratch.BlockType.COMMAND, - arguments: { KEY: { type: Scratch.ArgumentType.STRING, defaultValue: "key" } } - } - ] + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: "File1" + } + } + }, + + { blockType: Scratch.BlockType.LABEL, text: "Device files" }, + + { + opcode: "openFile", + blockType: Scratch.BlockType.REPORTER, + text: "Open file selector for types [types] as [format]", + arguments: { + types: { + type: Scratch.ArgumentType.STRING, defaultValue: "" + }, + format: { + type: Scratch.ArgumentType.STRING, + menu: "fileFormats", + defaultValue: "raw" + } + } + }, + + { + opcode: "downloadFile", + blockType: Scratch.BlockType.COMMAND, + text: "Download [text] as [filename]", + arguments: { + text: { + type: Scratch.ArgumentType.STRING, defaultValue: "Hello, world!" + }, + filename: { + type: Scratch.ArgumentType.STRING, defaultValue: "file.txt" + } + } + }, + { + opcode: "downloadDataURI", + blockType: Scratch.BlockType.COMMAND, + text: "Download file from data URI [url] as [filename]", + arguments: { + url: { + type: Scratch.ArgumentType.STRING, defaultValue: "data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==" + }, + filename: { + type: Scratch.ArgumentType.STRING, defaultValue: "file.txt" + } + } + }, + + { blockType: Scratch.BlockType.LABEL, text: "Cookies" }, + + { + opcode: 'getAllCookies', + blockType: Scratch.BlockType.REPORTER, + disableMonitor: true, + text: 'Get all cookie names', + }, + + { + opcode: 'setCookie', + blockType: Scratch.BlockType.COMMAND, + text: 'Set cookie [KEY] to [VALUE] with expiration [DATE]', + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Cookie1', + }, + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Hello, World!', + }, + DATE: { + type: Scratch.ArgumentType.STRING, + defaultValue: new Date(Date.now() + 86400000).toISOString(), + } + }, + }, + + { + opcode: 'getCookieValue', + blockType: Scratch.BlockType.REPORTER, + text: 'Get cookie value for [KEY]', + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Cookie1', + } + }, + }, + + { + opcode: 'removeCookie', + blockType: Scratch.BlockType.COMMAND, + text: 'Remove cookie [KEY]', + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'Cookie1', + } + }, + }, + ], + menus: { + fileFormats: { + acceptReporters: true, + items: ["raw", "data uri", "hex"] + } + } }; } getPrefix() { - return `P7_PROJECTSTORAGE_`; + return this.prefix; } getAllKeys() { @@ -115,7 +366,7 @@ this.serverFailedResponse = false; this.serverError = ""; - return fetch(url, options) + return Scratch.fetch(url, options) .then(response => response.ok ? response.text() : Promise.reject(response.text())) .then(text => { this.waitingForResponse = false; @@ -145,6 +396,15 @@ localStorage.removeItem(this.getPrefix() + args.KEY); } + async canUseOnlineStorage() { + try { + const response = await Scratch.fetch(this.currentServer, { method: 'HEAD' }); + return response.ok; + } catch { + return false; + } + } + waitingForConnection() { return this.waitingForResponse; } @@ -172,7 +432,160 @@ deleteServerValue(args) { return this.runPenguinWebRequest(`${this.currentServer}delete?key=${args.KEY}`, { method: "DELETE" }); } + + openFile(args) { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = args.types || ""; + + const outer = document.createElement("div"); + outer.style.pointerEvents = "auto"; + outer.style.position = "fixed"; + outer.style.top = "0"; + outer.style.left = "0"; + outer.style.width = "100%"; + outer.style.height = "100%"; + outer.style.display = "flex"; + outer.style.justifyContent = "center"; + outer.style.alignItems = "center"; + outer.style.background = "rgba(0, 0, 0, 0.5)"; + outer.style.zIndex = "10000"; + outer.style.opacity = "0"; + outer.style.transition = "opacity 0.3s ease-in"; + + setTimeout(() => { + outer.style.opacity = "1"; + }, 0); + + const modal = document.createElement("div"); + modal.style.background = "white"; + modal.style.padding = "20px"; + modal.style.borderRadius = "10px"; + modal.style.textAlign = "center"; + modal.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)"; + modal.innerHTML = ` +

Select a file or drop it here

+ + `; + + const selectButton = modal.querySelector("button"); + selectButton.addEventListener("click", () => input.click()); + + outer.appendChild(modal); + document.body.appendChild(outer); + + const cleanUp = () => { + outer.style.opacity = "0"; + setTimeout(() => { + document.body.removeChild(outer); + }, 300); + }; + + input.addEventListener("change", (e) => { + cleanUp(); + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result; + if (args.format === "raw") { + resolve(result); + } else if (args.format === "data uri") { + resolve(result); + } else if (args.format === "hex") { + resolve(Array.from(new Uint8Array(result)).map(b => b.toString(16).padStart(2, "0")).join("")); + } else { + resolve(""); + } + }; + reader.onerror = () => resolve(""); + if (args.format === "data uri") { + reader.readAsDataURL(file); + } else if (args.format === "hex") { + reader.readAsArrayBuffer(file); + } else { + reader.readAsText(file); + } + } else { + resolve(""); + } + }); + + input.addEventListener("cancel", () => { + cleanUp(); + resolve(""); + }); + + outer.addEventListener("click", (e) => { + if (e.target === outer) { + cleanUp(); + resolve(""); + } + }); + + input.click(); + }); + } + + async downloadFile(args) { + try { + const url = URL.createObjectURL(new Blob([args.text], { type: "text/plain" })); + await Scratch.download(url, args.filename); + URL.revokeObjectURL(url); + } catch (e) { + console.error(e); + } + } + + async downloadDataURI(args) { + try { + const url = args.url; + await Scratch.download(url, args.filename); + } catch (e) { + console.error(e); + } + } + + getAllCookies() { + const cookies = document.cookie.split(';'); + let result = []; + cookies.forEach(cookie => { + const [key] = cookie.split('='); + if (key.trim().startsWith(this.prefix)) { + result.push(key.trim().replace(this.prefix, '')); + } + }); + return result.join(', '); + } + + setCookie(args) { + const { KEY, VALUE, DATE } = args; + const cookieName = this.prefix + KEY; + let cookie = `${cookieName}=${VALUE}; path=/`; + if (DATE) { + cookie += `; expires=${new Date(DATE).toUTCString()}`; + } + document.cookie = cookie; + } + + getCookieValue(args) { + const cookieName = this.prefix + args.KEY; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const [key, value] = cookies[i].split('='); + if (key.trim() === cookieName) { + return value; + } + } + return ''; + } + + removeCookie(args) { + const cookieName = this.prefix + args.KEY; + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; + } } - Scratch.extensions.register(new StorageV2()); + Scratch.extensions.register(new SuperStorage()); })(Scratch);