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 @@
+
+