diff --git a/development/builder.js b/development/builder.js index 9a2f025994..3d755fd631 100644 --- a/development/builder.js +++ b/development/builder.js @@ -105,6 +105,39 @@ class SVGFile extends ImageFile { } } +class SitemapFile extends DiskFile { + constructor (build) { + super(null); + this.getDiskPath = null; + this.build = build; + } + + getType () { + return '.xml'; + } + + read () { + let xml = ''; + xml += '\n'; + xml += '\n'; + + xml += Object.keys(this.build.files) + .filter(file => file.endsWith('.html')) + .map(file => file.replace('index.html', '').replace('.html', '')) + .sort((a, b) => { + if (a.length < b.length) return -1; + if (a.length > b.length) return 1; + return a - b; + }) + .map(path => `https://extensions.turbowarp.org${path}`) + .map(absoluteURL => `${absoluteURL}`) + .join('\n'); + + xml += '\n'; + return xml; + } +} + const IMAGE_FORMATS = new Map(); IMAGE_FORMATS.set('.png', ImageFile); IMAGE_FORMATS.set('.jpg', ImageFile); @@ -116,7 +149,7 @@ class Build { } getFile (path) { - return this.files[path] || this.files[`${path}index.html`] || null; + return this.files[path] || this.files[`${path}.html`] || this.files[`${path}index.html`] || null; } export (root) { @@ -192,6 +225,10 @@ class Builder { build.files[oldPath] = build.files[newPath]; } + if (this.mode !== 'desktop') { + build.files['/sitemap.xml'] = new SitemapFile(build); + } + const mostRecentExtensions = extensionFiles .sort((a, b) => b.getLastModified() - a.getLastModified()) .slice(0, 5) diff --git a/extensions/Alestore/nfcwarp.js b/extensions/Alestore/nfcwarp.js new file mode 100644 index 0000000000..c731f04095 --- /dev/null +++ b/extensions/Alestore/nfcwarp.js @@ -0,0 +1,73 @@ +(function(Scratch) { + 'use strict'; + + /* globals NDEFReader */ + + const extIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAAACXBIWXMAAAsTAAALEwEAmpwYAAACN0lEQVR4nK2WzW7TQBDHDRIVHCr6CYV+PUG5hENvlpKdfyxX6q0SanmVqo+AkKqqPAG8CAVE1RJaKYn6cSiiIO70RBnYydi4ttcJjiPtIbsz/59nPZ4Zz9MfEz1nog8MXDHAhYvoGxPtMDAvvrXaHQZeJ2ysxnsGNiL9HgR4lRK6YOB8AOAPNmZJNJaX76lf2m73XyTZwwM2ZoGBswFgLfa8W6q16bBb9zTEDEgjnWei074wgMTemKeOh9nzHO9EQOIcBHNMdNInqheJ67vOOf/pOZxjkAjU67MMdAtAe4mkusyzKQTx6uooAxN6LY+ZqOMAXSYSq/v/oDBclMRoNCblf7P5iInaOaBrm+IKOiwLskKf2Pen9GoeMHCU8THmvoIOyoMGgdXrD6sBRd+MC2bMwvAg+y0RHcewIJjW1J+OsywMF6uIaF9Egc8Ka3MYzqjwfqWgRAStGGazsFKQvZ6b19XS/eO4Hg4F6hXWaO/IJoDs+/6UZGHSZ8iIZlJZ17EVQs5WVsYZ+FgNqCfGTpjvj8WwkqBDcVpbG2Hgdw6sbRNBbBqNSREvBSLqJIrkd5cNR5EBE7YAl4noawKU1xhZV9e2kNRY4ARlGx/RL/b9u+r4sgDE0hSDYK4QpI3vnUOkpineLARBS1Q0EeWD3tqDDYfzpjhubd2OS0/xOtPvLg/0LAp3N+fwi50BtBo8kdGqP+w8M3IR7aRnu3Xb+2+8M6I3cee0Qwqw7ZoJUutKtKJI/gr8AfOqgU5hKhA4AAAAAElFTkSuQmCC'; + const blocksIcon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMCIgd2lkdGg9IjE2MDAuMDAwMDAwcHQiIGhlaWdodD0iMTYwMC4wMDAwMDBwdCIgdmlld0JveD0iMCAwIDE2MDAuMDAwMDAwIDE2MDAuMDAwMDAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0Ij4KPG1ldGFkYXRhIGZpbGw9IiNmZmZmZmYiPgpDcmVhdGVkIGJ5IHBvdHJhY2UgMS4xNSwgd3JpdHRlbiBieSBQZXRlciBTZWxpbmdlciAyMDAxLTIwMTcKPC9tZXRhZGF0YT4KPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMDAwMDAsMTYwMC4wMDAwMDApIHNjYWxlKDAuMTAwMDAwLC0wLjEwMDAwMCkiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZT0ibm9uZSI+CjxwYXRoIGQ9Ik0yNjk3IDE1OTg5IGMtNTY1IC01MCAtMTEwOCAtMjY0IC0xNTY3IC02MTkgLTExNCAtODggLTM5MSAtMzYyIC00ODQgLTQ4MCAtMTY0IC0yMDYgLTM0MCAtNTA5IC00MzQgLTc0NSAtODQgLTIxMSAtMTQzIC00MzAgLTE4NCAtNjgwIGwtMjMgLTE0MCAtMyAtNTEwNSBjLTIgLTM3MDkgMCAtNTE0NSA4IC01MjUwIDcwIC05MDUgNTQ5IC0xNzI0IDEzMDQgLTIyMjkgMzU3IC0yMzkgNzc4IC00MDIgMTIwMyAtNDY2IDIwOSAtMzIgNDA0IC0zNiAxNDM4IC0zMyBsMTAzMCAzIC01MCAyOSBjLTQ3MyAyNzIgLTkwMCA5ODAgLTExMzUgMTg4MCAtMTM2IDUyMSAtMjAwIDk1MiAtMjYyIDE3NjYgLTkgMTE2IC0xMyAxNTYzIC0xNSA1NzE5IGwtMyA1NTY0IDU5IC01NCBjMzMgLTMwIDE0NTAgLTE0MzMgMzE1MCAtMzExOSBsMzA5MSAtMzA2NSAwIC05NjAgMCAtOTYwIC0xNjcgMTY2IGMtMjc2IDI3MiAtMzY2MiAzNjM3IC00Mjg4IDQyNjEgbC01ODAgNTc4IC0zIC0zMDI4IGMtMyAtMzA1NiAwIC0zMzg3IDM0IC0zOTU3IDc2IC0xMjg5IDI1NyAtMjI1MSA1NTggLTI5NjggNTggLTEzOCAxOTUgLTQwNCAyNzEgLTUyNyAxNTYgLTI1MSA0MzQgLTU2MCA2NDggLTcxOSAzNTcgLTI2NiA3NzkgLTQ0OSAxMzIwIC01NzEgbDE3NyAtNDEgMjYzMyA0IGMyODk0IDMgMjY2NyAtMiAyOTgzIDYyIDk2MSAxOTQgMTc4NyA4OTMgMjE0OCAxODE2IDg2IDIyMSAxNDAgNDMwIDE4MyA3MDkgMTYgMTA4IDE4IDQxMyAyMSA1MTg1IDIgMzQ1NCAwIDUxMTcgLTggNTIxNyAtMTkgMjc4IC02MSA0OTAgLTE0NCA3NDMgLTM3NCAxMTI2IC0xMzc3IDE5MTQgLTI1NjEgMjAxNSAtNzIgNiAtNTU1IDEwIC0xMjAwIDEwIGwtMTA4MCAwIDU4IC0zMyBjMTAwIC01NiAxNzkgLTExOSAyOTcgLTIzNyA0NjUgLTQ2MyA4MDggLTEyNzMgOTc5IC0yMzEwIDU0IC0zMjkgODkgLTY1NCAxMjMgLTExNDUgOCAtMTI1IDEyIC0xNjI3IDE1IC01NzA0IGw0IC01NTMzIC0zOSAzMyBjLTIyIDE5IC0xMDc0IDEwNjAgLTIzMzggMjMxNCAtMTI2NCAxMjU0IC0yNjY1IDI2NDIgLTMxMTEgMzA4NSBsLTgxMyA4MDUgMCA5NjQgMCA5NjMgMTg1MyAtMTg0MyBjMTAxOCAtMTAxNCAyMTUyIC0yMTQzIDI1MjAgLTI1MDggbDY2NyAtNjY1IDAgMzAyNSBjMCAxNzQ5IC00IDMxNTMgLTEwIDMzMjkgLTU3IDE3OTQgLTI3NCAyOTkyIC02OTUgMzgzOCAtMTUzIDMwOCAtMzExIDUzNiAtNTI1IDc1NiAtMjIxIDIyNyAtNDQxIDM4NiAtNzM1IDUzMSAtMjY2IDEzMSAtNTM2IDIyMyAtODgzIDMwMSBsLTE1NCAzNCAtMjU5NiAtMSBjLTE0MjkgLTEgLTI2MzYgLTUgLTI2ODUgLTEweiIgZmlsbD0iI2ZmZmZmZiIvPgo8L2c+Cjwvc3ZnPg=='; + + class NFCWarp { + getInfo() { + return { + id: 'alestorenfc', + name: 'NFCWarp', + color1: '#FF4646', + color2: '#FF0000', + color3: '#990033', + menuIconURI: extIcon, + blockIconURI: blocksIcon, + blocks: [ + { + blockType: Scratch.BlockType.LABEL, + text: 'Only works in Chrome on Android' + }, + { + opcode: 'supported', + blockType: Scratch.BlockType.BOOLEAN, + text: 'NFC supported?' + }, + { + opcode: 'nfcRead', + blockType: Scratch.BlockType.REPORTER, + text: 'read NFC tag', + disableMonitor: true + } + ] + }; + } + + supported () { + return typeof NDEFReader !== 'undefined'; + } + + nfcRead() { + if (!this.supported()) { + return 'NFC not supported'; + } + return new Promise((resolve, reject) => { + const ndef = new NDEFReader(); + ndef.scan() + .then(() => { + ndef.onreadingerror = event => { + console.log('Reading error', event); + resolve('Tag not supported'); + }; + ndef.onreading = evt => { + const decoder = new TextDecoder(); + const record = evt.message.records[0]; + console.log('Record type: ' + record.recordType); + console.log('Record encoding: ' + record.encoding); + console.log('Record data: ' + decoder.decode(record.data)); + resolve(decoder.decode(record.data)); + }; + }) + .catch(error => { + console.log('Scan error', error); + resolve(`Error: ${error}`); + }); + }); + } + } + + Scratch.extensions.register(new NFCWarp()); +})(Scratch); diff --git a/extensions/Lily/Cast.js b/extensions/Lily/Cast.js new file mode 100644 index 0000000000..5035acae5a --- /dev/null +++ b/extensions/Lily/Cast.js @@ -0,0 +1,73 @@ +(function (Scratch) { + 'use strict'; + + const Cast = Scratch.Cast; + + class CastUtil { + getInfo() { + return { + id: 'lmsCast', + name: 'Cast', + blocks: [ + { + opcode: 'toType', + blockType: Scratch.BlockType.REPORTER, + text: 'cast [INPUT] to [TYPE]', + allowDropAnywhere: true, + disableMonitor: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + }, + TYPE: { + type: Scratch.ArgumentType.STRING, + menu: 'type' + } + } + }, + { + opcode: 'typeOf', + blockType: Scratch.BlockType.REPORTER, + text: 'type of [INPUT]', + disableMonitor: true, + arguments: { + INPUT: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'apple' + } + } + } + ], + menus: { + type: { + acceptReporters: true, + items: ['number', 'string', 'boolean', 'default'] + } + } + }; + } + + toType(args) { + const input = args.INPUT; + switch (args.TYPE) { + case ('number'): return Cast.toNumber(input); + case ('string'): return Cast.toString(input); + case ('boolean'): return Cast.toBoolean(input); + default: return input; + } + } + + typeOf(args) { + const input = args.INPUT; + switch (typeof input) { + case ('number'): return 'number'; + case ('string'): return 'string'; + case ('boolean'): return 'boolean'; + default: return ''; + } + } + } + + Scratch.extensions.register(new CastUtil()); +})(Scratch); diff --git a/extensions/Lily/TempVariables2.js b/extensions/Lily/TempVariables2.js index ad131fbc18..13912281f8 100644 --- a/extensions/Lily/TempVariables2.js +++ b/extensions/Lily/TempVariables2.js @@ -14,7 +14,6 @@ }); function resetRuntimeVariables() { - console.log('runtime variables cleared'); runtimeVariables = Object.create(null); } diff --git a/extensions/clipboard.js b/extensions/clipboard.js new file mode 100644 index 0000000000..7064a0194f --- /dev/null +++ b/extensions/clipboard.js @@ -0,0 +1,118 @@ +/*! + * Copyright 2023 tomyo-code + AdamMady + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function(Scratch) { + 'use strict'; + + if (!Scratch.extensions.unsandboxed) { + throw new Error('Clipboard must run unsandboxed'); + } + + const extensionicon = "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSI4MC40NTQ1NCIgaGVpZ2h0PSI4MC40NTQ1NCIgdmlld0JveD0iMCwwLDgwLjQ1NDU0LDgwLjQ1NDU0Ij48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTk5Ljc3MjcyLC0xMzkuNzcyNzIpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTE5OS43NzI3MywxODBjMCwtMjIuMjE2OSAxOC4wMTAzNywtNDAuMjI3MjcgNDAuMjI3MjcsLTQwLjIyNzI3YzIyLjIxNjksMCA0MC4yMjcyNywxOC4wMTAzNyA0MC4yMjcyNyw0MC4yMjcyN2MwLDIyLjIxNjkgLTE4LjAxMDM3LDQwLjIyNzI3IC00MC4yMjcyNyw0MC4yMjcyN2MtMjIuMjE2OSwwIC00MC4yMjcyNywtMTguMDEwMzcgLTQwLjIyNzI3LC00MC4yMjcyN3oiIGZpbGw9IiMwMDgwODAiIHN0cm9rZS13aWR0aD0iMCIvPjxpbWFnZSB4PSI0MzQiIHk9IjMwMCIgdHJhbnNmb3JtPSJzY2FsZSgwLjUsMC41KSIgd2lkdGg9Ijk0IiBoZWlnaHQ9IjExOCIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFGNEFBQUIyQ0FZQUFBQkJMU1ExQUFBQUFYTlNSMElBcnM0YzZRQUFCckpKUkVGVWVGN3RuVnZJYmtNWXgyZTJuTUlPeVNHbDVCQjJTWExNc1lRTFo5cHNaMEl1aEhKQmlkeTZJaUVrNTFPT0VYZkVqZHk0c0pPemN0Z1hLRGtrNStTdmI3Ny9zMzNmM3U5YTYxbnZtdlhNdk85KzNwdXA5VDd6UERPLythOVpzMmF0bVJWRDVUOEFlN0tJMXpNOWx1bTJUTDlqK2dyVGV4ZlNHT09mTlZjdDFseTRoYkk1ZU9NV0FuQXpROTdLZEN0bEVlUU1XRTNsdjYzTVoycFdyZUlkdktrT1V0ZHlHME5LT3EwNHBJOVAxNFFZNDd2R1ZXa05OMjJsUnF1RGd4OE43V1RIQVBibFB4OHczVHhURVQ2bm4vMm8vSDh6K1Iza3BockZiN0xnQVd6QkpyeUo2UWxNdHh6VXRQOW5mcHFLdTN1U1B3Q1A4dmlsSGZIKzR2OC9NZDJaNllxR2ZPRHhreG4vOVliNGovRzRuSGxEcXkzbGU0SnhuMW5xY0wzaUhUeHN3UVBZbmkwaFYvMjloeloxUS80MzJQSW5OaWp1TXg3ZnB5SC9iengrTVAwa2V3RFg4TGljU1UzZFovby94bmhkUS94ZmVIeTd6UFdYTSs0cHhyODRwUTUrRVRNQWMvRFM5NnpKM05JYnVudVZMWDU2ZytLKzVmRmRHOHFSenNnWTQyRU4rZi9nOGFZNzNPZVkvN3lHL0QvdytJNGpjemhWRk8vZ0Z4VnZEcjVMS2JrRWNCa1ZKeGV4Wlg0QnlCekxMZzBCcFp4SDBjOTc3Q0p1cFAzdFRKdjYrT2VaNzl3R3hiL0c0NmZrcW5DRG4zV2llQWUvcUhoejhITFY3V3JvZjJnZzQrZ3VlMm5RQjZpMFc5b3lBT2pxNHlXN2xFTXVoanQwS0YzeWRmWHhLMm1ZN0VJSVJ6TGRyS09pY29adHJTeEhNbHNZMVRqNFJjVlhCLzVIdHVReFREL3Bram9WM210T0JNRDM5THVUeHY4VU51a0pWWXp4ekQ1NUFUVGRFWXNiK2Y4U0huaVFhV3MramVJZGZIdExqUVkrRFRkampCZjBVWXJXRnNCcHRIMVpveFN0M3dsMklxQzlXSitmQi9ocXpLbzljeldLZC9BOVdpZ24rTWVwa0s1WlExWHhBSnhFdzZ1WW5zRTAxL3g3Vnpua1J1bGhHcVpaMFJqalIxMFpOZjhyN2tlU0c0M2lIYnlHT0cxeWduK1Npa2l6YW4xL0FPUk84SDdtM1oxcExROWg1TDVBWm1mUFlYM2x2cUpYbFIyOEh0ZDhnUWR3Tk92K3BuRWZya2MrMlRMTnBZUVFWbEg1di9aeFdGenhEajQwVGZhcEw2NVQ5ZkVBWkpTd2Z4L0ZWR1I3SnhWL1E1OHkxYUI0QjkvU1lwcmhaQy9GQXppQThkNW4yalc3MTBkUWxyYmZVUEV5Q2xQRkxxWjRCOS81UUdlY1BuN0pVLzk3VkJLcDEwaUdtWHYwR2RlWFZMeThidUhnQy9YeHZlYTlLeFMrUEVGN2lJcVhKMTZ0UlMycGVMbTRPbmhMeFZlb1hOTWlGVk84YVMwckRPYmdDeldLZzNmd2hRZ1VDdXVLbjNmd0FPVFo2ZVhHZGIyTDQrdG5qZVBXTVk1MzhNdmJ3YXlyQVpEZWV3OGhwUGUrRFgvcHJlTVlZM29MdVphZmd5L1VFcGJnWlUyUjlQVldWYjZEaW4vRUtxQW1qb1BYVUJyQnhnejhDR1dmYVpjT3ZsRHo1UVF2NnpNdktsU1htUXJyNEFzMWw0TjM4SVVJRkFwcnBuZ0FCN0tPRTFkY0Y2cC9qckN5dzlOTHZGLzRYZVBVd1dzb3RkdFVELzVGbHYvczRYV3R5b01zUTVYZFF0WnFTcWRkcjZ0NWhhOTFPQW5Bd1M5cEVVdndhZVYyQ01GNlBsNGp3Q0UyZnkrOWRtblhTRG40SWNnWDgxWVBYdmI0emIyejBYQjB3enpJeXZTMDRqekdxRnFwYnFsNEIxK2lqeDhtcXZuTGJhYjQrVU0zckVZT2ZoaS9xWE03K0tuUkRjdG9CbjdKUnFHeVUrdXdrbytYTzYzd0dQdExDZzUrNHdhY08vQ3l3K21WNDRrMWkrY1BxZmhEc25ocmNHS3BlQWRmWWh3UElNMVhoeERPR2xOSkdYeW5mUzFqakx0bDhOWG93bEx4RHI2UTRnOW4zT1BHVkZJRzMxOVE4UzlrOEZXRjRoMThDY1dQcVo1WjlHM1d4ODhpbkRITDdPREhwTnZpMjhIUEFmaFJOL3dzeEdlMHNEa1Y3K0I3TkpNWmVBQlhzMXhWclVWU3NQcUs0L3J6RmJacUV3ZmZqV3Jtd1pkYTlkZU50dDBpdmFJWFk1UXZIUXoxbC9KYkt0N0JqM1RuMm5weEJYQXQ0MTZSUlRKMlRyNms0clBPcWxvcTNzR1hVTHlkUUdjamtwbmlad09IWFNrZHZCM3JaWkVjdklNdlJLQlFXRmY4dklNSGNCRHJlRVNodWtyWVR6a3VmNnRrT2N3VTcrQ1hON01sK0ZvV24zMU14Y3NXdTBXRTcrQ0xZTGVkSkx1dmtybWFkNmo0NHdzeE41K2RkUEFsNW1vQXlGZmhaUkZhS2NISi9IcXY3emJsTHF4bEgrL2dTeWcrdDJKbTNaK1o0bWNkVk83eU8vamNSSlgrSEx3U1ZHNHpCNSticU5LZmcxZUN5bTJXRTN6YW56M0d1Q1ozSWVmUlg4NDl5Ung4RDRYa0JKLzJhd2toSEVybGY5MmpISnVNS1lEVnJHeDZEeW1FMFBwVlQ4MmVaQTVlSVo4eHdFdFkyU3BLdm9HbktFNVZKbkdrMG9qZmJlaC9oU2FPUnZFT3ZwM2sxT0JGd1RMWnBXa3d0NW1ld0xwMHJRVGc0S2VIT0UzTzllRFRON2xEQ0JkTzQ4WHpxQW5JenEzcDYwRUxpbmZ3YW5hREREY0N2NUx1MGpQTEVNS3FRZTQ5ODRZRUJMaDhDVGw5UFdoQjhRNStYTEZNQlA4ZjVqR04yQ3N0cTkwQUFBQUFTVVZPUks1Q1lJST0iIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iMC41Ii8+PC9nPjwvZz48L3N2Zz4="; + + let lastPastedText = ''; + + window.addEventListener('copy', (event) => { + Scratch.vm.runtime.startHats('clipboard_whenCopied') ; + }); + window.addEventListener('paste', (event) => { + Scratch.vm.runtime.startHats('clipboard_whenPasted'); + const clipboardData = event.clipboardData || window.clipboardData; + const pastedText = clipboardData.getData('Text'); + lastPastedText = pastedText; + }); + + class Clipboard { + getInfo() { + return { + id: 'clipboard', + name: 'Clipboard', + blockIconURI: extensionicon, + color1: '#008080', + color2: '#006666', + blocks: [ + { + opcode: 'whenCopied', + blockType: Scratch.BlockType.HAT, + text: 'when something is copied', + isEdgeActivated: false + }, + { + opcode: 'whenPasted', + blockType: Scratch.BlockType.HAT, + text: 'when something is pasted', + isEdgeActivated: false + }, + '---', + { + opcode: 'setClipboard', + blockType: Scratch.BlockType.COMMAND, + text: 'copy to clipboard: [TEXT]', + arguments: { + TEXT: { + type: Scratch.ArgumentType.STRING + } + } + }, + { + opcode: 'resetClipboard', + blockType: Scratch.BlockType.COMMAND, + text: 'reset clipboard' + }, + '---', + { + opcode: 'clipboard', + blockType: Scratch.BlockType.REPORTER, + text: 'clipboard', + disableMonitor: true + }, + { + opcode: 'getLastPastedText', + blockType: Scratch.BlockType.REPORTER, + text: 'last pasted text', + disableMonitor: true + } + ], + }; + } + + setClipboard(args) { + navigator.clipboard.writeText(args.TEXT); + } + + resetClipboard() { + navigator.clipboard.writeText(''); + } + + clipboard() { + if (navigator.clipboard && navigator.clipboard.readText) { + return Scratch.canReadClipboard().then(allowed => { + if (allowed) { + return navigator.clipboard.readText(); + } + return ''; + }); + } + return ''; + } + + getLastPastedText() { + return lastPastedText; + } + } + + Scratch.extensions.register(new Clipboard()); +})(Scratch); diff --git a/extensions/files.js b/extensions/files.js index 25842fd6b2..2d1cea7860 100644 --- a/extensions/files.js +++ b/extensions/files.js @@ -165,21 +165,58 @@ }); /** - * @param {string} text Text to download - * @param {string} file Name of the file + * @param {string} url a data:, blob:, or same-origin URL + * @param {string} file */ - const download = (text, file) => { - const blob = new Blob([text]); - const url = URL.createObjectURL(blob); + const downloadURL = (url, file) => { const link = document.createElement('a'); link.href = url; link.download = file; document.body.appendChild(link); link.click(); link.remove(); + }; + + /** + * @param {Blob} blob Data to download + * @param {string} file Name of the file + */ + const downloadBlob = (blob, file) => { + const url = URL.createObjectURL(blob); + downloadURL(url, file); URL.revokeObjectURL(url); }; + /** + * @param {string} url + * @returns {boolean} + */ + const isDataURL = (url) => { + try { + const parsed = new URL(url); + return parsed.protocol === 'data:'; + } catch (e) { + return false; + } + }; + + /** + * @param {string} url + * @param {string} file + */ + const downloadUntrustedURL = (url, file) => { + // Don't want to return a Promise here when not actually needed + if (isDataURL(url)) { + downloadURL(url, file); + } else { + return Scratch.fetch(url) + .then(res => res.blob()) + .then(blob => { + downloadBlob(blob, file); + }); + } + }; + class Files { getInfo () { return { @@ -253,6 +290,24 @@ } } }, + { + opcode: 'downloadURL', + blockType: Scratch.BlockType.COMMAND, + text: 'download URL [url] as [file]', + arguments: { + url: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==' + }, + file: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'save.txt' + } + } + }, + + '---', + { opcode: 'setOpenMode', blockType: Scratch.BlockType.COMMAND, @@ -314,7 +369,11 @@ } download (args) { - download(args.text, args.file); + downloadBlob(new Blob([Scratch.Cast.toString(args.text)]), Scratch.Cast.toString(args.file)); + } + + downloadURL (args) { + return downloadUntrustedURL(Scratch.Cast.toString(args.url), Scratch.Cast.toString(args.file)); } setOpenMode (args) { diff --git a/images/Lily/Cast.svg b/images/Lily/Cast.svg new file mode 100644 index 0000000000..c133d27df6 --- /dev/null +++ b/images/Lily/Cast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index 9b629d4eb8..76bd6f0da2 100644 --- a/images/README.md +++ b/images/README.md @@ -233,3 +233,6 @@ All images in this folder are licensed under the [GNU General Public License ver ## Lily/LooksPlus.svg - Created by [@LilyMakesThings](https://github.com/LilyMakesThings) in https://github.com/TurboWarp/extensions/pull/656 + +## clipboard.svg + - Created by [@AdamMady](https://github.com/AdamMady/) diff --git a/images/clipboard.svg b/images/clipboard.svg new file mode 100644 index 0000000000..f02be5a0ee --- /dev/null +++ b/images/clipboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/cursor.png b/images/cursor.png new file mode 100644 index 0000000000..b53b880cd6 Binary files /dev/null and b/images/cursor.png differ diff --git a/images/cursor.svg b/images/cursor.svg deleted file mode 100644 index 5a0ab87dbb..0000000000 --- a/images/cursor.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/gamejolt.png b/images/gamejolt.png new file mode 100644 index 0000000000..d72c154054 Binary files /dev/null and b/images/gamejolt.png differ diff --git a/images/gamejolt.svg b/images/gamejolt.svg deleted file mode 100644 index 103ef7b390..0000000000 --- a/images/gamejolt.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/pointerlock.png b/images/pointerlock.png new file mode 100644 index 0000000000..e653ce69e4 Binary files /dev/null and b/images/pointerlock.png differ diff --git a/images/pointerlock.svg b/images/pointerlock.svg deleted file mode 100644 index 871de74ca4..0000000000 --- a/images/pointerlock.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/website/index.ejs b/website/index.ejs index a6a750270a..9f95fc9a41 100644 --- a/website/index.ejs +++ b/website/index.ejs @@ -476,6 +476,12 @@

Clipping outside of a specified rectangular area and additive color blending. Created by Vadik1.

+
+ <%- banner('clipboard') %> +

Clipboard

+

Read and write from the system clipboard.

+
+
<%- banner('penplus') %>

Pen Plus

@@ -572,6 +578,12 @@

A few adapter blocks. Created by TrueFantom.

+
+ <%- banner('Lily/Cast') %> +

Cast

+

Convert values between types. Created by LilyMakesThings

+
+
<%- banner('-SIPC-/time') %>

Time

@@ -710,6 +722,12 @@

Allows you to use webhooks. Created by CubesterYT.

+
+ <%- banner('Alestore/nfcwarp') %> +

NFCWarp

+

Allows reading data from NFC (NDEF) devices. Only works in Chrome on Android. Created by Alestore Games.

+
+
<%- banner('itchio') %>

itch.io