├── .gitignore ├── PrepareCodeForAsarInjectionPlugin.js ├── README.md ├── main ├── manual-copy-pasting.js └── startup-with-discord.js ├── package-lock.json ├── package.json ├── script ├── actions │ ├── backgroundChanger.ts │ ├── blurTweaker.ts │ ├── brightnessTweaker.ts │ ├── index.ts │ └── toggleTheme.ts ├── configs │ ├── classNames.ts │ ├── durations.ts │ ├── identifiers.ts │ └── texts.ts ├── initActionsPanel.ts ├── script.ts ├── stores │ ├── importantElementsStore.ts │ ├── index.ts │ ├── observersStore.ts │ └── preferencesStore.ts ├── styles │ ├── actionsPanelCSSCode.ts │ ├── alertsCSSCode.ts │ ├── createMainCSSCode.ts │ ├── index.ts │ └── rawCSSCode.ts └── utils │ ├── ClassChangeObserver.ts │ ├── ContextMenuObserver.ts │ ├── changeBackgroundImage.ts │ ├── contextMenuHandler.ts │ ├── createAlert.ts │ ├── createElement.ts │ ├── findPossibleMenuBar.ts │ ├── getElementWithAlert.ts │ ├── getSidebarThemeState.ts │ ├── initImportantElementsStore.ts │ ├── initLocalStorage.ts │ ├── initObservers.ts │ ├── initPreferencesStore.ts │ └── removeExistingCodeFootprint.ts ├── tsconfig.json ├── types ├── FilterPropertiesNotOfType.ts ├── IAction.ts ├── IImportantElementsStore.ts ├── IMutationObserver.ts ├── IPreferencesStore.ts ├── Modify.ts ├── NullableHTMLElement.ts ├── RemoveSignatures.ts └── index.ts └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .eslintrc.json 4 | .prettierrc 5 | .prettierignore -------------------------------------------------------------------------------- /PrepareCodeForAsarInjectionPlugin.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require("fs"); 2 | 3 | /** 4 | * This plugin automatically creates a "startup-with-discord" file ready for production, that contains 5 | * the code to inject in Discord. 6 | */ 7 | 8 | module.exports = class PrepareCodeForAsarInjectionPlugin { 9 | apply(compiler) { 10 | compiler.hooks.done.tap("PrepareCodeForAsarInjectionPlugin", () => { 11 | const { errorGettingScript, minifiedScriptCodeInBase64 } = getMinifiedScriptCodeInBase64(); 12 | if (errorGettingScript !== null) { 13 | console.error("ERROR - Error reading the file that contains the minified script source code"); 14 | console.error(errorGettingScript); 15 | return; 16 | } 17 | 18 | const errorWritingScript = writeScriptCodeToFinalFile(minifiedScriptCodeInBase64); 19 | if (errorWritingScript !== null) { 20 | console.error("ERROR - Error writing the file to be injected in Discord's source code"); 21 | console.error(errorWritingScript); 22 | } 23 | 24 | console.log("SUCCESS - The final file for injection in Discord's source code has been successfully written"); 25 | 26 | require("child_process") 27 | .spawn("clip") 28 | .stdin.end(require("fs").readFileSync("./main/manual-copy-pasting.js").toString("base64")); 29 | }); 30 | } 31 | }; 32 | 33 | /** 34 | * @returns an object containing an error key which is null if there isn't any, and the minified script itself 35 | */ 36 | 37 | function getMinifiedScriptCodeInBase64() { 38 | try { 39 | return { 40 | errorGettingScript: null, 41 | minifiedScriptCodeInBase64: readFileSync("./main/manual-copy-pasting.js", { encoding: "base64" }), 42 | }; 43 | } catch (error) { 44 | return { errorGettingScript: error, minifiedScriptCodeInBase64: null }; 45 | } 46 | } 47 | 48 | /** 49 | * 50 | * @param {string} minifiedScriptCodeInBase64 51 | * @returns an error if writing the script wasn't successful, and null otherwise 52 | */ 53 | function writeScriptCodeToFinalFile(minifiedScriptCodeInBase64) { 54 | try { 55 | writeFileSync( 56 | "./main/startup-with-discord.js", 57 | `mainWindow.webContents.on("did-finish-load", () => { 58 | mainWindow.webContents.executeJavaScript( 59 | Buffer.from( 60 | "${minifiedScriptCodeInBase64}", 61 | "base64" 62 | ).toString("utf8") 63 | ); 64 | }); 65 | `, 66 | { encoding: "utf8" } 67 | ); 68 | return null; 69 | } catch (error) { 70 | return error; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # discord-transparency 2 | 3 | This is a script that allows you to add a background image to Discord. 4 | 5 | ![ss](https://i.imgur.com/ITLqVm6.png) 6 | 7 | # ⚠ Disclaimer 8 | 9 | - This is against Discord's Terms Of Service. Specifically, any client-side modification is forbidden. 10 | However, Discord will not actively look out for such users, unless they are reported. But this script has measures against that; 11 | - With future updates to Discord, some elements might not appear properly anymore. 12 | I will try to mitigate these errors as soon as possible, but they will happen at some point. 13 | 14 | # How to install it 15 | 16 | - [Download the installer (look below "Assets")](https://github.com/MWR1/DTInstaller/releases); 17 | - Enjoy!
18 | ❗ If you're on Windows, make sure not to move the .exe file outside the folder it's in, otherwise it won't work anymore (this behavior is going to be fixed in the future).
19 | ❗ If you're on Linux, you have to run the installer through a terminal (also going to be fixed in the future). 20 | 21 | # Features 22 | 23 | All features of this script can be found by pressing the `CTRL + SHIFT + X` combination. That will bring up a panel with each action you can perform.
24 | 25 | If you want to be kept up to date with this project and its features, report some bugs you come across, contact me and whatnot, you can join my [Discord Server](https://discord.gg/Bd2JnFB). 26 | 27 | #### Here are some cool things that have been added: 28 | 29 | - You can change the background image by either right clicking on an image, or using the appropriate action in the actions panel; (I mean this is the whole point of the script) 30 | 31 | ⚠ If you choose to add a background image via a link: you have to post the image on Discord first then use the link provided by them. Press on "Open original", and copy the link. Paste that in the text box from the corresponding action, then press Enter. 32 | 33 | - You can change the brightness of the image, according to your liking. By pressing on the "Save brightness level" button, next time you open Discord, the brightness level will stay the same. Just, make it so the brightness is good enough that you can see the text; 34 | - You can change how blurry the image is, so you can get that awesome frosty glass effect. As much as I personally dislike white theme, this looks cool with it. Beware, this _might_ have some performance impacts; 35 | - You can deactivate the theme by pressing `CTRL + D`. This is especially useful when you want to take a screenshot of a message, or of the entire desktop but you're afraid that someone might report you for using this script. It will disable the theme, making Discord look untouched; 36 | - Blocked messages don't appear anymore. Your natural curiosity is now safe from any temptations. 37 | 38 | ## Prerequisites 39 | 40 | - If you want to implement the script in such a way that it launches at the same time with Discord, you have to install [NodeJS](https://nodejs.org). 41 | 42 | # Other ways of installing the script 43 | 44 | ## 1. Copy pasting the script inside the developer console 45 | 46 | - Enable Discord's developer tools: 47 | 48 | - Go where Discord is installed. On Windows, you would go to `%appdata%`; 49 | - look for the folder that matches the name of your Discord client, like (`discord`, `discordcanary`, etc.); 50 | - Search for a file called `settings.json`; 51 | - Copy and paste this line right below the opening bracket "{": `"DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING": true,` 52 | 53 | - Press `CTRL + SHIFT + I` inside Discord. A tab should open to the side; 54 | - Navigate to the `Console` tab, near the top right corner; 55 | - Type 'allow pasting' inside the console; 56 | - Copy the script from above, from the files section, in this page, in the folder `main`, from inside the file `manual-copy-pasting.js`; 57 | - Go to Discord, then inside the Console tab paste the script you've just copied;
58 | ❗ While this is the easier way to set it up, each time Discord launches, you will have to follow most of the same steps again, because the script will be deleted with each relaunch of the Discord client. But you can do this: 59 | - While in the Console tab, you can just press the `ARROW UP KEY`, then `ENTER`, and the script will appear, without having to copy and paste it again from this page, until a new update comes out. 60 | 61 | ## 2. Launching the script with Discord 62 | 63 | ❗ This is a little bit harder than the copy and pasting technique, but you won't have to open the developer console again and again, each time Discord launches. [This entire procedure is automated with the installer. To download it, look below "Assets"](https://github.com/MWR1/DTInstaller/releases). 64 | 65 | ### Windows 66 | 67 | - Search for `%localappdata%` in a folder search box; 68 | - Look for a folder that has "discord" in its name and open it (the name may differ, based on the build you've downloaded); 69 | - You may have more folders that have this name structure: `app-x.y.z`. Open the one that is **the most recent** by "Date modified"; 70 | - Open the folder called `modules`; 71 | - Locate the folder that has a name like `discord_desktop_core-xyz` and open it, then open the folder inside of it, whose name should just be `discord_desktop_core`; 72 | - In there, press `SHIFT` and `RIGHT CLICK` at the same time, then press on Open PowerShell window here. 73 | - Now, after the window opened, in there paste this line: `npx asar extract core.asar ./unpacked`. After a bit, a folder called `unpacked` should appear; 74 | - Open the folder `unpacked`, then open the folder `app`, and locate `mainScreen.js`; 75 | - Open `mainScreen.js` with Notepad, or any other text editor. We'll go with Notepad; 76 | - Paste the code at the end of the file. Said code is located in the `main` folder inside the `startup-with-discord.js` file from above, in the files section; 77 | - Press Ctrl + S to save the file, then close it. Also close the folder `unpacked`. Afterwards, go back to the terminal (in our case PowerShell) opened recently; 78 | - Inside the terminal, paste this line: `npx asar pack unpacked core.asar`, and then wait. When it's done, you can see a new line appearing in the terminal. You can then close everything, and restart Discord. 79 | 80 | ### Linux 81 | 82 | - Go to `~./config`; 83 | - Look for a folder that has "discord" in its name and open it (the name may differ, based on the build you've downloaded); 84 | - Inside of it, look for another folder that has a string of numbers as its name, like `0.0.20`. This is the Discord version. Open it and the folder called `modules` inside of it; 85 | - Look for yet another folder called `discord_desktop_core` and open it; 86 | - Finally, open a terminal at this location and in it paste `npx asar extract core.asar ./unpacked`. After a bit, a folder called `unpacked` should appear; 87 | - Open the folder `unpacked`, then open the folder `app`, locate `mainScreen.js` and open it with any text editor; 88 | - Paste the code at the end of the file. Said code is located in the `main` folder inside the `startup-with-discord.js` file from above, in the files section; 89 | - Save the `mainScreen.js` file and close it; 90 | - Inside the terminal, paste this line: `npx asar pack unpacked core.asar`, and then wait. When it's done, you can close everything, and restart Discord. 91 | -------------------------------------------------------------------------------- /main/manual-copy-pasting.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";const e="__BACKGROUND-IMAGE__",t="__BRIGHTNESS__",n="__BLUR__",r="TRANSPARENCY",o=`${r}__ACTIONS-PANEL`,a=`${o}-OVERLAY`,l=`${r}__ACTIONS_PANEL`,i=`${r}__ALERTS-STYLESHEET`,s=`${r}__IMAGE-INPUT-PICKER`,c=`${r}__SET-BACKGROUND`,d="theme-dark",u="theme-light",m=`${r}__ALERT`,p=`${m}--INACTIVE`,g=`${o}--SLIDE-IN`,b=`${r}__ACTION`,h=`${b}--ACTIVE`,f=`${b}-WARNING`,_=`${b}-INPUT`,v=`${b}__INPUT-PICKER`,y=`${b}-LEVEL-INDICATOR`,$=`${b}-SAVE-LEVEL`,w=new Map,x=new Map,k=new Map;function T(e){const t=w.get("mainStyleSheet"),n=w.get("titleBarElement");if(!t.hasAttribute("media"))return t.setAttribute("media","1px"),null!==n&&(n.style.backgroundColor="var(--color-tertiary)"),void(e.style.display="none");const r=k.get("isDarkTheme"),o=k.get("brightness");t.removeAttribute("media"),null!==n&&(n.style.backgroundColor=r?`rgba(0,0,0,0.${o}`:`rgba(255,255,255,0.${o}`),e.classList.contains(g)&&(e.style.display="block")}const E="Press here to browse for an image on your computer",N="Set as background image 🌟";function S(e){return`🛑 Oh no, there's been an error. If you see this, join the support server, and report the error. [[${e}]]`}const L=`.${b} {\n min-height: 100px;\n background-color: rgba(0, 0, 0, 0.5);\n margin: 10px;\n color: white;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n padding: 30px 50px;\n transition: background-color 300ms linear;\n}\n\n.${h} {\n background-color: rgba(0, 0, 0, 0.8);\n cursor: initial;\n}\n\n.${_} {\n box-sizing: border-box;\n text-align: center;\n padding: 15px 0;\n width: 100%;\n border: 0;\n background-color: rgba(255, 255, 255, 0.1);\n border-radius: 50px;\n color: white;\n}\n\n.${v} {\n padding: 20px 15px !important;\n cursor: pointer;\n font-family: var(--font-primary);\n font-size: smaller;\n}\n\n.${_}:focus,\n.${$}:focus {\n outline: none;\n border: 2px solid dodgerblue;\n}\n\n.${y} {\n margin: 0;\n opacity: 0.8;\n text-align: center;\n}\n\n.${f} {\n display: none;\n font-size: 11px;\n line-height: 20px;\n color: white;\n max-width: 310px;\n margin: 20px 0 0;\n opacity: 0.8;\n}\n\n.${_}:focus ~ .${f} {\n display: block;\n}\n\n.${$} {\n height: 40px;\n padding: 0 15px;\n max-width: 300px;\n margin-top: 10px;\n border: 0;\n background-color: rgba(255, 255, 255, 0.1);\n border-radius: 50px;\n color: white;\n cursor: pointer;\n}\n\n#${c} {\n position: absolute;\n left: -100%;\n bottom: 0;\n width: max-content;\n padding: 10px;\n margin-left: -15px;\n background-color: inherit;\n border-radius: inherit;\n font-size: 14px;\n cursor: pointer;\n}`,C=`.${m} { \n position: absolute;\n left: 0px;\n right: 0px;\n margin: auto;\n padding: 20px;\n font-size: 14px;\n width: fit-content;\n width: -moz-fit-content;\n max-width: 450px;\n background-color: rgba(0,0,0,0.6);\n backdrop-filter: blur(8px);\n color: white;\n border-bottom-right-radius: 20px;\n border-bottom-left-radius: 20px;\n z-index: 10000;\n box-shadow: 0px 13px 10px -5px rgba(0,0,0,0.5);\n transition: transform 500ms ease;\n animation: TRANSPARENCY__ALERT-SLIDE 500ms ease;\n overflow: hidden;\n}\n\n.${m}::after {\n content: "";\n position: absolute;\n bottom: 0;\n left: 0;\n background-color: hsl(235 100% 82% / 1);\n width: 100%;\n height: 5px;\n animation: TRANSPARENCY__ALERT-TIMER-BAR linear forwards;\n animation-duration: var(--timer-bar-timeout);\n}\n\n@keyframes TRANSPARENCY__ALERT-TIMER-BAR {\n from {\n transform: translateX(-100%);\n } to {\n transform: translateX(0); \n }\n}\n\n@keyframes TRANSPARENCY__ALERT-SLIDE {\n from { transform: translateY(-130%); }\n to { transform: translateY(0) }\n}\n\n.${p} {\n transform: translateY(-130%);\n}`,R=".typeWindows_a934d8.withFrame_a934d8.titleBar_a934d8, .typeWindows_a934d8.withFrame_a934d8.titleBar_a934d8.withBackgroundOverride_a934d8",P=".app_bd26cc",A=".layerContainer_cd0de5",I=".toast_f42767",B=".browser_f542fc",M=".messagesPopoutWrap_ac90a2",D=".recentMentionsPopout_ddb5b4",O=".quickswitcher_f4e139",H=".container_f24a96",K=".popout_f6639d",Y=".main_e3f8c2",V=".streamPreview_adbea6",W=".popout_a9414b",U=".applicationStore_cecc86",z=".container-3wLKDe",F=".themed-Hp1KC_",G=".directoryContainer_da3f59",q=".header_f1fd9c, .container_e44302, .chat_a7d72e, .scrollerContainer_c6b11b",j=".pageWrapper_a3a4ce",X=".searchResultsWrap_c2b47d",J=".themeEditor_c9dda6",Q=".editor_bcfa1e",Z=".container_d1c246",ee=".wrapper_f9f2ca",te=".blockedSystemMessage_c09d0f",ne=".embedFull_b0068a",re=".wrapper_a71a1c",oe=".code.inline, .markup-2BOw-j code, code",ae=".attachment_b52bef",le=".wrapperAudio_f72aac",ie=".wrapper_ef319f",se=".container_e40c16",ce=".reaction_ec6b19",de=".hoverButtonGroup_ab8b23",ue=".userProfileInnerThemedNonPremium_c69a7b",me=".container_ac201b",pe=".contentWrapper_af5dbb",ge=".wrapper_c6ee36",be=".container_f2bfbb, .reactors_f2bfbb",he=".wrapper_e06857",fe=".wrapper_c43059",_e=".premiumPromo_ca9b56",ve=".header_bd05f1",ye=".previewContainer_da1bd9";function $e(){const e=k.get("brightness"),t=k.get("isDarkTheme"),n=t?`rgba(0, 0, 0, 0.${e})`||"rgba(0,0,0,0.9)":`rgba(255, 255, 255, 0.${e})`||"rgba(255,255,255,0.7)",r=t?"rgba(0,0,0,0.78)":"rgba(255,255,255,0.85)",l=t?"rgba(0,0,0,0.92)":"rgba(255,255,255,0.95)";return`.theme-dark, .theme-light {\n --background-primary: transparent !important;\n --background-secondary: transparent !important;\n --background-tertiary: transparent !important;\n --channeltextarea-background: transparent !important;\n --deprecated-panel-background: transparent !important;\n --background-secondary-alt: transparent !important;\n --scrollbar-auto-track: transparent !important;\n --home-background: transparent !important;\n --card-secondary-bg: ${(i={backgroundImageURL:k.get("backgroundImageURL"),blurLevelPixels:`${k.get("blur")}px`,brightnessLevelRGBA:n,variousElementsBackground:r,variousElementsBackgroundMoreOpaque:l}).variousElementsBackground} !important;\n}\n\n.theme-dark {\n --scrollbar-auto-thumb:rgba(255,255,255,0.3) !important;\n --profile-body-background-color: rgba(255, 255, 255, 0.1) !important; \n}\n\n.theme-light {\n --scrollbar-auto-thumb:rgba(0,0,0,0.6) !important;\n --profile-body-background-color: rgba(0, 0, 0, 0.05) !important;\n}\n\n#app-mount {\n background: url(${i.backgroundImageURL}) center center no-repeat;\n background-size: cover;\n}\n\n${R} {\n margin-top: 0;\n padding-top: 4px;\n background: ${i.brightnessLevelRGBA};\n backdrop-filter: blur(${i.blurLevelPixels})\n}\n\n${P}, ${J}, ${Q} {\n background-color: ${i.brightnessLevelRGBA};\n backdrop-filter: blur(${i.blurLevelPixels});\n} \n\n${U}, ${z}, \n${G}, ${q}, ${j},\n${F} {\n background-color: transparent !important;\n}\n\n${me}, ${le}, \n${ie}, ${X}, ${pe},\n${se}, ${ce},\n${ne}, ${ae},\n${be}, ${re}, ${oe},\n${ve}, ${Q}, ${Z}, \n${H}, ${K}, ${W},\n${V} {\n background-color: ${i.variousElementsBackground} !important;\n}\n\n${ue}, ${Y}, ${D},\n${ge}, ${he},\n${O}, ${M}, ${B}, \n${I}, ${pe}, ${fe},\n${de}, ${_e} {\nbackground-color: ${i.variousElementsBackgroundMoreOpaque} !important;\n}\n\n${ye} {\n align-items: center;\n}\n\n${ve} {\n margin-left: -24px;\n padding-left: 24px;\n}\n\n${ee}:has(${te}) {\n display: none;\n}\n\n#${o} {\n font-family: "Whitney", "Helvetica Neue", "Helvetica", monospace, "Arial", sans-serif;\n width: 30%;\n height: 100%;\n background-color: rgba(0,0,0,0.6);\n position: absolute;\n right: 0;\n z-index: 200;\n backdrop-filter: blur(8px);\n transform: translateX(100%);\n transition: transform 500ms ease;\n}\n\n#${a} {\n display: none;\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 199;\n}\n\n.${g} {\n transform: translateX(0) !important;\n}\n`;var i}function we(t){w.get("mainStyleSheet").innerHTML+=function(e){return`#app-mount{background: url(${e}) center center no-repeat !important; background-size: cover !important;}`}(t),k.set("backgroundImageURL",t),window.localStorage.setItem(e,t)}function xe({elementName:e,appendTo:t,htmlProps:n}){const r=document.createElement(e);for(const[e,t]of Object.entries(n))r[e]=t;if(void 0!==n.style)for(const[e,t]of Object.entries(n.style))r.style[e]=t;return t.appendChild(r),r}function ke({text:e,timeout:t,containsHTML:n=!1}){const r=xe({elementName:"div",appendTo:document.body,htmlProps:{className:m,[n?"innerHTML":"textContent"]:e}});return r.style.setProperty("--timer-bar-timeout",`${t}ms`),new Promise((e=>{setTimeout((()=>{r.classList.add(p),setTimeout((()=>{r.remove(),e(!0)}),1e3)}),t)}))}function Te(){const e=k.get("blur");window.localStorage.setItem(n,e.toString()),ke({text:`Blur level updated! Now it is at level ${e}.`,containsHTML:!0,timeout:3e3})}function Ee(){const e=k.get("brightness");window.localStorage.setItem(t,e.toString()),ke({text:`Brightness level updated! Now it is at level ${e}.`,containsHTML:!0,timeout:3e3})}const Ne=[{name:"Change the background image",execute(e){const t=xe({elementName:"label",appendTo:e,htmlProps:{className:`${_} ${v}`,htmlFor:s,textContent:E}}),n=xe({elementName:"input",appendTo:e,htmlProps:{type:"file",id:s,accept:"image/*",style:{display:"none"}}});xe({elementName:"p",appendTo:e,htmlProps:{textContent:"- or -"}});const r=xe({elementName:"input",appendTo:e,htmlProps:{className:_,placeholder:"Paste image link here"}});xe({elementName:"p",appendTo:e,htmlProps:{className:f,textContent:"🛑 The image you want as your background must be sent as a message on Discord. You will have to click on the image, press on , and copy the link from there. After that, click on this input and press ENTER."}}),n.onchange=()=>function(e,t){if(null===e.files||0===e.files.length)return;const n=e.files[0],r=new FileReader;r.readAsDataURL(n),r.addEventListener("loadstart",(()=>{t.textContent="Loading image..."})),r.addEventListener("loadend",(()=>{const e=r.result;if(null===e)return void ke({text:S("image data could not be read"),timeout:1e4});we(e);const o=n.name.split(".")[1]||"???",a=n.name.length>25?`(${n.name.slice(0,25)}...).${o}`:n.name;t.innerHTML=null!==n?`${E} | Image name: ${n.name.length>25?`${a}`:n.name}`:E})),r.addEventListener("error",(e=>ke({text:S(`image reader gave error${e.lengthComputable?`, only ${(e.loaded/e.total*100).toPrecision(2)}% of image loaded`:""}}`),timeout:1e4})))}(n,t),r.onkeydown=e=>{const o=r.value.trim();"Enter"===e.code&&""!==o&&(t.textContent=E,n.value="",we(o))}}},{name:"Change the brightness level",execute(e){const t=k.get("isDarkTheme")?"0,0,0":"255,255,255",n=k.get("brightness"),r=xe({elementName:"h3",appendTo:e,htmlProps:{className:y,textContent:`Brightness level: ${n}`}}),o=xe({elementName:"input",appendTo:e,htmlProps:{className:_,type:"range",min:"0",max:"9",value:n.toString(),oninput:()=>function({overlayColor:e,brightnessLevelIndicator:t,brightnessLevelSlider:n}){const r=w.get("overlayDarkenerElement"),o=w.get("titleBarElement");r.style.backgroundColor=`rgba(${e}, .${n.value})`,null!==o&&(o.style.backgroundColor=`rgba(${e}, .${n.value})`),t.textContent=`Brightness level: ${n.value}`,k.set("brightness",parseInt(n.value))}({overlayColor:t,brightnessLevelIndicator:r,brightnessLevelSlider:o})}});xe({elementName:"button",appendTo:e,htmlProps:{className:$,textContent:"Save brightness level",onclick:Ee}})}},{name:"Change the blur level",execute(e){const t=k.get("blur"),n=xe({elementName:"h3",appendTo:e,htmlProps:{className:y,textContent:`Blur level: ${t}`}}),r=xe({elementName:"input",appendTo:e,htmlProps:{className:_,type:"range",min:"0",max:"50",value:t.toString(),oninput:()=>function(e,t){const n=w.get("overlayDarkenerElement"),r=w.get("titleBarElement");n.style.backdropFilter=`blur(${t.value}px)`,null!==r&&(r.style.backdropFilter=`blur(${t.value}px)`),e.textContent=`Blur level: ${t.value}`,k.set("blur",parseInt(t.value))}(n,r)}});xe({elementName:"button",appendTo:e,htmlProps:{className:$,textContent:"Save blur level",onclick:Te}})}}];function Se(e,t){t.classList.add(h),t.textContent="";try{t.onclick=null,e.execute(t)}catch(e){alert(S(e.message))}}class Le{constructor(e){this.targetElement=e,this._observer=new MutationObserver(((e,t)=>{if(null===this._callback)throw new ReferenceError("No callback has been supplied on trigger.");for(const n of e){const e=n.target;if("childList"!==n.type||!e.classList.contains(A.slice(1))||null===e.firstChild)return;this._callback(n,t)}}))}onTrigger(e){return e&&(this._callback=e),this}observe(e){return this._observer.observe(this.targetElement,e),this}unobserve(){return this._observer.disconnect(),this}}let Ce;function Re(e){if(clearTimeout(Ce),null===e.target)return;const t=e.target;if("A"!==t.nodeName||!t.hasAttribute("href")||!t.href.includes("cdn.discord"))return;let n=null;const r=new Le(document.body);try{r.onTrigger((e=>{const o=e.target;if(n=o.querySelector(".menu_d90b3d"),null===n||"menu"!==n.role)return ke({text:"Default menu element not found. Searching manually...",timeout:3e3}),r.unobserve(),void x.delete("contextMenuCreationObserver");Pe(n,t),clearTimeout(Ce),r.unobserve(),x.delete("contextMenuCreationObserver")})).observe({subtree:!0,childList:!0})}catch(e){return void alert(S(e.message))}x.set("contextMenuCreationObserver",r),Ce=setTimeout((()=>{r.unobserve(),x.delete("contextMenuCreationObserver"),n=function(){const e=Array.from(document.querySelectorAll(A));if(0===e.length)return null;for(const t of e){const e=t.querySelector("div[role='menu']");if(null==e?void 0:e.className.includes("menu"))return e}return null}(),null!==n&&Pe(n,t)}),5e3)}function Pe(e,t){const n=k.get("isDarkTheme");xe({elementName:"button",appendTo:e,htmlProps:{id:c,textContent:N,style:{color:n?"gold":"#7c7300",boxShadow:"0px 0px 13px 0px "+(n?"hsl(51deg 100% 50% / 45%)":"hsl(51deg 100% 22% / 45%)")}}}).onclick=()=>we(t.href)}function Ae({legacyStorageKey:e,newStorageKey:t,defaultValue:n}){try{let r=window.localStorage.getItem(t);return null===r&&(r=window.localStorage.getItem(e),null===r?r=n:(window.localStorage.removeItem(e),window.localStorage.setItem(t,r))),!0}catch(e){return alert(S(e.message)),!1}}class Ie{constructor({targetElement:e,from:t,to:n}){this._callback=null,this.targetElement=e,this._from=t,this._to=n,this._wasClassNamePreviouslyPresent=!this.targetElement.classList.contains(this._from)&&this.targetElement.classList.contains(this._to),this._observer=new MutationObserver(((e,t)=>{if(null===this._callback)throw new ReferenceError("No callback has been supplied on class change.");for(const n of e){if("attributes"!==n.type||"class"!==n.attributeName)continue;const e=n.target,r=!e.classList.contains(this._from)&&e.classList.contains(this._to);r!==this._wasClassNamePreviouslyPresent&&(this._wasClassNamePreviouslyPresent=r,this._callback(n,t))}}))}onTrigger(e){return this._callback=e,this}observe(e){return this._observer.observe(this.targetElement,e),this}unobserve(){return this._observer.disconnect(),this}}function Be(){const e=document.getElementById(r),t=document.getElementById(i),n=document.getElementById(o),s=document.getElementById(a),c=document.getElementById(l);if(null==t||t.remove(),null==n||n.remove(),null==s||s.remove(),null==c||c.remove(),null!==e&&(window.onkeydown=null,e.remove()),window.__TRANSPARENCY_OBSERVERS__){for(const e of window.__TRANSPARENCY_OBSERVERS__.values())e.unobserve();window.__TRANSPARENCY_OBSERVERS__=void 0}}!function(){if(Be(),!(function(){const e=xe({elementName:"iframe",appendTo:document.body,htmlProps:{}});return e.style.display="none",null===e.contentWindow?(alert(S("Could not create the localStorage object because the iframe's contentWindow isn't defined.")),!1):(window.localStorage=e.contentWindow.localStorage,!0)}()&&Ae({legacyStorageKey:"bgImg",newStorageKey:e,defaultValue:""})&&Ae({legacyStorageKey:"brghtns",newStorageKey:t,defaultValue:"9"}))||(k.set("isDarkTheme",document.documentElement.classList.contains(d)).set("brightness",parseInt(null!==(s=window.localStorage.getItem(t))&&void 0!==s?s:"unset")||9).set("blur",parseInt(null!==(c=window.localStorage.getItem(n))&&void 0!==c?c:"unset")||0).set("backgroundImageURL",null!==(m=window.localStorage.getItem(e))&&void 0!==m?m:""),0)||!function(){const e=k.get("isDarkTheme"),t=new Ie({targetElement:document.documentElement,from:e?d:u,to:e?u:d});try{t.onTrigger((()=>{ke({text:"🛑 It looks like the theme has changed. Discord will refresh in 5 seconds.",timeout:5e3}).then((()=>{t.unobserve(),location.reload()}))})).observe({attributes:!0,attributeFilter:["class"]})}catch(e){return alert(S(e.message)),!1}return x.set("themeChangeObserver",t),!0}()||(window.__TRANSPARENCY_OBSERVERS__=x,0)||!function(){const e=function(e){const t=document.querySelector(e);return null===t?(alert(S(`${e} is not in the DOM.`)),null):t}(P);if(null===e)return!1;const t=document.querySelector(R),n=xe({elementName:"style",appendTo:document.head,htmlProps:{id:r,innerHTML:$e()}});return w.set("titleBarElement",t).set("overlayDarkenerElement",e).set("mainStyleSheet",n),!0}())return void Be();var s,c,m;xe({elementName:"style",appendTo:document.head,htmlProps:{id:i,innerHTML:C}});const{actionsPanel:p,toggleActionsPanel:h}=function(){const e=xe({elementName:"div",appendTo:document.body,htmlProps:{id:o}}),t=xe({elementName:"div",appendTo:document.body,htmlProps:{id:a}}),n=function(e,t){const n=w.get("mainStyleSheet");let r;return function(){const o=e.classList.contains(g),a=n.hasAttribute("media");if(clearTimeout(r),!o&&!a)return e.style.display="block",t.style.display="block",void setTimeout((()=>e.classList.add(g)),20);o&&(e.classList.remove(g),t.style.display="none",r=setTimeout((()=>{e.style.display="none"}),1e3))}}(e,t);t.onclick=n,xe({elementName:"style",appendTo:document.head,htmlProps:{id:l,innerHTML:L}}),null!==w.get("titleBarElement")&&(e.style.paddingTop="20px");for(const t of Ne){const n=xe({elementName:"div",appendTo:e,htmlProps:{className:b,textContent:t.name}});n.onclick=()=>Se(t,n)}return{actionsPanel:e,toggleActionsPanel:n}}();ke({text:"Welcome!

To toggle the actions panel, press CTRL + SHIFT + X
To toggle the theme, press CTRL + D",timeout:1e4,containsHTML:!0}).then((()=>{(function(){const e=k.get("isDarkTheme"),t=w.get("titleBarElement");if(null!==t)return!e&&t.classList.contains(d);const n=document.querySelector(".bg__7adbf");return null!==n&&!e&&n.classList.contains(d)})()&&(ke({text:"🛑 Warning

Your sidebar is currently dark themed.
For proper text visibility, disable its dark theme in Settings > Appearance.

The theme has disabled itself. After disabling the sidebar's dark theme, you can reactivate the theme by pressing CTRL + D. No need to refresh.",timeout:15e3,containsHTML:!0}),T(p))})),window.onkeydown=e=>{e.ctrlKey&&(e.shiftKey&&"KeyX"===e.code&&(e.preventDefault(),h()),"KeyD"===e.code&&(e.preventDefault(),T(p)))},window.addEventListener("contextmenu",Re,{capture:!0})}()})(); -------------------------------------------------------------------------------- /main/startup-with-discord.js: -------------------------------------------------------------------------------- 1 | mainWindow.webContents.on("did-finish-load", () => { 2 | mainWindow.webContents.executeJavaScript( 3 | Buffer.from( 4 | "KCgpPT57InVzZSBzdHJpY3QiO2NvbnN0IGU9Il9fQkFDS0dST1VORC1JTUFHRV9fIix0PSJfX0JSSUdIVE5FU1NfXyIsbj0iX19CTFVSX18iLHI9IlRSQU5TUEFSRU5DWSIsbz1gJHtyfV9fQUNUSU9OUy1QQU5FTGAsYT1gJHtvfS1PVkVSTEFZYCxsPWAke3J9X19BQ1RJT05TX1BBTkVMYCxpPWAke3J9X19BTEVSVFMtU1RZTEVTSEVFVGAscz1gJHtyfV9fSU1BR0UtSU5QVVQtUElDS0VSYCxjPWAke3J9X19TRVQtQkFDS0dST1VORGAsZD0idGhlbWUtZGFyayIsdT0idGhlbWUtbGlnaHQiLG09YCR7cn1fX0FMRVJUYCxwPWAke219LS1JTkFDVElWRWAsZz1gJHtvfS0tU0xJREUtSU5gLGI9YCR7cn1fX0FDVElPTmAsaD1gJHtifS0tQUNUSVZFYCxmPWAke2J9LVdBUk5JTkdgLF89YCR7Yn0tSU5QVVRgLHY9YCR7Yn1fX0lOUFVULVBJQ0tFUmAseT1gJHtifS1MRVZFTC1JTkRJQ0FUT1JgLCQ9YCR7Yn0tU0FWRS1MRVZFTGAsdz1uZXcgTWFwLHg9bmV3IE1hcCxrPW5ldyBNYXA7ZnVuY3Rpb24gVChlKXtjb25zdCB0PXcuZ2V0KCJtYWluU3R5bGVTaGVldCIpLG49dy5nZXQoInRpdGxlQmFyRWxlbWVudCIpO2lmKCF0Lmhhc0F0dHJpYnV0ZSgibWVkaWEiKSlyZXR1cm4gdC5zZXRBdHRyaWJ1dGUoIm1lZGlhIiwiMXB4IiksbnVsbCE9PW4mJihuLnN0eWxlLmJhY2tncm91bmRDb2xvcj0idmFyKC0tY29sb3ItdGVydGlhcnkpIiksdm9pZChlLnN0eWxlLmRpc3BsYXk9Im5vbmUiKTtjb25zdCByPWsuZ2V0KCJpc0RhcmtUaGVtZSIpLG89ay5nZXQoImJyaWdodG5lc3MiKTt0LnJlbW92ZUF0dHJpYnV0ZSgibWVkaWEiKSxudWxsIT09biYmKG4uc3R5bGUuYmFja2dyb3VuZENvbG9yPXI/YHJnYmEoMCwwLDAsMC4ke299YDpgcmdiYSgyNTUsMjU1LDI1NSwwLiR7b31gKSxlLmNsYXNzTGlzdC5jb250YWlucyhnKSYmKGUuc3R5bGUuZGlzcGxheT0iYmxvY2siKX1jb25zdCBFPSJQcmVzcyBoZXJlIHRvIGJyb3dzZSBmb3IgYW4gaW1hZ2Ugb24geW91ciBjb21wdXRlciIsTj0iU2V0IGFzIGJhY2tncm91bmQgaW1hZ2Ug8J+MnyI7ZnVuY3Rpb24gUyhlKXtyZXR1cm5g8J+bkSBPaCBubywgdGhlcmUncyBiZWVuIGFuIGVycm9yLiBJZiB5b3Ugc2VlIHRoaXMsIGpvaW4gdGhlIHN1cHBvcnQgc2VydmVyLCBhbmQgcmVwb3J0IHRoZSBlcnJvci4gW1ske2V9XV1gfWNvbnN0IEw9YC4ke2J9IHtcbiAgbWluLWhlaWdodDogMTAwcHg7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMC41KTtcbiAgbWFyZ2luOiAxMHB4O1xuICBjb2xvcjogd2hpdGU7XG4gIGN1cnNvcjogcG9pbnRlcjtcbiAgZGlzcGxheTogZmxleDtcbiAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcbiAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gIHBhZGRpbmc6IDMwcHggNTBweDtcbiAgdHJhbnNpdGlvbjogYmFja2dyb3VuZC1jb2xvciAzMDBtcyBsaW5lYXI7XG59XG5cbi4ke2h9IHtcbiAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgwLCAwLCAwLCAwLjgpO1xuICBjdXJzb3I6IGluaXRpYWw7XG59XG5cbi4ke199IHtcbiAgYm94LXNpemluZzogYm9yZGVyLWJveDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICBwYWRkaW5nOiAxNXB4IDA7XG4gIHdpZHRoOiAxMDAlO1xuICBib3JkZXI6IDA7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC4xKTtcbiAgYm9yZGVyLXJhZGl1czogNTBweDtcbiAgY29sb3I6IHdoaXRlO1xufVxuXG4uJHt2fSB7XG4gIHBhZGRpbmc6IDIwcHggMTVweCAhaW1wb3J0YW50O1xuICBjdXJzb3I6IHBvaW50ZXI7XG4gIGZvbnQtZmFtaWx5OiB2YXIoLS1mb250LXByaW1hcnkpO1xuICBmb250LXNpemU6IHNtYWxsZXI7XG59XG5cbi4ke199OmZvY3VzLFxuLiR7JH06Zm9jdXMge1xuICBvdXRsaW5lOiBub25lO1xuICBib3JkZXI6IDJweCBzb2xpZCBkb2RnZXJibHVlO1xufVxuXG4uJHt5fSB7XG4gIG1hcmdpbjogMDtcbiAgb3BhY2l0eTogMC44O1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG59XG5cbi4ke2Z9IHtcbiAgZGlzcGxheTogbm9uZTtcbiAgZm9udC1zaXplOiAxMXB4O1xuICBsaW5lLWhlaWdodDogMjBweDtcbiAgY29sb3I6IHdoaXRlO1xuICBtYXgtd2lkdGg6IDMxMHB4O1xuICBtYXJnaW46IDIwcHggMCAwO1xuICBvcGFjaXR5OiAwLjg7XG59XG5cbi4ke199OmZvY3VzIH4gLiR7Zn0ge1xuICBkaXNwbGF5OiBibG9jaztcbn1cblxuLiR7JH0ge1xuICBoZWlnaHQ6IDQwcHg7XG4gIHBhZGRpbmc6IDAgMTVweDtcbiAgbWF4LXdpZHRoOiAzMDBweDtcbiAgbWFyZ2luLXRvcDogMTBweDtcbiAgYm9yZGVyOiAwO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMSk7XG4gIGJvcmRlci1yYWRpdXM6IDUwcHg7XG4gIGNvbG9yOiB3aGl0ZTtcbiAgY3Vyc29yOiBwb2ludGVyO1xufVxuXG4jJHtjfSB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgbGVmdDogLTEwMCU7XG4gIGJvdHRvbTogMDtcbiAgd2lkdGg6IG1heC1jb250ZW50O1xuICBwYWRkaW5nOiAxMHB4O1xuICBtYXJnaW4tbGVmdDogLTE1cHg7XG4gIGJhY2tncm91bmQtY29sb3I6IGluaGVyaXQ7XG4gIGJvcmRlci1yYWRpdXM6IGluaGVyaXQ7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgY3Vyc29yOiBwb2ludGVyO1xufWAsQz1gLiR7bX0geyBcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiAwcHg7XG4gIHJpZ2h0OiAwcHg7XG4gIG1hcmdpbjogYXV0bztcbiAgcGFkZGluZzogMjBweDtcbiAgZm9udC1zaXplOiAxNHB4O1xuICB3aWR0aDogZml0LWNvbnRlbnQ7XG4gIHdpZHRoOiAtbW96LWZpdC1jb250ZW50O1xuICBtYXgtd2lkdGg6IDQ1MHB4O1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsMCwwLDAuNik7XG4gIGJhY2tkcm9wLWZpbHRlcjogYmx1cig4cHgpO1xuICBjb2xvcjogd2hpdGU7XG4gIGJvcmRlci1ib3R0b20tcmlnaHQtcmFkaXVzOiAyMHB4O1xuICBib3JkZXItYm90dG9tLWxlZnQtcmFkaXVzOiAyMHB4O1xuICB6LWluZGV4OiAxMDAwMDtcbiAgYm94LXNoYWRvdzogMHB4IDEzcHggMTBweCAtNXB4IHJnYmEoMCwwLDAsMC41KTtcbiAgdHJhbnNpdGlvbjogdHJhbnNmb3JtIDUwMG1zIGVhc2U7XG4gIGFuaW1hdGlvbjogVFJBTlNQQVJFTkNZX19BTEVSVC1TTElERSA1MDBtcyBlYXNlO1xuICBvdmVyZmxvdzogaGlkZGVuO1xufVxuXG4uJHttfTo6YWZ0ZXIge1xuICAgIGNvbnRlbnQ6ICIiO1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICBib3R0b206IDA7XG4gICAgbGVmdDogMDtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiBoc2woMjM1IDEwMCUgODIlIC8gMSk7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgaGVpZ2h0OiA1cHg7XG4gICAgYW5pbWF0aW9uOiBUUkFOU1BBUkVOQ1lfX0FMRVJULVRJTUVSLUJBUiBsaW5lYXIgZm9yd2FyZHM7XG4gICAgYW5pbWF0aW9uLWR1cmF0aW9uOiB2YXIoLS10aW1lci1iYXItdGltZW91dCk7XG59XG5cbkBrZXlmcmFtZXMgVFJBTlNQQVJFTkNZX19BTEVSVC1USU1FUi1CQVIge1xuICBmcm9tIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTEwMCUpO1xuICB9IHRvIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoMCk7IFxuICB9XG59XG5cbkBrZXlmcmFtZXMgVFJBTlNQQVJFTkNZX19BTEVSVC1TTElERSB7XG4gIGZyb20geyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoLTEzMCUpOyB9XG4gIHRvIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDApIH1cbn1cblxuLiR7cH0ge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoLTEzMCUpO1xufWAsUj0iLnR5cGVXaW5kb3dzX2E5MzRkOC53aXRoRnJhbWVfYTkzNGQ4LnRpdGxlQmFyX2E5MzRkOCwgIC50eXBlV2luZG93c19hOTM0ZDgud2l0aEZyYW1lX2E5MzRkOC50aXRsZUJhcl9hOTM0ZDgud2l0aEJhY2tncm91bmRPdmVycmlkZV9hOTM0ZDgiLFA9Ii5hcHBfYmQyNmNjIixBPSIubGF5ZXJDb250YWluZXJfY2QwZGU1IixJPSIudG9hc3RfZjQyNzY3IixCPSIuYnJvd3Nlcl9mNTQyZmMiLE09Ii5tZXNzYWdlc1BvcG91dFdyYXBfYWM5MGEyIixEPSIucmVjZW50TWVudGlvbnNQb3BvdXRfZGRiNWI0IixPPSIucXVpY2tzd2l0Y2hlcl9mNGUxMzkiLEg9Ii5jb250YWluZXJfZjI0YTk2IixLPSIucG9wb3V0X2Y2NjM5ZCIsWT0iLm1haW5fZTNmOGMyIixWPSIuc3RyZWFtUHJldmlld19hZGJlYTYiLFc9Ii5wb3BvdXRfYTk0MTRiIixVPSIuYXBwbGljYXRpb25TdG9yZV9jZWNjODYiLHo9Ii5jb250YWluZXItM3dMS0RlIixGPSIudGhlbWVkLUhwMUtDXyIsRz0iLmRpcmVjdG9yeUNvbnRhaW5lcl9kYTNmNTkiLHE9Ii5oZWFkZXJfZjFmZDljLCAuY29udGFpbmVyX2U0NDMwMiwgLmNoYXRfYTdkNzJlLCAuc2Nyb2xsZXJDb250YWluZXJfYzZiMTFiIixqPSIucGFnZVdyYXBwZXJfYTNhNGNlIixYPSIuc2VhcmNoUmVzdWx0c1dyYXBfYzJiNDdkIixKPSIudGhlbWVFZGl0b3JfYzlkZGE2IixRPSIuZWRpdG9yX2JjZmExZSIsWj0iLmNvbnRhaW5lcl9kMWMyNDYiLGVlPSIud3JhcHBlcl9mOWYyY2EiLHRlPSIuYmxvY2tlZFN5c3RlbU1lc3NhZ2VfYzA5ZDBmIixuZT0iLmVtYmVkRnVsbF9iMDA2OGEiLHJlPSIud3JhcHBlcl9hNzFhMWMiLG9lPSIuY29kZS5pbmxpbmUsIC5tYXJrdXAtMkJPdy1qIGNvZGUsIGNvZGUiLGFlPSIuYXR0YWNobWVudF9iNTJiZWYiLGxlPSIud3JhcHBlckF1ZGlvX2Y3MmFhYyIsaWU9Ii53cmFwcGVyX2VmMzE5ZiIsc2U9Ii5jb250YWluZXJfZTQwYzE2IixjZT0iLnJlYWN0aW9uX2VjNmIxOSIsZGU9Ii5ob3ZlckJ1dHRvbkdyb3VwX2FiOGIyMyIsdWU9Ii51c2VyUHJvZmlsZUlubmVyVGhlbWVkTm9uUHJlbWl1bV9jNjlhN2IiLG1lPSIuY29udGFpbmVyX2FjMjAxYiIscGU9Ii5jb250ZW50V3JhcHBlcl9hZjVkYmIiLGdlPSIud3JhcHBlcl9jNmVlMzYiLGJlPSIuY29udGFpbmVyX2YyYmZiYiwgLnJlYWN0b3JzX2YyYmZiYiIsaGU9Ii53cmFwcGVyX2UwNjg1NyIsZmU9Ii53cmFwcGVyX2M0MzA1OSIsX2U9Ii5wcmVtaXVtUHJvbW9fY2E5YjU2Iix2ZT0iLmhlYWRlcl9iZDA1ZjEiLHllPSIucHJldmlld0NvbnRhaW5lcl9kYTFiZDkiO2Z1bmN0aW9uICRlKCl7Y29uc3QgZT1rLmdldCgiYnJpZ2h0bmVzcyIpLHQ9ay5nZXQoImlzRGFya1RoZW1lIiksbj10P2ByZ2JhKDAsIDAsIDAsIDAuJHtlfSlgfHwicmdiYSgwLDAsMCwwLjkpIjpgcmdiYSgyNTUsIDI1NSwgMjU1LCAwLiR7ZX0pYHx8InJnYmEoMjU1LDI1NSwyNTUsMC43KSIscj10PyJyZ2JhKDAsMCwwLDAuNzgpIjoicmdiYSgyNTUsMjU1LDI1NSwwLjg1KSIsbD10PyJyZ2JhKDAsMCwwLDAuOTIpIjoicmdiYSgyNTUsMjU1LDI1NSwwLjk1KSI7cmV0dXJuYC50aGVtZS1kYXJrLCAudGhlbWUtbGlnaHQge1xuICAtLWJhY2tncm91bmQtcHJpbWFyeTogdHJhbnNwYXJlbnQgIWltcG9ydGFudDtcbiAgLS1iYWNrZ3JvdW5kLXNlY29uZGFyeTogdHJhbnNwYXJlbnQgIWltcG9ydGFudDtcbiAgLS1iYWNrZ3JvdW5kLXRlcnRpYXJ5OiB0cmFuc3BhcmVudCAhaW1wb3J0YW50O1xuICAtLWNoYW5uZWx0ZXh0YXJlYS1iYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCAhaW1wb3J0YW50O1xuICAtLWRlcHJlY2F0ZWQtcGFuZWwtYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgIWltcG9ydGFudDtcbiAgLS1iYWNrZ3JvdW5kLXNlY29uZGFyeS1hbHQ6IHRyYW5zcGFyZW50ICFpbXBvcnRhbnQ7XG4gIC0tc2Nyb2xsYmFyLWF1dG8tdHJhY2s6IHRyYW5zcGFyZW50ICFpbXBvcnRhbnQ7XG4gIC0taG9tZS1iYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCAhaW1wb3J0YW50O1xuICAtLWNhcmQtc2Vjb25kYXJ5LWJnOiAkeyhpPXtiYWNrZ3JvdW5kSW1hZ2VVUkw6ay5nZXQoImJhY2tncm91bmRJbWFnZVVSTCIpLGJsdXJMZXZlbFBpeGVsczpgJHtrLmdldCgiYmx1ciIpfXB4YCxicmlnaHRuZXNzTGV2ZWxSR0JBOm4sdmFyaW91c0VsZW1lbnRzQmFja2dyb3VuZDpyLHZhcmlvdXNFbGVtZW50c0JhY2tncm91bmRNb3JlT3BhcXVlOmx9KS52YXJpb3VzRWxlbWVudHNCYWNrZ3JvdW5kfSAhaW1wb3J0YW50O1xufVxuXG4udGhlbWUtZGFyayB7XG4gIC0tc2Nyb2xsYmFyLWF1dG8tdGh1bWI6cmdiYSgyNTUsMjU1LDI1NSwwLjMpICFpbXBvcnRhbnQ7XG4gIC0tcHJvZmlsZS1ib2R5LWJhY2tncm91bmQtY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC4xKSAhaW1wb3J0YW50OyAgIFxufVxuXG4udGhlbWUtbGlnaHQge1xuICAtLXNjcm9sbGJhci1hdXRvLXRodW1iOnJnYmEoMCwwLDAsMC42KSAhaW1wb3J0YW50O1xuICAtLXByb2ZpbGUtYm9keS1iYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuMDUpICFpbXBvcnRhbnQ7XG59XG5cbiNhcHAtbW91bnQge1xuICBiYWNrZ3JvdW5kOiB1cmwoJHtpLmJhY2tncm91bmRJbWFnZVVSTH0pIGNlbnRlciBjZW50ZXIgbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvdmVyO1xufVxuXG4ke1J9IHtcbiAgbWFyZ2luLXRvcDogMDtcbiAgcGFkZGluZy10b3A6IDRweDtcbiAgYmFja2dyb3VuZDogJHtpLmJyaWdodG5lc3NMZXZlbFJHQkF9O1xuICBiYWNrZHJvcC1maWx0ZXI6IGJsdXIoJHtpLmJsdXJMZXZlbFBpeGVsc30pXG59XG5cbiR7UH0sICR7Sn0sICR7UX0ge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAke2kuYnJpZ2h0bmVzc0xldmVsUkdCQX07XG4gIGJhY2tkcm9wLWZpbHRlcjogYmx1cigke2kuYmx1ckxldmVsUGl4ZWxzfSk7XG59IFxuXG4ke1V9LCAke3p9LCBcbiR7R30sICR7cX0sICR7an0sXG4ke0Z9IHtcbiAgYmFja2dyb3VuZC1jb2xvcjogdHJhbnNwYXJlbnQgIWltcG9ydGFudDtcbn1cblxuJHttZX0sICR7bGV9LCBcbiR7aWV9LCAke1h9LCAke3BlfSxcbiR7c2V9LCAke2NlfSxcbiR7bmV9LCAke2FlfSxcbiR7YmV9LCAke3JlfSwgJHtvZX0sXG4ke3ZlfSwgJHtRfSwgJHtafSwgXG4ke0h9LCAke0t9LCAke1d9LFxuJHtWfSB7XG4gIGJhY2tncm91bmQtY29sb3I6ICR7aS52YXJpb3VzRWxlbWVudHNCYWNrZ3JvdW5kfSAhaW1wb3J0YW50O1xufVxuXG4ke3VlfSwgJHtZfSwgJHtEfSxcbiR7Z2V9LCAke2hlfSxcbiR7T30sICR7TX0sICR7Qn0sIFxuJHtJfSwgJHtwZX0sICR7ZmV9LFxuJHtkZX0sICR7X2V9IHtcbmJhY2tncm91bmQtY29sb3I6ICR7aS52YXJpb3VzRWxlbWVudHNCYWNrZ3JvdW5kTW9yZU9wYXF1ZX0gIWltcG9ydGFudDtcbn1cblxuJHt5ZX0ge1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xufVxuXG4ke3ZlfSB7XG4gIG1hcmdpbi1sZWZ0OiAtMjRweDtcbiAgcGFkZGluZy1sZWZ0OiAyNHB4O1xufVxuXG4ke2VlfTpoYXMoJHt0ZX0pIHtcbiAgZGlzcGxheTogbm9uZTtcbn1cblxuIyR7b30ge1xuICBmb250LWZhbWlseTogIldoaXRuZXkiLCAiSGVsdmV0aWNhIE5ldWUiLCAiSGVsdmV0aWNhIiwgbW9ub3NwYWNlLCAiQXJpYWwiLCBzYW5zLXNlcmlmO1xuICB3aWR0aDogMzAlO1xuICBoZWlnaHQ6IDEwMCU7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwwLDAsMC42KTtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICByaWdodDogMDtcbiAgei1pbmRleDogMjAwO1xuICBiYWNrZHJvcC1maWx0ZXI6IGJsdXIoOHB4KTtcbiAgdHJhbnNmb3JtOiB0cmFuc2xhdGVYKDEwMCUpO1xuICB0cmFuc2l0aW9uOiB0cmFuc2Zvcm0gNTAwbXMgZWFzZTtcbn1cblxuIyR7YX0ge1xuICBkaXNwbGF5OiBub25lO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgei1pbmRleDogMTk5O1xufVxuXG4uJHtnfSB7XG4gIHRyYW5zZm9ybTogdHJhbnNsYXRlWCgwKSAhaW1wb3J0YW50O1xufVxuYDt2YXIgaX1mdW5jdGlvbiB3ZSh0KXt3LmdldCgibWFpblN0eWxlU2hlZXQiKS5pbm5lckhUTUwrPWZ1bmN0aW9uKGUpe3JldHVybmAjYXBwLW1vdW50e2JhY2tncm91bmQ6IHVybCgke2V9KSBjZW50ZXIgY2VudGVyIG5vLXJlcGVhdCAhaW1wb3J0YW50OyBiYWNrZ3JvdW5kLXNpemU6IGNvdmVyICFpbXBvcnRhbnQ7fWB9KHQpLGsuc2V0KCJiYWNrZ3JvdW5kSW1hZ2VVUkwiLHQpLHdpbmRvdy5sb2NhbFN0b3JhZ2Uuc2V0SXRlbShlLHQpfWZ1bmN0aW9uIHhlKHtlbGVtZW50TmFtZTplLGFwcGVuZFRvOnQsaHRtbFByb3BzOm59KXtjb25zdCByPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoZSk7Zm9yKGNvbnN0W2UsdF1vZiBPYmplY3QuZW50cmllcyhuKSlyW2VdPXQ7aWYodm9pZCAwIT09bi5zdHlsZSlmb3IoY29uc3RbZSx0XW9mIE9iamVjdC5lbnRyaWVzKG4uc3R5bGUpKXIuc3R5bGVbZV09dDtyZXR1cm4gdC5hcHBlbmRDaGlsZChyKSxyfWZ1bmN0aW9uIGtlKHt0ZXh0OmUsdGltZW91dDp0LGNvbnRhaW5zSFRNTDpuPSExfSl7Y29uc3Qgcj14ZSh7ZWxlbWVudE5hbWU6ImRpdiIsYXBwZW5kVG86ZG9jdW1lbnQuYm9keSxodG1sUHJvcHM6e2NsYXNzTmFtZTptLFtuPyJpbm5lckhUTUwiOiJ0ZXh0Q29udGVudCJdOmV9fSk7cmV0dXJuIHIuc3R5bGUuc2V0UHJvcGVydHkoIi0tdGltZXItYmFyLXRpbWVvdXQiLGAke3R9bXNgKSxuZXcgUHJvbWlzZSgoZT0+e3NldFRpbWVvdXQoKCgpPT57ci5jbGFzc0xpc3QuYWRkKHApLHNldFRpbWVvdXQoKCgpPT57ci5yZW1vdmUoKSxlKCEwKX0pLDFlMyl9KSx0KX0pKX1mdW5jdGlvbiBUZSgpe2NvbnN0IGU9ay5nZXQoImJsdXIiKTt3aW5kb3cubG9jYWxTdG9yYWdlLnNldEl0ZW0obixlLnRvU3RyaW5nKCkpLGtlKHt0ZXh0OmBCbHVyIGxldmVsIHVwZGF0ZWQhIE5vdyBpdCBpcyBhdCBsZXZlbCA8Yj4ke2V9PC9iPi5gLGNvbnRhaW5zSFRNTDohMCx0aW1lb3V0OjNlM30pfWZ1bmN0aW9uIEVlKCl7Y29uc3QgZT1rLmdldCgiYnJpZ2h0bmVzcyIpO3dpbmRvdy5sb2NhbFN0b3JhZ2Uuc2V0SXRlbSh0LGUudG9TdHJpbmcoKSksa2Uoe3RleHQ6YEJyaWdodG5lc3MgbGV2ZWwgdXBkYXRlZCEgTm93IGl0IGlzIGF0IGxldmVsIDxiPiR7ZX08L2I+LmAsY29udGFpbnNIVE1MOiEwLHRpbWVvdXQ6M2UzfSl9Y29uc3QgTmU9W3tuYW1lOiJDaGFuZ2UgdGhlIGJhY2tncm91bmQgaW1hZ2UiLGV4ZWN1dGUoZSl7Y29uc3QgdD14ZSh7ZWxlbWVudE5hbWU6ImxhYmVsIixhcHBlbmRUbzplLGh0bWxQcm9wczp7Y2xhc3NOYW1lOmAke199ICR7dn1gLGh0bWxGb3I6cyx0ZXh0Q29udGVudDpFfX0pLG49eGUoe2VsZW1lbnROYW1lOiJpbnB1dCIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e3R5cGU6ImZpbGUiLGlkOnMsYWNjZXB0OiJpbWFnZS8qIixzdHlsZTp7ZGlzcGxheToibm9uZSJ9fX0pO3hlKHtlbGVtZW50TmFtZToicCIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e3RleHRDb250ZW50OiItIG9yIC0ifX0pO2NvbnN0IHI9eGUoe2VsZW1lbnROYW1lOiJpbnB1dCIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e2NsYXNzTmFtZTpfLHBsYWNlaG9sZGVyOiJQYXN0ZSBpbWFnZSBsaW5rIGhlcmUifX0pO3hlKHtlbGVtZW50TmFtZToicCIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e2NsYXNzTmFtZTpmLHRleHRDb250ZW50OiLwn5uRIFRoZSBpbWFnZSB5b3Ugd2FudCBhcyB5b3VyIGJhY2tncm91bmQgbXVzdCBiZSBzZW50IGFzIGEgbWVzc2FnZSBvbiBEaXNjb3JkLiBZb3Ugd2lsbCBoYXZlIHRvIGNsaWNrIG9uIHRoZSBpbWFnZSwgcHJlc3Mgb24gPE9wZW4gb3JpZ2luYWw+LCBhbmQgY29weSB0aGUgbGluayBmcm9tIHRoZXJlLiBBZnRlciB0aGF0LCBjbGljayBvbiB0aGlzIGlucHV0IGFuZCBwcmVzcyBFTlRFUi4ifX0pLG4ub25jaGFuZ2U9KCk9PmZ1bmN0aW9uKGUsdCl7aWYobnVsbD09PWUuZmlsZXN8fDA9PT1lLmZpbGVzLmxlbmd0aClyZXR1cm47Y29uc3Qgbj1lLmZpbGVzWzBdLHI9bmV3IEZpbGVSZWFkZXI7ci5yZWFkQXNEYXRhVVJMKG4pLHIuYWRkRXZlbnRMaXN0ZW5lcigibG9hZHN0YXJ0IiwoKCk9Pnt0LnRleHRDb250ZW50PSJMb2FkaW5nIGltYWdlLi4uIn0pKSxyLmFkZEV2ZW50TGlzdGVuZXIoImxvYWRlbmQiLCgoKT0+e2NvbnN0IGU9ci5yZXN1bHQ7aWYobnVsbD09PWUpcmV0dXJuIHZvaWQga2Uoe3RleHQ6UygiaW1hZ2UgZGF0YSBjb3VsZCBub3QgYmUgcmVhZCIpLHRpbWVvdXQ6MWU0fSk7d2UoZSk7Y29uc3Qgbz1uLm5hbWUuc3BsaXQoIi4iKVsxXXx8Ij8/PyIsYT1uLm5hbWUubGVuZ3RoPjI1P2AoJHtuLm5hbWUuc2xpY2UoMCwyNSl9Li4uKS4ke299YDpuLm5hbWU7dC5pbm5lckhUTUw9bnVsbCE9PW4/YCR7RX0gfCBJbWFnZSBuYW1lOiAke24ubmFtZS5sZW5ndGg+MjU/YDxzcGFuIHRpdGxlPSIke24ubmFtZX0iIHN0eWxlPSJ0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsgY3Vyc29yOiBoZWxwIj4ke2F9PC9zcGFuPmA6bi5uYW1lfWA6RX0pKSxyLmFkZEV2ZW50TGlzdGVuZXIoImVycm9yIiwoZT0+a2Uoe3RleHQ6UyhgaW1hZ2UgcmVhZGVyIGdhdmUgZXJyb3Ike2UubGVuZ3RoQ29tcHV0YWJsZT9gLCBvbmx5ICR7KGUubG9hZGVkL2UudG90YWwqMTAwKS50b1ByZWNpc2lvbigyKX0lIG9mIGltYWdlIGxvYWRlZGA6IiJ9fWApLHRpbWVvdXQ6MWU0fSkpKX0obix0KSxyLm9ua2V5ZG93bj1lPT57Y29uc3Qgbz1yLnZhbHVlLnRyaW0oKTsiRW50ZXIiPT09ZS5jb2RlJiYiIiE9PW8mJih0LnRleHRDb250ZW50PUUsbi52YWx1ZT0iIix3ZShvKSl9fX0se25hbWU6IkNoYW5nZSB0aGUgYnJpZ2h0bmVzcyBsZXZlbCIsZXhlY3V0ZShlKXtjb25zdCB0PWsuZ2V0KCJpc0RhcmtUaGVtZSIpPyIwLDAsMCI6IjI1NSwyNTUsMjU1IixuPWsuZ2V0KCJicmlnaHRuZXNzIikscj14ZSh7ZWxlbWVudE5hbWU6ImgzIixhcHBlbmRUbzplLGh0bWxQcm9wczp7Y2xhc3NOYW1lOnksdGV4dENvbnRlbnQ6YEJyaWdodG5lc3MgbGV2ZWw6ICR7bn1gfX0pLG89eGUoe2VsZW1lbnROYW1lOiJpbnB1dCIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e2NsYXNzTmFtZTpfLHR5cGU6InJhbmdlIixtaW46IjAiLG1heDoiOSIsdmFsdWU6bi50b1N0cmluZygpLG9uaW5wdXQ6KCk9PmZ1bmN0aW9uKHtvdmVybGF5Q29sb3I6ZSxicmlnaHRuZXNzTGV2ZWxJbmRpY2F0b3I6dCxicmlnaHRuZXNzTGV2ZWxTbGlkZXI6bn0pe2NvbnN0IHI9dy5nZXQoIm92ZXJsYXlEYXJrZW5lckVsZW1lbnQiKSxvPXcuZ2V0KCJ0aXRsZUJhckVsZW1lbnQiKTtyLnN0eWxlLmJhY2tncm91bmRDb2xvcj1gcmdiYSgke2V9LCAuJHtuLnZhbHVlfSlgLG51bGwhPT1vJiYoby5zdHlsZS5iYWNrZ3JvdW5kQ29sb3I9YHJnYmEoJHtlfSwgLiR7bi52YWx1ZX0pYCksdC50ZXh0Q29udGVudD1gQnJpZ2h0bmVzcyBsZXZlbDogJHtuLnZhbHVlfWAsay5zZXQoImJyaWdodG5lc3MiLHBhcnNlSW50KG4udmFsdWUpKX0oe292ZXJsYXlDb2xvcjp0LGJyaWdodG5lc3NMZXZlbEluZGljYXRvcjpyLGJyaWdodG5lc3NMZXZlbFNsaWRlcjpvfSl9fSk7eGUoe2VsZW1lbnROYW1lOiJidXR0b24iLGFwcGVuZFRvOmUsaHRtbFByb3BzOntjbGFzc05hbWU6JCx0ZXh0Q29udGVudDoiU2F2ZSBicmlnaHRuZXNzIGxldmVsIixvbmNsaWNrOkVlfX0pfX0se25hbWU6IkNoYW5nZSB0aGUgYmx1ciBsZXZlbCIsZXhlY3V0ZShlKXtjb25zdCB0PWsuZ2V0KCJibHVyIiksbj14ZSh7ZWxlbWVudE5hbWU6ImgzIixhcHBlbmRUbzplLGh0bWxQcm9wczp7Y2xhc3NOYW1lOnksdGV4dENvbnRlbnQ6YEJsdXIgbGV2ZWw6ICR7dH1gfX0pLHI9eGUoe2VsZW1lbnROYW1lOiJpbnB1dCIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e2NsYXNzTmFtZTpfLHR5cGU6InJhbmdlIixtaW46IjAiLG1heDoiNTAiLHZhbHVlOnQudG9TdHJpbmcoKSxvbmlucHV0OigpPT5mdW5jdGlvbihlLHQpe2NvbnN0IG49dy5nZXQoIm92ZXJsYXlEYXJrZW5lckVsZW1lbnQiKSxyPXcuZ2V0KCJ0aXRsZUJhckVsZW1lbnQiKTtuLnN0eWxlLmJhY2tkcm9wRmlsdGVyPWBibHVyKCR7dC52YWx1ZX1weClgLG51bGwhPT1yJiYoci5zdHlsZS5iYWNrZHJvcEZpbHRlcj1gYmx1cigke3QudmFsdWV9cHgpYCksZS50ZXh0Q29udGVudD1gQmx1ciBsZXZlbDogJHt0LnZhbHVlfWAsay5zZXQoImJsdXIiLHBhcnNlSW50KHQudmFsdWUpKX0obixyKX19KTt4ZSh7ZWxlbWVudE5hbWU6ImJ1dHRvbiIsYXBwZW5kVG86ZSxodG1sUHJvcHM6e2NsYXNzTmFtZTokLHRleHRDb250ZW50OiJTYXZlIGJsdXIgbGV2ZWwiLG9uY2xpY2s6VGV9fSl9fV07ZnVuY3Rpb24gU2UoZSx0KXt0LmNsYXNzTGlzdC5hZGQoaCksdC50ZXh0Q29udGVudD0iIjt0cnl7dC5vbmNsaWNrPW51bGwsZS5leGVjdXRlKHQpfWNhdGNoKGUpe2FsZXJ0KFMoZS5tZXNzYWdlKSl9fWNsYXNzIExle2NvbnN0cnVjdG9yKGUpe3RoaXMudGFyZ2V0RWxlbWVudD1lLHRoaXMuX29ic2VydmVyPW5ldyBNdXRhdGlvbk9ic2VydmVyKCgoZSx0KT0+e2lmKG51bGw9PT10aGlzLl9jYWxsYmFjayl0aHJvdyBuZXcgUmVmZXJlbmNlRXJyb3IoIk5vIGNhbGxiYWNrIGhhcyBiZWVuIHN1cHBsaWVkIG9uIHRyaWdnZXIuIik7Zm9yKGNvbnN0IG4gb2YgZSl7Y29uc3QgZT1uLnRhcmdldDtpZigiY2hpbGRMaXN0IiE9PW4udHlwZXx8IWUuY2xhc3NMaXN0LmNvbnRhaW5zKEEuc2xpY2UoMSkpfHxudWxsPT09ZS5maXJzdENoaWxkKXJldHVybjt0aGlzLl9jYWxsYmFjayhuLHQpfX0pKX1vblRyaWdnZXIoZSl7cmV0dXJuIGUmJih0aGlzLl9jYWxsYmFjaz1lKSx0aGlzfW9ic2VydmUoZSl7cmV0dXJuIHRoaXMuX29ic2VydmVyLm9ic2VydmUodGhpcy50YXJnZXRFbGVtZW50LGUpLHRoaXN9dW5vYnNlcnZlKCl7cmV0dXJuIHRoaXMuX29ic2VydmVyLmRpc2Nvbm5lY3QoKSx0aGlzfX1sZXQgQ2U7ZnVuY3Rpb24gUmUoZSl7aWYoY2xlYXJUaW1lb3V0KENlKSxudWxsPT09ZS50YXJnZXQpcmV0dXJuO2NvbnN0IHQ9ZS50YXJnZXQ7aWYoIkEiIT09dC5ub2RlTmFtZXx8IXQuaGFzQXR0cmlidXRlKCJocmVmIil8fCF0LmhyZWYuaW5jbHVkZXMoImNkbi5kaXNjb3JkIikpcmV0dXJuO2xldCBuPW51bGw7Y29uc3Qgcj1uZXcgTGUoZG9jdW1lbnQuYm9keSk7dHJ5e3Iub25UcmlnZ2VyKChlPT57Y29uc3Qgbz1lLnRhcmdldDtpZihuPW8ucXVlcnlTZWxlY3RvcigiLm1lbnVfZDkwYjNkIiksbnVsbD09PW58fCJtZW51IiE9PW4ucm9sZSlyZXR1cm4ga2Uoe3RleHQ6IkRlZmF1bHQgbWVudSBlbGVtZW50IG5vdCBmb3VuZC4gU2VhcmNoaW5nIG1hbnVhbGx5Li4uIix0aW1lb3V0OjNlM30pLHIudW5vYnNlcnZlKCksdm9pZCB4LmRlbGV0ZSgiY29udGV4dE1lbnVDcmVhdGlvbk9ic2VydmVyIik7UGUobix0KSxjbGVhclRpbWVvdXQoQ2UpLHIudW5vYnNlcnZlKCkseC5kZWxldGUoImNvbnRleHRNZW51Q3JlYXRpb25PYnNlcnZlciIpfSkpLm9ic2VydmUoe3N1YnRyZWU6ITAsY2hpbGRMaXN0OiEwfSl9Y2F0Y2goZSl7cmV0dXJuIHZvaWQgYWxlcnQoUyhlLm1lc3NhZ2UpKX14LnNldCgiY29udGV4dE1lbnVDcmVhdGlvbk9ic2VydmVyIixyKSxDZT1zZXRUaW1lb3V0KCgoKT0+e3IudW5vYnNlcnZlKCkseC5kZWxldGUoImNvbnRleHRNZW51Q3JlYXRpb25PYnNlcnZlciIpLG49ZnVuY3Rpb24oKXtjb25zdCBlPUFycmF5LmZyb20oZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbChBKSk7aWYoMD09PWUubGVuZ3RoKXJldHVybiBudWxsO2Zvcihjb25zdCB0IG9mIGUpe2NvbnN0IGU9dC5xdWVyeVNlbGVjdG9yKCJkaXZbcm9sZT0nbWVudSddIik7aWYobnVsbD09ZT92b2lkIDA6ZS5jbGFzc05hbWUuaW5jbHVkZXMoIm1lbnUiKSlyZXR1cm4gZX1yZXR1cm4gbnVsbH0oKSxudWxsIT09biYmUGUobix0KX0pLDVlMyl9ZnVuY3Rpb24gUGUoZSx0KXtjb25zdCBuPWsuZ2V0KCJpc0RhcmtUaGVtZSIpO3hlKHtlbGVtZW50TmFtZToiYnV0dG9uIixhcHBlbmRUbzplLGh0bWxQcm9wczp7aWQ6Yyx0ZXh0Q29udGVudDpOLHN0eWxlOntjb2xvcjpuPyJnb2xkIjoiIzdjNzMwMCIsYm94U2hhZG93OiIwcHggMHB4IDEzcHggMHB4ICIrKG4/ImhzbCg1MWRlZyAxMDAlIDUwJSAvIDQ1JSkiOiJoc2woNTFkZWcgMTAwJSAyMiUgLyA0NSUpIil9fX0pLm9uY2xpY2s9KCk9PndlKHQuaHJlZil9ZnVuY3Rpb24gQWUoe2xlZ2FjeVN0b3JhZ2VLZXk6ZSxuZXdTdG9yYWdlS2V5OnQsZGVmYXVsdFZhbHVlOm59KXt0cnl7bGV0IHI9d2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKHQpO3JldHVybiBudWxsPT09ciYmKHI9d2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKGUpLG51bGw9PT1yP3I9bjood2luZG93LmxvY2FsU3RvcmFnZS5yZW1vdmVJdGVtKGUpLHdpbmRvdy5sb2NhbFN0b3JhZ2Uuc2V0SXRlbSh0LHIpKSksITB9Y2F0Y2goZSl7cmV0dXJuIGFsZXJ0KFMoZS5tZXNzYWdlKSksITF9fWNsYXNzIElle2NvbnN0cnVjdG9yKHt0YXJnZXRFbGVtZW50OmUsZnJvbTp0LHRvOm59KXt0aGlzLl9jYWxsYmFjaz1udWxsLHRoaXMudGFyZ2V0RWxlbWVudD1lLHRoaXMuX2Zyb209dCx0aGlzLl90bz1uLHRoaXMuX3dhc0NsYXNzTmFtZVByZXZpb3VzbHlQcmVzZW50PSF0aGlzLnRhcmdldEVsZW1lbnQuY2xhc3NMaXN0LmNvbnRhaW5zKHRoaXMuX2Zyb20pJiZ0aGlzLnRhcmdldEVsZW1lbnQuY2xhc3NMaXN0LmNvbnRhaW5zKHRoaXMuX3RvKSx0aGlzLl9vYnNlcnZlcj1uZXcgTXV0YXRpb25PYnNlcnZlcigoKGUsdCk9PntpZihudWxsPT09dGhpcy5fY2FsbGJhY2spdGhyb3cgbmV3IFJlZmVyZW5jZUVycm9yKCJObyBjYWxsYmFjayBoYXMgYmVlbiBzdXBwbGllZCBvbiBjbGFzcyBjaGFuZ2UuIik7Zm9yKGNvbnN0IG4gb2YgZSl7aWYoImF0dHJpYnV0ZXMiIT09bi50eXBlfHwiY2xhc3MiIT09bi5hdHRyaWJ1dGVOYW1lKWNvbnRpbnVlO2NvbnN0IGU9bi50YXJnZXQscj0hZS5jbGFzc0xpc3QuY29udGFpbnModGhpcy5fZnJvbSkmJmUuY2xhc3NMaXN0LmNvbnRhaW5zKHRoaXMuX3RvKTtyIT09dGhpcy5fd2FzQ2xhc3NOYW1lUHJldmlvdXNseVByZXNlbnQmJih0aGlzLl93YXNDbGFzc05hbWVQcmV2aW91c2x5UHJlc2VudD1yLHRoaXMuX2NhbGxiYWNrKG4sdCkpfX0pKX1vblRyaWdnZXIoZSl7cmV0dXJuIHRoaXMuX2NhbGxiYWNrPWUsdGhpc31vYnNlcnZlKGUpe3JldHVybiB0aGlzLl9vYnNlcnZlci5vYnNlcnZlKHRoaXMudGFyZ2V0RWxlbWVudCxlKSx0aGlzfXVub2JzZXJ2ZSgpe3JldHVybiB0aGlzLl9vYnNlcnZlci5kaXNjb25uZWN0KCksdGhpc319ZnVuY3Rpb24gQmUoKXtjb25zdCBlPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKHIpLHQ9ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoaSksbj1kb2N1bWVudC5nZXRFbGVtZW50QnlJZChvKSxzPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGEpLGM9ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQobCk7aWYobnVsbD09dHx8dC5yZW1vdmUoKSxudWxsPT1ufHxuLnJlbW92ZSgpLG51bGw9PXN8fHMucmVtb3ZlKCksbnVsbD09Y3x8Yy5yZW1vdmUoKSxudWxsIT09ZSYmKHdpbmRvdy5vbmtleWRvd249bnVsbCxlLnJlbW92ZSgpKSx3aW5kb3cuX19UUkFOU1BBUkVOQ1lfT0JTRVJWRVJTX18pe2Zvcihjb25zdCBlIG9mIHdpbmRvdy5fX1RSQU5TUEFSRU5DWV9PQlNFUlZFUlNfXy52YWx1ZXMoKSllLnVub2JzZXJ2ZSgpO3dpbmRvdy5fX1RSQU5TUEFSRU5DWV9PQlNFUlZFUlNfXz12b2lkIDB9fSFmdW5jdGlvbigpe2lmKEJlKCksIShmdW5jdGlvbigpe2NvbnN0IGU9eGUoe2VsZW1lbnROYW1lOiJpZnJhbWUiLGFwcGVuZFRvOmRvY3VtZW50LmJvZHksaHRtbFByb3BzOnt9fSk7cmV0dXJuIGUuc3R5bGUuZGlzcGxheT0ibm9uZSIsbnVsbD09PWUuY29udGVudFdpbmRvdz8oYWxlcnQoUygiQ291bGQgbm90IGNyZWF0ZSB0aGUgbG9jYWxTdG9yYWdlIG9iamVjdCBiZWNhdXNlIHRoZSBpZnJhbWUncyBjb250ZW50V2luZG93IGlzbid0IGRlZmluZWQuIikpLCExKTood2luZG93LmxvY2FsU3RvcmFnZT1lLmNvbnRlbnRXaW5kb3cubG9jYWxTdG9yYWdlLCEwKX0oKSYmQWUoe2xlZ2FjeVN0b3JhZ2VLZXk6ImJnSW1nIixuZXdTdG9yYWdlS2V5OmUsZGVmYXVsdFZhbHVlOiIifSkmJkFlKHtsZWdhY3lTdG9yYWdlS2V5OiJicmdodG5zIixuZXdTdG9yYWdlS2V5OnQsZGVmYXVsdFZhbHVlOiI5In0pKXx8KGsuc2V0KCJpc0RhcmtUaGVtZSIsZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LmNsYXNzTGlzdC5jb250YWlucyhkKSkuc2V0KCJicmlnaHRuZXNzIixwYXJzZUludChudWxsIT09KHM9d2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKHQpKSYmdm9pZCAwIT09cz9zOiJ1bnNldCIpfHw5KS5zZXQoImJsdXIiLHBhcnNlSW50KG51bGwhPT0oYz13aW5kb3cubG9jYWxTdG9yYWdlLmdldEl0ZW0obikpJiZ2b2lkIDAhPT1jP2M6InVuc2V0Iil8fDApLnNldCgiYmFja2dyb3VuZEltYWdlVVJMIixudWxsIT09KG09d2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKGUpKSYmdm9pZCAwIT09bT9tOiIiKSwwKXx8IWZ1bmN0aW9uKCl7Y29uc3QgZT1rLmdldCgiaXNEYXJrVGhlbWUiKSx0PW5ldyBJZSh7dGFyZ2V0RWxlbWVudDpkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQsZnJvbTplP2Q6dSx0bzplP3U6ZH0pO3RyeXt0Lm9uVHJpZ2dlcigoKCk9PntrZSh7dGV4dDoi8J+bkSBJdCBsb29rcyBsaWtlIHRoZSB0aGVtZSBoYXMgY2hhbmdlZC4gRGlzY29yZCB3aWxsIHJlZnJlc2ggaW4gNSBzZWNvbmRzLiIsdGltZW91dDo1ZTN9KS50aGVuKCgoKT0+e3QudW5vYnNlcnZlKCksbG9jYXRpb24ucmVsb2FkKCl9KSl9KSkub2JzZXJ2ZSh7YXR0cmlidXRlczohMCxhdHRyaWJ1dGVGaWx0ZXI6WyJjbGFzcyJdfSl9Y2F0Y2goZSl7cmV0dXJuIGFsZXJ0KFMoZS5tZXNzYWdlKSksITF9cmV0dXJuIHguc2V0KCJ0aGVtZUNoYW5nZU9ic2VydmVyIix0KSwhMH0oKXx8KHdpbmRvdy5fX1RSQU5TUEFSRU5DWV9PQlNFUlZFUlNfXz14LDApfHwhZnVuY3Rpb24oKXtjb25zdCBlPWZ1bmN0aW9uKGUpe2NvbnN0IHQ9ZG9jdW1lbnQucXVlcnlTZWxlY3RvcihlKTtyZXR1cm4gbnVsbD09PXQ/KGFsZXJ0KFMoYCR7ZX0gaXMgbm90IGluIHRoZSBET00uYCkpLG51bGwpOnR9KFApO2lmKG51bGw9PT1lKXJldHVybiExO2NvbnN0IHQ9ZG9jdW1lbnQucXVlcnlTZWxlY3RvcihSKSxuPXhlKHtlbGVtZW50TmFtZToic3R5bGUiLGFwcGVuZFRvOmRvY3VtZW50LmhlYWQsaHRtbFByb3BzOntpZDpyLGlubmVySFRNTDokZSgpfX0pO3JldHVybiB3LnNldCgidGl0bGVCYXJFbGVtZW50Iix0KS5zZXQoIm92ZXJsYXlEYXJrZW5lckVsZW1lbnQiLGUpLnNldCgibWFpblN0eWxlU2hlZXQiLG4pLCEwfSgpKXJldHVybiB2b2lkIEJlKCk7dmFyIHMsYyxtO3hlKHtlbGVtZW50TmFtZToic3R5bGUiLGFwcGVuZFRvOmRvY3VtZW50LmhlYWQsaHRtbFByb3BzOntpZDppLGlubmVySFRNTDpDfX0pO2NvbnN0e2FjdGlvbnNQYW5lbDpwLHRvZ2dsZUFjdGlvbnNQYW5lbDpofT1mdW5jdGlvbigpe2NvbnN0IGU9eGUoe2VsZW1lbnROYW1lOiJkaXYiLGFwcGVuZFRvOmRvY3VtZW50LmJvZHksaHRtbFByb3BzOntpZDpvfX0pLHQ9eGUoe2VsZW1lbnROYW1lOiJkaXYiLGFwcGVuZFRvOmRvY3VtZW50LmJvZHksaHRtbFByb3BzOntpZDphfX0pLG49ZnVuY3Rpb24oZSx0KXtjb25zdCBuPXcuZ2V0KCJtYWluU3R5bGVTaGVldCIpO2xldCByO3JldHVybiBmdW5jdGlvbigpe2NvbnN0IG89ZS5jbGFzc0xpc3QuY29udGFpbnMoZyksYT1uLmhhc0F0dHJpYnV0ZSgibWVkaWEiKTtpZihjbGVhclRpbWVvdXQociksIW8mJiFhKXJldHVybiBlLnN0eWxlLmRpc3BsYXk9ImJsb2NrIix0LnN0eWxlLmRpc3BsYXk9ImJsb2NrIix2b2lkIHNldFRpbWVvdXQoKCgpPT5lLmNsYXNzTGlzdC5hZGQoZykpLDIwKTtvJiYoZS5jbGFzc0xpc3QucmVtb3ZlKGcpLHQuc3R5bGUuZGlzcGxheT0ibm9uZSIscj1zZXRUaW1lb3V0KCgoKT0+e2Uuc3R5bGUuZGlzcGxheT0ibm9uZSJ9KSwxZTMpKX19KGUsdCk7dC5vbmNsaWNrPW4seGUoe2VsZW1lbnROYW1lOiJzdHlsZSIsYXBwZW5kVG86ZG9jdW1lbnQuaGVhZCxodG1sUHJvcHM6e2lkOmwsaW5uZXJIVE1MOkx9fSksbnVsbCE9PXcuZ2V0KCJ0aXRsZUJhckVsZW1lbnQiKSYmKGUuc3R5bGUucGFkZGluZ1RvcD0iMjBweCIpO2Zvcihjb25zdCB0IG9mIE5lKXtjb25zdCBuPXhlKHtlbGVtZW50TmFtZToiZGl2IixhcHBlbmRUbzplLGh0bWxQcm9wczp7Y2xhc3NOYW1lOmIsdGV4dENvbnRlbnQ6dC5uYW1lfX0pO24ub25jbGljaz0oKT0+U2UodCxuKX1yZXR1cm57YWN0aW9uc1BhbmVsOmUsdG9nZ2xlQWN0aW9uc1BhbmVsOm59fSgpO2tlKHt0ZXh0OiJXZWxjb21lISA8YnI+PGJyPlRvIHRvZ2dsZSB0aGUgYWN0aW9ucyBwYW5lbCwgcHJlc3MgPGI+Q1RSTCArIFNISUZUICsgWDwvYj48YnI+VG8gdG9nZ2xlIHRoZSB0aGVtZSwgcHJlc3MgPGI+Q1RSTCArIEQ8L2I+Iix0aW1lb3V0OjFlNCxjb250YWluc0hUTUw6ITB9KS50aGVuKCgoKT0+eyhmdW5jdGlvbigpe2NvbnN0IGU9ay5nZXQoImlzRGFya1RoZW1lIiksdD13LmdldCgidGl0bGVCYXJFbGVtZW50Iik7aWYobnVsbCE9PXQpcmV0dXJuIWUmJnQuY2xhc3NMaXN0LmNvbnRhaW5zKGQpO2NvbnN0IG49ZG9jdW1lbnQucXVlcnlTZWxlY3RvcigiLmJnX183YWRiZiIpO3JldHVybiBudWxsIT09biYmIWUmJm4uY2xhc3NMaXN0LmNvbnRhaW5zKGQpfSkoKSYmKGtlKHt0ZXh0OiI8Yj7wn5uRIFdhcm5pbmc8L2I+PGJyPjxicj5Zb3VyIHNpZGViYXIgaXMgY3VycmVudGx5IGRhcmsgdGhlbWVkLjxicj5Gb3IgcHJvcGVyIHRleHQgdmlzaWJpbGl0eSwgZGlzYWJsZSBpdHMgZGFyayB0aGVtZSBpbiBTZXR0aW5ncyA+IEFwcGVhcmFuY2UuPGJyPjxicj5UaGUgdGhlbWUgaGFzIGRpc2FibGVkIGl0c2VsZi4gQWZ0ZXIgZGlzYWJsaW5nIHRoZSBzaWRlYmFyJ3MgZGFyayB0aGVtZSwgeW91IGNhbiByZWFjdGl2YXRlIHRoZSB0aGVtZSBieSBwcmVzc2luZyA8Yj5DVFJMICsgRDwvYj4uIDx1Pk5vIG5lZWQgdG8gcmVmcmVzaDwvdT4uIix0aW1lb3V0OjE1ZTMsY29udGFpbnNIVE1MOiEwfSksVChwKSl9KSksd2luZG93Lm9ua2V5ZG93bj1lPT57ZS5jdHJsS2V5JiYoZS5zaGlmdEtleSYmIktleVgiPT09ZS5jb2RlJiYoZS5wcmV2ZW50RGVmYXVsdCgpLGgoKSksIktleUQiPT09ZS5jb2RlJiYoZS5wcmV2ZW50RGVmYXVsdCgpLFQocCkpKX0sd2luZG93LmFkZEV2ZW50TGlzdGVuZXIoImNvbnRleHRtZW51IixSZSx7Y2FwdHVyZTohMH0pfSgpfSkoKTs=", 5 | "base64" 6 | ).toString("utf8") 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-transparency", 3 | "description": "A script allowing you to use custom background images for Discord", 4 | "scripts": { 5 | "build": "webpack script/script.ts", 6 | "build:no-injection-plugin": "webpack --env noInjectionPlugin script/script.ts" 7 | }, 8 | "author": "MWR", 9 | "license": "ISC", 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "@typescript-eslint/eslint-plugin": "^4.33.0", 13 | "@typescript-eslint/parser": "^4.33.0", 14 | "eslint": "^7.32.0", 15 | "eslint-config-prettier": "^8.5.0", 16 | "eslint-config-standard": "^16.0.3", 17 | "eslint-plugin-import": "^2.26.0", 18 | "eslint-plugin-node": "^11.1.0", 19 | "eslint-plugin-prettier": "^3.4.1", 20 | "eslint-plugin-promise": "^4.3.1", 21 | "prettier": "^2.7.1", 22 | "ts-loader": "^8.4.0", 23 | "typescript": "^4.8.3", 24 | "webpack": "^5.76.0", 25 | "webpack-cli": "^4.10.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /script/actions/backgroundChanger.ts: -------------------------------------------------------------------------------- 1 | import { IAction } from "../../types"; 2 | import { warningDuration } from "../configs/durations"; 3 | import { 4 | actionInputClassName, 5 | actionWarningClassName, 6 | imageInputPickerClassName, 7 | imageInputPickerHiddenID, 8 | } from "../configs/identifiers"; 9 | import { 10 | backgroundChangerTipText, 11 | imagePickerButtonText, 12 | imagePickerInputText, 13 | newErrorAlertText, 14 | } from "../configs/texts"; 15 | import changeBackgroundImage from "../utils/changeBackgroundImage"; 16 | import createAlert from "../utils/createAlert"; 17 | import createElement from "../utils/createElement"; 18 | 19 | /** 20 | * Changes the background image of an existing HTML element that sits behind every UI element. 21 | */ 22 | 23 | const backgroundChangerAction: IAction = { 24 | name: "Change the background image", 25 | 26 | execute(actionBox: HTMLDivElement) { 27 | const imageInputPickerLabel: HTMLLabelElement = createElement({ 28 | elementName: "label", 29 | appendTo: actionBox, 30 | htmlProps: { 31 | className: `${actionInputClassName} ${imageInputPickerClassName}`, 32 | htmlFor: imageInputPickerHiddenID, 33 | textContent: imagePickerButtonText, 34 | }, 35 | }); 36 | 37 | const imageInputPickerHidden: HTMLInputElement = createElement({ 38 | elementName: "input", 39 | appendTo: actionBox, 40 | htmlProps: { type: "file", id: imageInputPickerHiddenID, accept: "image/*", style: { display: "none" } }, 41 | }); 42 | 43 | createElement({ 44 | elementName: "p", 45 | appendTo: actionBox, 46 | htmlProps: { textContent: "- or -" }, 47 | }); 48 | 49 | const imageChangerByURLInput: HTMLInputElement = createElement({ 50 | elementName: "input", 51 | appendTo: actionBox, 52 | htmlProps: { className: actionInputClassName, placeholder: imagePickerInputText }, 53 | }); 54 | 55 | createElement({ 56 | elementName: "p", 57 | appendTo: actionBox, 58 | htmlProps: { className: actionWarningClassName, textContent: backgroundChangerTipText }, 59 | }); 60 | 61 | imageInputPickerHidden.onchange = () => initialiseImageReader(imageInputPickerHidden, imageInputPickerLabel); 62 | 63 | imageChangerByURLInput.onkeydown = (event: KeyboardEvent) => { 64 | const backgroundImageURL: string = imageChangerByURLInput.value.trim(); 65 | if (event.code === "Enter" && backgroundImageURL !== "") { 66 | imageInputPickerLabel.textContent = imagePickerButtonText; 67 | imageInputPickerHidden.value = ""; 68 | changeBackgroundImage(backgroundImageURL); 69 | } 70 | }; 71 | }, 72 | }; 73 | 74 | function initialiseImageReader(imageInputPickerHidden: HTMLInputElement, imageInputPickerLabel: HTMLLabelElement) { 75 | if (imageInputPickerHidden.files === null || imageInputPickerHidden.files.length === 0) return; 76 | 77 | const chosenImage: File = imageInputPickerHidden.files[0]; 78 | const imageReader = new FileReader(); 79 | 80 | imageReader.readAsDataURL(chosenImage); 81 | imageReader.addEventListener("loadstart", () => { 82 | imageInputPickerLabel.textContent = "Loading image..."; 83 | }); 84 | 85 | imageReader.addEventListener("loadend", () => { 86 | // May only be string | null because reading was done in a Data URL format. 87 | const imageData: string | null = imageReader.result as string | null; 88 | 89 | if (imageData === null) { 90 | createAlert({ text: newErrorAlertText("image data could not be read"), timeout: warningDuration }); 91 | return; 92 | } 93 | 94 | changeBackgroundImage(imageData); 95 | const imageExtension: string = chosenImage.name.split(".")[1] || "???"; 96 | const shortenedImageName: string = 97 | chosenImage.name.length > 25 ? `(${chosenImage.name.slice(0, 25)}...).${imageExtension}` : chosenImage.name; 98 | 99 | imageInputPickerLabel.innerHTML = 100 | chosenImage !== null 101 | ? `${imagePickerButtonText} | Image name: ${ 102 | chosenImage.name.length > 25 103 | ? `${shortenedImageName}` 104 | : chosenImage.name 105 | }` 106 | : imagePickerButtonText; 107 | }); 108 | 109 | imageReader.addEventListener("error", (readerEvent: ProgressEvent) => 110 | createAlert({ 111 | text: newErrorAlertText( 112 | `image reader gave error${ 113 | readerEvent.lengthComputable 114 | ? `, only ${((readerEvent.loaded / readerEvent.total) * 100).toPrecision(2)}% of image loaded` 115 | : "" 116 | }}` 117 | ), 118 | timeout: warningDuration, 119 | }) 120 | ); 121 | } 122 | 123 | export default backgroundChangerAction; 124 | -------------------------------------------------------------------------------- /script/actions/blurTweaker.ts: -------------------------------------------------------------------------------- 1 | import { IAction, NullableHTMLElement } from "../../types"; 2 | import { levelUpdateDuration } from "../configs/durations"; 3 | import { 4 | actionInputClassName, 5 | actionLevelIndicatorClassName, 6 | blurStorageKey, 7 | saveLevelClassName, 8 | } from "../configs/identifiers"; 9 | import { importantElementsStore, preferencesStore } from "../stores"; 10 | import createAlert from "../utils/createAlert"; 11 | import createElement from "../utils/createElement"; 12 | 13 | /** 14 | * Tweaks the blur of the background picture by changing the CSS backdrop-filter 15 | * property of an HTML element that stays on top of the background image. 16 | */ 17 | 18 | const blurTweaker: IAction = { 19 | name: "Change the blur level", 20 | execute(actionBox: HTMLDivElement) { 21 | const blurLevel = preferencesStore.get("blur") as number; 22 | 23 | const blurLevelIndicator: HTMLHeadingElement = createElement({ 24 | elementName: "h3", 25 | appendTo: actionBox, 26 | htmlProps: { className: actionLevelIndicatorClassName, textContent: `Blur level: ${blurLevel}` }, 27 | }); 28 | 29 | const blurLevelSlider: HTMLInputElement = createElement({ 30 | elementName: "input", 31 | appendTo: actionBox, 32 | htmlProps: { 33 | className: actionInputClassName, 34 | type: "range", 35 | min: "0", 36 | max: "50", 37 | value: blurLevel.toString(), 38 | oninput: () => updateBlur(blurLevelIndicator, blurLevelSlider), 39 | }, 40 | }); 41 | 42 | createElement({ 43 | elementName: "button", 44 | appendTo: actionBox, 45 | htmlProps: { className: saveLevelClassName, textContent: "Save blur level", onclick: saveBlurLevel }, 46 | }); 47 | }, 48 | }; 49 | 50 | function updateBlur(blurLevelIndicator: HTMLHeadingElement, blurLevelSlider: HTMLInputElement): void { 51 | const overlayDarkenerElement = importantElementsStore.get("overlayDarkenerElement") as HTMLDivElement; 52 | const titleBarElement = importantElementsStore.get("titleBarElement") as NullableHTMLElement; 53 | 54 | // https://githubmemory.com/repo/microsoft/TypeScript-DOM-lib-generator/issues/1118 55 | type BackdropFilterSupport = CSSStyleDeclaration & { backdropFilter: string }; 56 | 57 | (overlayDarkenerElement.style as BackdropFilterSupport).backdropFilter = `blur(${blurLevelSlider.value}px)`; 58 | 59 | if (titleBarElement !== null) 60 | (titleBarElement.style as BackdropFilterSupport).backdropFilter = `blur(${blurLevelSlider.value}px)`; 61 | 62 | blurLevelIndicator.textContent = `Blur level: ${blurLevelSlider.value}`; 63 | preferencesStore.set("blur", parseInt(blurLevelSlider.value)); 64 | } 65 | 66 | function saveBlurLevel(): void { 67 | const blurLevel = preferencesStore.get("blur") as number; 68 | window.localStorage.setItem(blurStorageKey, blurLevel.toString()); 69 | 70 | createAlert({ 71 | text: `Blur level updated! Now it is at level ${blurLevel}.`, 72 | containsHTML: true, 73 | timeout: levelUpdateDuration, 74 | }); 75 | } 76 | 77 | export default blurTweaker; 78 | -------------------------------------------------------------------------------- /script/actions/brightnessTweaker.ts: -------------------------------------------------------------------------------- 1 | import { IAction, NullableHTMLElement } from "../../types"; 2 | import { levelUpdateDuration } from "../configs/durations"; 3 | import { 4 | actionInputClassName, 5 | actionLevelIndicatorClassName, 6 | brightnessStorageKey, 7 | saveLevelClassName, 8 | } from "../configs/identifiers"; 9 | import { importantElementsStore, preferencesStore } from "../stores"; 10 | import createAlert from "../utils/createAlert"; 11 | import createElement from "../utils/createElement"; 12 | 13 | /** 14 | * Tweaks the brightness of the background picture by changing the opacity of an 15 | * HTML element that stays on top of the background image. 16 | */ 17 | 18 | const brightnessTweakerAction: IAction = { 19 | name: "Change the brightness level", 20 | execute(actionBox: HTMLDivElement) { 21 | const overlayColor = (preferencesStore.get("isDarkTheme") as boolean) ? "0,0,0" : "255,255,255"; 22 | const brightnessLevel = preferencesStore.get("brightness") as number; 23 | 24 | const brightnessLevelIndicator: HTMLHeadingElement = createElement({ 25 | elementName: "h3", 26 | appendTo: actionBox, 27 | htmlProps: { className: actionLevelIndicatorClassName, textContent: `Brightness level: ${brightnessLevel}` }, 28 | }); 29 | 30 | const brightnessLevelSlider: HTMLInputElement = createElement({ 31 | elementName: "input", 32 | appendTo: actionBox, 33 | htmlProps: { 34 | className: actionInputClassName, 35 | type: "range", 36 | min: "0", 37 | max: "9", 38 | value: brightnessLevel.toString(), 39 | oninput: () => updateBrightness({ overlayColor, brightnessLevelIndicator, brightnessLevelSlider }), 40 | }, 41 | }); 42 | 43 | createElement({ 44 | elementName: "button", 45 | appendTo: actionBox, 46 | htmlProps: { className: saveLevelClassName, textContent: "Save brightness level", onclick: saveBrightnessLevel }, 47 | }); 48 | }, 49 | }; 50 | 51 | interface UpdateBrightnessParams { 52 | overlayColor: string; 53 | brightnessLevelIndicator: HTMLHeadingElement; 54 | brightnessLevelSlider: HTMLInputElement; 55 | } 56 | 57 | function updateBrightness({ 58 | overlayColor, 59 | brightnessLevelIndicator, 60 | brightnessLevelSlider, 61 | }: UpdateBrightnessParams): void { 62 | const overlayDarkenerElement = importantElementsStore.get("overlayDarkenerElement") as HTMLDivElement; 63 | const titleBarElement = importantElementsStore.get("titleBarElement") as NullableHTMLElement; 64 | 65 | overlayDarkenerElement.style.backgroundColor = `rgba(${overlayColor}, .${brightnessLevelSlider.value})`; 66 | 67 | if (titleBarElement !== null) 68 | titleBarElement.style.backgroundColor = `rgba(${overlayColor}, .${brightnessLevelSlider.value})`; 69 | 70 | brightnessLevelIndicator.textContent = `Brightness level: ${brightnessLevelSlider.value}`; 71 | preferencesStore.set("brightness", parseInt(brightnessLevelSlider.value)); 72 | } 73 | 74 | function saveBrightnessLevel(): void { 75 | const brightnessLevel = preferencesStore.get("brightness") as number; 76 | window.localStorage.setItem(brightnessStorageKey, brightnessLevel.toString()); 77 | 78 | createAlert({ 79 | text: `Brightness level updated! Now it is at level ${brightnessLevel}.`, 80 | containsHTML: true, 81 | timeout: levelUpdateDuration, 82 | }); 83 | } 84 | 85 | export default brightnessTweakerAction; 86 | -------------------------------------------------------------------------------- /script/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { IAction } from "../../types"; 2 | import backgroundChanger from "./backgroundChanger"; 3 | import blurTweaker from "./blurTweaker"; 4 | import brightnessTweaker from "./brightnessTweaker"; 5 | 6 | const actions: IAction[] = [backgroundChanger, brightnessTweaker, blurTweaker]; 7 | 8 | export default actions; 9 | -------------------------------------------------------------------------------- /script/actions/toggleTheme.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { actionsPanelActiveClassName, titleBarElementBackgroundColor } from "../configs/identifiers"; 3 | import { importantElementsStore, preferencesStore } from "../stores"; 4 | 5 | /** 6 | * Toggles the appearance between the default look of Discord and the transparency theme. 7 | */ 8 | 9 | export default function toggleTheme(actionsPanel: HTMLElement): void { 10 | const mainStyleSheet = importantElementsStore.get("mainStyleSheet") as HTMLStyleElement; 11 | const titleBarElement = importantElementsStore.get("titleBarElement") as NullableHTMLElement; 12 | 13 | if (!mainStyleSheet.hasAttribute("media")) { 14 | mainStyleSheet.setAttribute("media", "1px"); 15 | if (titleBarElement !== null) titleBarElement.style.backgroundColor = titleBarElementBackgroundColor; 16 | actionsPanel.style.display = "none"; 17 | return; 18 | } 19 | 20 | const isDarkTheme = preferencesStore.get("isDarkTheme") as boolean; 21 | const brightnessLevel = preferencesStore.get("brightness") as number; 22 | 23 | mainStyleSheet.removeAttribute("media"); 24 | 25 | if (titleBarElement !== null) 26 | titleBarElement.style.backgroundColor = isDarkTheme 27 | ? `rgba(0,0,0,0.${brightnessLevel}` 28 | : `rgba(255,255,255,0.${brightnessLevel}`; 29 | 30 | const isActionsPanelActive = actionsPanel.classList.contains(actionsPanelActiveClassName); 31 | if (isActionsPanelActive) actionsPanel.style.display = "block"; 32 | } 33 | -------------------------------------------------------------------------------- /script/configs/classNames.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * All the affected elements that are styled so everything looks correctly. 3 | */ 4 | 5 | // Overlays 6 | export const titleBar = 7 | ".typeWindows_a934d8.withFrame_a934d8.titleBar_a934d8, .typeWindows_a934d8.withFrame_a934d8.titleBar_a934d8.withBackgroundOverride_a934d8"; 8 | export const overlayDarkener = ".app_bd26cc"; 9 | export const popupsContainer = ".layerContainer_cd0de5"; 10 | 11 | // Special elements with purpose other than styling the elements itself 12 | export const sidebarDarkThemeIndicator = ".bg__7adbf"; 13 | 14 | // Popups/Popouts of any kind 15 | export const accountSwitchPopup = ".toast_f42767"; 16 | export const threadsPopup = ".browser_f542fc"; 17 | export const pinnedMessagesPopup = ".messagesPopoutWrap_ac90a2"; 18 | export const inboxPopup = ".recentMentionsPopout_ddb5b4"; 19 | export const quickSwitcher = ".quickswitcher_f4e139"; 20 | export const createPoll = ".container_f24a96"; 21 | export const pollDurationPicker = ".popout_f6639d"; 22 | export const onJoinServerPopout = ".main_e3f8c2"; 23 | export const messageContextMenu = ".menu_d90b3d"; 24 | export const streamPreviewPopup = ".streamPreview_adbea6"; 25 | export const userVoiceActivityHoverPopout = ".popout_a9414b"; 26 | 27 | // Tabs 28 | export const nitroTab = ".applicationStore_cecc86"; 29 | export const discussionsTab = ".container-3wLKDe"; 30 | export const discussionsTabTitle = ".themed-Hp1KC_"; 31 | export const appDirectoryTab = ".directoryContainer_da3f59"; 32 | export const rolesAndChannelsTab = ".header_f1fd9c, .container_e44302, .chat_a7d72e, .scrollerContainer_c6b11b"; 33 | export const discoverServersTab = ".pageWrapper_a3a4ce"; 34 | 35 | // Significant subsections of the screen (panes) 36 | export const searchResultsPane = ".searchResultsWrap_c2b47d"; 37 | export const themeEditorPane = ".themeEditor_c9dda6"; 38 | export const previewAppIconsPane = ".editor_bcfa1e"; 39 | export const threadPane = ".container_d1c246"; 40 | 41 | // Messages and everything about them 42 | export const allMessages = ".wrapper_f9f2ca"; 43 | export const blockedMessages = ".blockedSystemMessage_c09d0f"; 44 | export const embedBackground = ".embedFull_b0068a"; 45 | export const invites = ".wrapper_a71a1c"; 46 | export const codeBlocks = ".code.inline, .markup-2BOw-j code, code"; 47 | export const downloadAttachment = ".attachment_b52bef"; 48 | export const audioAttachment = ".wrapperAudio_f72aac"; 49 | export const messageOptionsHoverPopout = ".wrapper_ef319f"; 50 | export const retryConnectionOverlay = ".container_e40c16"; 51 | export const reactionsBackground = ".reaction_ec6b19"; 52 | export const attachmentButtons = ".hoverButtonGroup_ab8b23"; 53 | 54 | // User info modals and elements 55 | export const oldUserInfoModalNonPremium = ".userProfileInnerThemedNonPremium_c69a7b"; 56 | export const rolesContainerManualAdd = ".container_ac201b"; 57 | 58 | // Emoji pickers and everything about emojis 59 | export const emojiGIFStickerPickers = ".contentWrapper_af5dbb"; 60 | export const reactionsPicker = ".wrapper_c6ee36"; 61 | export const viewReactions = ".container_f2bfbb, .reactors_f2bfbb"; 62 | export const stickyEmojiReactionPickerHeaders = ".wrapper_e06857"; 63 | export const superReactionsNoNitro = ".wrapper_c43059"; 64 | export const cantReactWithAnimatedEmote = ".premiumPromo_ca9b56"; 65 | 66 | // Server settings tab 67 | export const stickyRoleManageHeader = ".header_bd05f1"; 68 | export const rolePreviewArea = ".previewContainer_da1bd9"; 69 | -------------------------------------------------------------------------------- /script/configs/durations.ts: -------------------------------------------------------------------------------- 1 | export const welcomeDuration = 10000; 2 | export const warningDuration = 10000; 3 | export const sidebarDarkThemedWarningDuration = 15000; 4 | export const themeChangeWarningDuration = 5000; 5 | export const levelUpdateDuration = 3000; 6 | export const actionSlideInAnimationDuration = 1000; 7 | export const noFindContextMenuObserverTimeoutDuration = 5000; 8 | export const missingMenuBarAlertDuration = 3000; 9 | -------------------------------------------------------------------------------- /script/configs/identifiers.ts: -------------------------------------------------------------------------------- 1 | export const backgroundImageStorageKey = "__BACKGROUND-IMAGE__"; 2 | export const legacyBackgroundImageStorageKey = "bgImg"; 3 | 4 | export const brightnessStorageKey = "__BRIGHTNESS__"; 5 | export const legacyBrightnessStorageKey = "brghtns"; 6 | 7 | export const blurStorageKey = "__BLUR__"; 8 | 9 | export const generalTransparencyID = "TRANSPARENCY"; 10 | export const actionsPanelID = `${generalTransparencyID}__ACTIONS-PANEL`; 11 | export const actionsPanelOverlayID = `${actionsPanelID}-OVERLAY`; 12 | export const actionsPanelStyleSheetID = `${generalTransparencyID}__ACTIONS_PANEL`; 13 | export const alertsStyleSheetID = `${generalTransparencyID}__ALERTS-STYLESHEET`; 14 | export const imageInputPickerHiddenID = `${generalTransparencyID}__IMAGE-INPUT-PICKER`; 15 | export const setAsBackgroundImageButtonID = `${generalTransparencyID}__SET-BACKGROUND`; 16 | 17 | export const generalDarkThemeClassName = "theme-dark"; 18 | export const generalWhiteThemeClassName = "theme-light"; 19 | export const transparencyAlertClassName = `${generalTransparencyID}__ALERT`; 20 | export const transparencyAlertInactiveClassName = `${transparencyAlertClassName}--INACTIVE`; 21 | export const actionsPanelActiveClassName = `${actionsPanelID}--SLIDE-IN`; 22 | export const actionClassName = `${generalTransparencyID}__ACTION`; 23 | export const actionActiveClassName = `${actionClassName}--ACTIVE`; 24 | export const actionWarningClassName = `${actionClassName}-WARNING`; 25 | export const actionInputClassName = `${actionClassName}-INPUT`; 26 | export const imageInputPickerClassName = `${actionClassName}__INPUT-PICKER`; 27 | export const actionLevelIndicatorClassName = `${actionClassName}-LEVEL-INDICATOR`; 28 | export const saveLevelClassName = `${actionClassName}-SAVE-LEVEL`; 29 | 30 | export const titleBarElementBackgroundColor = "var(--color-tertiary)"; 31 | -------------------------------------------------------------------------------- /script/configs/texts.ts: -------------------------------------------------------------------------------- 1 | // Texts 2 | export const welcomeText = 3 | "Welcome!

To toggle the actions panel, press CTRL + SHIFT + X
To toggle the theme, press CTRL + D"; 4 | export const sidebarAlertText = 5 | "🛑 Warning

Your sidebar is currently dark themed.
For proper text visibility, disable its dark theme in Settings > Appearance.

The theme has disabled itself. After disabling the sidebar's dark theme, you can reactivate the theme by pressing CTRL + D. No need to refresh."; 6 | export const backgroundChangerTipText = 7 | "🛑 The image you want as your background must be sent as a message on Discord. You will have to click on the image, press on , and copy the link from there. After that, click on this input and press ENTER."; 8 | export const themeChangeDetectionText = "🛑 It looks like the theme has changed. Discord will refresh in 5 seconds."; 9 | export const imagePickerButtonText = "Press here to browse for an image on your computer"; 10 | export const imagePickerInputText = "Paste image link here"; 11 | export const setBackgroundButtonText = "Set as background image 🌟"; 12 | export const missingMenuBarText = "Default menu element not found. Searching manually..."; 13 | 14 | // Functions 15 | export function newErrorAlertText(errorMessage: string): string { 16 | return `🛑 Oh no, there's been an error. If you see this, join the support server, and report the error. [[${errorMessage}]]`; 17 | } 18 | -------------------------------------------------------------------------------- /script/initActionsPanel.ts: -------------------------------------------------------------------------------- 1 | import { IAction, NullableHTMLElement } from "../types"; 2 | import actions from "./actions"; 3 | import { actionSlideInAnimationDuration } from "./configs/durations"; 4 | import { 5 | actionActiveClassName, 6 | actionClassName, 7 | actionsPanelActiveClassName, 8 | actionsPanelID, 9 | actionsPanelOverlayID, 10 | actionsPanelStyleSheetID, 11 | } from "./configs/identifiers"; 12 | import { newErrorAlertText } from "./configs/texts"; 13 | import { importantElementsStore } from "./stores"; 14 | import { actionsPanelCSSCode } from "./styles"; 15 | import createElement from "./utils/createElement"; 16 | 17 | /** 18 | * @returns an object with the actions panel and a toggling function 19 | */ 20 | export default function initActionsPanel(): { 21 | actionsPanel: HTMLDivElement; 22 | toggleActionsPanel: () => void; 23 | } { 24 | const actionsPanel: HTMLDivElement = createElement({ 25 | elementName: "div", 26 | appendTo: document.body, 27 | htmlProps: { id: actionsPanelID }, 28 | }); 29 | 30 | // An element to toggle the actions panel without having to perform the key combination. 31 | const actionsPanelOverlay: HTMLDivElement = createElement({ 32 | elementName: "div", 33 | appendTo: document.body, 34 | htmlProps: { id: actionsPanelOverlayID }, 35 | }); 36 | 37 | const toggleActionsPanel: () => void = initActionsPanelToggler(actionsPanel, actionsPanelOverlay); 38 | actionsPanelOverlay.onclick = toggleActionsPanel; 39 | 40 | // Styles the actions panel. 41 | createElement({ 42 | elementName: "style", 43 | appendTo: document.head, 44 | htmlProps: { id: actionsPanelStyleSheetID, innerHTML: actionsPanelCSSCode }, 45 | }); 46 | 47 | const titleBarElement = importantElementsStore.get("titleBarElement") as NullableHTMLElement; 48 | if (titleBarElement !== null) actionsPanel.style.paddingTop = "20px"; 49 | 50 | for (const action of actions) { 51 | const actionBox: HTMLDivElement = createElement({ 52 | elementName: "div", 53 | appendTo: actionsPanel, 54 | htmlProps: { 55 | className: actionClassName, 56 | textContent: action.name, 57 | }, 58 | }); 59 | 60 | actionBox.onclick = () => activateAction(action, actionBox); 61 | } 62 | 63 | return { actionsPanel, toggleActionsPanel }; 64 | } 65 | 66 | /** 67 | * Initialises some variables related to the toggling of the panel inside its body. 68 | * @param {HTMLDivElement} actionsPanel - the actions panel 69 | * @param {HTMLDivElement} actionsPanelOverlay - the actions panel overlay 70 | * @returns a function as a closure which accesses those variables, in order to minimise the need 71 | * of global variables. This returned function toggles the actions panel. 72 | */ 73 | 74 | function initActionsPanelToggler(actionsPanel: HTMLDivElement, actionsPanelOverlay: HTMLDivElement): () => void { 75 | const mainStyleSheet = importantElementsStore.get("mainStyleSheet") as HTMLStyleElement; 76 | /** 77 | * @var previousActionsPanelRemovalTimeoutID - the timeout ID 78 | * of the previous time the action panel was to be removed. We need this in order 79 | * to reduce visual glitches when spamming or timing, just right, the theme and 80 | * actions panel toggling. 81 | */ 82 | let previousActionsPanelRemovalTimeoutID: ReturnType; 83 | 84 | return function toggleActionsPanel(): void { 85 | const isActionsPanelActive: boolean = actionsPanel.classList.contains(actionsPanelActiveClassName); 86 | const isThemeDisabled: boolean = mainStyleSheet.hasAttribute("media"); 87 | clearTimeout(previousActionsPanelRemovalTimeoutID); 88 | 89 | if (!isActionsPanelActive && !isThemeDisabled) { 90 | actionsPanel.style.display = "block"; 91 | actionsPanelOverlay.style.display = "block"; 92 | 93 | // When you time toggling the actions panel just right, you can make it appear without an animation. 94 | // So, delay adding the class for a bit, until the DOM registers that the panel now has a block display. 95 | setTimeout(() => actionsPanel.classList.add(actionsPanelActiveClassName), 20); 96 | return; 97 | } 98 | 99 | if (isActionsPanelActive) { 100 | actionsPanel.classList.remove(actionsPanelActiveClassName); 101 | actionsPanelOverlay.style.display = "none"; 102 | 103 | previousActionsPanelRemovalTimeoutID = setTimeout(() => { 104 | actionsPanel.style.display = "none"; 105 | }, actionSlideInAnimationDuration); 106 | } 107 | }; 108 | } 109 | 110 | /** 111 | * @param {IAction} action - the action to activate 112 | * @param {HTMLDivElement} actionBox - the rectangle in the actions panel, that when clicked, reveals the action 113 | */ 114 | 115 | function activateAction(action: IAction, actionBox: HTMLDivElement): void { 116 | actionBox.classList.add(actionActiveClassName); 117 | actionBox.textContent = ""; 118 | try { 119 | actionBox.onclick = null; 120 | action.execute(actionBox); 121 | } catch (error) { 122 | alert(newErrorAlertText(error.message)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /script/script.ts: -------------------------------------------------------------------------------- 1 | import toggleTheme from "./actions/toggleTheme"; 2 | import { sidebarDarkThemedWarningDuration, welcomeDuration } from "./configs/durations"; 3 | import { alertsStyleSheetID } from "./configs/identifiers"; 4 | import { sidebarAlertText, welcomeText } from "./configs/texts"; 5 | import initActionsPanel from "./initActionsPanel"; 6 | import { alertsCSSCode } from "./styles"; 7 | import contextMenuHandler from "./utils/contextMenuHandler"; 8 | import createAlert from "./utils/createAlert"; 9 | import createElement from "./utils/createElement"; 10 | import getSidebarThemeState from "./utils/getSidebarThemeState"; 11 | import initImportantElementsStore from "./utils/initImportantElementsStore"; 12 | import initLocalStorage from "./utils/initLocalStorage"; 13 | import initObservers from "./utils/initObservers"; 14 | import initPreferencesStore from "./utils/initPreferencesStore"; 15 | import removeExistingCodeFootprint from "./utils/removeExistingCodeFootprint"; 16 | 17 | (function main(): void { 18 | removeExistingCodeFootprint(); 19 | // Order of calls matters. 20 | if (!initLocalStorage() || !initPreferencesStore() || !initObservers() || !initImportantElementsStore()) { 21 | removeExistingCodeFootprint(); 22 | return; 23 | } 24 | 25 | createElement({ 26 | elementName: "style", 27 | appendTo: document.head, 28 | htmlProps: { id: alertsStyleSheetID, innerHTML: alertsCSSCode }, 29 | }); 30 | 31 | const { actionsPanel, toggleActionsPanel }: { actionsPanel: HTMLDivElement; toggleActionsPanel: () => void } = 32 | initActionsPanel(); 33 | 34 | createAlert({ text: welcomeText, timeout: welcomeDuration, containsHTML: true }).then(() => { 35 | const isSidebarDarkThemed: boolean = getSidebarThemeState(); 36 | if (isSidebarDarkThemed) { 37 | createAlert({ text: sidebarAlertText, timeout: sidebarDarkThemedWarningDuration, containsHTML: true }); 38 | toggleTheme(actionsPanel); 39 | } 40 | }); 41 | 42 | window.onkeydown = (event: KeyboardEvent) => { 43 | if (!event.ctrlKey) return; 44 | 45 | if (event.shiftKey && event.code === "KeyX") { 46 | event.preventDefault(); // Prevents the right-to-left left-to-right text toggle in some browsers. 47 | toggleActionsPanel(); 48 | } 49 | 50 | if (event.code === "KeyD") { 51 | event.preventDefault(); // Prevents the bookmark shortcut in some browsers. 52 | toggleTheme(actionsPanel); 53 | } 54 | }; 55 | 56 | /** 57 | * Event property handlers do not allow setting the capture flag. We need to use the capture phase because Discord 58 | * disabled event propagation. Because of that, we can't add a custom context menu event handler to happen in the 59 | * bubbling phase. Discord applied it to a more specific element than the "window" object. 60 | */ 61 | window.addEventListener("contextmenu", contextMenuHandler, { capture: true }); 62 | })(); 63 | -------------------------------------------------------------------------------- /script/stores/importantElementsStore.ts: -------------------------------------------------------------------------------- 1 | import { IImportantElementsStore } from "../../types"; 2 | export default new Map(); 3 | -------------------------------------------------------------------------------- /script/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as importantElementsStore } from "./importantElementsStore"; 2 | export { default as observersStore } from "./observersStore"; 3 | export { default as preferencesStore } from "./preferencesStore"; 4 | -------------------------------------------------------------------------------- /script/stores/observersStore.ts: -------------------------------------------------------------------------------- 1 | import { IMutationObserver } from "../../types"; 2 | 3 | export default new Map(); 4 | -------------------------------------------------------------------------------- /script/stores/preferencesStore.ts: -------------------------------------------------------------------------------- 1 | import { IPreferencesStore } from "../../types"; 2 | export default new Map(); 3 | -------------------------------------------------------------------------------- /script/styles/actionsPanelCSSCode.ts: -------------------------------------------------------------------------------- 1 | import { 2 | actionActiveClassName, 3 | actionClassName, 4 | actionInputClassName, 5 | actionLevelIndicatorClassName, 6 | actionWarningClassName, 7 | imageInputPickerClassName, 8 | saveLevelClassName, 9 | setAsBackgroundImageButtonID, 10 | } from "../configs/identifiers"; 11 | 12 | export default `.${actionClassName} { 13 | min-height: 100px; 14 | background-color: rgba(0, 0, 0, 0.5); 15 | margin: 10px; 16 | color: white; 17 | cursor: pointer; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | align-items: center; 22 | padding: 30px 50px; 23 | transition: background-color 300ms linear; 24 | } 25 | 26 | .${actionActiveClassName} { 27 | background-color: rgba(0, 0, 0, 0.8); 28 | cursor: initial; 29 | } 30 | 31 | .${actionInputClassName} { 32 | box-sizing: border-box; 33 | text-align: center; 34 | padding: 15px 0; 35 | width: 100%; 36 | border: 0; 37 | background-color: rgba(255, 255, 255, 0.1); 38 | border-radius: 50px; 39 | color: white; 40 | } 41 | 42 | .${imageInputPickerClassName} { 43 | padding: 20px 15px !important; 44 | cursor: pointer; 45 | font-family: var(--font-primary); 46 | font-size: smaller; 47 | } 48 | 49 | .${actionInputClassName}:focus, 50 | .${saveLevelClassName}:focus { 51 | outline: none; 52 | border: 2px solid dodgerblue; 53 | } 54 | 55 | .${actionLevelIndicatorClassName} { 56 | margin: 0; 57 | opacity: 0.8; 58 | text-align: center; 59 | } 60 | 61 | .${actionWarningClassName} { 62 | display: none; 63 | font-size: 11px; 64 | line-height: 20px; 65 | color: white; 66 | max-width: 310px; 67 | margin: 20px 0 0; 68 | opacity: 0.8; 69 | } 70 | 71 | .${actionInputClassName}:focus ~ .${actionWarningClassName} { 72 | display: block; 73 | } 74 | 75 | .${saveLevelClassName} { 76 | height: 40px; 77 | padding: 0 15px; 78 | max-width: 300px; 79 | margin-top: 10px; 80 | border: 0; 81 | background-color: rgba(255, 255, 255, 0.1); 82 | border-radius: 50px; 83 | color: white; 84 | cursor: pointer; 85 | } 86 | 87 | #${setAsBackgroundImageButtonID} { 88 | position: absolute; 89 | left: -100%; 90 | bottom: 0; 91 | width: max-content; 92 | padding: 10px; 93 | margin-left: -15px; 94 | background-color: inherit; 95 | border-radius: inherit; 96 | font-size: 14px; 97 | cursor: pointer; 98 | }`; 99 | -------------------------------------------------------------------------------- /script/styles/alertsCSSCode.ts: -------------------------------------------------------------------------------- 1 | import { transparencyAlertClassName, transparencyAlertInactiveClassName } from "../configs/identifiers"; 2 | 3 | export default `.${transparencyAlertClassName} { 4 | position: absolute; 5 | left: 0px; 6 | right: 0px; 7 | margin: auto; 8 | padding: 20px; 9 | font-size: 14px; 10 | width: fit-content; 11 | width: -moz-fit-content; 12 | max-width: 450px; 13 | background-color: rgba(0,0,0,0.6); 14 | backdrop-filter: blur(8px); 15 | color: white; 16 | border-bottom-right-radius: 20px; 17 | border-bottom-left-radius: 20px; 18 | z-index: 10000; 19 | box-shadow: 0px 13px 10px -5px rgba(0,0,0,0.5); 20 | transition: transform 500ms ease; 21 | animation: TRANSPARENCY__ALERT-SLIDE 500ms ease; 22 | overflow: hidden; 23 | } 24 | 25 | .${transparencyAlertClassName}::after { 26 | content: ""; 27 | position: absolute; 28 | bottom: 0; 29 | left: 0; 30 | background-color: hsl(235 100% 82% / 1); 31 | width: 100%; 32 | height: 5px; 33 | animation: TRANSPARENCY__ALERT-TIMER-BAR linear forwards; 34 | animation-duration: var(--timer-bar-timeout); 35 | } 36 | 37 | @keyframes TRANSPARENCY__ALERT-TIMER-BAR { 38 | from { 39 | transform: translateX(-100%); 40 | } to { 41 | transform: translateX(0); 42 | } 43 | } 44 | 45 | @keyframes TRANSPARENCY__ALERT-SLIDE { 46 | from { transform: translateY(-130%); } 47 | to { transform: translateY(0) } 48 | } 49 | 50 | .${transparencyAlertInactiveClassName} { 51 | transform: translateY(-130%); 52 | }`; 53 | -------------------------------------------------------------------------------- /script/styles/createMainCSSCode.ts: -------------------------------------------------------------------------------- 1 | import { preferencesStore } from "../stores"; 2 | import rawCSSCode from "./rawCSSCode"; 3 | 4 | /** 5 | * Assembles the CSS code for the main stylesheet, that styles everything and allows for transparency. It concatenates 6 | * the class names of the elements needed for everything to be displayed correctly. 7 | * @returns stylesheet's CSS code 8 | */ 9 | 10 | export default function createMainCSSCode(): string { 11 | const brightnessLevel = preferencesStore.get("brightness") as number; 12 | const isDarkTheme = preferencesStore.get("isDarkTheme") as boolean; 13 | /** 14 | * @var {brightnessLevelRGBA} - controls how opaque the background image should be 15 | */ 16 | const brightnessLevelRGBA = isDarkTheme 17 | ? `rgba(0, 0, 0, 0.${brightnessLevel})` || "rgba(0,0,0,0.9)" 18 | : `rgba(255, 255, 255, 0.${brightnessLevel})` || "rgba(255,255,255,0.7)"; 19 | 20 | /** 21 | * @var {variousElementsBackground} & @var {variousElementsBackgroundMoreOpaque} - style 22 | * various elements of Discord such as invites, popups, and other elements of the sort, 23 | * that need a background color to display properly 24 | */ 25 | const variousElementsBackground = isDarkTheme ? `rgba(0,0,0,0.78)` : `rgba(255,255,255,0.85)`; 26 | const variousElementsBackgroundMoreOpaque = isDarkTheme ? `rgba(0,0,0,0.92)` : `rgba(255,255,255,0.95)`; 27 | 28 | return rawCSSCode({ 29 | backgroundImageURL: preferencesStore.get("backgroundImageURL") as string, 30 | blurLevelPixels: `${preferencesStore.get("blur") as number}px`, 31 | brightnessLevelRGBA, 32 | variousElementsBackground, 33 | variousElementsBackgroundMoreOpaque, 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /script/styles/index.ts: -------------------------------------------------------------------------------- 1 | export { default as actionsPanelCSSCode } from "./actionsPanelCSSCode"; 2 | export { default as alertsCSSCode } from "./alertsCSSCode"; 3 | export { default as createMainCSSCode } from "./createMainCSSCode"; 4 | 5 | /** 6 | * Overwrites previous CSS rules in regards to the background image. 7 | * @param {string} backgroundImageURL 8 | * @returns CSS rule for overwriting the old rules in regards to the background image 9 | */ 10 | export function newBackgroundImageCSSCode(backgroundImageURL: string): string { 11 | return `#app-mount{background: url(${backgroundImageURL}) center center no-repeat !important; background-size: cover !important;}`; 12 | } 13 | -------------------------------------------------------------------------------- /script/styles/rawCSSCode.ts: -------------------------------------------------------------------------------- 1 | import * as classNames from "../configs/classNames"; 2 | import * as identifiers from "../configs/identifiers"; 3 | 4 | // for class name groupings 5 | // prettier-ignore 6 | export default function rawCSSCode(options: { [key: string]: string }) { 7 | return `.theme-dark, .theme-light { 8 | --background-primary: transparent !important; 9 | --background-secondary: transparent !important; 10 | --background-tertiary: transparent !important; 11 | --channeltextarea-background: transparent !important; 12 | --deprecated-panel-background: transparent !important; 13 | --background-secondary-alt: transparent !important; 14 | --scrollbar-auto-track: transparent !important; 15 | --home-background: transparent !important; 16 | --card-secondary-bg: ${options.variousElementsBackground} !important; 17 | } 18 | 19 | .theme-dark { 20 | --scrollbar-auto-thumb:rgba(255,255,255,0.3) !important; 21 | --profile-body-background-color: rgba(255, 255, 255, 0.1) !important; 22 | } 23 | 24 | .theme-light { 25 | --scrollbar-auto-thumb:rgba(0,0,0,0.6) !important; 26 | --profile-body-background-color: rgba(0, 0, 0, 0.05) !important; 27 | } 28 | 29 | #app-mount { 30 | background: url(${options.backgroundImageURL}) center center no-repeat; 31 | background-size: cover; 32 | } 33 | 34 | ${classNames.titleBar} { 35 | margin-top: 0; 36 | padding-top: 4px; 37 | background: ${options.brightnessLevelRGBA}; 38 | backdrop-filter: blur(${options.blurLevelPixels}) 39 | } 40 | 41 | ${classNames.overlayDarkener}, ${classNames.themeEditorPane}, ${classNames.previewAppIconsPane} { 42 | background-color: ${options.brightnessLevelRGBA}; 43 | backdrop-filter: blur(${options.blurLevelPixels}); 44 | } 45 | 46 | ${classNames.nitroTab}, ${classNames.discussionsTab}, 47 | ${classNames.appDirectoryTab}, ${classNames.rolesAndChannelsTab}, ${classNames.discoverServersTab}, 48 | ${classNames.discussionsTabTitle} { 49 | background-color: transparent !important; 50 | } 51 | 52 | ${classNames.rolesContainerManualAdd}, ${classNames.audioAttachment}, 53 | ${classNames.messageOptionsHoverPopout}, ${classNames.searchResultsPane}, ${classNames.emojiGIFStickerPickers}, 54 | ${classNames.retryConnectionOverlay}, ${classNames.reactionsBackground}, 55 | ${classNames.embedBackground}, ${classNames.downloadAttachment}, 56 | ${classNames.viewReactions}, ${classNames.invites}, ${classNames.codeBlocks}, 57 | ${classNames.stickyRoleManageHeader}, ${classNames.previewAppIconsPane}, ${classNames.threadPane}, 58 | ${classNames.createPoll}, ${classNames.pollDurationPicker}, ${classNames.userVoiceActivityHoverPopout}, 59 | ${classNames.streamPreviewPopup} { 60 | background-color: ${options.variousElementsBackground} !important; 61 | } 62 | 63 | ${classNames.oldUserInfoModalNonPremium}, ${classNames.onJoinServerPopout}, ${classNames.inboxPopup}, 64 | ${classNames.reactionsPicker}, ${classNames.stickyEmojiReactionPickerHeaders}, 65 | ${classNames.quickSwitcher}, ${classNames.pinnedMessagesPopup}, ${classNames.threadsPopup}, 66 | ${classNames.accountSwitchPopup}, ${classNames.emojiGIFStickerPickers}, ${classNames.superReactionsNoNitro}, 67 | ${classNames.attachmentButtons}, ${classNames.cantReactWithAnimatedEmote} { 68 | background-color: ${options.variousElementsBackgroundMoreOpaque} !important; 69 | } 70 | 71 | ${classNames.rolePreviewArea} { 72 | align-items: center; 73 | } 74 | 75 | ${classNames.stickyRoleManageHeader} { 76 | margin-left: -24px; 77 | padding-left: 24px; 78 | } 79 | 80 | ${classNames.allMessages}:has(${classNames.blockedMessages}) { 81 | display: none; 82 | } 83 | 84 | #${identifiers.actionsPanelID} { 85 | font-family: "Whitney", "Helvetica Neue", "Helvetica", monospace, "Arial", sans-serif; 86 | width: 30%; 87 | height: 100%; 88 | background-color: rgba(0,0,0,0.6); 89 | position: absolute; 90 | right: 0; 91 | z-index: 200; 92 | backdrop-filter: blur(8px); 93 | transform: translateX(100%); 94 | transition: transform 500ms ease; 95 | } 96 | 97 | #${identifiers.actionsPanelOverlayID} { 98 | display: none; 99 | width: 100%; 100 | height: 100%; 101 | position: absolute; 102 | top: 0; 103 | left: 0; 104 | z-index: 199; 105 | } 106 | 107 | .${identifiers.actionsPanelActiveClassName} { 108 | transform: translateX(0) !important; 109 | } 110 | `; 111 | } 112 | -------------------------------------------------------------------------------- /script/utils/ClassChangeObserver.ts: -------------------------------------------------------------------------------- 1 | import { IMutationObserver } from "../../types"; 2 | import { mutationObserverCallbackType } from "../../types/IMutationObserver"; 3 | interface IClassChangeObserverParams { 4 | targetElement: HTMLElement; 5 | from: string; 6 | to: string; 7 | } 8 | 9 | /** 10 | * Observes class changes and executes a callback when a class changed into another. 11 | */ 12 | 13 | export default class ClassChangeObserver implements IMutationObserver { 14 | public targetElement: HTMLElement; 15 | private _from: string; 16 | private _to: string; 17 | private _wasClassNamePreviouslyPresent: boolean; 18 | private _observer: MutationObserver; 19 | private _callback: mutationObserverCallbackType | null = null; 20 | 21 | constructor({ targetElement, from, to }: IClassChangeObserverParams) { 22 | this.targetElement = targetElement; 23 | this._from = from; 24 | this._to = to; 25 | this._wasClassNamePreviouslyPresent = 26 | !this.targetElement.classList.contains(this._from) && this.targetElement.classList.contains(this._to); 27 | 28 | this._observer = new MutationObserver((mutations: MutationRecord[], observer: MutationObserver) => { 29 | if (this._callback === null) throw new ReferenceError("No callback has been supplied on class change."); 30 | 31 | for (const mutation of mutations) { 32 | if (mutation.type !== "attributes" || mutation.attributeName !== "class") continue; 33 | 34 | // mutation.target is of type Node for some reason. ok. 35 | const mutationTarget = mutation.target as HTMLElement; 36 | const didClassNameChange: boolean = 37 | !mutationTarget.classList.contains(this._from) && mutationTarget.classList.contains(this._to); 38 | 39 | if (didClassNameChange !== this._wasClassNamePreviouslyPresent) { 40 | this._wasClassNamePreviouslyPresent = didClassNameChange; 41 | 42 | this._callback(mutation, observer); 43 | } 44 | } 45 | }); 46 | } 47 | 48 | public onTrigger(callback: mutationObserverCallbackType): IMutationObserver { 49 | this._callback = callback; 50 | return this; 51 | } 52 | 53 | public observe(options?: MutationObserverInit): IMutationObserver { 54 | this._observer.observe(this.targetElement, options); 55 | return this; 56 | } 57 | 58 | public unobserve(): IMutationObserver { 59 | this._observer.disconnect(); 60 | return this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /script/utils/ContextMenuObserver.ts: -------------------------------------------------------------------------------- 1 | import { IMutationObserver } from "../../types"; 2 | import { mutationObserverCallbackType } from "../../types/IMutationObserver"; 3 | import { popupsContainer } from "../configs/classNames"; 4 | 5 | /** 6 | * Observes whether the context menu element has been created. 7 | */ 8 | 9 | export default class ContextMenuObserver implements IMutationObserver { 10 | public targetElement: HTMLElement; 11 | private _callback: mutationObserverCallbackType; 12 | private _observer: MutationObserver; 13 | 14 | constructor(targetElement: HTMLElement) { 15 | this.targetElement = targetElement; 16 | this._observer = new MutationObserver((mutations: MutationRecord[], observer: MutationObserver) => { 17 | if (this._callback === null) throw new ReferenceError("No callback has been supplied on trigger."); 18 | 19 | for (const mutation of mutations) { 20 | const mutationTarget: HTMLElement = mutation.target as HTMLElement; 21 | if ( 22 | mutation.type !== "childList" || 23 | !mutationTarget.classList.contains(popupsContainer.slice(1) /* remove dot */) || 24 | mutationTarget.firstChild === null 25 | ) 26 | return; 27 | 28 | this._callback(mutation, observer); 29 | } 30 | }); 31 | } 32 | 33 | public onTrigger(callback: mutationObserverCallbackType | null): IMutationObserver { 34 | if (callback) this._callback = callback; 35 | return this; 36 | } 37 | 38 | public observe(options?: MutationObserverInit): IMutationObserver { 39 | this._observer.observe(this.targetElement, options); 40 | return this; 41 | } 42 | 43 | public unobserve(): IMutationObserver { 44 | this._observer.disconnect(); 45 | return this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /script/utils/changeBackgroundImage.ts: -------------------------------------------------------------------------------- 1 | import { backgroundImageStorageKey } from "../configs/identifiers"; 2 | import { importantElementsStore, preferencesStore } from "../stores"; 3 | import { newBackgroundImageCSSCode } from "../styles"; 4 | 5 | export default function changeBackgroundImage(backgroundImageURL: string): void { 6 | const mainStyleSheet = importantElementsStore.get("mainStyleSheet") as HTMLStyleElement; 7 | 8 | mainStyleSheet.innerHTML += newBackgroundImageCSSCode(backgroundImageURL); 9 | preferencesStore.set("backgroundImageURL", backgroundImageURL); 10 | window.localStorage.setItem(backgroundImageStorageKey, backgroundImageURL); 11 | } 12 | -------------------------------------------------------------------------------- /script/utils/contextMenuHandler.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { messageContextMenu } from "../configs/classNames"; 3 | import { missingMenuBarAlertDuration, noFindContextMenuObserverTimeoutDuration } from "../configs/durations"; 4 | import { setAsBackgroundImageButtonID } from "../configs/identifiers"; 5 | import { missingMenuBarText, newErrorAlertText, setBackgroundButtonText } from "../configs/texts"; 6 | import { observersStore, preferencesStore } from "../stores"; 7 | import ContextMenuObserver from "./ContextMenuObserver"; 8 | import changeBackgroundImage from "./changeBackgroundImage"; 9 | import createAlert from "./createAlert"; 10 | import createElement from "./createElement"; 11 | import findPossibleMenuBar from "./findPossibleMenuBar"; 12 | 13 | /* 14 | * As the handler tries to search for the context menu created by Discord, the class names could change. 15 | * There is a backup for this scenario represented by some code that runs in a setTimeout. If the user cancels the 16 | * context menu, we should also cancel the timeout, which is what this variable is for. 17 | */ 18 | let noFindContextMenuObserverTimeoutID: ReturnType; 19 | /** 20 | * Handles the stitching of a "Set as background image" button to the context menu created by Discord. 21 | * @param {Event} event - the event associated with the "contextmenu" event listener 22 | * @remarks Since the capture phase happens before the bubbling phase, the context menu element will 23 | * not exist when the custom handler fires. We have to detect when the element is created. 24 | */ 25 | export default function contextMenuHandler(event: Event): void { 26 | clearTimeout(noFindContextMenuObserverTimeoutID); 27 | if (event.target === null) return; 28 | 29 | const imageElement: HTMLAnchorElement = event.target as HTMLAnchorElement; 30 | if (imageElement.nodeName !== "A" || !imageElement.hasAttribute("href") || !imageElement.href.includes("cdn.discord")) 31 | return; 32 | 33 | let contextMenu: NullableHTMLElement = null; 34 | 35 | // Here we need a context menu observer because between the moment the handler is called, and the moment the menu element 36 | // is added to the DOM by Discord's code, there's a nondeterministic period of time. 37 | const observer = new ContextMenuObserver(document.body); 38 | try { 39 | observer 40 | .onTrigger((mutation: MutationRecord) => { 41 | const mutationTarget: HTMLElement = mutation.target as HTMLElement; 42 | contextMenu = mutationTarget.querySelector(messageContextMenu); 43 | 44 | if (contextMenu === null || contextMenu.role !== "menu") { 45 | createAlert({ text: missingMenuBarText, timeout: missingMenuBarAlertDuration }); 46 | observer.unobserve(); 47 | observersStore.delete("contextMenuCreationObserver"); 48 | return; 49 | } 50 | 51 | createSetBackgroundImageButton(contextMenu, imageElement); 52 | 53 | clearTimeout(noFindContextMenuObserverTimeoutID); 54 | observer.unobserve(); 55 | observersStore.delete("contextMenuCreationObserver"); 56 | }) 57 | .observe({ subtree: true, childList: true }); 58 | } catch (error) { 59 | alert(newErrorAlertText(error.message)); 60 | return; 61 | } 62 | 63 | observersStore.set("contextMenuCreationObserver", observer); 64 | 65 | noFindContextMenuObserverTimeoutID = setTimeout(() => { 66 | observer.unobserve(); 67 | observersStore.delete("contextMenuCreationObserver"); 68 | 69 | contextMenu = findPossibleMenuBar(); 70 | if (contextMenu !== null) createSetBackgroundImageButton(contextMenu, imageElement); 71 | }, noFindContextMenuObserverTimeoutDuration); 72 | } 73 | 74 | /** 75 | * Creates a "Set as background image" button on an image the user right clicked on. 76 | * @param {HTMLElement} contextMenu - the context menu element created by Discord 77 | * @param {HTMLAnchorElement} imageElement - the image element which triggered the "contextmenu" event 78 | */ 79 | function createSetBackgroundImageButton(contextMenu: HTMLElement, imageElement: HTMLAnchorElement): void { 80 | const isDarkTheme = preferencesStore.get("isDarkTheme") as boolean; 81 | const setAsBackgroundButton = createElement({ 82 | elementName: "button", 83 | appendTo: contextMenu, 84 | htmlProps: { 85 | id: setAsBackgroundImageButtonID, 86 | textContent: setBackgroundButtonText, 87 | style: { 88 | color: isDarkTheme ? "gold" : "#7c7300", 89 | boxShadow: `0px 0px 13px 0px ${isDarkTheme ? "hsl(51deg 100% 50% / 45%)" : "hsl(51deg 100% 22% / 45%)"}`, 90 | }, 91 | }, 92 | }); 93 | 94 | setAsBackgroundButton.onclick = () => changeBackgroundImage(imageElement.href); 95 | } 96 | -------------------------------------------------------------------------------- /script/utils/createAlert.ts: -------------------------------------------------------------------------------- 1 | import { transparencyAlertClassName, transparencyAlertInactiveClassName } from "../configs/identifiers"; 2 | import createElement from "./createElement"; 3 | 4 | interface CreateAlertParams { 5 | text: string; 6 | timeout: number; 7 | containsHTML?: boolean; 8 | } 9 | 10 | /** 11 | * Creates an alert that shows various informative things, or warnings. 12 | * @param {CreateAlertParams} alertParams - the parameters for createAlert 13 | * @param {string} alertParams.text - the text to be displayed 14 | * @param {string} alertParams.timeout - how long should the alert be displayed for 15 | * @param {boolean} alertParams.containsHTML - allows for HTML markup to be displayed in the alert, if necessary 16 | * @returns a promise that resolves after the alert disappears 17 | */ 18 | 19 | export default function createAlert({ text, timeout, containsHTML = false }: CreateAlertParams): Promise { 20 | const alert: HTMLDivElement = createElement({ 21 | elementName: "div", 22 | appendTo: document.body, 23 | htmlProps: { 24 | className: transparencyAlertClassName, 25 | [containsHTML ? "innerHTML" : "textContent"]: text, 26 | }, 27 | }); 28 | 29 | alert.style.setProperty("--timer-bar-timeout", `${timeout}ms`); 30 | 31 | return new Promise((resolve) => { 32 | setTimeout(() => { 33 | alert.classList.add(transparencyAlertInactiveClassName); 34 | 35 | setTimeout(() => { 36 | alert.remove(); 37 | resolve(true); 38 | }, 1000); 39 | }, timeout); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /script/utils/createElement.ts: -------------------------------------------------------------------------------- 1 | import { FilterPropertiesNotOfType, Modify, RemoveSignatures } from "../../types"; 2 | 3 | type CSSStyleRules = Partial, string>>; 4 | interface CreateElementParams { 5 | elementName: string; 6 | appendTo: HTMLElement; 7 | htmlProps: Partial>; 8 | } 9 | 10 | /** 11 | * Creates an element. It's basically a wrapper for document.createElement, nothing fancy here. 12 | * @param {CreateElementParams} element 13 | * @param {string} element.elementName - the name of the element 14 | * @param {HTMLElement} element.appendTo - the parent element to append this new element to 15 | * @param {Partial>} element.htmlProps - the HTML attributes represented as JS objects 16 | * @returns {ElementType} a generic HTML element 17 | */ 18 | 19 | export default function createElement({ 20 | elementName, 21 | appendTo, 22 | htmlProps, 23 | }: CreateElementParams): ElementType { 24 | const element = document.createElement(elementName) as ElementType; 25 | 26 | for (const [property, value] of Object.entries(htmlProps)) 27 | element[property as keyof ElementType] = value as ElementType[keyof ElementType]; 28 | 29 | if (htmlProps.style !== undefined) 30 | for (const [property, value] of Object.entries(htmlProps.style)) 31 | element.style[property as keyof CSSStyleRules] = value; 32 | 33 | appendTo.appendChild(element); 34 | return element; 35 | } 36 | -------------------------------------------------------------------------------- /script/utils/findPossibleMenuBar.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { popupsContainer } from "../configs/classNames"; 3 | 4 | /** 5 | * Tries to find the context menu associated with the image. Discord uses the class name held by the variable 6 | * "popupsContainer" to hold context menus. It's just an overlay that dismisses context menus when clicked on it. 7 | * We go through any such containers to check if there are any elements that have a "menu" role and contain "menu" 8 | * in their class name. 9 | * @returns the element if it's found it, and null otherwise. 10 | * @remarks This function does not ensure that the found menu bar is the one associated with the image. In the worst case, 11 | * the button may not be positioned at its intended location. 12 | */ 13 | 14 | export default function findPossibleMenuBar(): NullableHTMLElement { 15 | const popupsContainersElements: HTMLElement[] = Array.from(document.querySelectorAll(popupsContainer)); 16 | if (popupsContainersElements.length === 0) return null; 17 | 18 | for (const popupsContainersElement of popupsContainersElements) { 19 | const possibleMenuBar: NullableHTMLElement = popupsContainersElement.querySelector("div[role='menu']"); 20 | if (possibleMenuBar?.className.includes("menu")) return possibleMenuBar; 21 | } 22 | 23 | return null; 24 | } 25 | -------------------------------------------------------------------------------- /script/utils/getElementWithAlert.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { newErrorAlertText } from "../configs/texts"; 3 | 4 | /** 5 | * Gets elements from the DOM, and if they're not there, the function alerts the user with an error. 6 | * We do this in case Discord changes some class names or IDs, to make such errors more "user friendly". 7 | * @param {string} selector - the CSS selector of an HTML element 8 | * @returns {NullableHTMLElement} the element from the DOM 9 | */ 10 | 11 | export default function getElementWithAlert(selector: string): NullableHTMLElement { 12 | const element: NullableHTMLElement = document.querySelector(selector); 13 | 14 | if (element === null) { 15 | alert(newErrorAlertText(`${selector} is not in the DOM.`)); 16 | return null; 17 | } 18 | 19 | return element; 20 | } 21 | -------------------------------------------------------------------------------- /script/utils/getSidebarThemeState.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { sidebarDarkThemeIndicator } from "../configs/classNames"; 3 | import { generalDarkThemeClassName } from "../configs/identifiers"; 4 | import { importantElementsStore, preferencesStore } from "../stores"; 5 | 6 | /** 7 | * Checks to see if the user has a white theme, but a dark sidebar. If that's so, the user must 8 | * disable it, because it messes up the text contrast. 9 | * @returns true if the sidebar has a dark theme, and false otherwise. 10 | */ 11 | export default function getSidebarThemeState() { 12 | const isDarkTheme = preferencesStore.get("isDarkTheme") as boolean; 13 | const titleBarElement = importantElementsStore.get("titleBarElement") as NullableHTMLElement; 14 | // We can check whether the sidebar is dark themed by checking whether the titleBar 15 | // is dark themed, and the global theme is white. 16 | // If titleBar doesn't exist, then we use another element, represented by the class name 17 | // of "sidebarDarkThemeIndicator". 18 | if (titleBarElement !== null) return !isDarkTheme && titleBarElement.classList.contains(generalDarkThemeClassName); 19 | 20 | const sidebarDarkThemeIndicatorElement: NullableHTMLElement = document.querySelector(sidebarDarkThemeIndicator); 21 | if (sidebarDarkThemeIndicatorElement !== null) 22 | return !isDarkTheme && sidebarDarkThemeIndicatorElement.classList.contains(generalDarkThemeClassName); 23 | 24 | // If the indicator element doesn't exist anymore, then don't don't say the sidebar is dark themed. 25 | return false; 26 | } 27 | -------------------------------------------------------------------------------- /script/utils/initImportantElementsStore.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { overlayDarkener, titleBar } from "../configs/classNames"; 3 | import { generalTransparencyID } from "../configs/identifiers"; 4 | import { importantElementsStore } from "../stores"; 5 | import createMainCSSCode from "../styles/createMainCSSCode"; 6 | import createElement from "./createElement"; 7 | import getElementWithAlert from "./getElementWithAlert"; 8 | 9 | /** 10 | * @returns true if all necessary elements have been found, and false if there's been any errors. 11 | */ 12 | export default function initImportantElementsStore(): boolean { 13 | const overlayDarkenerElement: NullableHTMLElement = getElementWithAlert(overlayDarkener); 14 | if (overlayDarkenerElement === null) return false; 15 | 16 | // We didn't use getElementWithAlert here, because it shows an alert if the element 17 | // is not defined - which is good, but not in this case. 18 | // In this case the titleBar element doesn't exist in the browser version of Discord. 19 | const titleBarElement: NullableHTMLElement = document.querySelector(titleBar); 20 | const mainStyleSheet: HTMLStyleElement = createElement({ 21 | elementName: "style", 22 | appendTo: document.head, 23 | htmlProps: { id: generalTransparencyID, innerHTML: createMainCSSCode() }, 24 | }); 25 | 26 | importantElementsStore 27 | .set("titleBarElement", titleBarElement) 28 | .set("overlayDarkenerElement", overlayDarkenerElement) 29 | .set("mainStyleSheet", mainStyleSheet); 30 | 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /script/utils/initLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | backgroundImageStorageKey, 3 | brightnessStorageKey, 4 | legacyBackgroundImageStorageKey, 5 | legacyBrightnessStorageKey, 6 | } from "../configs/identifiers"; 7 | import { newErrorAlertText } from "../configs/texts"; 8 | import createElement from "./createElement"; 9 | 10 | /** 11 | * Creates a local storage object, by creating an invisible iframe. 12 | * The Discord client deletes the localStorage object, but it's needed to store data 13 | * such as the brightness level. 14 | * @returns true if the localStorage object has been created, and false if there's been an error. 15 | */ 16 | 17 | function createLocalStorage(): boolean { 18 | const localStorageIframe: HTMLIFrameElement = createElement({ 19 | elementName: "iframe", 20 | appendTo: document.body, 21 | htmlProps: {}, 22 | }); 23 | localStorageIframe.style.display = "none"; 24 | 25 | if (localStorageIframe.contentWindow === null) { 26 | alert( 27 | newErrorAlertText("Could not create the localStorage object because the iframe's contentWindow isn't defined.") 28 | ); 29 | return false; 30 | } 31 | 32 | // Performed this type assertion because window.localStorage is a readonly property. 33 | (window.localStorage as Storage) = localStorageIframe.contentWindow.localStorage; 34 | return true; 35 | } 36 | 37 | interface ILocalStorageLegacyKeyHandler { 38 | legacyStorageKey: string; 39 | newStorageKey: string; 40 | defaultValue: string; 41 | } 42 | 43 | /** 44 | * Migrates the local storage legacy keys to the new ones. 45 | * @param {ILocalStorageLegacyKeyHandler} keyData 46 | * @param {string} keyData.legacyStorageKey - the legacy key name of this key 47 | * @param {string} keyData.newStorageKey - the current key name 48 | * @param {string} keyData.defaultValue - the default value pointed to by the key in the local storage 49 | * @returns {boolean} true if there was no problem migrating, and false otherwise 50 | */ 51 | function handleLocalStorageLegacyKey({ 52 | legacyStorageKey, 53 | newStorageKey, 54 | defaultValue, 55 | }: ILocalStorageLegacyKeyHandler): boolean { 56 | try { 57 | let newKeyResult: string | null = window.localStorage.getItem(newStorageKey); 58 | if (newKeyResult === null) { 59 | newKeyResult = window.localStorage.getItem(legacyStorageKey); 60 | 61 | if (newKeyResult === null) newKeyResult = defaultValue; 62 | else { 63 | window.localStorage.removeItem(legacyStorageKey); 64 | window.localStorage.setItem(newStorageKey, newKeyResult); 65 | } 66 | } 67 | return true; 68 | } catch (error) { 69 | // localStorage.setItem could fail. 70 | alert(newErrorAlertText(error.message)); 71 | return false; 72 | } 73 | } 74 | 75 | export default function initLocalStorage(): boolean { 76 | return ( 77 | createLocalStorage() && 78 | handleLocalStorageLegacyKey({ 79 | legacyStorageKey: legacyBackgroundImageStorageKey, 80 | newStorageKey: backgroundImageStorageKey, 81 | defaultValue: "", 82 | }) && 83 | handleLocalStorageLegacyKey({ 84 | legacyStorageKey: legacyBrightnessStorageKey, 85 | newStorageKey: brightnessStorageKey, 86 | defaultValue: "9", 87 | }) 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /script/utils/initObservers.ts: -------------------------------------------------------------------------------- 1 | import { themeChangeWarningDuration } from "../configs/durations"; 2 | import { generalDarkThemeClassName, generalWhiteThemeClassName } from "../configs/identifiers"; 3 | import { newErrorAlertText, themeChangeDetectionText } from "../configs/texts"; 4 | import { observersStore, preferencesStore } from "../stores"; 5 | import ClassChangeObserver from "./ClassChangeObserver"; 6 | import createAlert from "./createAlert"; 7 | 8 | export default function initObservers(): boolean { 9 | if (initThemeChangeObserver()) { 10 | // @ts-ignore 11 | window.__TRANSPARENCY_OBSERVERS__ = observersStore; 12 | return true; 13 | } 14 | 15 | return false; 16 | } 17 | 18 | /** 19 | * @returns true if the observer has has been successfully created, and false if there's been an error. 20 | */ 21 | function initThemeChangeObserver(): boolean { 22 | const isDarkTheme = preferencesStore.get("isDarkTheme") as boolean; 23 | 24 | const observer = new ClassChangeObserver({ 25 | targetElement: document.documentElement, 26 | from: isDarkTheme ? generalDarkThemeClassName : generalWhiteThemeClassName, 27 | to: isDarkTheme ? generalWhiteThemeClassName : generalDarkThemeClassName, 28 | }); 29 | 30 | try { 31 | observer 32 | .onTrigger(() => { 33 | createAlert({ text: themeChangeDetectionText, timeout: themeChangeWarningDuration }).then(() => { 34 | observer.unobserve(); 35 | location.reload(); 36 | }); 37 | }) 38 | .observe({ attributes: true, attributeFilter: ["class"] }); 39 | } catch (error) { 40 | alert(newErrorAlertText(error.message)); 41 | return false; 42 | } 43 | 44 | observersStore.set("themeChangeObserver", observer); 45 | return true; 46 | } 47 | -------------------------------------------------------------------------------- /script/utils/initPreferencesStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | backgroundImageStorageKey, 3 | blurStorageKey, 4 | brightnessStorageKey, 5 | generalDarkThemeClassName, 6 | } from "../configs/identifiers"; 7 | import { preferencesStore } from "../stores"; 8 | 9 | /** 10 | * Initialises the preferences store, which holds data about the user's brightness level, background image 11 | * URL, and so on. Also deals with legacy storage keys used for the background image URL and brightness level. 12 | * @returns true if the preferences store has been successfully created, and false if there's been an error. 13 | */ 14 | 15 | export default function initPreferencesStore(): boolean { 16 | preferencesStore 17 | .set("isDarkTheme", document.documentElement.classList.contains(generalDarkThemeClassName)) 18 | .set("brightness", parseInt(window.localStorage.getItem(brightnessStorageKey) ?? "unset") || 9) 19 | .set("blur", parseInt(window.localStorage.getItem(blurStorageKey) ?? "unset") || 0) 20 | .set("backgroundImageURL", window.localStorage.getItem(backgroundImageStorageKey) ?? ""); 21 | 22 | return true; 23 | } 24 | -------------------------------------------------------------------------------- /script/utils/removeExistingCodeFootprint.ts: -------------------------------------------------------------------------------- 1 | import { NullableHTMLElement } from "../../types"; 2 | import { 3 | actionsPanelID, 4 | actionsPanelOverlayID, 5 | actionsPanelStyleSheetID, 6 | alertsStyleSheetID, 7 | generalTransparencyID, 8 | } from "../configs/identifiers"; 9 | 10 | /** 11 | * Helps clean up old stylesheets, removes other event listeners, etc. 12 | * It's especially useful when re-applying the script more than once. 13 | */ 14 | 15 | export default function removeExistingCodeFootprint(): void { 16 | const existingStyleSheet: NullableHTMLElement = document.getElementById(generalTransparencyID); 17 | const existingAlertsStyleSheet: NullableHTMLElement = document.getElementById(alertsStyleSheetID); 18 | const existingActionsPanel: NullableHTMLElement = document.getElementById(actionsPanelID); 19 | const existingActionsPanelOverlay: NullableHTMLElement = document.getElementById(actionsPanelOverlayID); 20 | const existingActionsPanelStyleSheet: NullableHTMLElement = document.getElementById(actionsPanelStyleSheetID); 21 | 22 | existingAlertsStyleSheet?.remove(); 23 | existingActionsPanel?.remove(); 24 | existingActionsPanelOverlay?.remove(); 25 | existingActionsPanelStyleSheet?.remove(); 26 | 27 | if (existingStyleSheet !== null) { 28 | // If the main stylesheet created by the script exists, then it's safe to assume a keydown event 29 | // listener has also been set. 30 | window.onkeydown = null; 31 | existingStyleSheet.remove(); 32 | } 33 | 34 | if ((window as any).__TRANSPARENCY_OBSERVERS__) { 35 | for (const observer of (window as any).__TRANSPARENCY_OBSERVERS__.values()) observer.unobserve(); 36 | (window as any).__TRANSPARENCY_OBSERVERS__ = undefined; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["ESNext", "DOM"], 5 | "noImplicitAny": true, 6 | "strictNullChecks": true, 7 | "resolveJsonModule": true, 8 | "moduleResolution": "node", 9 | "types": ["node"], 10 | }, 11 | "files": ["./script/script.ts"], 12 | "compileOnSave": true 13 | } 14 | -------------------------------------------------------------------------------- /types/FilterPropertiesNotOfType.ts: -------------------------------------------------------------------------------- 1 | type FilterPropertiesNotOfType = { 2 | [key in keyof ObjectType as ObjectType[key] extends WantedType ? key : never]: WantedType; 3 | }; 4 | 5 | export default FilterPropertiesNotOfType; 6 | -------------------------------------------------------------------------------- /types/IAction.ts: -------------------------------------------------------------------------------- 1 | export default interface IAction { 2 | name: string; 3 | execute(actionBox: HTMLDivElement): void; 4 | } 5 | -------------------------------------------------------------------------------- /types/IImportantElementsStore.ts: -------------------------------------------------------------------------------- 1 | import NullableHTMLElement from "./NullableHTMLElement"; 2 | 3 | export default interface IImportantElementsStore { 4 | titleBarElement: NullableHTMLElement; 5 | overlayDarkenerElement: HTMLDivElement; 6 | mainStyleSheet: HTMLStyleElement; 7 | } 8 | -------------------------------------------------------------------------------- /types/IMutationObserver.ts: -------------------------------------------------------------------------------- 1 | export type mutationObserverCallbackType = (mutation: MutationRecord, observer: MutationObserver) => void; 2 | 3 | export default interface IMutationObserver { 4 | targetElement: HTMLElement; 5 | onTrigger(callback: mutationObserverCallbackType): IMutationObserver; 6 | observe(options?: MutationObserverInit): IMutationObserver; 7 | unobserve(options?: MutationObserverInit): IMutationObserver; 8 | } 9 | -------------------------------------------------------------------------------- /types/IPreferencesStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an object to be passed by reference when updating levels 3 | * like the brightness or blur levels, so the changes can reflect throughout the script. 4 | */ 5 | export default interface IPreferencesStore { 6 | backgroundImageURL: string; 7 | brightness: number; 8 | blur: number; 9 | isDarkTheme: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /types/Modify.ts: -------------------------------------------------------------------------------- 1 | type Modify = Omit & NewObjectSubtype; 2 | export default Modify; 3 | -------------------------------------------------------------------------------- /types/NullableHTMLElement.ts: -------------------------------------------------------------------------------- 1 | type NullableHTMLElement = HTMLElement | null; 2 | export default NullableHTMLElement; 3 | -------------------------------------------------------------------------------- /types/RemoveSignatures.ts: -------------------------------------------------------------------------------- 1 | type RemoveSignatures = { 2 | [key in keyof Type as string extends key 3 | ? never 4 | : number extends key 5 | ? never 6 | : symbol extends key 7 | ? never 8 | : key]: Type[key]; 9 | }; 10 | 11 | export default RemoveSignatures; 12 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FilterPropertiesNotOfType } from "./FilterPropertiesNotOfType"; 2 | export { default as IAction } from "./IAction"; 3 | export { default as IImportantElementsStore } from "./IImportantElementsStore"; 4 | export { default as IMutationObserver } from "./IMutationObserver"; 5 | export { default as IPreferencesStore } from "./IPreferencesStore"; 6 | export { default as Modify } from "./Modify"; 7 | export { default as NullableHTMLElement } from "./NullableHTMLElement"; 8 | export { default as RemoveSignatures } from "./RemoveSignatures"; 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const PrepareCodeForAsarInjectionPlugin = require("./PrepareCodeForAsarInjectionPlugin"); 3 | module.exports = (env) => ({ 4 | mode: "production", 5 | entry: "script/script.ts", 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.ts?$/, 10 | use: "ts-loader", 11 | exclude: /node_modules/, 12 | }, 13 | ], 14 | }, 15 | plugins: env.noInjectionPlugin ? [] : [new PrepareCodeForAsarInjectionPlugin()], 16 | resolve: { 17 | extensions: [".ts", ".js"], 18 | preferRelative: true, 19 | }, 20 | output: { 21 | path: resolve(__dirname, "./main"), 22 | filename: "manual-copy-pasting.js", 23 | }, 24 | optimization: { 25 | minimize: true, 26 | }, 27 | }); 28 | --------------------------------------------------------------------------------