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