diff --git a/extensions/SharkPool/Font-Manager.js b/extensions/SharkPool/Font-Manager.js new file mode 100644 index 0000000000..ee3627d1cd --- /dev/null +++ b/extensions/SharkPool/Font-Manager.js @@ -0,0 +1,363 @@ +// Name: Font Manager +// ID: SPASfontManager +// Description: Add, delete, and manage fonts. +// By: SharkPool +// By: Ashimee +// License: MIT + +// Version V.1.1.0 + +(function (Scratch) { + "use strict"; + + if (!Scratch.extensions.unsandboxed) { + throw new Error("Font Manager must be run unsandboxed"); + } + + const extensionId = "SPASfontManager"; + const vm = Scratch.vm; + const runtime = vm.runtime; + const storage = runtime.storage; + const fontManager = runtime.fontManager; + + const FONT_EXTENSIONS = [ + storage.DataFormat.TTF, + storage.DataFormat.OTF, + storage.DataFormat.WOFF, + storage.DataFormat.WOFF2, + ]; + + const menuIconURI = + "data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMDIuMzMzMzQiIGhlaWdodD0iMTAyLjMzMzM0IiB2aWV3Qm94PSIwLDAsMTAyLjMzMzM0LDEwMi4zMzMzNCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE4OC44MzUyMywtMTI4LjU2MDQ0KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0xODguODM1MjMsMTc5LjcyNzExYzAsLTI4LjI1ODU3IDIyLjkwODEsLTUxLjE2NjY3IDUxLjE2NjY3LC01MS4xNjY2N2MyOC4yNTg1NywwIDUxLjE2NjY3LDIyLjkwODEgNTEuMTY2NjcsNTEuMTY2NjdjMCwyOC4yNTg1NyAtMjIuOTA4MSw1MS4xNjY2NyAtNTEuMTY2NjcsNTEuMTY2NjdjLTI4LjI1ODU3LDAgLTUxLjE2NjY3LC0yMi45MDgxIC01MS4xNjY2NywtNTEuMTY2Njd6IiBmaWxsPSIjMWM0ZTQ1IiBzdHJva2Utd2lkdGg9IjAiLz48cGF0aCBkPSJNMTkzLjgxMTkyLDE3OS43MjcxMmMwLC0yNS41MTAwMiAyMC42Nzk5NSwtNDYuMTg5OTcgNDYuMTg5OTcsLTQ2LjE4OTk3YzI1LjUxMDAyLDAgNDYuMTg5OTcsMjAuNjc5OTUgNDYuMTg5OTcsNDYuMTg5OTdjMCwyNS41MTAwMiAtMjAuNjc5OTUsNDYuMTg5OTcgLTQ2LjE4OTk3LDQ2LjE4OTk3Yy0yNS41MTAwMiwwIC00Ni4xODk5NywtMjAuNjc5OTUgLTQ2LjE4OTk3LC00Ni4xODk5N3oiIGZpbGw9IiMyYjdkNmUiIHN0cm9rZS13aWR0aD0iMCIvPjxwYXRoIGQ9Ik0yNTcuNDI2MTUsMTg3LjY2NzUxYzAsMi42OTA0OSAtMi4xODg4Myw0Ljg3OTMyIC00Ljg3OTMyLDQuODc5MzJjLTIuNjkwNDksMCAtNC44NzkzMiwtMi4xODg4MyAtNC44NzkzMiwtNC44NzkzMmMwLC0yLjY5MDQ5IDIuMTg4ODMsLTQuODc5MzIgNC44NzkzMiwtNC44NzkzMmMyLjY5MDQ5LDAgNC44NzkzMiwyLjE4ODgzIDQuODc5MzIsNC44NzkzMnoiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMSIvPjxwYXRoIGQ9Ik0yNjcuODgxODUsMTYzLjc0NjRjMCw2LjYzMDAxIDAsMzguNTU5MDggMCwzOC41NTkwOGMwLDIuMzA5NzIgLTEuODcyNTUsNC4xODIyOCAtNC4xODIyOCw0LjE4MjI4aC00Ny4zOTkxM2MtMi4zMDk3MiwwIC00LjE4MjI4LC0xLjg3MjU1IC00LjE4MjI4LC00LjE4MjI4YzAsMCAwLC00MC4yNTQ1IDAsLTQ2LjE3NDk2YzAsLTEuNDA5ODEgMS4zNDM5OSwtMi42MTgyNyAyLjUzMDAyLC0yLjYxODI3YzIuMTEwNzUsMCA5LjA2MzMyLDAgMTIuMzkxOTMsMGMxLjEwMjI5LDAgMi4zMTA2LDAuNTAzNDQgMi45ODcyMSwxLjE4MDA2YzEuMDgzODcsMS4wODM4NyAyLjYxMjExLDIuNjEyMSAzLjIyNzA3LDMuMjI3MDZjMC41MTQ0OSwwLjUxNDQ5IDEuODI5MDEsMS4xNjkyNSAyLjk1MDU4LDEuMTY5MjVjNS4zOTM3NSwwIDIxLjQ2NzgxLDAgMjYuMjAzNzcsMGMyLjUyOTkyLDAgNS40NzMxLDIuMzUzNDcgNS40NzMxLDQuNjU3Nzh6TTI0My45Mzc3OCwxOTUuNTc2MTljLTIuMDM3NTUsLTQuNTQzNzUgLTkuNDkxNTMsLTIxLjE2NjIyIC0xMS4zNTMzNSwtMjUuMzE4MTFjLTAuODEzMjUsLTEuODEzNTUgLTIuNDgyOTcsLTEuOTQxMDQgLTMuMjUyNTEsLTAuMjE2ODJjLTEuNzYxNTIsMy45NDY4NCAtOC44Mzg1LDE5LjgwMzQyIC0xMS4xNjU0MiwyNS4wMTcwOGMtMC40Njk1NSwxLjA1MjA3IC0wLjIxMTUyLDEuNjcwNzcgMC40NTYyMiwxLjY3MDc3YzAuODQ0MjIsMCAxLjkwMTk1LDAgMi4zNjM2NCwwYzAuNDQxMDYsMCAxLjIyOTk2LC0wLjQ4Mjc4IDEuNDQyMDksLTAuOTU4MDdjMC42MTY5NCwtMS4zODIyOCAzLjYxNjc4LC04LjEwMzUzIDMuNjE2NzgsLTguMTAzNTNoOS43NjI3OWMwLDAgMy4wODg4LDYuODg4MjkgMy42ODQ5Nyw4LjIxNzc5YzAuMTkxMDMsMC40MjYwMiAwLjg3MDQsMC44NDM4MSAxLjQ5MzM3LDAuODQzODFjMC43OTQxMywwIDEuODAxMDQsMCAyLjI3MjczLDBjMC41MDAwNSwwIDEuMDA3ODgsLTAuNDE4ODIgMC42Nzg2OSwtMS4xNTI5MnpNMjYwLjQ0MDkxLDE3OC42MDU5MWMtMC40NjM5MSwwIC0xLjQ2MTEsMCAtMi4xODE4MiwwYy0wLjQ3NzAzLDAgLTAuODMyOTQsMC41NjM4MiAtMC44MzI5NCwwLjgxNjgyYzAsMC4yMTk1NCAwLDAuNjE1MDYgMCwwLjYxNTA2Yy0xLjQwOTU2LC0wLjkwNDc0IC0zLjA4MzU2LC0xLjQzMTg4IC00Ljg3OTMyLC0xLjQzMTg4Yy00Ljk5NjYyLDAgLTkuMDYxNiw0LjA2NTA4IC05LjA2MTYsOS4wNjE2YzAsNC45OTY2MiA0LjA2NDk4LDkuMDYxNiA5LjA2MTYsOS4wNjE2YzEuNzk1NzYsMCAzLjQ2OTc2LC0wLjUyNzE0IDQuODc5MzIsLTEuNDMyYzAsMCAwLDAuMzY4MjYgMCwwLjU4MDE2YzAsMC4yNTY3NiAwLjM1NDYsMC44NTE4NCAwLjgzMjk0LDAuODUxODRjMC43NTAwMiwwIDEuODA0MjQsMCAyLjI3MjczLDBjMC40NTE3NiwwIDEuMDc2NjEsLTAuNzE3MTkgMS4wNzY2MSwtMS44NTE4NGMwLC0zLjY4NjggMCwtMTEuNzgxMDUgMCwtMTRjMCwtMS4wOTg0NCAtMC42ODc3NCwtMi4yNzEzNiAtMS4xNjc1MiwtMi4yNzEzNnoiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMSIvPjxwYXRoIGQ9Ik0yMzIuOTc3OTEsMTgzLjQ4NTIzYy0wLjcwOTExLDAgLTIuODcxOTMsMCAtNC4xMzA2NCwwYy0wLjU1NTk4LDAgLTAuNzA3MjYsLTAuNTExNTUgLTAuMzg5NTMsLTEuMjIzNDNjMC41NjUzNywtMS4yNjY3NCAxLjQxMzg1LC0zLjE2Nzg0IDEuNzYyODcsLTMuOTQ5ODVjMC4yOTg5NywtMC42Njk4NiAxLjEzNzkxLC0wLjU2Njc3IDEuNDYwNDgsMC4xNTI1NmMwLjM2NTE0LDAuODE0MjUgMS4yMjk3LDIuNzQyMjEgMS43NzQ5NSwzLjk1ODExYzAuMjgwNTEsMC42MjU1NCAtMC4wMzQzNCwxLjA2MjYxIC0wLjQ3ODEzLDEuMDYyNjF6IiBmaWxsPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEiLz48ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXdlaWdodD0ibm9ybWFsIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ic3RhcnQiLz48ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXdlaWdodD0ibm9ybWFsIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ic3RhcnQiLz48ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXdlaWdodD0ibm9ybWFsIiBmb250LXNpemU9IjEyIiB0ZXh0LWFuY2hvcj0ic3RhcnQiLz48cGF0aCBkPSJNMjM2LjI4NDgyLDE1Ni4yNzQzYzAsMCAxLjM0Mzk4LC0yLjYxODI3IDIuNTMwMDIsLTIuNjE4MjdjMi4xMTA3NSwwIDkuMDYzMzIsMCAxMi4zOTE5MywwYzEuMTAyMjksMCAyLjMxMDYsMC41MDM0NCAyLjk4NzIxLDEuMTgwMDZjMS4wODM4NywxLjA4Mzg3IDEuOTk4MTQsMS40NzQ4MSAwLjgxNDY3LDEuNDcyNjNjLTMuMjk3MDcsLTAuMDA2MDYgLTE4LjcyMzgzLC0wLjAzNDQyIC0xOC43MjM4MywtMC4wMzQ0MnoiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMSIvPjxwYXRoIGQ9IiIgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iIiBmaWxsPSIjZmZmZmZmIiBzdHJva2Utd2lkdGg9IjEiLz48cGF0aCBkPSIiIGZpbGw9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMSIvPjxwYXRoIGQ9IiIgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIxIi8+PC9nPjwvZz48L3N2Zz4="; + + class SPASfontManager { + constructor() { + /** @type {string[]} */ + this.oldFonts = []; + + /** @type {string[]} */ + this.addedFonts = []; + + /** @type {string[]} */ + this.removedFonts = []; + + fontManager.on("change", () => { + this._onchange(); + }); + } + + getInfo() { + return { + id: extensionId, + name: Scratch.translate("Font Manager"), + color1: "#2b7d6e", + color2: "#24675b", + color3: "#1c4e45", + menuIconURI, + blocks: [ + { + opcode: "fontNames", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("added font names"), + disableMonitor: true, + }, + { + opcode: "fontAdded", + blockType: Scratch.BlockType.BOOLEAN, + text: Scratch.translate("font [NAME] added?"), + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Comic Sans MS", + }, + }, + }, + { + opcode: "fontDetail", + blockType: Scratch.BlockType.REPORTER, + text: Scratch.translate("[DATA] of font [NAME]"), + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Comic Sans MS", + }, + DATA: { + type: Scratch.ArgumentType.STRING, + menu: "DATA", + }, + }, + }, + "---", + { + opcode: "addSystemFont", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "add system font named [NAME] with fallback [BACKUP]" + ), + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Comic Sans MS", + }, + BACKUP: { + type: Scratch.ArgumentType.STRING, + menu: "FALLBACKS", + }, + }, + }, + { + opcode: "addCustomFont", + blockType: Scratch.BlockType.COMMAND, + text: Scratch.translate( + "add custom font named [NAME] with fallback [BACKUP] from URL [URL]" + ), + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Pusab", + }, + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "", + }, + BACKUP: { + type: Scratch.ArgumentType.STRING, + menu: "FALLBACKS", + }, + }, + }, + { + opcode: "removeFont", + blockType: Scratch.BlockType.COMMAND, + text: "remove font [NAME]", + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: "Comic Sans MS", + }, + }, + }, + { + opcode: "removeAllFonts", + blockType: Scratch.BlockType.COMMAND, + text: "remove all fonts", + }, + "---", + { + opcode: "whenFont", + blockType: Scratch.BlockType.EVENT, + text: "when font is [ADDED]", + isEdgeActivated: false, + arguments: { + ADDED: { + type: Scratch.ArgumentType.STRING, + menu: "ADDED_FIELD", + }, + }, + }, + { + disableMonitor: true, + opcode: "fontsChanged", + blockType: Scratch.BlockType.REPORTER, + text: "[ADDED] fonts", + arguments: { + ADDED: { + type: Scratch.ArgumentType.STRING, + menu: "ADDED_INPUT", + }, + }, + }, + ], + menus: { + DATA: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("fallback"), + value: "fallback", + }, + { + text: Scratch.translate("is system"), + value: "is system", + }, + { + text: Scratch.translate("data: uri"), + value: "data: uri", + }, + { + text: Scratch.translate("format"), + value: "format", + }, + ], + }, + ADDED_FIELD: { + acceptReporters: false, + items: [ + { + text: Scratch.translate("added"), + value: "added", + }, + { + text: Scratch.translate("removed"), + value: "removed", + }, + ], + }, + ADDED_INPUT: { + acceptReporters: true, + items: [ + { + text: Scratch.translate("added"), + value: "added", + }, + { + text: Scratch.translate("removed"), + value: "removed", + }, + ], + }, + FALLBACKS: [ + "Sans Serif", + "Serif", + "Handwriting", + "Marker", + "Curly", + "Pixel", + "Scratch", + ], + FILES: { + acceptReporters: true, + items: FONT_EXTENSIONS.map((i) => `.${i}`), + }, + }, + }; + } + + fontNames() { + return JSON.stringify(fontManager.fonts.map((i) => i.family)); + } + + fontAdded(args) { + return fontManager.hasFont(Scratch.Cast.toString(args.NAME)); + } + + fontDetail(args) { + const name = Scratch.Cast.toString(args.NAME); + const font = fontManager.fonts.find((item) => item.family === name); + if (!font) return ""; + switch (Scratch.Cast.toString(args.DATA)) { + case "is system": + return font.system; + case "data: uri": + return font.asset ? font.asset.encodeDataURI() : ""; + case "format": + return font.asset ? font.asset.dataFormat : ""; + case "fallback": + return font.fallback; + } + return ""; + } + + addSystemFont(args) { + const name = Scratch.Cast.toString(args.NAME); + if (fontManager.isValidFamily(name)) { + fontManager.addSystemFont(name, Scratch.Cast.toString(args.BACKUP)); + } + } + + async addCustomFont(args) { + const name = Scratch.Cast.toString(args.NAME); + if (!fontManager.isValidFamily(name)) { + return; + } + + try { + const response = await Scratch.fetch(Scratch.Cast.toString(args.URL)); + const arrayBuffer = await response.arrayBuffer(); + const uint8array = new Uint8Array(arrayBuffer); + + // font files should have a content-type of font/ttf, font/otf, etc. + // if we can't detect it, we'll just assume it's ttf, browser can figure it out anyways + const contentType = ( + response.headers.get("content-type") || "" + ).toLowerCase(); + let dataFormat = vm.runtime.storage.DataFormat.TTF; + for (const extension of FONT_EXTENSIONS) { + if (contentType === `font/${extension}`) { + dataFormat = extension; + break; + } + } + + const asset = vm.runtime.storage.createAsset( + vm.runtime.storage.AssetType.Font, + dataFormat, + uint8array, + null, + true + ); + fontManager.addCustomFont( + name, + Scratch.Cast.toString(args.BACKUP), + asset + ); + } catch (e) { + console.warn(e); + } + } + + removeFont(args) { + const name = args.NAME; + const index = fontManager.fonts.findIndex( + (i) => i.family === Scratch.Cast.toString(name) + ); + if (index !== -1) { + fontManager.deleteFont(index); + } + } + + removeAllFonts() { + fontManager.clear(); + } + + fontsChanged(args, util) { + const added = Scratch.Cast.toString(args.ADDED); + if (added === "added") { + return JSON.stringify(this.addedFonts); + } else if (added === "removed") { + return JSON.stringify(this.removedFonts); + } else { + return ""; + } + } + + _onchange() { + this.removedFonts = []; + this.addedFonts = []; + for (const family of this.oldFonts) { + if (!fontManager.hasFont(family)) { + this.removedFonts.push(family); + } + } + for (const { family } of fontManager.fonts) { + if (!this.oldFonts.includes(family)) { + this.addedFonts.push(family); + } + } + this.oldFonts = fontManager.fonts.map((i) => i.family); + + if (this.addedFonts.length) { + Scratch.vm.runtime.startHats(`${extensionId}_whenFont`, { + ADDED: "added", + }); + } + if (this.removedFonts.length) { + Scratch.vm.runtime.startHats(`${extensionId}_whenFont`, { + ADDED: "removed", + }); + } + } + } + + Scratch.extensions.register(new SPASfontManager()); +})(Scratch); diff --git a/extensions/extensions.json b/extensions/extensions.json index c3d5a4c9e5..0d6b5f5078 100644 --- a/extensions/extensions.json +++ b/extensions/extensions.json @@ -61,6 +61,7 @@ "ZXMushroom63/searchApi", "TheShovel/ShovelUtils", "Lily/Assets", + "SharkPool/Font-Manager", "DNin/wake-lock", "Skyhigh173/json", "mbw/xml", diff --git a/images/SharkPool/Font-Manager.svg b/images/SharkPool/Font-Manager.svg new file mode 100644 index 0000000000..30d441454b --- /dev/null +++ b/images/SharkPool/Font-Manager.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +