diff --git a/.eslintrc.js b/.eslintrc.js index 1ca8091ca7..7e9fd14fee 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,9 @@ module.exports = { ecmaVersion: 'latest' }, globals: { + Blockly: 'readonly', Scratch: 'readonly', + ScratchBlocks: 'readonly', ScratchExtensions: 'readonly', scaffolding: 'readonly' }, diff --git a/extensions/Lily/AllMenus.js b/extensions/Lily/AllMenus.js new file mode 100644 index 0000000000..b81746d39b --- /dev/null +++ b/extensions/Lily/AllMenus.js @@ -0,0 +1,52 @@ +(function (Scratch) { + 'use strict'; + + var blockXML; + const blacklist = ['looks_costumenumbername', 'extension_wedo_tilt_menu']; + + Scratch.vm.addListener('BLOCKSINFO_UPDATE', refreshMenus); + + function refreshMenus() { + if (!window.ScratchBlocks) return; + Scratch.vm.removeListener('BLOCKSINFO_UPDATE', refreshMenus); + + let allBlocks = Object.keys(ScratchBlocks.Blocks); + + allBlocks = allBlocks.filter( + item => item.includes('menu') && + !blacklist.includes(item) + ); + + const menuBlocks = allBlocks.map( + item => '' + ); + + blockXML = menuBlocks.join(''); + Scratch.vm.runtime.extensionManager.refreshBlocks(); + } + + class AllMenus { + constructor () { + Scratch.vm.runtime.on('EXTENSION_ADDED', () => { + refreshMenus(); + }); + } + + getInfo() { + return { + id: 'lmsAllMenus', + name: 'All Menus', + blocks: [ + { + blockType: Scratch.BlockType.XML, + xml: blockXML + } + ] + }; + } + } + + refreshMenus(); + +Scratch.extensions.register(new AllMenus()); +})(Scratch); diff --git a/extensions/Lily/Skins.js b/extensions/Lily/Skins.js new file mode 100644 index 0000000000..0f793c0f08 --- /dev/null +++ b/extensions/Lily/Skins.js @@ -0,0 +1,440 @@ +(function (Scratch) { + 'use strict'; + + const requireNonPackagedRuntime = (blockName) => { + if (Scratch.vm.runtime.isPackaged) { + alert(`To use the Skins ${blockName} block, the creator of the packaged project must uncheck "Remove raw asset data after loading to save RAM" under advanced settings in the packager.`); + return false; + } + return true; + }; + + /** + * @param {RenderWebGL.SVGSkin} svgSkin + * @returns {Promise} + */ + const svgSkinFinishedLoading = svgSkin => new Promise(resolve => { + if (svgSkin._svgImageLoaded) { + resolve(); + } else { + svgSkin._svgImage.addEventListener('load', () => { + resolve(); + }); + svgSkin._svgImage.addEventListener('error', () => { + resolve(); + }); + } + }); + + const vm = Scratch.vm; + const runtime = vm.runtime; + const renderer = runtime.renderer; + const Cast = Scratch.Cast; + + var createdSkins = []; + + class Skins { + constructor() { + runtime.on('PROJECT_START', () => { + this._refreshTargets(); + }); + + runtime.on('PROJECT_STOP_ALL', () => { + this._refreshTargets(); + }); + } + + getInfo() { + return { + id: 'lmsSkins', + name: 'Skins', + color1: '#6b56ff', + blocks: [ + { + opcode: 'registerSVGSkin', + blockType: Scratch.BlockType.COMMAND, + text: 'create SVG skin [SVG] as [NAME]', + arguments: { + SVG: { + type: Scratch.ArgumentType.STRING, + defaultValue: '' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + + '---', + + { + opcode: 'registerCostumeSkin', + blockType: Scratch.BlockType.COMMAND, + text: 'load skin from [COSTUME] as [NAME]', + arguments: { + COSTUME: { + type: Scratch.ArgumentType.COSTUME + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + { + opcode: 'registerURLSkin', + blockType: Scratch.BlockType.COMMAND, + text: 'load skin from URL [URL] as [NAME]', + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'https://extensions.turbowarp.org/dango.png' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + { + opcode: 'getSkinLoaded', + blockType: Scratch.BlockType.BOOLEAN, + text: 'skin [NAME] is loaded?', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + + '---', + + { + opcode: 'setSkin', + blockType: Scratch.BlockType.COMMAND, + text: 'set skin of [TARGET] to [NAME]', + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: 'targetMenu' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + { + opcode: 'restoreSkin', + blockType: Scratch.BlockType.COMMAND, + text: 'restore skin of [TARGET]', + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: 'targetMenu' + } + } + }, + { + opcode: 'restoreTargets', + blockType: Scratch.BlockType.COMMAND, + text: 'restore targets with skin [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + + '---', + + { + opcode: 'getCurrentSkin', + blockType: Scratch.BlockType.REPORTER, + text: 'current skin of [TARGET]', + arguments: { + TARGET: { + type: Scratch.ArgumentType.STRING, + menu: 'targetMenu' + } + } + }, + { + opcode: 'getSkinAttribute', + blockType: Scratch.BlockType.REPORTER, + text: '[ATTRIBUTE] of skin [NAME]', + arguments: { + ATTRIBUTE: { + type: Scratch.ArgumentType.STRING, + menu: 'skinAttributes' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + + '---', + + { + opcode: 'deleteSkin', + blockType: Scratch.BlockType.COMMAND, + text: 'delete skin [NAME]', + arguments: { + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my skin' + } + } + }, + { + opcode: 'deleteAllSkins', + blockType: Scratch.BlockType.COMMAND, + text: 'delete all skins' + } + ], + menus: { + targetMenu: { + acceptReporters: true, + items: '_getTargets' + }, + skinAttributes: { + acceptReporters: true, + items: ['width', 'height'] + } + } + }; + } + + async registerSVGSkin (args) { + const skinName = Cast.toString(args.NAME); + const svgData = Cast.toString(args.SVG); + + let oldSkinId = null; + if (createdSkins[skinName]) { + oldSkinId = createdSkins[skinName]; + } + + // This generally takes a few frames, so yield the block + const skinId = renderer.createSVGSkin(svgData); + createdSkins[skinName] = skinId; + + await svgSkinFinishedLoading(renderer._allSkins[skinId]); + + if (oldSkinId) { + this._refreshTargetsFromID(oldSkinId, false, skinId); + renderer.destroySkin(oldSkinId); + } + } + + async registerCostumeSkin (args, util) { + if (!requireNonPackagedRuntime('add costume skin')) { + return; + } + + const skinName = Cast.toString(args.NAME); + const costumeIndex = util.target.getCostumeIndexByName(args.COSTUME); + if (costumeIndex === -1) return; + const costume = util.target.sprite.costumes[costumeIndex]; + + const url = costume.asset.encodeDataURI(); + const rotationCenterX = costume.rotationCenterX; + const rotationCenterY = costume.rotationCenterY; + + let rotationCenter = [rotationCenterX, rotationCenterY]; + if (!rotationCenterX || !rotationCenterY) rotationCenter = null; + + let oldSkinId = null; + if (createdSkins[skinName]) { + oldSkinId = createdSkins[skinName]; + } + + const skinId = await this._createURLSkin(url, rotationCenter); + createdSkins[skinName] = skinId; + + if (oldSkinId) { + this._refreshTargetsFromID(oldSkinId, false, skinId); + renderer.destroySkin(oldSkinId); + } + } + + async registerURLSkin (args) { + const skinName = Cast.toString(args.NAME); + const url = Cast.toString(args.URL); + + let oldSkinId = null; + if (createdSkins[skinName]) { + oldSkinId = createdSkins[skinName]; + } + + const skinId = await this._createURLSkin(url); + if (!skinId) return; + createdSkins[skinName] = skinId; + + if (oldSkinId) { + this._refreshTargetsFromID(oldSkinId, false, skinId); + renderer.destroySkin(oldSkinId); + } + } + + getSkinLoaded (args) { + const skinName = Cast.toString(args.NAME); + return !!(createdSkins[skinName]); + } + + setSkin (args, util) { + const skinName = Cast.toString(args.NAME); + if (!createdSkins[skinName]) return; + + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + const drawableID = target.drawableID; + + const skinId = createdSkins[skinName]; + renderer._allDrawables[drawableID].skin = renderer._allSkins[skinId]; + } + + restoreSkin (args, util) { + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + target.updateAllDrawableProperties(); + } + + getCurrentSkin (args, util) { + const targetName = Cast.toString(args.TARGET); + const target = this._getTargetFromMenu(targetName, util); + if (!target) return; + const drawableID = target.drawableID; + + const skinId = renderer._allDrawables[drawableID].skin._id; + const skinName = this._getSkinNameFromID(skinId); + return (skinName) ? skinName : ''; + } + + getSkinAttribute (args) { + const skins = renderer._allSkins; + const skinName = Cast.toString(args.NAME); + + if (!createdSkins[skinName]) return 0; + const skinId = createdSkins[skinName]; + if (!skins[skinId]) return 0; + + const size = skins[skinId].size; + const attribute = Cast.toString(args.ATTRIBUTE).toLowerCase(); + + switch (attribute) { + case ('width'): return Math.ceil(size[0]); + case ('height'): return Math.ceil(size[1]); + default: return 0; + } + } + + deleteSkin (args) { + const skinName = Cast.toString(args.NAME); + if (!createdSkins[skinName]) return; + const skinId = createdSkins[skinName]; + + this._refreshTargetsFromID(skinId, true); + renderer.destroySkin(skinId); + Reflect.deleteProperty(createdSkins, skinName); + } + + deleteAllSkins () { + this._refreshTargets(); + for (let i = 0; i < createdSkins.length; i++) renderer.destroySkin(createdSkins[i]); + createdSkins = []; + } + + restoreTargets (args) { + const skinName = Cast.toString(args.NAME); + if (!createdSkins[skinName]) return; + const skinId = createdSkins[skinName]; + + this._refreshTargetsFromID(skinId, false); + } + + // Utility Functions + + _refreshTargetsFromID (skinId, reset, newId) { + const drawables = renderer._allDrawables; + const skins = renderer._allSkins; + + for (const target of runtime.targets) { + const drawableID = target.drawableID; + const targetSkin = drawables[drawableID].skin.id; + + if (targetSkin === skinId) { + target.updateAllDrawableProperties(); + if (!reset) drawables[drawableID].skin = (newId) ? skins[newId] : skins[skinId]; + } + } + } + + _refreshTargets () { + for (const target of runtime.targets) { + target.updateAllDrawableProperties(); + } + } + + _getSkinNameFromID (skinId) { + for (const skinName in createdSkins) { + if (createdSkins[skinName] === skinId) return skinName; + } + } + + _getTargetFromMenu (targetName, util) { + let target = Scratch.vm.runtime.getSpriteTargetByName(targetName); + if (targetName === '_myself_') target = util.target; + if (targetName === '_stage_') target = runtime.getTargetForStage(); + return target; + } + + async _createURLSkin (URL, rotationCenter) { + let imageData; + if (await Scratch.canFetch(URL)) { + imageData = await Scratch.fetch(URL); + } else { + return; + } + + const contentType = imageData.headers.get("Content-Type"); + if (contentType === 'image/svg+xml') { + return renderer.createSVGSkin(await imageData.text(), rotationCenter); + } else if (contentType === 'image/png' || contentType === 'image/jpeg' || contentType === 'image/bmp') { + // eslint-disable-next-line no-restricted-syntax + const output = new Image(); + output.src = URL; + output.crossOrigin = 'anonymous'; + await output.decode(); + return renderer.createBitmapSkin(output); + } + } + + _getTargets() { + const spriteNames = [ + {text: 'myself', value: '_myself_'}, + {text: 'Stage', value: '_stage_'} + ]; + const targets = Scratch.vm.runtime.targets; + for (let index = 1; index < targets.length; index++) { + const target = targets[index]; + if (target.isOriginal) { + const targetName = target.getName(); + spriteNames.push({ + text: targetName, + value: targetName + }); + } + } + return spriteNames; + } + + } + Scratch.extensions.register(new Skins()); +})(Scratch); diff --git a/extensions/TheShovel/CustomStyles.js b/extensions/TheShovel/CustomStyles.js new file mode 100644 index 0000000000..656b6bb29f --- /dev/null +++ b/extensions/TheShovel/CustomStyles.js @@ -0,0 +1,735 @@ +// Thanks LilyMakesThings for the awesome banner! +(function(Scratch) { + 'use strict'; + + // Styles + let monitorText = ''; + let monitorBorder = ''; + let monitorBackgroundColor = ''; + let variableValueBackground = ''; + let variableValueTextColor = ''; + let listFooterBackground = ''; + let listHeaderBackground = ''; + let listValueText = ''; + let listValueBackground = ''; + let variableValueRoundness = -1; + let listValueRoundness = -1; + let monitorBackgroundRoundness = -1; + let monitorBackgroundBorderWidth = -1; + let allowScrolling = ''; + let askBackground = ''; + let askBackgroundRoundness = -1; + let askBackgroundBorderWidth = -1; + let askButtonBackground = ''; + let askButtonRoundness = -1; + let askInputBackground = ''; + let askInputRoundness = -1; + let askInputBorderWidth = -1; + let askBoxIcon = ''; + let askInputText = ''; + let askButtonImage = ''; + let askInputBorder = ''; + + // CSS selectors + let monitorRoot; + let monitorValue; + let monitorListHeader; + let monitorListFooter; + let monitorRowValueOuter; + let monitorRowsInner; + let monitorRowsScroller; + let monitorRowIndex; + let monitorValueLarge; + let askBoxBG; + let askBoxButton; + let askBoxInner; + let askBoxBorderMain; + let askBoxBorderOuter; + if (typeof scaffolding !== 'undefined') { + monitorRoot = '.sc-monitor-root'; + monitorValue = '.sc-monitor-value'; + monitorListHeader = '.sc-monitor-list-label'; + monitorListFooter = '.sc-monitor-list-footer'; + monitorRowValueOuter = '.sc-monitor-row-value-outer'; + monitorRowsInner = '.sc-monitor-rows-inner'; + monitorRowsScroller = monitorRowsInner; + monitorRowIndex = '.sc-monitor-row-index'; + monitorValueLarge = '.sc-monitor-large-value'; + askBoxBG = '.sc-question-inner'; + askBoxButton = '.sc-question-submit-button'; + askBoxInner = '.sc-question-input'; + askBoxBorderMain = '.sc-question-input:hover'; + askBoxBorderOuter = '.sc-question-input:focus'; + } else { + monitorRoot = 'div[class^="monitor_monitor-container_"]'; + monitorValue = 'div[class^="monitor_value_"]'; + monitorListHeader = 'div[class^="monitor_list-header_"]'; + monitorListFooter = 'div[class^="monitor_list-footer_"]'; + monitorRowValueOuter = 'div[class^="monitor_list-value_"]'; + monitorRowsInner = 'div[class^="monitor_list-body_"]'; + monitorRowsScroller = 'div[class^="monitor_list-body_"] > .ReactVirtualized__List'; + monitorRowIndex = 'div[class^="monitor_list-index_"]'; + monitorValueLarge = 'div[class^="monitor_large-value_"]'; + askBoxBG = 'div[class^="question_question-container_"]'; + askBoxButton = 'button[class^="question_question-submit-button_"]'; + askBoxInner = '[class^="question_question-container_"] input[class^="input_input-form_"]'; + askBoxIcon = 'img[class^="question_question-submit-button-icon_"]'; + askBoxBorderMain = '[class^="question_question-input_"] input:focus, [class^="question_question-input_"] input:hover'; + askBoxBorderOuter = '[class^="question_question-input_"] > input:focus'; + } + + const ColorIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTYuNDE5NjQiIGhlaWdodD0iMTE4LjM0OTk0IiB2aWV3Qm94PSIwLDAsMTE2LjQxOTY0LDExOC4zNDk5NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC45MjgwOSw3LjY1MTk0KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTMxLjc3NzE4LDEwLjU1MDMzYy0xMC4yOTkxNywxLjEwODUyIC0xNi4zNDU2NSw0LjY2NTg3IC0xOS4xNDcxOSw5LjE4MDU3Yy0xLjg5MjIsMy4yNDc5NyAtMi4zNDE1Niw3LjEzODcyIC0xLjIzOTUzLDEwLjczMjVsMC4wOTA3LDAuMzQyNjRsMjAuMjY1NzksLTIwLjI1NTcxek03OC4yODQ2OSwyMS43NTY0OGMtNy4zNDY0NywtNy4zNTY1NSAtMTQuNzczNTcsLTEzLjEwMDcgLTIwLjg5MDU5LC0xNi4yOTUyN2MtNC4zNDMzOSwtMi4yMzcyIC03LjcyOTQyLC0zLjEzNDA5IC05LjM5MjIsLTIuMTg2ODFsLTAuODI2MzYsMC44MjYzNmMtMS4yMjk0NSwyLjAxNTUgLTAuNTEzOTYsNi4zMDg0OSAxLjc5Mzc5LDExLjY2OTdjMy41NjMzNSw3LjczMjE5IDguNTIwMTUsMTQuNzQyMDggMTQuNjIyNCwyMC42Nzg5N2M2Ljk5Mzc3LDYuOTkzNzcgMTQuNTAxNDgsMTIuMjM0MDQgMjAuODkwNTksMTUuMTE2MmM0LjU0NDk0LDIuMDE1NSA4LjI3MzYsMi45MjI0NyAxMC40MzAxOCwyLjMzNzk3bDEuOTI0OCwtMS45MzQ4OGMwLjQ1MzQ4LC0xLjkyNDggLTAuNTAzODgsLTUuMTc5ODIgLTIuNTA5MjksLTkuMjAwNzNjLTMuMTEzOTQsLTYuMjU4MTEgLTguNzg3NTUsLTEzLjc2NTgyIC0xNi4wNDMzMywtMjEuMDExNTJ6TTYwLjI4NjMzLC0wLjE1MTk0YzYuNzExNTksMy40NTY1NyAxNC42OTI5NSw5LjY0NDE0IDIyLjQ4MjgzLDE3LjQzNDAyYzcuNzg5ODksNy43ODk4OSAxMy44NTY1MiwxNS44MjE2MyAxNy4yMjIzOSwyMi43MjQ2OWMzLjc3OTA1LDcuNTY4MTggNC4zODM3LDE0LjAyNzg0IDAuNjk1MzQsMTcuNzE2MTljLTAuNjA1OCwwLjU3NzQ5IC0xLjI5NTUyLDEuMDU5OTYgLTIuMDQ1NzIsMS40MzFsLTM5LjQyMzA2LDM5LjQwMjljLTIuMzY4MiwyLjM4ODM2IC0zLjM1NTgsMy4zOTYxMSAtNy40MzcxNyw0LjMxMzE1Yy0yLjAzMTMyLDAuNDQwMzcgLTQuMTE4NywwLjU2Mjc2IC02LjE4NzU3LDAuMzYyNzhjLTIuMTQxODMsLTAuMjA3OCAtNC4yNTIxNSwtMC42NjQzNyAtNi4yODgzNCwtMS4zNjA0NmMtOC4zOTQ1MywtMi44MjE2OSAtMTcuMTMxNjksLTguNzI3MDkgLTI0LjQwNzYyLC0xNS45ODI4NmMtNy4yNzU5MywtNy4yNTU3NyAtMTMuMzcyOCwtMTYuMDUzNDEgLTE2LjMyNTUsLTI0LjQ2ODFjLTAuNTUwNDYsLTEuNTI1NTIgLTAuOTc4NDMsLTMuMDkyNDggLTEuMjc5ODQsLTQuNjg2MDJjLTAuMjg2MDMsLTEuNDc3NzUgLTAuNDMxMTIsLTIuOTc5MyAtMC40MzMzNCwtNC40ODQ0N2MtMC4xMTgxNywtMi4zNTIzNCAwLjMwNTkyLC00LjcwMDM3IDEuMjM5NTMsLTYuODYyNzVjMC45Njc0LC0xLjg2MTczIDIuMjUzMiwtMy41Mzk3NiAzLjc5OTIxLC00Ljk1ODExbDAuMTYxMjQsLTAuMTcxMzJsNC43NDY0OSwtNC43MzY0MWMtMC40OTc0NywtMS4xMjYwMiAtMC45MDg0OCwtMi4yODgyOCAtMS4yMjk0NSwtMy40NzY3M2MtMS41NTYxLC01LjE5NTg5IC0wLjg1MzgyLC0xMC44MDY3NSAxLjkzNDg4LC0xNS40NTg4NGM0LjMwMzA4LC03LjAzNDA3IDEzLjk1NzMsLTEyLjI2NDI4IDMwLjY2NTczLC0xMi40MzU2bDQuMjAyMzEsLTQuMTkyMjNjMC40NDM2OCwtMC42MDgzMiAwLjk4MTExLC0xLjE0MjM1IDEuNTkyMjQsLTEuNTgyMTZjMC4xOTg1NywtMC4xODczMSAwLjQyNjcsLTAuMzQwNTMgMC42NzUxOSwtMC40NTM0OGMzLjczODc0LC0yLjI1NzM1IDkuMjcxMjcsLTEuMzYwNDYgMTUuNjQwMjMsMS45MjQ4ek04OS4wNDc0Myw1OS43Njg2OWMtMi40NjcyLC0wLjU1OTY2IC00Ljg2ODc0LC0xLjM3NzA3IC03LjE2NTA4LC0yLjQzODc1Yy03LjA1NDIzLC0zLjE4NDQ4IC0xNS4yMjcwNiwtOC44NzgyNSAtMjIuNzY1LC0xNi40MDYxMmMtNi42NjcyNywtNi41MDY3NiAtMTIuMDc4NTEsLTE0LjE4NjM2IC0xNS45NjI3MSwtMjIuNjU0MTVjLTEuMDk4MjYsLTIuNDc4NDUgLTEuOTA2NDYsLTUuMDc1NTEgLTIuNDA4NTIsLTcuNzM5NDlsLTI2LjQxODk4LDI2LjMwMjJjMTMuODgzNTcsMTUuMDM3NTkgNTUuMzcyMzcsMjAuMzU1OTQgNjguMjQxMywxNC44NDM1NmMxLjUyOTk3LC0wLjYyNjEyIDMuMjc5MTUsMC4wOTI3NiAzLjkyNjY5LDEuNjEzOGMwLjY0NzU0LDEuNTIxMDMgLTAuMDQ2NzEsMy4yODAxNCAtMS41NTg0OSwzLjk0ODk3Yy0xNS4zMjc4Myw2LjU2MDQzIC02Mi41NTg3NiwtMS41MjU3NiAtNzQuODY2MzQsLTE2LjA4MzA5bC0zLjY5ODQzLDMuNzM4NzRjLTEuMDE3OSwwLjkxNDA1IC0xLjg4MjE3LDEuOTg1ODcgLTIuNTU5NjgsMy4xNzQ0Yy0wLjUxNTM2LDEuMzI3MzcgLTAuNzM1NDUsMi43NTEwNSAtMC42NDQ5Niw0LjE3MjA3YzAuMDA3NywxLjExNjQyIDAuMTE5MDQsMi4yMjk3MiAwLjMzMjU2LDMuMzI1NTZjMC4yMzkxMywxLjI4MDIzIDAuNTc2MDMsMi41NDAyNCAxLjAwNzc0LDMuNzY4OTdjMi42MzAyMiw3LjQ4NzU2IDguMDYxOTcsMTUuMzg4MjkgMTQuODIzOTYsMjIuMDU5NThjNi43NjE5OSw2LjY3MTI5IDE0LjUyMTYzLDEyLjAyMjQyIDIxLjk3ODk2LDE0LjQ5MTRjMS41NjYyMSwwLjU0IDMuMTg5MjMsMC44OTg0MSA0LjgzNzE5LDEuMDY4MjF2MGMxLjQxNDg2LDAuMTM5NCAyLjg0Mjc3LDAuMDU3ODEgNC4yMzI1MywtMC4yNDE4NmMxLjcwOTMzLC0wLjI5ODk0IDMuMjQ4NTEsLTEuMjE3NDEgNC4zMjMyMywtMi41Nzk4M2MwLjA2MDQ2LC0wLjA3MDU0IDAuNjQ0OTYsLTAuNjQ0OTYgMC42MzQ4OCwtMC42NTUwNHoiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlLW9wYWNpdHk9IjAuMTI5NDEiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxNSIvPjxwYXRoIGQ9Ik0zLjI5NzAyLDQ5LjQyMTZjMi40NDg2OSwwLjQ4ODgxIDE0LjYwMzczLDI0LjI5Nzc5IDMxLjc5OSwyMS40MjkyM2MxNS42MzU4MywtMi42MDg0MSA5LjA4MDY3LDE0LjMzMjQ1IDQ4LjU0ODY1LC01LjUwNjgybC0yOC41MDkxNiwyOC40Nzg5M2MwLDAgLTAuNTc0NDIsMC41ODQ1IC0wLjYzNDg4LDAuNjU1MDRjLTEuMDc0NzIsMS4zNjI0MSAtMi42MTM5LDIuMjgwOSAtNC4zMjMyMywyLjU3OTgzYy0xLjM4OTc2LDAuMjk5NjYgLTIuODE3NjgsMC4zODEyNiAtNC4yMzI1MywwLjI0MTg2djBjLTEuNjQ3OTYsLTAuMTY5NzkgLTMuMjcwOTcsLTAuNTI4MjEgLTQuODM3MTksLTEuMDY4MjFjLTcuNDU3MzMsLTIuNDk5MjEgLTE1LjI4NzUyLC03Ljg2MDQzIC0yMS45NTg4LC0xNC40ODEzMmMtNi42NzEyOSwtNi42MjA4OSAtMTIuMTYzNSwtMTQuNTcyMDIgLTE0Ljc5MzcyLC0yMi4wNTk1OGMtMC40MzE3MiwtMS4yMjg3NCAtMC43Njg2MiwtMi40ODg3NSAtMS4wMDc3NCwtMy43Njg5N2MtMC4yMTM1MiwtMS4wOTU4NSAtMC4zMjQ4NiwtMi4yMDkxNCAtMC4zMzI1NiwtMy4zMjU1NmMtMC4wMzY5LC0xLjA2NTcxIDAuMDU3ODcsLTIuMTMxOSAwLjI4MjE2LC0zLjE3NDR6IiBmaWxsPSIjZjU0MjQyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTTMxLjc3NzE4LDEwLjU1MDMzYy0xMC4yOTkxNywxLjEwODUyIC0xNi4zNDU2NSw0LjY2NTg3IC0xOS4xNDcxOSw5LjE4MDU3Yy0xLjg5MjIsMy4yNDc5NyAtMi4zNDE1Niw3LjEzODcyIC0xLjIzOTUzLDEwLjczMjVsMC4wOTA3LDAuMzQyNjRsMjAuMjY1NzksLTIwLjI1NTcxek03OC4yODQ2OSwyMS43NTY0OGMtNy4zNDY0NywtNy4zNTY1NSAtMTQuNzczNTcsLTEzLjEwMDcgLTIwLjg5MDU5LC0xNi4yOTUyN2MtNC4zNDMzOSwtMi4yMzcyIC03LjcyOTQyLC0zLjEzNDA5IC05LjM5MjIsLTIuMTg2ODFsLTAuODI2MzYsMC44MjYzNmMtMS4yMjk0NSwyLjAxNTUgLTAuNTEzOTYsNi4zMDg0OSAxLjc5Mzc5LDExLjY2OTdjMy41NjMzNSw3LjczMjE5IDguNTIwMTUsMTQuNzQyMDggMTQuNjIyNCwyMC42Nzg5N2M2Ljk5Mzc3LDYuOTkzNzcgMTQuNTAxNDgsMTIuMjM0MDQgMjAuODkwNTksMTUuMTE2MmM0LjU0NDk0LDIuMDE1NSA4LjI3MzYsMi45MjI0NyAxMC40MzAxOCwyLjMzNzk3bDEuOTI0OCwtMS45MzQ4OGMwLjQ1MzQ4LC0xLjkyNDggLTAuNTAzODgsLTUuMTc5ODIgLTIuNTA5MjksLTkuMjAwNzNjLTMuMTEzOTQsLTYuMjU4MTEgLTguNzg3NTUsLTEzLjc2NTgyIC0xNi4wNDMzMywtMjEuMDExNTJ6TTYwLjI4NjMzLC0wLjE1MTk0YzYuNzExNTksMy40NTY1NyAxNC42OTI5NSw5LjY0NDE0IDIyLjQ4MjgzLDE3LjQzNDAyYzcuNzg5ODksNy43ODk4OSAxMy44NTY1MiwxNS44MjE2MyAxNy4yMjIzOSwyMi43MjQ2OWMzLjc3OTA1LDcuNTY4MTggNC4zODM3LDE0LjAyNzg0IDAuNjk1MzQsMTcuNzE2MTljLTAuNjA1OCwwLjU3NzQ5IC0xLjI5NTUyLDEuMDU5OTYgLTIuMDQ1NzIsMS40MzFsLTM5LjQyMzA2LDM5LjQwMjljLTIuMzY4MiwyLjM4ODM2IC0zLjM1NTgsMy4zOTYxMSAtNy40MzcxNyw0LjMxMzE1Yy0yLjAzMTMyLDAuNDQwMzcgLTQuMTE4NywwLjU2Mjc2IC02LjE4NzU3LDAuMzYyNzhjLTIuMTQxODMsLTAuMjA3OCAtNC4yNTIxNSwtMC42NjQzNyAtNi4yODgzNCwtMS4zNjA0NmMtOC4zOTQ1MywtMi44MjE2OSAtMTcuMTMxNjksLTguNzI3MDkgLTI0LjQwNzYyLC0xNS45ODI4NmMtNy4yNzU5MywtNy4yNTU3NyAtMTMuMzcyOCwtMTYuMDUzNDEgLTE2LjMyNTUsLTI0LjQ2ODFjLTAuNTUwNDYsLTEuNTI1NTIgLTAuOTc4NDMsLTMuMDkyNDggLTEuMjc5ODQsLTQuNjg2MDJjLTAuMjg2MDMsLTEuNDc3NzUgLTAuNDMxMTIsLTIuOTc5MyAtMC40MzMzNCwtNC40ODQ0N2MtMC4xMTgxNywtMi4zNTIzNCAwLjMwNTkyLC00LjcwMDM3IDEuMjM5NTMsLTYuODYyNzVjMC45Njc0LC0xLjg2MTczIDIuMjUzMiwtMy41Mzk3NiAzLjc5OTIxLC00Ljk1ODExbDAuMTYxMjQsLTAuMTcxMzJsNC43NDY0OSwtNC43MzY0MWMtMC40OTc0NywtMS4xMjYwMiAtMC45MDg0OCwtMi4yODgyOCAtMS4yMjk0NSwtMy40NzY3M2MtMS41NTYxLC01LjE5NTg5IC0wLjg1MzgyLC0xMC44MDY3NSAxLjkzNDg4LC0xNS40NTg4NGM0LjMwMzA4LC03LjAzNDA3IDEzLjk1NzMsLTEyLjI2NDI4IDMwLjY2NTczLC0xMi40MzU2bDQuMjAyMzEsLTQuMTkyMjNjMC40NDM2OCwtMC42MDgzMiAwLjk4MTExLC0xLjE0MjM1IDEuNTkyMjQsLTEuNTgyMTZjMC4xOTg1NywtMC4xODczMSAwLjQyNjcsLTAuMzQwNTMgMC42NzUxOSwtMC40NTM0OGMzLjczODc0LC0yLjI1NzM1IDkuMjcxMjcsLTEuMzYwNDYgMTUuNjQwMjMsMS45MjQ4ek04OS4wNDc0Myw1OS43Njg2OWMtMi40NjcyLC0wLjU1OTY2IC00Ljg2ODc0LC0xLjM3NzA3IC03LjE2NTA4LC0yLjQzODc1Yy03LjA1NDIzLC0zLjE4NDQ4IC0xNS4yMjcwNiwtOC44NzgyNSAtMjIuNzY1LC0xNi40MDYxMmMtNi42NjcyNywtNi41MDY3NiAtMTIuMDc4NTEsLTE0LjE4NjM2IC0xNS45NjI3MSwtMjIuNjU0MTVjLTEuMDk4MjYsLTIuNDc4NDUgLTEuOTA2NDYsLTUuMDc1NTEgLTIuNDA4NTIsLTcuNzM5NDlsLTI2LjQxODk4LDI2LjMwMjJjMTMuODgzNTcsMTUuMDM3NTkgNTUuMzcyMzcsMjAuMzU1OTQgNjguMjQxMywxNC44NDM1NmMxLjUyOTk3LC0wLjYyNjEyIDMuMjc5MTUsMC4wOTI3NiAzLjkyNjY5LDEuNjEzOGMwLjY0NzU0LDEuNTIxMDMgLTAuMDQ2NzEsMy4yODAxNCAtMS41NTg0OSwzLjk0ODk3Yy0xNS4zMjc4Myw2LjU2MDQzIC02Mi41NTg3NiwtMS41MjU3NiAtNzQuODY2MzQsLTE2LjA4MzA5bC0zLjY5ODQzLDMuNzM4NzRjLTEuMDE3OSwwLjkxNDA1IC0xLjg4MjE3LDEuOTg1ODcgLTIuNTU5NjgsMy4xNzQ0Yy0wLjUxNTM2LDEuMzI3MzcgLTAuNzM1NDUsMi43NTEwNSAtMC42NDQ5Niw0LjE3MjA3YzAuMDA3NywxLjExNjQyIDAuMTE5MDQsMi4yMjk3MiAwLjMzMjU2LDMuMzI1NTZjMC4yMzkxMywxLjI4MDIzIDAuNTc2MDMsMi41NDAyNCAxLjAwNzc0LDMuNzY4OTdjMi42MzAyMiw3LjQ4NzU2IDguMDYxOTcsMTUuMzg4MjkgMTQuODIzOTYsMjIuMDU5NThjNi43NjE5OSw2LjY3MTI5IDE0LjUyMTYzLDEyLjAyMjQyIDIxLjk3ODk2LDE0LjQ5MTRjMS41NjYyMSwwLjU0IDMuMTg5MjMsMC44OTg0MSA0LjgzNzE5LDEuMDY4MjF2MGMxLjQxNDg2LDAuMTM5NCAyLjg0Mjc3LDAuMDU3ODEgNC4yMzI1MywtMC4yNDE4NmMxLjcwOTMzLC0wLjI5ODk0IDMuMjQ4NTEsLTEuMjE3NDEgNC4zMjMyMywtMi41Nzk4M2MwLjA2MDQ2LC0wLjA3MDU0IDAuNjQ0OTYsLTAuNjQ0OTYgMC42MzQ4OCwtMC42NTUwNHoiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEwIi8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6NTguOTI4MDk6NTcuNjUxOTM5OTk5OTk5OTk2LS0+'; + const BorderIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMjkuMDA4NDIiIGhlaWdodD0iMTI5LjAwODQzIiB2aWV3Qm94PSIwLDAsMTI5LjAwODQyLDEyOS4wMDg0MyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQuNTA0MjEsMTQuNTA0MjIpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0tMi4wMDQyMSw4Ny40NTM4OGMwLC0yMi44MTE2IDAsLTY1LjYwODczIDAsLTc3LjM5MjMxYzAsLTUuOTA0NzkgNy4wNTUxNiwtMTIuMDY1NzkgMTMuMTA3OTYsLTEyLjA2NTc5YzExLjkwNTQsMCA1NC4yNjQ2NSwwIDc2LjcwNzQyLDBjOC41Mzc2NywwIDE0LjE5MzA0LDcuMTQ4NzcgMTQuMTkzMDQsMTcuNTQ0ODljMCwyMy44MjMyNSAwLDY0LjY5OTA0IDAsNzUuNjgwMDljMCw1LjM2NDQ4IC02LjcyOTAyLDEwLjc4MzQ1IC0xNi41OTAxNSwxMC43ODM0NWMtMjIuODg5MjQsMCAtNjIuNjUzNTUsMCAtNzQuMzEwMzEsMGMtNi4zMzM1NSwwIC0xMy4xMDc5NiwtNS44MDcyNCAtMTMuMTA3OTYsLTE0LjU1MDMzeiIgc3Ryb2tlLW9wYWNpdHk9IjAuMTI5NDEiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyNSIvPjxwYXRoIGQ9Ik0tMi4wMDQyMSw4Ny40NTM4OGMwLC0yMi44MTE2IDAsLTY1LjYwODczIDAsLTc3LjM5MjMxYzAsLTUuOTA0NzkgNy4wNTUxNiwtMTIuMDY1NzkgMTMuMTA3OTYsLTEyLjA2NTc5YzExLjkwNTQsMCA1NC4yNjQ2NSwwIDc2LjcwNzQyLDBjOC41Mzc2NywwIDE0LjE5MzA0LDcuMTQ4NzcgMTQuMTkzMDQsMTcuNTQ0ODljMCwyMy44MjMyNSAwLDY0LjY5OTA0IDAsNzUuNjgwMDljMCw1LjM2NDQ4IC02LjcyOTAyLDEwLjc4MzQ1IC0xNi41OTAxNSwxMC43ODM0NWMtMjIuODg5MjQsMCAtNjIuNjUzNTUsMCAtNzQuMzEwMzEsMGMtNi4zMzM1NSwwIC0xMy4xMDc5NiwtNS44MDcyNCAtMTMuMTA3OTYsLTE0LjU1MDMzeiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIwIi8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6NjQuNTA0MjE6NjQuNTA0MjItLT4='; + const extensionIcon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgMjk2Mjk3IDMzMzMzMyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHNoYXBlLXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIiB0ZXh0LXJlbmRlcmluZz0iZ2VvbWV0cmljUHJlY2lzaW9uIiBpbWFnZS1yZW5kZXJpbmc9Im9wdGltaXplUXVhbGl0eSIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iaWQ0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjU0MTI4LjciIHkxPSI3OTM1NS41IiB4Mj0iMjQwMzE4IiB5Mj0iNzkzNTUuNSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZThlN2U1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImlkNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSI2MjAxOS4zIiB5MT0iMjAyODY4IiB4Mj0iMjMzNTE1IiB5Mj0iMjAyODY4Ij48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlOGU3ZTUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iaWQ2IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjEwNDk2MyIgeTE9Ijk5NjE2LjkiIHgyPSIxMDQ5NjMiIHkyPSIxNzEwMjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2QxZDNkNCIvPjxzdG9wIG9mZnNldD0iLjM4OCIgc3RvcC1jb2xvcj0iI2QxZDNkNCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2QxZDNkNCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJpZDciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4bGluazpocmVmPSIjaWQ2IiB4MT0iMTk0MTc5IiB5MT0iNjExODUuOCIgeDI9IjE5NDE3OSIgeTI9IjEzNTQwNyIvPjxtYXNrIGlkPSJpZDAiPjxsaW5lYXJHcmFkaWVudCBpZD0iaWQxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjEwNDk2MyIgeTE9Ijk5NjE2LjkiIHgyPSIxMDQ5NjMiIHkyPSIxNzEwMjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1vcGFjaXR5PSIwIiBzdG9wLWNvbG9yPSIjZmZmIi8+PHN0b3Agb2Zmc2V0PSIuMzg4IiBzdG9wLWNvbG9yPSIjZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii44MzEiIHN0b3AtY29sb3I9IiNmZmYiLz48L2xpbmVhckdyYWRpZW50PjxwYXRoIGZpbGw9InVybCgjaWQxKSIgZD0iTTYxNzM3IDk5NDY3aDg2NDUzdjcxNzA0SDYxNzM3eiIvPjwvbWFzaz48bWFzayBpZD0iaWQyIj48bGluZWFyR3JhZGllbnQgaWQ9ImlkMyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIHgxPSIxOTQxNzkiIHkxPSI2MTE4NS44IiB4Mj0iMTk0MTc5IiB5Mj0iMTM1NDA3Ij48c3RvcCBvZmZzZXQ9IjAiIHN0b3Atb3BhY2l0eT0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPjxzdG9wIG9mZnNldD0iLjM4OCIgc3RvcC1jb2xvcj0iI2ZmZiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1vcGFjaXR5PSIuODMxIiBzdG9wLWNvbG9yPSIjZmZmIi8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBmaWxsPSJ1cmwoI2lkMykiIGQ9Ik0xNDc4OTAgNjEwMzZoOTI1Nzh2NzQ1MjFoLTkyNTc4eiIvPjwvbWFzaz48c3R5bGU+LmZpbDZ7ZmlsbDojMDAwO2ZpbGwtb3BhY2l0eTouMDUwOTh9PC9zdHlsZT48L2RlZnM+PGcgaWQ9IkxheWVyX3gwMDIwXzEiPjxnIGlkPSJfNTEzMDg1MzA0Ij48cGF0aCBmaWxsPSIjMjA2MmFmIiBkPSJNMjY4NTE3IDMwMDkyMmwtMTIwMzY5IDMyNDExLTEyMDM3MS0zMjQxMUwwIDBoMjk2Mjk3eiIvPjxwYXRoIGZpbGw9IiMzYzljZDciIGQ9Ik0xNDgxNDYgMjQzNzR2MjgzMTA5bDI3MyA3NCA5NzQwOS0yNjIyOSAyMjQ4NS0yNTY5NTR6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE0ODA0MCA5OTYxN2wtODYxNTMgMzU4ODAgMjg1NyAzNTUyNCA4MzI5Ni0zNTYxNCA4ODYwNC0zNzg4MyAzNjc0LTM2MzM5LTkyMjc4IDM4NDMyeiIvPjxwYXRoIG1hc2s9InVybCgjaWQwKSIgZmlsbD0idXJsKCNpZDYpIiBkPSJNNjE4ODcgMTM1NDk3bDI4NTcgMzU1MjQgODMyOTUtMzU2MTRWOTk2MTd6Ii8+PHBhdGggbWFzaz0idXJsKCNpZDIpIiBmaWxsPSJ1cmwoI2lkNykiIGQ9Ik0yNDAzMTggNjExODZsLTkyMjc4IDM4NDMxdjM1NzkwbDg4NjA0LTM3ODgzeiIvPjxwYXRoIGZpbGw9InVybCgjaWQ1KSIgZD0iTTYyMDE5IDEzNTQ5N2wyODU4IDM1NTI0IDEyNzgwNiA0MDctMjg1OSA0NzM2NS00MjA1NSAxMTg0MC00MDQyOC0xMDIwOC0yNDUwLTI5Mzk5SDY3MzI3bDQ5MDAgNTY3NTYgNzU5NTAgMjI0NTcgNzU1MzgtMjIwNTAgOTgwMC0xMTI2OTJ6Ii8+PHBhdGggY2xhc3M9ImZpbDYiIGQ9Ik0xNDgwNDAgMTM1NDk3SDYxODg4bDI4NTcgMzU1MjQgODMyOTUgMjY2di0zNTc5MHptMCA5NTAyMmwtNDA4IDExNC00MDQyMi0xMDIwOC0yNDUwLTI5Mzk5SDY3MTk3bDQ4OTkgNTY3NTYgNzU5NDQgMjI0NTd2LTM5NzIweiIvPjxwYXRoIGZpbGw9InVybCgjaWQ0KSIgZD0iTTU0MTI5IDYxMTg2aDE4NjE4OWwtMzY3NCAzNjMzOUg1ODYyMGwtNDQ5MS0zNjMzOXoiLz48cGF0aCBjbGFzcz0iZmlsNiIgZD0iTTE0ODA0MCA2MTE4Nkg1NDEyOWw0NDkxIDM2MzM5aDg5NDIweiIvPjwvZz48L2c+PC9zdmc+'; + const miscIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzUuMzc5IiBoZWlnaHQ9IjEzNS4zNzciIHZpZXdCb3g9IjAsMCwxMzUuMzc5LDEzNS4zNzciPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzIuMzEsLTgyLjMxMSkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTM0OC45NjcsMTEyLjA4YzIuMzIxLDIuMzIxIDIuMzIxLDYuMTE4IDAsOC40MzlsLTcuMTAxLDcuMTAxYzEuOTU5LDMuNjU4IDMuNDU0LDcuNjAxIDQuNDA1LDExLjc1Mmg5LjE5OWMzLjI4MywwIDUuOTY5LDIuNjg2IDUuOTY5LDUuOTY4djEyLjQ3MWMwLDMuMjgzIC0yLjY4Niw1Ljk2OSAtNS45NjksNS45NjloLTEwLjAzOWMtMS4yMzEsNC4wNjMgLTIuOTkyLDcuODk2IC01LjIwNCwxMS40MThsNi41MTIsNi41MWMyLjMyMSwyLjMyMyAyLjMyMSw2LjEyIDAsOC40NGwtOC44MTgsOC44MTljLTIuMzIxLDIuMzIgLTYuMTE5LDIuMzIgLTguNDM5LDBsLTcuMTAyLC03LjEwMmMtMy42NTcsMS45NiAtNy42MDEsMy40NTYgLTExLjc1Myw0LjQwNnY5LjE5OWMwLDMuMjgyIC0yLjY4NSw1Ljk2OCAtNS45NjgsNS45NjhoLTEyLjQ3Yy0zLjI4MywwIC01Ljk2OSwtMi42ODYgLTUuOTY5LC01Ljk2OHYtMTAuMDM5Yy00LjA2MywtMS4yMzIgLTcuODk2LC0yLjk5MyAtMTEuNDE3LC01LjIwNWwtNi41MTEsNi41MTJjLTIuMzIzLDIuMzIxIC02LjEyLDIuMzIxIC04LjQ0MSwwbC04LjgxOCwtOC44MThjLTIuMzIxLC0yLjMyMSAtMi4zMjEsLTYuMTE4IDAsLTguNDM5bDcuMTAyLC03LjEwMmMtMS45NiwtMy42NTcgLTMuNDU2LC03LjYgLTQuNDA1LC0xMS43NTFoLTkuMjAyYy0zLjI4MiwwIC01Ljk2OCwtMi42ODUgLTUuOTY4LC01Ljk2OHYtMTIuNDcxYzAsLTMuMjgzIDIuNjg2LC01Ljk2OCA1Ljk2OCwtNS45NjhoMTAuMDM5YzEuMjMyLC00LjA2MyAyLjk5MywtNy44OTYgNS4yMDQsLTExLjQxOGwtNi41MTEsLTYuNTFjLTIuMzIxLC0yLjMyMiAtMi4zMjEsLTYuMTIgMCwtOC40NGw4LjgxOSwtOC44MTljMi4zMjEsLTIuMzIxIDYuMTE4LC0yLjMyMSA4LjQzOSwwbDcuMTAxLDcuMTAxYzMuNjU4LC0xLjk2IDcuNjAxLC0zLjQ1NiAxMS43NTMsLTQuNDA2di05LjE5OWMwLC0zLjI4MyAyLjY4NiwtNS45NjkgNS45NjgsLTUuOTY5aDEyLjQ3MWMzLjI4MiwwIDUuOTY4LDIuNjg2IDUuOTY4LDUuOTY5djEwLjAzNmM0LjA2NCwxLjIzMSA3Ljg5OCwyLjk5MiAxMS40MjIsNS4yMDRsNi41MDcsLTYuNTA5YzIuMzIzLC0yLjMyMSA2LjEyLC0yLjMyMSA4LjQ0MSwwek0zMjQuNTE5LDE1MGMwLDEzLjUzOCAtMTAuOTc5LDI0LjUxOSAtMjQuNTE5LDI0LjUxOWMtMTMuNTM5LDAgLTI0LjUxOSwtMTAuOTggLTI0LjUxOSwtMjQuNTE5YzAsLTEzLjUzOSAxMC45OCwtMjQuNTE5IDI0LjUxOSwtMjQuNTE5YzEzLjU0LDAgMjQuNTE5LDEwLjk4IDI0LjUxOSwyNC41MTl6IiBzdHJva2Utb3BhY2l0eT0iMC4xMjk0MSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEyLjUiLz48cGF0aCBkPSJNMzQ4Ljk2NywxMTIuMDhjMi4zMjEsMi4zMjEgMi4zMjEsNi4xMTggMCw4LjQzOWwtNy4xMDEsNy4xMDFjMS45NTksMy42NTggMy40NTQsNy42MDEgNC40MDUsMTEuNzUyaDkuMTk5YzMuMjgzLDAgNS45NjksMi42ODYgNS45NjksNS45Njh2MTIuNDcxYzAsMy4yODMgLTIuNjg2LDUuOTY5IC01Ljk2OSw1Ljk2OWgtMTAuMDM5Yy0xLjIzMSw0LjA2MyAtMi45OTIsNy44OTYgLTUuMjA0LDExLjQxOGw2LjUxMiw2LjUxYzIuMzIxLDIuMzIzIDIuMzIxLDYuMTIgMCw4LjQ0bC04LjgxOCw4LjgxOWMtMi4zMjEsMi4zMiAtNi4xMTksMi4zMiAtOC40MzksMGwtNy4xMDIsLTcuMTAyYy0zLjY1NywxLjk2IC03LjYwMSwzLjQ1NiAtMTEuNzUzLDQuNDA2djkuMTk5YzAsMy4yODIgLTIuNjg1LDUuOTY4IC01Ljk2OCw1Ljk2OGgtMTIuNDdjLTMuMjgzLDAgLTUuOTY5LC0yLjY4NiAtNS45NjksLTUuOTY4di0xMC4wMzljLTQuMDYzLC0xLjIzMiAtNy44OTYsLTIuOTkzIC0xMS40MTcsLTUuMjA1bC02LjUxMSw2LjUxMmMtMi4zMjMsMi4zMjEgLTYuMTIsMi4zMjEgLTguNDQxLDBsLTguODE4LC04LjgxOGMtMi4zMjEsLTIuMzIxIC0yLjMyMSwtNi4xMTggMCwtOC40MzlsNy4xMDIsLTcuMTAyYy0xLjk2LC0zLjY1NyAtMy40NTYsLTcuNiAtNC40MDUsLTExLjc1MWgtOS4yMDJjLTMuMjgyLDAgLTUuOTY4LC0yLjY4NSAtNS45NjgsLTUuOTY4di0xMi40NzFjMCwtMy4yODMgMi42ODYsLTUuOTY4IDUuOTY4LC01Ljk2OGgxMC4wMzljMS4yMzIsLTQuMDYzIDIuOTkzLC03Ljg5NiA1LjIwNCwtMTEuNDE4bC02LjUxMSwtNi41MWMtMi4zMjEsLTIuMzIyIC0yLjMyMSwtNi4xMiAwLC04LjQ0bDguODE5LC04LjgxOWMyLjMyMSwtMi4zMjEgNi4xMTgsLTIuMzIxIDguNDM5LDBsNy4xMDEsNy4xMDFjMy42NTgsLTEuOTYgNy42MDEsLTMuNDU2IDExLjc1MywtNC40MDZ2LTkuMTk5YzAsLTMuMjgzIDIuNjg2LC01Ljk2OSA1Ljk2OCwtNS45NjloMTIuNDcxYzMuMjgyLDAgNS45NjgsMi42ODYgNS45NjgsNS45Njl2MTAuMDM2YzQuMDY0LDEuMjMxIDcuODk4LDIuOTkyIDExLjQyMiw1LjIwNGw2LjUwNywtNi41MDljMi4zMjMsLTIuMzIxIDYuMTIsLTIuMzIxIDguNDQxLDB6TTMyNC41MTksMTUwYzAsMTMuNTM4IC0xMC45NzksMjQuNTE5IC0yNC41MTksMjQuNTE5Yy0xMy41MzksMCAtMjQuNTE5LC0xMC45OCAtMjQuNTE5LC0yNC41MTljMCwtMTMuNTM5IDEwLjk4LC0yNC41MTkgMjQuNTE5LC0yNC41MTljMTMuNTQsMCAyNC41MTksMTAuOTggMjQuNTE5LDI0LjUxOXoiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI3LjUiLz48L2c+PC9nPjwvc3ZnPjwhLS1yb3RhdGlvbkNlbnRlcjo2Ny42OTo2Ny42ODktLT4='; + const TransparentIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTYuNDE5NjQiIGhlaWdodD0iMTE4LjM0OTk0IiB2aWV3Qm94PSIwLDAsMTE2LjQxOTY0LDExOC4zNDk5NCI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC45MjgwOSw3LjY1MTk0KSI+PGcgZGF0YS1wYXBlci1kYXRhPSJ7JnF1b3Q7aXNQYWludGluZ0xheWVyJnF1b3Q7OnRydWV9IiBzdHJva2UtbGluZWNhcD0iYnV0dCIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtZGFzaGFycmF5PSIiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PHBhdGggZD0iTTQuNDk0OTYsNDYuMzAwNDRjMi40Mjk4NiwwLjQ4NTA1IDE0LjQ5MTQ3LDI0LjExMSAzMS41NTQ1NSwyMS4yNjQ1YzE1LjUxNTYzLC0yLjU4ODM2IDEuNjU1NjMsMjMuNDU1NDUgNDAuODIwMjEsMy43Njg2OWwtMjAuOTM0NzYsMTkuMDI2ODJjMCwwIC0wLjU3LDAuNTggLTAuNjMsMC42NWMtMS4wNjY0NiwxLjM1MTk0IC0yLjU5MzgxLDIuMjYzMzYgLTQuMjksMi41NmMtMS4zNzkwOCwwLjI5NzM2IC0yLjc5NjAyLDAuMzc4MzMgLTQuMiwwLjI0djBjLTEuNjM1MjksLTAuMTY4NDkgLTMuMjQ1ODMsLTAuNTI0MTUgLTQuOCwtMS4wNmMtNy40LC0yLjQ4IC0xNS4xNywtNy44IC0yMS43OSwtMTQuMzdjLTYuNjIsLTYuNTcgLTEyLjA3LC0xNC40NiAtMTQuNjgsLTIxLjg5Yy0wLjQyODQsLTEuMjE5MjkgLTAuNzYyNzEsLTIuNDY5NjIgLTEsLTMuNzRjLTAuMjExODgsLTEuMDg3NDIgLTAuMzIyMzYsLTIuMTkyMTYgLTAuMzMsLTMuM2MtMC4wMzY2MiwtMS4wNTc1MiAwLjA1NzQyLC0yLjExNTUyIDAuMjgsLTMuMTV6IiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxwYXRoIGQ9Ik0zMS43NzcxOCwxMC41NTAzM2MtMTAuMjk5MTcsMS4xMDg1MiAtMTYuMzQ1NjUsNC42NjU4NyAtMTkuMTQ3MTksOS4xODA1N2MtMS44OTIyLDMuMjQ3OTcgLTIuMzQxNTYsNy4xMzg3MiAtMS4yMzk1MywxMC43MzI1bDAuMDkwNywwLjM0MjY0bDIwLjI2NTc5LC0yMC4yNTU3MXpNNzguMjg0NjksMjEuNzU2NDhjLTcuMzQ2NDcsLTcuMzU2NTUgLTE0Ljc3MzU3LC0xMy4xMDA3IC0yMC44OTA1OSwtMTYuMjk1MjdjLTQuMzQzMzksLTIuMjM3MiAtNy43Mjk0MiwtMy4xMzQwOSAtOS4zOTIyLC0yLjE4NjgxbC0wLjgyNjM2LDAuODI2MzZjLTEuMjI5NDUsMi4wMTU1IC0wLjUxMzk2LDYuMzA4NDkgMS43OTM3OSwxMS42Njk3YzMuNTYzMzUsNy43MzIxOSA4LjUyMDE1LDE0Ljc0MjA4IDE0LjYyMjQsMjAuNjc4OTdjNi45OTM3Nyw2Ljk5Mzc3IDE0LjUwMTQ4LDEyLjIzNDA0IDIwLjg5MDU5LDE1LjExNjJjNC41NDQ5NCwyLjAxNTUgOC4yNzM2LDIuOTIyNDcgMTAuNDMwMTgsMi4zMzc5N2wxLjkyNDgsLTEuOTM0ODhjMC40NTM0OCwtMS45MjQ4IC0wLjUwMzg4LC01LjE3OTgyIC0yLjUwOTI5LC05LjIwMDczYy0zLjExMzk0LC02LjI1ODExIC04Ljc4NzU1LC0xMy43NjU4MiAtMTYuMDQzMzMsLTIxLjAxMTUyek02MC4yODYzMywtMC4xNTE5NGM2LjcxMTU5LDMuNDU2NTcgMTQuNjkyOTUsOS42NDQxNCAyMi40ODI4MywxNy40MzQwMmM3Ljc4OTg5LDcuNzg5ODkgMTMuODU2NTIsMTUuODIxNjMgMTcuMjIyMzksMjIuNzI0NjljMy43NzkwNSw3LjU2ODE4IDQuMzgzNywxNC4wMjc4NCAwLjY5NTM0LDE3LjcxNjE5Yy0wLjYwNTgsMC41Nzc0OSAtMS4yOTU1MiwxLjA1OTk2IC0yLjA0NTcyLDEuNDMxbC0zOS40MjMwNiwzOS40MDI5Yy0yLjM2ODIsMi4zODgzNiAtMy4zNTU4LDMuMzk2MTEgLTcuNDM3MTcsNC4zMTMxNWMtMi4wMzEzMiwwLjQ0MDM3IC00LjExODcsMC41NjI3NiAtNi4xODc1NywwLjM2Mjc4Yy0yLjE0MTgzLC0wLjIwNzggLTQuMjUyMTUsLTAuNjY0MzcgLTYuMjg4MzQsLTEuMzYwNDZjLTguMzk0NTMsLTIuODIxNjkgLTE3LjEzMTY5LC04LjcyNzA5IC0yNC40MDc2MiwtMTUuOTgyODZjLTcuMjc1OTMsLTcuMjU1NzcgLTEzLjM3MjgsLTE2LjA1MzQxIC0xNi4zMjU1LC0yNC40NjgxYy0wLjU1MDQ2LC0xLjUyNTUyIC0wLjk3ODQzLC0zLjA5MjQ4IC0xLjI3OTg0LC00LjY4NjAyYy0wLjI4NjAzLC0xLjQ3Nzc1IC0wLjQzMTEyLC0yLjk3OTMgLTAuNDMzMzQsLTQuNDg0NDdjLTAuMTE4MTcsLTIuMzUyMzQgMC4zMDU5MiwtNC43MDAzNyAxLjIzOTUzLC02Ljg2Mjc1YzAuOTY3NCwtMS44NjE3MyAyLjI1MzIsLTMuNTM5NzYgMy43OTkyMSwtNC45NTgxMWwwLjE2MTI0LC0wLjE3MTMybDQuNzQ2NDksLTQuNzM2NDFjLTAuNDk3NDcsLTEuMTI2MDIgLTAuOTA4NDgsLTIuMjg4MjggLTEuMjI5NDUsLTMuNDc2NzNjLTEuNTU2MSwtNS4xOTU4OSAtMC44NTM4MiwtMTAuODA2NzUgMS45MzQ4OCwtMTUuNDU4ODRjNC4zMDMwOCwtNy4wMzQwNyAxMy45NTczLC0xMi4yNjQyOCAzMC42NjU3MywtMTIuNDM1Nmw0LjIwMjMxLC00LjE5MjIzYzAuNDQzNjgsLTAuNjA4MzIgMC45ODExMSwtMS4xNDIzNSAxLjU5MjI0LC0xLjU4MjE2YzAuMTk4NTcsLTAuMTg3MzEgMC40MjY3LC0wLjM0MDUzIDAuNjc1MTksLTAuNDUzNDhjMy43Mzg3NCwtMi4yNTczNSA5LjI3MTI3LC0xLjM2MDQ2IDE1LjY0MDIzLDEuOTI0OHpNODkuMDQ3NDMsNTkuNzY4NjljLTIuNDY3MiwtMC41NTk2NiAtNC44Njg3NCwtMS4zNzcwNyAtNy4xNjUwOCwtMi40Mzg3NWMtNy4wNTQyMywtMy4xODQ0OCAtMTUuMjI3MDYsLTguODc4MjUgLTIyLjc2NSwtMTYuNDA2MTJjLTYuNjY3MjcsLTYuNTA2NzYgLTEyLjA3ODUxLC0xNC4xODYzNiAtMTUuOTYyNzEsLTIyLjY1NDE1Yy0xLjA5ODI2LC0yLjQ3ODQ1IC0xLjkwNjQ2LC01LjA3NTUxIC0yLjQwODUyLC03LjczOTQ5bC0yNi40MTg5OCwyNi4zMDIyYzEzLjg4MzU3LDE1LjAzNzU5IDU1LjM3MjM3LDIwLjM1NTk0IDY4LjI0MTMsMTQuODQzNTZjMS41Mjk5NywtMC42MjYxMiAzLjI3OTE1LDAuMDkyNzYgMy45MjY2OSwxLjYxMzhjMC42NDc1NCwxLjUyMTAzIC0wLjA0NjcxLDMuMjgwMTQgLTEuNTU4NDksMy45NDg5N2MtMTUuMzI3ODMsNi41NjA0MyAtNjIuNTU4NzYsLTEuNTI1NzYgLTc0Ljg2NjM0LC0xNi4wODMwOWwtMy42OTg0MywzLjczODc0Yy0xLjAxNzksMC45MTQwNSAtMS44ODIxNywxLjk4NTg3IC0yLjU1OTY4LDMuMTc0NGMtMC41MTUzNiwxLjMyNzM3IC0wLjczNTQ1LDIuNzUxMDUgLTAuNjQ0OTYsNC4xNzIwN2MwLjAwNzcsMS4xMTY0MiAwLjExOTA0LDIuMjI5NzIgMC4zMzI1NiwzLjMyNTU2YzAuMjM5MTMsMS4yODAyMyAwLjU3NjAzLDIuNTQwMjQgMS4wMDc3NCwzLjc2ODk3YzIuNjMwMjIsNy40ODc1NiA4LjA2MTk3LDE1LjM4ODI5IDE0LjgyMzk2LDIyLjA1OTU4YzYuNzYxOTksNi42NzEyOSAxNC41MjE2MywxMi4wMjI0MiAyMS45Nzg5NiwxNC40OTE0YzEuNTY2MjEsMC41NCAzLjE4OTIzLDAuODk4NDEgNC44MzcxOSwxLjA2ODIxdjBjMS40MTQ4NiwwLjEzOTQgMi44NDI3NywwLjA1NzgxIDQuMjMyNTMsLTAuMjQxODZjMS43MDkzMywtMC4yOTg5NCAzLjI0ODUxLC0xLjIxNzQxIDQuMzIzMjMsLTIuNTc5ODNjMC4wNjA0NiwtMC4wNzA1NCAwLjY0NDk2LC0wLjY0NDk2IDAuNjM0ODgsLTAuNjU1MDR6IiBmaWxsPSIjMDAwMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZS1vcGFjaXR5PSIwLjEyOTQxIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMTUiLz48cGF0aCBkPSJNMzEuNzc3MTgsMTAuNTUwMzNjLTEwLjI5OTE3LDEuMTA4NTIgLTE2LjM0NTY1LDQuNjY1ODcgLTE5LjE0NzE5LDkuMTgwNTdjLTEuODkyMiwzLjI0Nzk3IC0yLjM0MTU2LDcuMTM4NzIgLTEuMjM5NTMsMTAuNzMyNWwwLjA5MDcsMC4zNDI2NGwyMC4yNjU3OSwtMjAuMjU1NzF6TTc4LjI4NDY5LDIxLjc1NjQ4Yy03LjM0NjQ3LC03LjM1NjU1IC0xNC43NzM1NywtMTMuMTAwNyAtMjAuODkwNTksLTE2LjI5NTI3Yy00LjM0MzM5LC0yLjIzNzIgLTcuNzI5NDIsLTMuMTM0MDkgLTkuMzkyMiwtMi4xODY4MWwtMC44MjYzNiwwLjgyNjM2Yy0xLjIyOTQ1LDIuMDE1NSAtMC41MTM5Niw2LjMwODQ5IDEuNzkzNzksMTEuNjY5N2MzLjU2MzM1LDcuNzMyMTkgOC41MjAxNSwxNC43NDIwOCAxNC42MjI0LDIwLjY3ODk3YzYuOTkzNzcsNi45OTM3NyAxNC41MDE0OCwxMi4yMzQwNCAyMC44OTA1OSwxNS4xMTYyYzQuNTQ0OTQsMi4wMTU1IDguMjczNiwyLjkyMjQ3IDEwLjQzMDE4LDIuMzM3OTdsMS45MjQ4LC0xLjkzNDg4YzAuNDUzNDgsLTEuOTI0OCAtMC41MDM4OCwtNS4xNzk4MiAtMi41MDkyOSwtOS4yMDA3M2MtMy4xMTM5NCwtNi4yNTgxMSAtOC43ODc1NSwtMTMuNzY1ODIgLTE2LjA0MzMzLC0yMS4wMTE1MnpNNjAuMjg2MzMsLTAuMTUxOTRjNi43MTE1OSwzLjQ1NjU3IDE0LjY5Mjk1LDkuNjQ0MTQgMjIuNDgyODMsMTcuNDM0MDJjNy43ODk4OSw3Ljc4OTg5IDEzLjg1NjUyLDE1LjgyMTYzIDE3LjIyMjM5LDIyLjcyNDY5YzMuNzc5MDUsNy41NjgxOCA0LjM4MzcsMTQuMDI3ODQgMC42OTUzNCwxNy43MTYxOWMtMC42MDU4LDAuNTc3NDkgLTEuMjk1NTIsMS4wNTk5NiAtMi4wNDU3MiwxLjQzMWwtMzkuNDIzMDYsMzkuNDAyOWMtMi4zNjgyLDIuMzg4MzYgLTMuMzU1OCwzLjM5NjExIC03LjQzNzE3LDQuMzEzMTVjLTIuMDMxMzIsMC40NDAzNyAtNC4xMTg3LDAuNTYyNzYgLTYuMTg3NTcsMC4zNjI3OGMtMi4xNDE4MywtMC4yMDc4IC00LjI1MjE1LC0wLjY2NDM3IC02LjI4ODM0LC0xLjM2MDQ2Yy04LjM5NDUzLC0yLjgyMTY5IC0xNy4xMzE2OSwtOC43MjcwOSAtMjQuNDA3NjIsLTE1Ljk4Mjg2Yy03LjI3NTkzLC03LjI1NTc3IC0xMy4zNzI4LC0xNi4wNTM0MSAtMTYuMzI1NSwtMjQuNDY4MWMtMC41NTA0NiwtMS41MjU1MiAtMC45Nzg0MywtMy4wOTI0OCAtMS4yNzk4NCwtNC42ODYwMmMtMC4yODYwMywtMS40Nzc3NSAtMC40MzExMiwtMi45NzkzIC0wLjQzMzM0LC00LjQ4NDQ3Yy0wLjExODE3LC0yLjM1MjM0IDAuMzA1OTIsLTQuNzAwMzcgMS4yMzk1MywtNi44NjI3NWMwLjk2NzQsLTEuODYxNzMgMi4yNTMyLC0zLjUzOTc2IDMuNzk5MjEsLTQuOTU4MTFsMC4xNjEyNCwtMC4xNzEzMmw0Ljc0NjQ5LC00LjczNjQxYy0wLjQ5NzQ3LC0xLjEyNjAyIC0wLjkwODQ4LC0yLjI4ODI4IC0xLjIyOTQ1LC0zLjQ3NjczYy0xLjU1NjEsLTUuMTk1ODkgLTAuODUzODIsLTEwLjgwNjc1IDEuOTM0ODgsLTE1LjQ1ODg0YzQuMzAzMDgsLTcuMDM0MDcgMTMuOTU3MywtMTIuMjY0MjggMzAuNjY1NzMsLTEyLjQzNTZsNC4yMDIzMSwtNC4xOTIyM2MwLjQ0MzY4LC0wLjYwODMyIDAuOTgxMTEsLTEuMTQyMzUgMS41OTIyNCwtMS41ODIxNmMwLjE5ODU3LC0wLjE4NzMxIDAuNDI2NywtMC4zNDA1MyAwLjY3NTE5LC0wLjQ1MzQ4YzMuNzM4NzQsLTIuMjU3MzUgOS4yNzEyNywtMS4zNjA0NiAxNS42NDAyMywxLjkyNDh6TTg5LjA0NzQzLDU5Ljc2ODY5Yy0yLjQ2NzIsLTAuNTU5NjYgLTQuODY4NzQsLTEuMzc3MDcgLTcuMTY1MDgsLTIuNDM4NzVjLTcuMDU0MjMsLTMuMTg0NDggLTE1LjIyNzA2LC04Ljg3ODI1IC0yMi43NjUsLTE2LjQwNjEyYy02LjY2NzI3LC02LjUwNjc2IC0xMi4wNzg1MSwtMTQuMTg2MzYgLTE1Ljk2MjcxLC0yMi42NTQxNWMtMS4wOTgyNiwtMi40Nzg0NSAtMS45MDY0NiwtNS4wNzU1MSAtMi40MDg1MiwtNy43Mzk0OWwtMjYuNDE4OTgsMjYuMzAyMmMxMy44ODM1NywxNS4wMzc1OSA1NS4zNzIzNywyMC4zNTU5NCA2OC4yNDEzLDE0Ljg0MzU2YzEuNTI5OTcsLTAuNjI2MTIgMy4yNzkxNSwwLjA5Mjc2IDMuOTI2NjksMS42MTM4YzAuNjQ3NTQsMS41MjEwMyAtMC4wNDY3MSwzLjI4MDE0IC0xLjU1ODQ5LDMuOTQ4OTdjLTE1LjMyNzgzLDYuNTYwNDMgLTYyLjU1ODc2LC0xLjUyNTc2IC03NC44NjYzNCwtMTYuMDgzMDlsLTMuNjk4NDMsMy43Mzg3NGMtMS4wMTc5LDAuOTE0MDUgLTEuODgyMTcsMS45ODU4NyAtMi41NTk2OCwzLjE3NDRjLTAuNTE1MzYsMS4zMjczNyAtMC43MzU0NSwyLjc1MTA1IC0wLjY0NDk2LDQuMTcyMDdjMC4wMDc3LDEuMTE2NDIgMC4xMTkwNCwyLjIyOTcyIDAuMzMyNTYsMy4zMjU1NmMwLjIzOTEzLDEuMjgwMjMgMC41NzYwMywyLjU0MDI0IDEuMDA3NzQsMy43Njg5N2MyLjYzMDIyLDcuNDg3NTYgOC4wNjE5NywxNS4zODgyOSAxNC44MjM5NiwyMi4wNTk1OGM2Ljc2MTk5LDYuNjcxMjkgMTQuNTIxNjMsMTIuMDIyNDIgMjEuOTc4OTYsMTQuNDkxNGMxLjU2NjIxLDAuNTQgMy4xODkyMywwLjg5ODQxIDQuODM3MTksMS4wNjgyMXYwYzEuNDE0ODYsMC4xMzk0IDIuODQyNzcsMC4wNTc4MSA0LjIzMjUzLC0wLjI0MTg2YzEuNzA5MzMsLTAuMjk4OTQgMy4yNDg1MSwtMS4yMTc0MSA0LjMyMzIzLC0yLjU3OTgzYzAuMDYwNDYsLTAuMDcwNTQgMC42NDQ5NiwtMC42NDQ5NiAwLjYzNDg4LC0wLjY1NTA0eiIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz48L2c+PC9nPjwvc3ZnPjwhLS1yb3RhdGlvbkNlbnRlcjo1OC45MjgwOTo1Ny42NTE5Mzk5OTk5OTk5OTYtLT4='; + const GradientIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMTYuNDE5NjQiIGhlaWdodD0iMTE4LjM0OTk0IiB2aWV3Qm94PSIwLDAsMTE2LjQxOTY0LDExOC4zNDk5NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IHgxPSIzLjM2ODMzIiB5MT0iNzMuMjEzOCIgeDI9IjgzLjM4NjAzIiB5Mj0iNzMuMjEzOCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjb2xvci0xIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmNTQyNDIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM0Mjk3ZjUiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4LjkyODA5LDcuNjUxOTQpIj48ZyBkYXRhLXBhcGVyLWRhdGE9InsmcXVvdDtpc1BhaW50aW5nTGF5ZXImcXVvdDs6dHJ1ZX0iIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMy42NTYwMyw0OS40MjYwM2MyLjQyOTg2LDAuNDg1MDUgMTQuNDkxNDcsMjQuMTExIDMxLjU1NDU1LDIxLjI2NDVjMTUuNTE1NjMsLTIuNTg4MzYgOS4wMTA4NywxNC4yMjIyNyA0OC4xNzU0NSwtNS40NjQ0OWwtMjguMjksMjguMjZjMCwwIC0wLjU3LDAuNTggLTAuNjMsMC42NWMtMS4wNjY0NiwxLjM1MTk0IC0yLjU5MzgxLDIuMjYzMzYgLTQuMjksMi41NmMtMS4zNzkwOCwwLjI5NzM2IC0yLjc5NjAyLDAuMzc4MzMgLTQuMiwwLjI0djBjLTEuNjM1MjksLTAuMTY4NDkgLTMuMjQ1ODMsLTAuNTI0MTUgLTQuOCwtMS4wNmMtNy40LC0yLjQ4IC0xNS4xNywtNy44IC0yMS43OSwtMTQuMzdjLTYuNjIsLTYuNTcgLTEyLjA3LC0xNC40NiAtMTQuNjgsLTIxLjg5Yy0wLjQyODQsLTEuMjE5MjkgLTAuNzYyNzEsLTIuNDY5NjIgLTEsLTMuNzRjLTAuMjExODgsLTEuMDg3NDIgLTAuMzIyMzYsLTIuMTkyMTYgLTAuMzMsLTMuM2MtMC4wMzY2MiwtMS4wNTc1MiAwLjA1NzQyLC0yLjExNTUyIDAuMjgsLTMuMTV6IiBmaWxsPSJ1cmwoI2NvbG9yLTEpIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHBhdGggZD0iTTMxLjc3NzE4LDEwLjU1MDMzYy0xMC4yOTkxNywxLjEwODUyIC0xNi4zNDU2NSw0LjY2NTg3IC0xOS4xNDcxOSw5LjE4MDU3Yy0xLjg5MjIsMy4yNDc5NyAtMi4zNDE1Niw3LjEzODcyIC0xLjIzOTUzLDEwLjczMjVsMC4wOTA3LDAuMzQyNjRsMjAuMjY1NzksLTIwLjI1NTcxek03OC4yODQ2OSwyMS43NTY0OGMtNy4zNDY0NywtNy4zNTY1NSAtMTQuNzczNTcsLTEzLjEwMDcgLTIwLjg5MDU5LC0xNi4yOTUyN2MtNC4zNDMzOSwtMi4yMzcyIC03LjcyOTQyLC0zLjEzNDA5IC05LjM5MjIsLTIuMTg2ODFsLTAuODI2MzYsMC44MjYzNmMtMS4yMjk0NSwyLjAxNTUgLTAuNTEzOTYsNi4zMDg0OSAxLjc5Mzc5LDExLjY2OTdjMy41NjMzNSw3LjczMjE5IDguNTIwMTUsMTQuNzQyMDggMTQuNjIyNCwyMC42Nzg5N2M2Ljk5Mzc3LDYuOTkzNzcgMTQuNTAxNDgsMTIuMjM0MDQgMjAuODkwNTksMTUuMTE2MmM0LjU0NDk0LDIuMDE1NSA4LjI3MzYsMi45MjI0NyAxMC40MzAxOCwyLjMzNzk3bDEuOTI0OCwtMS45MzQ4OGMwLjQ1MzQ4LC0xLjkyNDggLTAuNTAzODgsLTUuMTc5ODIgLTIuNTA5MjksLTkuMjAwNzNjLTMuMTEzOTQsLTYuMjU4MTEgLTguNzg3NTUsLTEzLjc2NTgyIC0xNi4wNDMzMywtMjEuMDExNTJ6TTYwLjI4NjMzLC0wLjE1MTk0YzYuNzExNTksMy40NTY1NyAxNC42OTI5NSw5LjY0NDE0IDIyLjQ4MjgzLDE3LjQzNDAyYzcuNzg5ODksNy43ODk4OSAxMy44NTY1MiwxNS44MjE2MyAxNy4yMjIzOSwyMi43MjQ2OWMzLjc3OTA1LDcuNTY4MTggNC4zODM3LDE0LjAyNzg0IDAuNjk1MzQsMTcuNzE2MTljLTAuNjA1OCwwLjU3NzQ5IC0xLjI5NTUyLDEuMDU5OTYgLTIuMDQ1NzIsMS40MzFsLTM5LjQyMzA2LDM5LjQwMjljLTIuMzY4MiwyLjM4ODM2IC0zLjM1NTgsMy4zOTYxMSAtNy40MzcxNyw0LjMxMzE1Yy0yLjAzMTMyLDAuNDQwMzcgLTQuMTE4NywwLjU2Mjc2IC02LjE4NzU3LDAuMzYyNzhjLTIuMTQxODMsLTAuMjA3OCAtNC4yNTIxNSwtMC42NjQzNyAtNi4yODgzNCwtMS4zNjA0NmMtOC4zOTQ1MywtMi44MjE2OSAtMTcuMTMxNjksLTguNzI3MDkgLTI0LjQwNzYyLC0xNS45ODI4NmMtNy4yNzU5MywtNy4yNTU3NyAtMTMuMzcyOCwtMTYuMDUzNDEgLTE2LjMyNTUsLTI0LjQ2ODFjLTAuNTUwNDYsLTEuNTI1NTIgLTAuOTc4NDMsLTMuMDkyNDggLTEuMjc5ODQsLTQuNjg2MDJjLTAuMjg2MDMsLTEuNDc3NzUgLTAuNDMxMTIsLTIuOTc5MyAtMC40MzMzNCwtNC40ODQ0N2MtMC4xMTgxNywtMi4zNTIzNCAwLjMwNTkyLC00LjcwMDM3IDEuMjM5NTMsLTYuODYyNzVjMC45Njc0LC0xLjg2MTczIDIuMjUzMiwtMy41Mzk3NiAzLjc5OTIxLC00Ljk1ODExbDAuMTYxMjQsLTAuMTcxMzJsNC43NDY0OSwtNC43MzY0MWMtMC40OTc0NywtMS4xMjYwMiAtMC45MDg0OCwtMi4yODgyOCAtMS4yMjk0NSwtMy40NzY3M2MtMS41NTYxLC01LjE5NTg5IC0wLjg1MzgyLC0xMC44MDY3NSAxLjkzNDg4LC0xNS40NTg4NGM0LjMwMzA4LC03LjAzNDA3IDEzLjk1NzMsLTEyLjI2NDI4IDMwLjY2NTczLC0xMi40MzU2bDQuMjAyMzEsLTQuMTkyMjNjMC40NDM2OCwtMC42MDgzMiAwLjk4MTExLC0xLjE0MjM1IDEuNTkyMjQsLTEuNTgyMTZjMC4xOTg1NywtMC4xODczMSAwLjQyNjcsLTAuMzQwNTMgMC42NzUxOSwtMC40NTM0OGMzLjczODc0LC0yLjI1NzM1IDkuMjcxMjcsLTEuMzYwNDYgMTUuNjQwMjMsMS45MjQ4ek04OS4wNDc0Myw1OS43Njg2OWMtMi40NjcyLC0wLjU1OTY2IC00Ljg2ODc0LC0xLjM3NzA3IC03LjE2NTA4LC0yLjQzODc1Yy03LjA1NDIzLC0zLjE4NDQ4IC0xNS4yMjcwNiwtOC44NzgyNSAtMjIuNzY1LC0xNi40MDYxMmMtNi42NjcyNywtNi41MDY3NiAtMTIuMDc4NTEsLTE0LjE4NjM2IC0xNS45NjI3MSwtMjIuNjU0MTVjLTEuMDk4MjYsLTIuNDc4NDUgLTEuOTA2NDYsLTUuMDc1NTEgLTIuNDA4NTIsLTcuNzM5NDlsLTI2LjQxODk4LDI2LjMwMjJjMTMuODgzNTcsMTUuMDM3NTkgNTUuMzcyMzcsMjAuMzU1OTQgNjguMjQxMywxNC44NDM1NmMxLjUyOTk3LC0wLjYyNjEyIDMuMjc5MTUsMC4wOTI3NiAzLjkyNjY5LDEuNjEzOGMwLjY0NzU0LDEuNTIxMDMgLTAuMDQ2NzEsMy4yODAxNCAtMS41NTg0OSwzLjk0ODk3Yy0xNS4zMjc4Myw2LjU2MDQzIC02Mi41NTg3NiwtMS41MjU3NiAtNzQuODY2MzQsLTE2LjA4MzA5bC0zLjY5ODQzLDMuNzM4NzRjLTEuMDE3OSwwLjkxNDA1IC0xLjg4MjE3LDEuOTg1ODcgLTIuNTU5NjgsMy4xNzQ0Yy0wLjUxNTM2LDEuMzI3MzcgLTAuNzM1NDUsMi43NTEwNSAtMC42NDQ5Niw0LjE3MjA3YzAuMDA3NywxLjExNjQyIDAuMTE5MDQsMi4yMjk3MiAwLjMzMjU2LDMuMzI1NTZjMC4yMzkxMywxLjI4MDIzIDAuNTc2MDMsMi41NDAyNCAxLjAwNzc0LDMuNzY4OTdjMi42MzAyMiw3LjQ4NzU2IDguMDYxOTcsMTUuMzg4MjkgMTQuODIzOTYsMjIuMDU5NThjNi43NjE5OSw2LjY3MTI5IDE0LjUyMTYzLDEyLjAyMjQyIDIxLjk3ODk2LDE0LjQ5MTRjMS41NjYyMSwwLjU0IDMuMTg5MjMsMC44OTg0MSA0LjgzNzE5LDEuMDY4MjF2MGMxLjQxNDg2LDAuMTM5NCAyLjg0Mjc3LDAuMDU3ODEgNC4yMzI1MywtMC4yNDE4NmMxLjcwOTMzLC0wLjI5ODk0IDMuMjQ4NTEsLTEuMjE3NDEgNC4zMjMyMywtMi41Nzk4M2MwLjA2MDQ2LC0wLjA3MDU0IDAuNjQ0OTYsLTAuNjQ0OTYgMC42MzQ4OCwtMC42NTUwNHoiIGZpbGw9IiMwMDAwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlLW9wYWNpdHk9IjAuMTI5NDEiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxNSIvPjxwYXRoIGQ9Ik0zMS43NzcxOCwxMC41NTAzM2MtMTAuMjk5MTcsMS4xMDg1MiAtMTYuMzQ1NjUsNC42NjU4NyAtMTkuMTQ3MTksOS4xODA1N2MtMS44OTIyLDMuMjQ3OTcgLTIuMzQxNTYsNy4xMzg3MiAtMS4yMzk1MywxMC43MzI1bDAuMDkwNywwLjM0MjY0bDIwLjI2NTc5LC0yMC4yNTU3MXpNNzguMjg0NjksMjEuNzU2NDhjLTcuMzQ2NDcsLTcuMzU2NTUgLTE0Ljc3MzU3LC0xMy4xMDA3IC0yMC44OTA1OSwtMTYuMjk1MjdjLTQuMzQzMzksLTIuMjM3MiAtNy43Mjk0MiwtMy4xMzQwOSAtOS4zOTIyLC0yLjE4NjgxbC0wLjgyNjM2LDAuODI2MzZjLTEuMjI5NDUsMi4wMTU1IC0wLjUxMzk2LDYuMzA4NDkgMS43OTM3OSwxMS42Njk3YzMuNTYzMzUsNy43MzIxOSA4LjUyMDE1LDE0Ljc0MjA4IDE0LjYyMjQsMjAuNjc4OTdjNi45OTM3Nyw2Ljk5Mzc3IDE0LjUwMTQ4LDEyLjIzNDA0IDIwLjg5MDU5LDE1LjExNjJjNC41NDQ5NCwyLjAxNTUgOC4yNzM2LDIuOTIyNDcgMTAuNDMwMTgsMi4zMzc5N2wxLjkyNDgsLTEuOTM0ODhjMC40NTM0OCwtMS45MjQ4IC0wLjUwMzg4LC01LjE3OTgyIC0yLjUwOTI5LC05LjIwMDczYy0zLjExMzk0LC02LjI1ODExIC04Ljc4NzU1LC0xMy43NjU4MiAtMTYuMDQzMzMsLTIxLjAxMTUyek02MC4yODYzMywtMC4xNTE5NGM2LjcxMTU5LDMuNDU2NTcgMTQuNjkyOTUsOS42NDQxNCAyMi40ODI4MywxNy40MzQwMmM3Ljc4OTg5LDcuNzg5ODkgMTMuODU2NTIsMTUuODIxNjMgMTcuMjIyMzksMjIuNzI0NjljMy43NzkwNSw3LjU2ODE4IDQuMzgzNywxNC4wMjc4NCAwLjY5NTM0LDE3LjcxNjE5Yy0wLjYwNTgsMC41Nzc0OSAtMS4yOTU1MiwxLjA1OTk2IC0yLjA0NTcyLDEuNDMxbC0zOS40MjMwNiwzOS40MDI5Yy0yLjM2ODIsMi4zODgzNiAtMy4zNTU4LDMuMzk2MTEgLTcuNDM3MTcsNC4zMTMxNWMtMi4wMzEzMiwwLjQ0MDM3IC00LjExODcsMC41NjI3NiAtNi4xODc1NywwLjM2Mjc4Yy0yLjE0MTgzLC0wLjIwNzggLTQuMjUyMTUsLTAuNjY0MzcgLTYuMjg4MzQsLTEuMzYwNDZjLTguMzk0NTMsLTIuODIxNjkgLTE3LjEzMTY5LC04LjcyNzA5IC0yNC40MDc2MiwtMTUuOTgyODZjLTcuMjc1OTMsLTcuMjU1NzcgLTEzLjM3MjgsLTE2LjA1MzQxIC0xNi4zMjU1LC0yNC40NjgxYy0wLjU1MDQ2LC0xLjUyNTUyIC0wLjk3ODQzLC0zLjA5MjQ4IC0xLjI3OTg0LC00LjY4NjAyYy0wLjI4NjAzLC0xLjQ3Nzc1IC0wLjQzMTEyLC0yLjk3OTMgLTAuNDMzMzQsLTQuNDg0NDdjLTAuMTE4MTcsLTIuMzUyMzQgMC4zMDU5MiwtNC43MDAzNyAxLjIzOTUzLC02Ljg2Mjc1YzAuOTY3NCwtMS44NjE3MyAyLjI1MzIsLTMuNTM5NzYgMy43OTkyMSwtNC45NTgxMWwwLjE2MTI0LC0wLjE3MTMybDQuNzQ2NDksLTQuNzM2NDFjLTAuNDk3NDcsLTEuMTI2MDIgLTAuOTA4NDgsLTIuMjg4MjggLTEuMjI5NDUsLTMuNDc2NzNjLTEuNTU2MSwtNS4xOTU4OSAtMC44NTM4MiwtMTAuODA2NzUgMS45MzQ4OCwtMTUuNDU4ODRjNC4zMDMwOCwtNy4wMzQwNyAxMy45NTczLC0xMi4yNjQyOCAzMC42NjU3MywtMTIuNDM1Nmw0LjIwMjMxLC00LjE5MjIzYzAuNDQzNjgsLTAuNjA4MzIgMC45ODExMSwtMS4xNDIzNSAxLjU5MjI0LC0xLjU4MjE2YzAuMTk4NTcsLTAuMTg3MzEgMC40MjY3LC0wLjM0MDUzIDAuNjc1MTksLTAuNDUzNDhjMy43Mzg3NCwtMi4yNTczNSA5LjI3MTI3LC0xLjM2MDQ2IDE1LjY0MDIzLDEuOTI0OHpNODkuMDQ3NDMsNTkuNzY4NjljLTIuNDY3MiwtMC41NTk2NiAtNC44Njg3NCwtMS4zNzcwNyAtNy4xNjUwOCwtMi40Mzg3NWMtNy4wNTQyMywtMy4xODQ0OCAtMTUuMjI3MDYsLTguODc4MjUgLTIyLjc2NSwtMTYuNDA2MTJjLTYuNjY3MjcsLTYuNTA2NzYgLTEyLjA3ODUxLC0xNC4xODYzNiAtMTUuOTYyNzEsLTIyLjY1NDE1Yy0xLjA5ODI2LC0yLjQ3ODQ1IC0xLjkwNjQ2LC01LjA3NTUxIC0yLjQwODUyLC03LjczOTQ5bC0yNi40MTg5OCwyNi4zMDIyYzEzLjg4MzU3LDE1LjAzNzU5IDU1LjM3MjM3LDIwLjM1NTk0IDY4LjI0MTMsMTQuODQzNTZjMS41Mjk5NywtMC42MjYxMiAzLjI3OTE1LDAuMDkyNzYgMy45MjY2OSwxLjYxMzhjMC42NDc1NCwxLjUyMTAzIC0wLjA0NjcxLDMuMjgwMTQgLTEuNTU4NDksMy45NDg5N2MtMTUuMzI3ODMsNi41NjA0MyAtNjIuNTU4NzYsLTEuNTI1NzYgLTc0Ljg2NjM0LC0xNi4wODMwOWwtMy42OTg0MywzLjczODc0Yy0xLjAxNzksMC45MTQwNSAtMS44ODIxNywxLjk4NTg3IC0yLjU1OTY4LDMuMTc0NGMtMC41MTUzNiwxLjMyNzM3IC0wLjczNTQ1LDIuNzUxMDUgLTAuNjQ0OTYsNC4xNzIwN2MwLjAwNzcsMS4xMTY0MiAwLjExOTA0LDIuMjI5NzIgMC4zMzI1NiwzLjMyNTU2YzAuMjM5MTMsMS4yODAyMyAwLjU3NjAzLDIuNTQwMjQgMS4wMDc3NCwzLjc2ODk3YzIuNjMwMjIsNy40ODc1NiA4LjA2MTk3LDE1LjM4ODI5IDE0LjgyMzk2LDIyLjA1OTU4YzYuNzYxOTksNi42NzEyOSAxNC41MjE2MywxMi4wMjI0MiAyMS45Nzg5NiwxNC40OTE0YzEuNTY2MjEsMC41NCAzLjE4OTIzLDAuODk4NDEgNC44MzcxOSwxLjA2ODIxdjBjMS40MTQ4NiwwLjEzOTQgMi44NDI3NywwLjA1NzgxIDQuMjMyNTMsLTAuMjQxODZjMS43MDkzMywtMC4yOTg5NCAzLjI0ODUxLC0xLjIxNzQxIDQuMzIzMjMsLTIuNTc5ODNjMC4wNjA0NiwtMC4wNzA1NCAwLjY0NDk2LC0wLjY0NDk2IDAuNjM0ODgsLTAuNjU1MDR6IiBmaWxsPSIjMDAwMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIxMCIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjU4LjkyODA4ODk1NTAxODk4OjU3LjY1MTk0MTAyMjI0MTgzLS0+'; + const PictureIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIxMzguNjA3MDUiIGhlaWdodD0iMTE0LjIxMjI3IiB2aWV3Qm94PSIwLDAsMTM4LjYwNzA1LDExNC4yMTIyNyI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTkuMzAzNTIsNy4xMDYxNCkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2Utb3BhY2l0eT0iMC4xMjk0MSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjcuNSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiPjxwYXRoIGQ9Ik0xMTUuNTUzNTIsNS43OTA3NnY4OC40MTg0OGMwLDUuMDUxMzcgLTQuMDk1NTIsOS4xNDY5IC05LjE0NjksOS4xNDY5aC0xMTIuODEzMjVjLTUuMDUxMzcsMCAtOS4xNDY5LC00LjA5NTUyIC05LjE0NjksLTkuMTQ2OXYtODguNDE4NDhjMCwtNS4wNTEzNyA0LjA5NTUyLC05LjE0NjkgOS4xNDY5LC05LjE0NjloMTEyLjgxMzI1YzUuMDUxMzcsMCA5LjE0NjksNC4wOTU1MiA5LjE0NjksOS4xNDY5ek0yLjc0MDI3LDU3LjU4OTY0bDE1LjE2MDk4LC0xMS4xNDc3OGMzLjgxODgzLC0yLjgxMjY3IDkuMTYwNjIsLTIuMjM0MTMgMTIuMjkzNDMsMS4zMzU0NWwxMC4yMjM5NCwxMS42NDg1N2wyNC41NDU3LC0xMi4yNzA1NmMzLjAxNjE5LC0xLjUxMTUyIDYuNjI0NjQsLTEuMjM3MTIgOS4zODAxNCwwLjcxNTc0bDIyLjkxNTI2LDE2LjIzMzQ2di00OS4xNjY4NmgtOTQuNTE5NDZ6TTM4LjU3NzgxLDg1LjA2MjM0bC0xNi41OTcwNCwtMTguOTA4OTJsLTE5LjE1NTg5LDE0LjA4MTY1Yy0wLjAyNzQ0LDAuMDIwNTggLTAuMDU3MTcsMC4wMjk3MyAtMC4wODQ2MSwwLjA1MDMxdjQuNzc2OTd6TTk1LjE5NzEsODUuMDYyMzRsLTI2Ljk1NTkxLC0xOS4wOTE4NmwtMTUuMzQ2MjEsNy42NzE5NmwxMC4wMjI3MSwxMS40MTk5ek0zMC4xODA5NiwyOS44MDEzNmMwLC01LjQ3MjEzIC00LjQzNjI1LC05LjkwODM4IC05LjkwODM4LC05LjkwODM4Yy01LjQ3MjEzLDAgLTkuOTA4MzgsNC40MzYyNSAtOS45MDgzOCw5LjkwODM4YzAsNS40NzIxMyA0LjQzNjI1LDkuOTA4MzggOS45MDgzOCw5LjkwODM4YzUuNDcyMTMsMCA5LjkwODM4LC00LjQzNjI1IDkuOTA4MzgsLTkuOTA4Mzh6Ii8+PC9nPjwvZz48L3N2Zz48IS0tcm90YXRpb25DZW50ZXI6NjkuMzAzNTIzOTQ2MzExODo1Ny4xMDYxMzY4MjA3MDk5Ni0tPg=='; + const ResetIcon = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHdpZHRoPSIzNDkuMzIzMjQiIGhlaWdodD0iMzI3LjM0OTEyIiB2aWV3Qm94PSIwLDAsMzQ5LjMyMzI0LDMyNy4zNDkxMiI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTY1LjMzODM4LC0xNi4zMjU0NCkiPjxnIGRhdGEtcGFwZXItZGF0YT0ieyZxdW90O2lzUGFpbnRpbmdMYXllciZxdW90Ozp0cnVlfSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTogbm9ybWFsIj48cGF0aCBkPSJNMTM2LjQxNTc0LDk3LjkwMTk0Yy0zOC4yNDgwNiw4Mi43NzIyNSAtMzAuNTkzMDYsMjMyLjc5NzI4IDEzOS43MzA3NywyMjAuMDA0MTZjMTQyLjQzOSwtMTAuNjk4NjggMTQzLjc0MTk3LC0xOTIuNTE4ODQgNDMuMzc5NjksLTI0OS41MzgxIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iNTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxwYXRoIGQ9Ik05NS42NDc3MSw0OS43ODA2N2w5OS44NTIyMiwtMC40MDI2MyIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTkxLjQ1MzI4LDY1LjUxNDIxbDEwOS42ODM0NywtMjQuMTg4NzciIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTIwNC4wMjE1Miw0My4yOTQzNWwtOS44MjI0LDEwNi4wMTExMyIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjUwIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48cGF0aCBkPSJNOTAuMzM4MzgsNjcuNDI2MDhsMTAzLjUyMzY2LDgyLjIwODIiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHBhdGggZD0iTTE1My45MTUxOCwxMDMuODQ5ODV2LTMyLjA5NTloMTQuNjk0NTF2MzIuMDk1OXoiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSI1MCIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiLz48cGF0aCBkPSJNMTk1LjQ5OTkyLDQ5LjM3ODA0IiBzdHJva2U9IiNmZjAwMDAiIHN0cm9rZS13aWR0aD0iNTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvZz48L2c+PC9zdmc+PCEtLXJvdGF0aW9uQ2VudGVyOjE3NC42NjE2MjoxNjMuNjc0NTU5OTk5OTk5OTktLT4='; + + const stylesheet = document.createElement('style'); + stylesheet.className = 'shovelcss-style'; + // end of for higher precedence than other sheets + document.body.appendChild(stylesheet); + + const applyCSS = () => { + let css = ''; + + // We assume all values are sanitized when they are set, so then we can just use them as-is here. + + if (monitorText) { + css += `${monitorRoot}, ${monitorListFooter}, ${monitorListHeader}, ${monitorRowIndex} { color: ${monitorText}; }`; + } + if (monitorBackgroundColor) { + css += `${monitorRoot}, ${monitorRowsInner} { background: ${monitorBackgroundColor}; }`; + } + if (monitorBorder) { + css += `${monitorRoot} { border-color: ${monitorBorder}; }`; + } + if (monitorBackgroundRoundness >= 0) { + css += `${monitorRoot} { border-radius: ${monitorBackgroundRoundness}px; }`; + } + if (monitorBackgroundBorderWidth >= 0) { + css += `${monitorRoot} { border-width: ${monitorBackgroundBorderWidth}px; }`; + } + if (variableValueBackground) { + css += `${monitorValue}, ${monitorValueLarge} { background: ${variableValueBackground} !important; }`; + } + if (variableValueTextColor) { + css += `${monitorValue}, ${monitorValueLarge} { color: ${variableValueTextColor}; }`; + } + if (variableValueRoundness >= 0) { + css += `${monitorValue} { border-radius: ${variableValueRoundness}px; }`; + } + if (listHeaderBackground) { + css += `${monitorListHeader} { background: ${listHeaderBackground}; }`; + } + if (listFooterBackground) { + css += `${monitorListFooter} { background: ${listHeaderBackground}; }`; + } + if (listValueBackground) { + css += `${monitorRowValueOuter} { background: ${listValueBackground} !important; }`; + } + if (listValueText) { + css += `${monitorRowValueOuter} { color: ${listValueText}; }`; + } + if (listValueRoundness >= 0) { + css += `${monitorRowValueOuter} { border-radius: ${listValueRoundness}px; }`; + } + if (allowScrolling) { + css += `${monitorRowsScroller} { overflow: ${allowScrolling} !important; }`; + } + if (askBackground) { + css += `${askBoxBG} { background: ${askBackground} !important; border: none !important; }`; + } + if (askBackgroundRoundness >= 0) { + css += `${askBoxBG} { border-radius: ${askBackgroundRoundness}px !important; }`; + } + if (askBackgroundBorderWidth >= 0) { + css += `${askBoxBG} { border-width: ${askBackgroundBorderWidth}px !important; }`; + } + if (askButtonBackground) { + css += `${askBoxButton} { background-color: ${askButtonBackground}; }`; + } + if (askButtonRoundness >= 0) { + css += `${askBoxButton} { border-radius: ${askButtonRoundness}px !important; }`; + } + if (askInputBackground) { + css += `${askBoxInner} { background: ${askInputBackground} !important; }`; + css += `${askBoxInner} { border: none !important; }`; + } + if (askInputText) { + css += `${askBoxInner} { color: ${askInputText} !important; }`; + } + if (askInputRoundness >= 0) { + css += `${askBoxInner} { border-radius: ${askInputRoundness}px !important; }`; + } + if (askInputBorderWidth >= 0) { + css += `${askBoxInner} { border-width: ${askInputBorderWidth}px !important; }`; + } + if (askButtonImage) { + css += `${askBoxButton} { background-image: url("${encodeURI(askButtonImage)}") !important; background-repeat: no-repeat; background-size: contain; }`; + css += `${askBoxIcon} { visibility: hidden; }`; + } + if (askInputBorder) { + css += `${askBoxBorderMain}, ${askBoxBorderOuter} { border-color: ${askInputBorder} !important; }`; + css += `${askBoxBorderOuter} { box-shadow: none !important; }`; + } + + stylesheet.textContent = css; + }; + + const getMonitorRoot = (id) => { + const allMonitors = document.querySelectorAll(monitorRoot); + for (const monitor of allMonitors) { + if (monitor.dataset.id === id) { + return monitor; + } + } + return null; + }; + + /** + * @param {string} id + * @param {number} x + * @param {number} y + */ + const setMonitorPosition = (id, x, y) => { + const root = getMonitorRoot(id); + if (root) { + root.style.transform = `translate(${x}px, ${y}px)`; + root.style.left = '0px'; + root.style.top = '0px'; + } + }; + + /** + * @param {VM.Target} target + * @param {string} name + * @param {VM.VariableType} type + * @param {number} x + * @param {number} y + */ + const setVariableMonitorPosition = (target, name, type, x, y) => { + // @ts-expect-error + const variable = target.lookupVariableByNameAndType(name, type); + if (variable) { + // @ts-expect-error + setMonitorPosition(variable.id, x, y); + } + }; + + const parseColor = (color, callback) => { + color = Scratch.Cast.toString(color); + + // These might have some exponential backtracking/ReDoS, but that's not really a concern here. + // If a project wanted to get stuck in an infinite loop, there are so many other ways to do that. + + // Simple color code or name + if (/^#?[a-z0-9]+$/.test(color)) { + callback(color); + return; + } + + // Simple linear gradient + if (/^linear-gradient\(\d+deg,#?[a-z0-9]+,#?[a-z0-9]+\)$/.test(color)) { + callback(color); + return; + } + + // URL + // see list of non-escaped characters: + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI#description + const match = color.match(/^url\("([A-Za-z0-9\-_.!~*'();/?:@&=+$,#]+)"\)$/); + if (match) { + const url = match[1]; + return Scratch.canFetch(url).then(allowed => { + if (allowed) { + callback(color); + } + }); + } + + console.error('Invalid color', color); + }; + + class MonitorStyles { + getInfo() { + return { + id: 'shovelcss', + name: 'Custom Styles', + menuIconURI: extensionIcon, + color1: '#0072d6', + color2: '#0064bc', + color3: '#01539b', + blocks: [ + { + blockIconURI: ColorIcon, + opcode: 'changecss', + blockType: Scratch.BlockType.COMMAND, + text: 'set [COLORABLE] to [COLOR]', + arguments: { + COLORABLE: { + type: Scratch.ArgumentType.STRING, + menu: 'COLORABLE_MENU' + }, + COLOR: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#ff0000' + } + } + }, + { + blockIconURI: GradientIcon, + opcode: 'gradientAngle', + blockType: Scratch.BlockType.REPORTER, + text: 'make a gradient with [COLOR1] and [COLOR2] at angle [ANGLE]', + arguments: { + COLOR1: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#ff0000' + }, + COLOR2: { + type: Scratch.ArgumentType.COLOR, + defaultValue: '#6ed02d' + }, + ANGLE: { + type: Scratch.ArgumentType.ANGLE, + defaultValue: '90' + } + } + }, + { + blockIconURI: TransparentIcon, + disableMonitor: true, + opcode: 'transparentinput', + blockType: Scratch.BlockType.REPORTER, + text: 'transparent', + }, + { + blockIconURI: PictureIcon, + disableMonitor: true, + opcode: 'pictureinput', + blockType: Scratch.BlockType.REPORTER, + text: 'image [URL]', + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'https://extensions.turbowarp.org/dango.png' + } + } + }, + '---', + { + blockIconURI: PictureIcon, + disableMonitor: true, + opcode: 'setAskURI', + blockType: Scratch.BlockType.COMMAND, + text: 'set ask prompt button image to [URL]', + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'https://extensions.turbowarp.org/dango.png' + } + } + }, + '---', + { + blockIconURI: BorderIcon, + opcode: 'setbordersize', + blockType: Scratch.BlockType.COMMAND, + text: 'set border width of [BORDER] to [SIZE]', + arguments: { + BORDER: { + type: Scratch.ArgumentType.STRING, + menu: 'BORDER_WIDTH_MENU' + }, + SIZE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '2' + } + } + }, + { + blockIconURI: BorderIcon, + opcode: 'setborderradius', + blockType: Scratch.BlockType.COMMAND, + text: 'set roundness of [CORNER] to [SIZE]', + arguments: { + SIZE: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '4' + }, + CORNER: { + type: Scratch.ArgumentType.STRING, + menu: 'BORDER_ROUNDNESS_MENU' + } + } + }, + '---', + { + blockIconURI: ResetIcon, + opcode: 'clearCSS', + blockType: Scratch.BlockType.COMMAND, + text: 'reset styles' + }, + '---', + { + blockIconURI: miscIcon, + opcode: 'allowscrollrule', + blockType: Scratch.BlockType.COMMAND, + text: 'set list scrolling to [SCROLLRULE]', + arguments: { + SCROLLRULE: { + type: Scratch.ArgumentType.STRING, + menu: 'SCROLL_MENU' + } + } + }, + { + blockIconURI: miscIcon, + opcode: 'getValue', + blockType: Scratch.BlockType.REPORTER, + text: 'get [ITEM]', + arguments: { + ITEM: { + type: Scratch.ArgumentType.STRING, + menu: 'VALUEGET_LIST' + } + } + }, + '---', + { + blockIconURI: miscIcon, + opcode: 'setvarpos', + blockType: Scratch.BlockType.COMMAND, + text: 'set position of variable [NAME] to x: [X] y: [Y]', + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0' + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my variable' + } + } + }, + { + blockIconURI: miscIcon, + opcode: 'setlistpos', + blockType: Scratch.BlockType.COMMAND, + text: 'set position of list [NAME] to x: [X] y: [Y]', + arguments: { + X: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0' + }, + Y: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: '0' + }, + NAME: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'my variable' + } + } + }, + ], + // Accepting reporters because there can't be errors in case the value is not correct + menus: { + COLORABLE_MENU: { + acceptReporters: true, + items: [ + 'monitor text', + 'monitor background', + 'monitor border', + 'variable value background', + 'variable value text', + 'list header background', + 'list footer background', + 'list value background', + 'list value text', + 'ask prompt background', + 'ask prompt button background', + 'ask prompt input background', + 'ask prompt input text', + 'ask prompt input border' + ] + }, + BORDER_WIDTH_MENU: { + acceptReporters: true, + items: [ + 'monitor background', + 'ask prompt background', + 'ask prompt input' + ] + }, + BORDER_ROUNDNESS_MENU: { + acceptReporters: true, + items: [ + 'monitor background', + 'variable value', + 'list value', + 'ask prompt background', + 'ask prompt button', + 'ask prompt input' + ] + }, + SCROLL_MENU: { + acceptReporters: true, + items: [ + 'enabled', + 'disabled' + ] + }, + VALUEGET_LIST: { + acceptReporters: true, + items: [ + 'monitor text', + 'monitor background', + 'monitor border color', + 'variable value background', + 'variable value text', + 'list header background', + 'list footer background', + 'list value background', + 'list value text', + 'ask prompt background', + 'ask prompt button background', + 'ask prompt input background', + 'ask prompt input text', + 'ask prompt input border', + 'monitor background border width', + 'ask prompt background border width', + 'ask prompt input border width', + 'monitor background roundness', + 'variable value roundness', + 'list value roundness', + 'ask prompt background roundness', + 'ask prompt button roundness', + 'ask prompt input roundness', + 'ask prompt button image', + 'list scroll rule' + ] + } + } + }; + } + + changecss(args) { + return parseColor(args.COLOR, color => { + if (args.COLORABLE === 'monitor text') { + monitorText = color; + } else if (args.COLORABLE === 'monitor background') { + monitorBackgroundColor = color; + } else if (args.COLORABLE === 'monitor border') { + monitorBorder = color; + } else if (args.COLORABLE === 'variable value background') { + variableValueBackground = color; + } else if (args.COLORABLE === 'variable value text') { + variableValueTextColor = color; + } else if (args.COLORABLE === 'list header background') { + listHeaderBackground = color; + } else if (args.COLORABLE === 'list footer background') { + listFooterBackground = color; + } else if (args.COLORABLE === 'list value background') { + listValueBackground = color; + } else if (args.COLORABLE === 'list value text') { + listValueText = color; + } else if (args.COLORABLE === 'ask prompt background') { + askBackground = color; + } else if (args.COLORABLE === 'ask prompt button background') { + askButtonBackground = color; + } else if (args.COLORABLE === 'ask prompt input background') { + askInputBackground = color; + } else if (args.COLORABLE === 'ask prompt input text') { + askInputText = color; + } else if (args.COLORABLE === 'ask prompt input border') { + askInputBorder = color; + } + + applyCSS(); + }); + } + + gradientAngle(args) { + return 'linear-gradient(' + args.ANGLE + 'deg,' + args.COLOR1 + ',' + args.COLOR2 + ')'; + } + + setbordersize(args) { + const size = Scratch.Cast.toNumber(args.SIZE); + if (args.BORDER === 'monitor background') { + monitorBackgroundBorderWidth = size; + } else if (args.BORDER === 'ask prompt background') { + askBackgroundBorderWidth = size; + } else if (args.BORDER === 'ask prompt input') { + askInputBorderWidth = size; + } + applyCSS(); + } + + setborderradius(args) { + const size = Scratch.Cast.toNumber(args.SIZE); + if (args.CORNER === 'monitor background') { + monitorBackgroundRoundness = size; + } else if (args.CORNER === 'variable value') { + variableValueRoundness = size; + } else if (args.CORNER === 'list value') { + listValueRoundness = size; + } else if (args.CORNER === 'ask prompt background') { + askBackgroundRoundness = size; + } else if (args.CORNER === 'ask prompt button') { + askButtonRoundness = size; + } else if (args.CORNER === 'ask prompt input') { + askInputRoundness = size; + } + applyCSS(); + } + + allowscrollrule(args) { + if (args.SCROLLRULE === 'enabled'){ + allowScrolling = 'auto'; + } else { + allowScrolling = 'hidden'; + } + applyCSS(); + } + + setvarpos(args, util) { + setVariableMonitorPosition( + util.target, + args.NAME, + '', + Scratch.Cast.toNumber(args.X) + Scratch.vm.runtime.stageWidth / 2, + Scratch.vm.runtime.stageHeight / 2 - Scratch.Cast.toNumber(args.Y) + ); + } + + setlistpos(args, util) { + setVariableMonitorPosition( + util.target, + args.NAME, + 'list', + Scratch.Cast.toNumber(args.X) + Scratch.vm.runtime.stageWidth / 2, + Scratch.vm.runtime.stageHeight / 2 - Scratch.Cast.toNumber(args.Y) + ); + } + + help() { + alert("\nThis is a short introduction to how to use the Monitor Styles extension!\n\n𝗟𝗼𝗼𝗸𝘀 𝗯𝗹𝗼𝗰𝗸𝘀\nThese blocks change the appearance of the variable and list didsplays. You can use the drop-down menu to select what component you want to modify. 𝙏𝙝𝙚 𝙘𝙤𝙡𝙤𝙧 𝙗𝙡𝙤𝙘𝙠 modifieas the color of a component. You can use the 𝙜𝙧𝙖𝙙𝙞𝙚𝙣𝙩 block inside the color input, to create gradients or the 𝙄𝙢𝙖𝙜𝙚 block to use a image instead of solid colors. 𝙏𝙝𝙚𝙨𝙚 𝙩𝙬𝙤 𝙤𝙣𝙡𝙮 𝙬𝙤𝙧𝙠 𝙤𝙣 𝙘𝙚𝙧𝙩𝙖𝙞𝙣 𝙘𝙤𝙢𝙥𝙤𝙣𝙚𝙣𝙩𝙨! You can also use the 𝙩𝙧𝙖𝙣𝙨𝙥𝙖𝙧𝙚𝙣𝙩 𝙗𝙡𝙤𝙘𝙠 as a color input, to make components invisible. The 𝙗𝙤𝙧𝙙𝙚𝙧 𝙗𝙡𝙤𝙘𝙠𝙨 modify the borders of components.\n\n𝗦𝗲𝗻𝘀𝗶𝗻𝗴 𝗯𝗹𝗼𝗰𝗸𝘀\nThese blocks can change the behaviour of certain components. The 𝙨𝙘𝙧𝙤𝙡𝙡 𝙧𝙪𝙡𝙚 block change the behaviour for lists. On 'auto' they will show the scroll bar, and allow you to school, but on 'hidden', they won't let you do that, and the scroll bar will be hidden.\n\n𝗠𝗼𝘁𝗶𝗼𝗻 𝗯𝗹𝗼𝗰𝗸𝘀\nThese blocks allow you to move variable and list displays around. You need to use their 𝙡𝙖𝙗𝙚𝙡 𝙣𝙖𝙢𝙚. The label name is the text that displays on the monitor. For example, a 'for this sprite only' variable will be like 'Sprite1: my variable'."); + } + + transparentinput() { + return 'transparent'; + } + + pictureinput(args) { + return `url("${encodeURI(args.URL)}")`; + } + + clearCSS() { + monitorText = ''; + monitorBorder = ''; + monitorBackgroundColor = ''; + variableValueBackground = ''; + variableValueTextColor = ''; + listFooterBackground = ''; + listHeaderBackground = ''; + listValueText = ''; + listValueBackground = ''; + variableValueRoundness = -1; + listValueRoundness = -1; + monitorBackgroundRoundness = -1; + monitorBackgroundBorderWidth = -1; + allowScrolling = ''; + askBackground = ''; + askBackgroundRoundness = -1; + askBackgroundBorderWidth = -1; + askButtonBackground = ''; + askButtonRoundness = -1; + askInputBackground = ''; + askInputRoundness = -1; + askInputBorderWidth = -1; + askBoxIcon = ''; + askInputText = ''; + askButtonImage = ''; + askInputBorder = ''; + applyCSS(); + } + + getValue(args) { + if (args.ITEM === 'monitor text') { + return monitorText; + } else if (args.ITEM === 'monitor background') { + return monitorBackgroundColor; + } else if (args.ITEM === 'monitor border color') { + return monitorBorder; + } else if (args.ITEM === 'variable value background') { + return variableValueBackground; + } else if (args.ITEM === 'variable value text') { + return variableValueTextColor; + } else if (args.ITEM === 'list header background') { + return listHeaderBackground; + } else if (args.ITEM === 'list footer background') { + return listFooterBackground; + } else if (args.ITEM === 'list value background') { + return listValueBackground; + } else if (args.ITEM === 'list value text') { + return listValueText; + } else if (args.ITEM === 'ask prompt background') { + return askBackground; + } else if (args.ITEM === 'ask prompt button background') { + return askButtonBackground; + } else if (args.ITEM === 'ask prompt input background') { + return askInputBackground; + } else if (args.ITEM === 'ask prompt input text') { + return askInputText; + } else if (args.ITEM === 'ask prompt input border') { + return askInputBorder; + } else if (args.ITEM === 'monitor background border width') { + return monitorBackgroundBorderWidth; + } else if (args.ITEM === 'ask prompt background border width') { + return askBackgroundBorderWidth; + } else if (args.ITEM === 'ask prompt input border width') { + return askInputBorderWidth; + } else if (args.ITEM === 'monitor background roundness') { + return monitorBackgroundRoundness; + } else if (args.ITEM === 'variable value roundness') { + return variableValueRoundness; + } else if (args.ITEM === 'list value roundness') { + return listValueRoundness; + } else if (args.ITEM === 'ask prompt background roundness') { + return askBackgroundRoundness; + } else if (args.ITEM === 'ask prompt button roundness') { + return askButtonRoundness; + } else if (args.ITEM === 'ask prompt input roundness') { + return askInputRoundness; + } else if (args.ITEM === 'ask prompt button image') { + return askButtonImage; + } else if (args.ITEM === 'list scrolling') { + if (allowScrolling === 'auto') { + return 'enabled'; + } else { + return 'disabled'; + } + } + return ''; + } + + setAskURI(args) { + return Scratch.canFetch(args.URL).then(allowed => { + if (allowed) { + askButtonImage = args.URL; + applyCSS(); + } + }); + } + } + + Scratch.extensions.register(new MonitorStyles()); +})(Scratch); diff --git a/extensions/godslayerakp/http.js b/extensions/godslayerakp/http.js index 46321fa848..7e14d4a03f 100644 --- a/extensions/godslayerakp/http.js +++ b/extensions/godslayerakp/http.js @@ -2,7 +2,6 @@ 'use strict'; if (!Scratch.extensions.unsandboxed) throw new Error('can not load out side unsandboxed mode'); - const pathRegex = /[^.]+/g; const setType = (value, type) => { switch (type) { case 'string': @@ -81,7 +80,7 @@ } }; const getPathArray = path => { - const names = path.match(pathRegex); + const names = path.split('.'); for (let index = 0; index < names.length; index++) { let name = names[index]; name = name.replaceAll(/(? { for (const name of path) { - object = object[name]; - if (typeof object !== 'object') return; + object = object?.[name]; } - return object; + return setType(object, 'string'); }; const setValueAtPath = (object, path, value) => { - for (const name of path) { + for (const name of path.slice(0, -1)) { object = object[name]; - if (typeof object !== 'object') return; } - return object = value; + object[path.at(-1)] = value; }; const {vm} = Scratch; @@ -153,6 +150,10 @@ return this.options.headers['Content-Type']; }, set mimeType(value) { + if (this.options.headers['Content-Type'] === 'multipart/form-data' && + value !== 'multipart/form-data') { + this.options.body = ''; + } this.options.headers['Content-Type'] = value; }, set method(val) { @@ -173,6 +174,14 @@ }, set body(val) { if (this.method === 'GET') return; + if (val instanceof FormData && !(this.options.body instanceof FormData)) { + this.options.body = val; + this.options.headers['Content-Type'] = 'multipart/form-data'; + } + if (!(val instanceof FormData) && this.options.body instanceof FormData) { + this.options.body = ''; + this.options.headers['Content-Type'] = 'text/plain'; + } this.options.body = val; }, get body() { @@ -349,11 +358,56 @@ blockType: BlockType.COMMAND, arguments: { text: { - type: ArgumentType.STRING + type: ArgumentType.STRING, + default: 'Apple!' } }, text: 'set request body to [text]' }, + '---', + { + opcode: 'setBodyToForm', + blockType: BlockType.COMMAND, + text: 'set request body to a form' + }, + { + opcode: 'getFormProperty', + blockType: BlockType.REPORTER, + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'name' + } + }, + text: 'get [name] in request form' + }, + { + opcode: 'setFormProperty', + blockType: BlockType.COMMAND, + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'name' + }, + value: { + type: ArgumentType.STRING, + defaultValue: 'value' + } + }, + text: 'set [name] to [value] in request form' + }, + { + opcode: 'deleteFormProperty', + blockType: BlockType.COMMAND, + arguments: { + name: { + type: ArgumentType.STRING, + defaultValue: 'name' + } + }, + text: 'delete [name] from request form' + }, + '---', { opcode: 'sendRequest', blockType: BlockType.COMMAND, @@ -587,6 +641,29 @@ this.request.body = body; } + setBodyToForm() { + this.request.body = new FormData(); + } + + getFormProperty(args) { + if (!(this.request.options.body instanceof FormData)) return; + const name = Cast.toString(args.name); + return this.request.body.get(name); + } + + setFormProperty(args) { + if (!(this.request.options.body instanceof FormData)) return; + const name = Cast.toString(args.name); + const value = Cast.toString(args.value); + this.request.body.set(name, value); + } + + deleteFormProperty(args) { + if (!(this.request.options.body instanceof FormData)) return; + const name = Cast.toString(args.name); + this.request.body.delete(name); + } + // eslint-disable-next-line require-await async sendRequest(args) { const url = Cast.toString(args.url); @@ -609,6 +686,15 @@ this.request.events.activate('reqFail'); } this.request.end = true; + if (res.headers.get('Content-Type') === 'multipart/form-data') { + const form = await res.formData(); + const json = {}; + for (const [key, value] of form.entries()) { + json[key] = value; + } + this.response.text = JSON.stringify(json); + return; + } const body = await res.text(); this.response.text = body; } catch (err) { @@ -624,16 +710,18 @@ showExtra() { this.showingExtra = true; + // @ts-ignore vm.extensionManager.refreshBlocks(); } hideExtra() { this.showingExtra = false; + // @ts-ignore vm.extensionManager.refreshBlocks(); } setUnkownProperty(args) { - const name = Cast.toString(args.name); + const name = Cast.toString(args.path); const text = Cast.toString(args.value); const path = getPathArray(name); @@ -642,7 +730,7 @@ } setUnkownPropertyType(args) { - const name = Cast.toString(args.name); + const name = Cast.toString(args.path); const type = Cast.toString(args.type); const path = getPathArray(name); @@ -652,14 +740,14 @@ } getUnkownProperty(args) { - const name = Cast.toString(args.name); + const name = Cast.toString(args.path); const path = getPathArray(name); return getValueAtPath(this.request.options, path); } getUnkownPropertyType(args) { - const name = Cast.toString(args.name); + const name = Cast.toString(args.path); const path = getPathArray(name); const value = getValueAtPath(this.request.options, path); @@ -668,5 +756,6 @@ } const instance = new WebRequests(); + // @ts-ignore Scratch.extensions.register(instance); })(Scratch); diff --git a/images/Lily/Skins.svg b/images/Lily/Skins.svg new file mode 100644 index 0000000000..d072deefe8 --- /dev/null +++ b/images/Lily/Skins.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/README.md b/images/README.md index 7495122b82..0600c79be7 100644 --- a/images/README.md +++ b/images/README.md @@ -247,3 +247,8 @@ All images in this folder are licensed under the [GNU General Public License ver ## veggiecan/LongmanDictionary.svg - Created by [veggiecan](https://github.com/veggiecan0419) - The ship is based on [this](https://www.ldoceonline.com/external/images/logo_home_smartphone.svg?version=1.2.61) logo from the [ldoceonline](https://www.ldoceonline.com/) website + +## Lily/Skins.svg + - Created by [@LilyMakesThings](https://github.com/LilyMakesThings). + - Dango based on dango from [Twemoji](https://twemoji.twitter.com/) under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). + - Background "blobs" by Scratch. diff --git a/images/TheShovel/CustomStyles.svg b/images/TheShovel/CustomStyles.svg new file mode 100644 index 0000000000..63a16f6c00 --- /dev/null +++ b/images/TheShovel/CustomStyles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a64b2f43cc..bec80ceaa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,15 +20,15 @@ } }, "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -60,9 +60,9 @@ } }, "@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", "dev": true }, "@humanwhocodes/config-array": { @@ -412,27 +412,27 @@ "dev": true }, "eslint": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", - "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -483,9 +483,9 @@ } }, "eslint-scope": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz", - "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -493,9 +493,9 @@ } }, "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", "dev": true }, "espree": { diff --git a/package.json b/package.json index 470d6ee969..529b6c7d34 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "image-size": "^1.0.2" }, "devDependencies": { - "eslint": "^8.45.0" + "eslint": "^8.46.0" }, "private": true } diff --git a/website/index.ejs b/website/index.ejs index 0003d77561..40f9a8d423 100644 --- a/website/index.ejs +++ b/website/index.ejs @@ -488,6 +488,12 @@

Advanced rendering capabilities. Created by ObviousAlexC.

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

Skins

+

Have your sprites render as other images or costumes. Created by LilyMakesThings.

+
+
<%- banner('obviousAlexC/SensingPlus') %>

Sensing Plus

@@ -524,6 +530,12 @@

Access information about the battery of phones or laptops. May not work on all devices and browsers.

+
+ <%- banner('TheShovel/CustomStyles') %> +

Custom Styles

+

Customize the appearance of variable monitors and prompts in your project. Created by TheShovel.

+
+
<%- banner('mdwalters/notifications') %>

Notifications

@@ -542,6 +554,12 @@

Encode and decode strings into their unicode numbers, base 64, or URLs. Created by -SIPC-.

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

Temporary Variables

+

Create disposable runtime or thread variables. Created by LilyMakesThings.

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

More Timers

@@ -584,6 +602,12 @@

A few adapter blocks. Created by TrueFantom.

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

All Menus

+

Special category with every menu from every Scratch category and extensions. Created by LilyMakesThings

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

Cast

@@ -644,12 +668,6 @@

Only render or stamp certain RGB channels.

-
- <%- banner('Lily/TempVariables2') %> -

Temporary Variables

-

Create disposable runtime or thread variables. Created by LilyMakesThings.

-
-
<%- banner('CST1229/zip') %>

Zip