├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── announcements.json ├── core.js ├── img └── welcome.png ├── modal.js ├── mydiscord ├── __init__.py ├── __main__.py ├── app.py ├── asar.py ├── discord.js └── discord.js.config.json ├── requirements.txt ├── setup.py └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.buildinfo 3 | *.egg-info 4 | .DS_Store 5 | node_modules 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 leovoel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MyDiscord 2 | ================ 3 | 4 | **This project isn't maintained, and hasn't been in a while. If you want, you can submit a PR and I'll review it, but I'm not making any changes myself.** 5 | 6 | Simple Python script that adds CSS hot-reload and Custom JavaScript support to Discord. 7 | 8 | psst: there's a [rewrite effort](https://github.com/justinoboyle/mydiscord/tree/rewrite) going on over here. 9 | 10 | ## Credit where it's due 11 | 12 | I quite liked [leovoel's BeautifulDiscord](https://github.com/leovoel/BeautifulDiscord)'s lightweight implementation of stylesheets in Discord, so I modified leovoel's script to also include JavaScript support. 13 | 14 | Because this is a fork, most of the code (and the usage section) was written by [leovoel](https://github.com/leovoel), so go show him some love. 15 | 16 | ## Disclaimer 17 | 18 | I am not responsible for anything stupid you do with this. Use common sense. 19 | 20 | ## Usage 21 | 22 | * Install python 3+ 23 | 24 | * Open the command line - (cmd.exe AS ADMIN on Windows, Terminal on macOS/*nix) 25 | 26 | * Open Discord 27 | 28 | * Run the following commands: 29 | 30 | ``` 31 | python3 -m pip install -U https://github.com/justinoboyle/MyDiscord/archive/master.zip 32 | mydiscord 33 | ``` 34 | 35 | (If that fails, then run this): 36 | 37 | ``` 38 | python -m pip install -U https://github.com/justinoboyle/MyDiscord/archive/master.zip 39 | mydiscord 40 | ``` 41 | 42 | * Have fun! 43 | 44 | ## More detailed command line usage 45 | 46 | Just invoke the script when installed. If you don't pass the `--css` and `--js` flags, the resources 47 | will be placed wherever the Discord app resources are found. 48 | 49 | **NOTE:** Discord has to be running for this to work in first place. 50 | The script works by scanning the active processes and looking for the Discord ones. 51 | 52 | (yes, this also means you can fool the program into trying to apply this to some random program named Discord) 53 | 54 | ``` 55 | $ mydiscord --css ~/discord.css --js ~/discord.js 56 | Found Discord Canary under /Applications/Discord Canary.app/Contents/MacOS 57 | 58 | Done! 59 | 60 | You may now edit your CSS in /Users/justin/discord.css, 61 | which will be reloaded whenever it's saved. 62 | You can also edit your JavaScript in /Users/justin/discord.js 63 | ,but you must reload (CMD/CTRL + R) Discord to re-run it 64 | 65 | *Do not insert code that you do not understand, as it could steal your account!* 66 | 67 | Relaunching Discord now... 68 | $ 69 | ``` 70 | 71 | Pass the `--revert` flag to remove the extracted `app.asar` (it's the `resources/app` folder) 72 | and rename `original_app.asar` to `app.asar`. You can also do this manually if your Discord 73 | install gets screwed up. 74 | 75 | ``` 76 | $ mydiscord --revert 77 | Found Discord Canary under /Applications/Discord Canary.app/Contents/MacOS 78 | 79 | Reverted changes, no more CSS hot-reload :( 80 | $ 81 | ``` 82 | 83 | You can also run it as a package - i.e. `python3 -m mydiscord` - if somehow you cannot 84 | install it as a script that you can run from anywhere. 85 | 86 | ## Requirements 87 | 88 | - Python 3.x (no interest in compatibility with 2.x, untested on Python 3.x versions below 3.4) 89 | - `psutil` library: https://github.com/giampaolo/psutil 90 | 91 | Normally, `pip` should install any required dependencies. 92 | 93 | ## Themes 94 | 95 | Some people have started a theming community for the original BeautifulDiscord over here: 96 | https://github.com/beautiful-discord-community/resources/ 97 | 98 | They have a Discord server as well: 99 | https://discord.gg/EDwd5wr 100 | 101 | ## Plugins 102 | 103 | We started a scripting community for MyDiscord over here: 104 | https://github.com/justinoboyle/mydiscord-resources 105 | 106 | We have a small chat on the BeautifulDiscord's server: 107 | https://discord.gg/rN3WMWn 108 | -------------------------------------------------------------------------------- /announcements.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "main": "Welcome to MyDiscord!", 4 | "subtext": "Feel free to check the GitHub page if you have any questions!" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | /* Global */ 3 | if (!window.saveConfig) 4 | window.saveConfig = () => console.log("Could not save config!"); 5 | if (!window.config) 6 | window.config = {}; 7 | 8 | window.addEventListener("beforeunload", saveConfig); 9 | 10 | if(!window.mydiscord) 11 | window.mydiscord = {}; 12 | 13 | let customOptions = []; 14 | 15 | // TODO: allow no callback 16 | function getCheckbox(title, descText, state, clickCallback){ 17 | /* Create elements */ 18 | let container = document.createElement('div'); 19 | container.className = "settings-container"; 20 | 21 | let option = document.createElement('div'); 22 | option.className = "flex"; 23 | 24 | let label = document.createElement('h3'); 25 | label.className = "settings-label"; 26 | label.innerHTML = title; 27 | 28 | let checkbox_wrap = document.createElement('div'); 29 | checkbox_wrap.className = "settings-checkbox-wrap"; 30 | 31 | let checkbox_switch = document.createElement('div'); 32 | checkbox_switch.className = state?'settings-checkbox-switch settings-checkbox-checked':'settings-checkbox-switch'; 33 | 34 | let checkbox = document.createElement('input'); 35 | checkbox.type = 'checkbox'; 36 | checkbox.value = state?'on':'off'; 37 | checkbox.className = 'settings-checkbox' 38 | checkbox.addEventListener("click", function(){ 39 | isOn = this.value === "on"; 40 | this.value = isOn?'off':'on'; 41 | 42 | checkbox_switch.className = isOn?'settings-checkbox-switch':'settings-checkbox-switch settings-checkbox-checked'; 43 | 44 | clickCallback(); 45 | }); 46 | 47 | let divider = document.createElement('div'); 48 | divider.className = "settings-divider"; 49 | 50 | /* Build element */ 51 | checkbox_wrap.appendChild(checkbox); 52 | checkbox_wrap.appendChild(checkbox_switch); 53 | option.appendChild(label); 54 | option.appendChild(checkbox_wrap); 55 | container.appendChild(option); 56 | 57 | /* Add desc */ 58 | if(descText !== null){ 59 | let desc = document.createElement('div') 60 | desc.className = 'settings-desc'; 61 | desc.innerHTML = descText; 62 | container.appendChild(desc); 63 | } 64 | 65 | container.appendChild(divider); 66 | 67 | return container; 68 | } 69 | 70 | // TODO: allow no callback 71 | function getInput(title, descText, properties, inputCallback){ 72 | /* Create elements */ 73 | let container = document.createElement('div'); 74 | container.className = "settings-container"; 75 | 76 | let option = document.createElement('div'); 77 | option.className = "settings-input-field"; 78 | 79 | let label = document.createElement('h3'); 80 | label.className = "settings-label"; 81 | label.innerHTML = title; 82 | 83 | let input = document.createElement('input'); 84 | input.type = properties.type || "text"; 85 | input.placeholder = properties.placeholder || ""; 86 | input.id = properties.id || ""; 87 | input.name = properties.name || ""; 88 | input.value = properties.value || ""; 89 | input.className = 'settings-input' 90 | input.addEventListener("change", inputCallback); 91 | 92 | let divider = document.createElement('div'); 93 | divider.className = "settings-divider"; 94 | 95 | /* Build element */ 96 | option.appendChild(label); 97 | option.appendChild(input); 98 | container.appendChild(option); 99 | 100 | /* Add desc */ 101 | if(descText !== null){ 102 | let desc = document.createElement('div') 103 | desc.className = 'settings-desc'; 104 | desc.innerHTML = descText; 105 | container.appendChild(desc); 106 | } 107 | 108 | container.appendChild(divider); 109 | 110 | return container; 111 | } 112 | 113 | window.mydiscord.loadCSS = function(link){ 114 | let stylesheet = document.createElement("link"); 115 | stylesheet.rel = 'stylesheet'; 116 | stylesheet.href = link + '?nocache=' + Math.random() * (666 - 100) + 100; 117 | document.getElementsByTagName('head')[0].appendChild(stylesheet); 118 | } 119 | 120 | // TODO: options, & remap css classes (thx discord... :( ) 121 | window.mydiscord.addSettingsConnection = function(name, icon, color, deleteCallback){ 122 | if(document.querySelector(".user-settings-connections") !== null){ 123 | /* Create elements */ 124 | let connection = document.createElement("div"); 125 | connection.className = "connection elevation-low margin-bottom-8"; 126 | connection.setAttribute("style", "border-color: " + color + "; background-color: " + color + ";"); 127 | 128 | let connection_header = document.createElement("div"); 129 | connection_header.className = "connection-header"; 130 | 131 | let img = document.createElement("img"); 132 | img.className = "connection-icon no-user-drag"; 133 | img.src = icon; 134 | 135 | let connection_name_wrapper = document.createElement("div"); 136 | 137 | let connection_name = document.createElement("div"); 138 | connection_name.className = "connection-account-value"; 139 | connection_name.innerHTML = name; 140 | 141 | let connection_label = document.createElement("div"); 142 | connection_label.className = "connection-account-label"; 143 | connection_label.innerHTML = "Account Name"; 144 | 145 | let connection_delete = document.createElement("div"); 146 | connection_delete.className = "connection-delete flex-center"; 147 | connection_delete.innerHTML = "Disconnect"; 148 | connection_delete.addEventListener("click", function(){ 149 | this.parentNode.parentNode.remove(); 150 | deleteCallback(); 151 | }); 152 | 153 | /* Build elements */ 154 | connection_name_wrapper.appendChild(connection_name); 155 | connection_name_wrapper.appendChild(connection_label); 156 | connection_header.appendChild(img); 157 | connection_header.appendChild(connection_name_wrapper); 158 | connection_header.appendChild(connection_delete); 159 | connection.appendChild(connection_header); 160 | 161 | document.querySelector(".user-settings-connections .connection-list").appendChild(connection); 162 | } 163 | } 164 | 165 | // TODO: remap css classes 166 | window.mydiscord.addConnectButton = function(icon, callback){ 167 | let connect = document.createElement("div"); 168 | connect.className = "connect-account-btn"; 169 | 170 | let btn = document.createElement("button"); 171 | btn.className = "connect-account-btn-inner"; 172 | btn.type = "button"; 173 | btn.setAttribute("style", "background-image: url('" + icon + "');"); 174 | btn.addEventListener("click", callback); 175 | 176 | connect.appendChild(btn); 177 | document.querySelector(".connect-account-list .settings-connected-accounts").appendChild(connect); 178 | } 179 | 180 | // TODO: remap css classes 181 | window.mydiscord.addProfileConnection = function(connectionName, connectionIcon, isVerified, externalLink){ 182 | if(document.querySelector("#user-profile-modal") != null && document.querySelector("#user-profile-modal .tab-bar :first-child").className == "tab-bar-item selected"){ 183 | let account = document.createElement("div"); 184 | account.className = "connected-account"; 185 | 186 | let icon = document.createElement("img"); 187 | icon.className = "connected-account-icon"; 188 | icon.src = connectionIcon; 189 | 190 | let name_wrapper = document.createElement("div"); 191 | name_wrapper.className = "connected-account-name-inner"; 192 | 193 | let name = document.createElement("div"); 194 | name.className = "connected-account-name"; 195 | name.innerHTML = connectionName; 196 | 197 | let verified = document.createElement("i"); 198 | verified.className = "connected-account-verified-icon"; 199 | 200 | let link = document.createElement("a"); 201 | link.href = externalLink; 202 | link.rel = "noreferrer"; 203 | link.target = "_blank"; 204 | link.innerHTML = "
"; 205 | 206 | name_wrapper.appendChild(name); 207 | if(isVerified) name_wrapper.appendChild(verified); 208 | 209 | account.appendChild(icon); 210 | account.appendChild(name_wrapper); 211 | account.appendChild(link); 212 | 213 | if(document.querySelector("#user-profile-modal .connected-accounts") == null){ 214 | let section = document.createElement("div"); 215 | section.className = "section"; 216 | 217 | let account_wrap = document.createElement("div"); 218 | account_wrap.className = "connected-accounts"; 219 | 220 | section.appendChild(account_wrap); 221 | document.querySelector("#user-profile-modal .guilds").appendChild(section); 222 | } 223 | document.querySelector("#user-profile-modal .connected-accounts").appendChild(account); 224 | } 225 | } 226 | 227 | window.mydiscord.addOptionCheckbox = function(title, desc, state, callback){ 228 | if(customOptions[title] != undefined) throw new Error("Key " + title + " already exists !"); 229 | customOptions[title] = getCheckbox(title, desc, state, callback); 230 | customOptions.length++; 231 | 232 | if(document.querySelector('.mydiscord-options') !== null){ 233 | buildUi(); 234 | } 235 | } 236 | 237 | window.mydiscord.addOptionInput = function(title, desc, inputProperties, callback){ 238 | if(customOptions[title] != undefined) throw new Error("Key " + title + " already exists !"); 239 | customOptions[title] = getInput(title, desc, inputProperties, callback); 240 | customOptions.length++; 241 | 242 | if(document.querySelector('.mydiscord-options') !== null){ 243 | buildUi(); 244 | } 245 | } 246 | 247 | window.mydiscord.removeOption = function(title){ 248 | if(customOptions[title] == undefined) throw new Error("Key " + title + " not found !"); 249 | delete customOptions[title]; 250 | customOptions.length--; 251 | 252 | if(document.querySelector('.mydiscord-options') !== null){ 253 | buildUi(); 254 | } 255 | } 256 | 257 | // TODO: dialog types (info, question, error...), form inputs 258 | window.mydiscord.dialog = function(title, contents, doneCallback){ 259 | if(doneCallback === undefined || doneCallback === null) doneCallback = function(){}; 260 | 261 | if(document.querySelector(".mydiscord-dialog") !== null) document.querySelector(".mydiscord-dialog").remove(); 262 | 263 | let global_container = document.createElement("div"); 264 | global_container.className = "theme-dark mydiscord-dialog"; 265 | 266 | let overlay = document.createElement("div"); 267 | overlay.className = "callout-backdrop"; 268 | overlay.setAttribute("style", "opacity: 0.85; background-color: rgb(0, 0, 0); transform: translateZ(0px);") 269 | overlay.addEventListener("click", function(){ 270 | document.querySelector(".mydiscord-dialog").remove(); 271 | doneCallback(); 272 | }); 273 | 274 | let modal = document.createElement("div"); 275 | modal.className = "mydiscord-modal"; 276 | 277 | let modal_inner = document.createElement("div"); 278 | modal_inner.className = "mydiscord-modal-inner"; 279 | 280 | let header = document.createElement("div"); 281 | header.className = "mydiscord-modal-header"; 282 | 283 | let header_title = document.createElement("h4"); 284 | header_title.innerHTML = title; 285 | 286 | let modal_contents_wrap = document.createElement("div"); 287 | modal_contents_wrap.className = "mydiscord-modal-scollerWrap"; 288 | 289 | let modal_contents = document.createElement("div"); 290 | modal_contents.className = "mydiscord-modal-scroller"; 291 | modal_contents.innerHTML = contents; 292 | 293 | let modal_footer = document.createElement("div"); 294 | modal_footer.className = "mydiscord-modal-footer"; 295 | 296 | let modal_button = document.createElement("button"); 297 | modal_button.className = "mydiscord-modal-button-done"; 298 | modal_button.type = "button"; 299 | modal_button.innerHTML = "
Done
"; 300 | modal_button.addEventListener("click", function(){ 301 | document.querySelector(".mydiscord-dialog").remove(); 302 | doneCallback(); 303 | }); 304 | 305 | modal_footer.appendChild(modal_button); 306 | modal_contents_wrap.appendChild(modal_contents); 307 | 308 | header.appendChild(header_title); 309 | 310 | modal_inner.appendChild(header); 311 | modal_inner.appendChild(modal_contents_wrap); 312 | modal_inner.appendChild(modal_footer); 313 | modal.appendChild(modal_inner); 314 | 315 | global_container.appendChild(overlay); 316 | global_container.appendChild(modal); 317 | 318 | document.querySelector("#app-mount > div").appendChild(global_container); 319 | } 320 | 321 | let _baseUrl = "https://rawgit.com/justinoboyle/mydiscord/master/"; 322 | 323 | const request = require('request'); 324 | 325 | /* Google Analytics */ // google analytics, sorry!! but you can disable this if you want by setting a global variable called "noAnalyze" or directly from myDiscord options in Discord 326 | function initGa(){ 327 | (function (i, s, o, g, r, a, m) { 328 | i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () { 329 | (i[r].q = i[r].q || []).push(arguments) 330 | }, i[r].l = 1 * new Date(); a = s.createElement(o), 331 | m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m) 332 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga'); 333 | ga('create', 'UA-78491625-4', 'auto'); 334 | ga('send', 'pageview'); 335 | ga('set', 'userId', document.getElementsByClassName("username")[0].textContent + document.getElementsByClassName("discriminator")[0].textContent); 336 | } 337 | 338 | function sendGa(data){ 339 | if(!global.noAnalyze && ga){ 340 | ga('send', data); 341 | } 342 | } 343 | 344 | if (!global.noAnalyze) { 345 | setTimeout(() => { // Fix https://bowser65.tk/data/mydiscord-bug1.png 346 | initGa(); 347 | }, 2000); 348 | } 349 | 350 | mydiscord.loadCSS(_baseUrl + 'styles.css'); 351 | 352 | /* Toast */ 353 | let openToasts = {}; 354 | let id = 0; 355 | if (typeof (window.config.toasts) === "undefined") { 356 | window.config.toasts = {}; 357 | saveConfig(); 358 | } 359 | function toast(main, subtext, _anaid) { 360 | if (_anaid) { 361 | if(openToasts[_anaid]) 362 | return; 363 | openToasts[_anaid] = true; 364 | sendGa('toast-open-' + _anaid); 365 | } 366 | let _id = id++; 367 | const html = ` 368 |
369 | ${main} 370 | ${subtext ? `${subtext}` : ``} 371 |
372 |
X
373 | `; 374 | const el = document.createElement("div"); 375 | el.setAttribute("class", "toast toast-dying"); 376 | el.setAttribute("id", "toast" + _id); 377 | el.innerHTML = html; 378 | document.body.insertBefore(el, document.getElementById('app-mount')); 379 | setTimeout(() => { 380 | el.setAttribute("class", "toast"); 381 | }, 200); 382 | return el; 383 | } 384 | function closeToast(id, _anaid) { 385 | if (!document.getElementById('toast' + id)) 386 | return; 387 | if (_anaid && _anaid !== "no") { 388 | sendGa('toast-close-' + _anaid); 389 | if (openToasts[_anaid]) 390 | delete openToasts[_anaid]; 391 | if(!window.config.toasts) 392 | window.config.toasts = {}; 393 | window.config.toasts[_anaid] = "seen"; 394 | window.saveConfig(); 395 | } 396 | let toast = document.getElementById('toast' + id); 397 | toast.setAttribute('class', 'toast toast-dying'); 398 | setTimeout(() => { 399 | toast.parentNode.removeChild(toast); 400 | }, 200); 401 | } 402 | global._toast = toast; 403 | global._closeToast = closeToast; 404 | function check() { 405 | request(_baseUrl + 'announcements.json', function (error, response, body) { 406 | let parse = JSON.parse(body); 407 | for (let key in parse) { 408 | if (!window.config.toasts[key] && !openToasts[key]) { 409 | let obj = parse[key]; 410 | toast(obj.main, obj.subtext, key); 411 | return; // one at a time. 412 | } 413 | } 414 | }); 415 | } 416 | check(); 417 | setInterval(check, 10 * 1000); 418 | 419 | /* MyDiscord UI */ 420 | 421 | setInterval(() => { 422 | if(document.querySelector('.app .layers .layer+.layer .btn-close') !== null && 423 | document.querySelector('.app .layers .layer+.layer .sidebar > div').innerHTML.includes('User Settings') && 424 | !document.querySelector('.app .layers .layer+.layer .sidebar > div').innerHTML.includes('MyDiscord')){ 425 | 426 | let header_class = document.querySelector('.app .layers .layer+.layer .sidebar > div > div').className; 427 | let unselected_class = document.querySelector('.app .layers .layer+.layer .sidebar > div > div + div + div').className; 428 | let selected_class = document.querySelector('.app .layers .layer+.layer .sidebar > div > div[class*=itemDefaultSelected]').className; 429 | let social_class = document.querySelector('.app .layers .layer+.layer .sidebar > div > div[class*=socialLinks]').className; 430 | let social_link_class = document.querySelector('.app .layers .layer+.layer .sidebar > div > div[class*=socialLinks] a').className; 431 | 432 | let button = document.createElement('div'); 433 | button.className = unselected_class; 434 | button.innerHTML = "myDiscord"; 435 | button.addEventListener("click", buildUi); 436 | 437 | let header = document.createElement("div"); 438 | header.className = header_class; 439 | header.innerHTML = "myDiscord links"; 440 | 441 | let social_links = document.createElement("div"); 442 | social_links.className = social_class + " settings-social-mydiscord"; 443 | 444 | let github_link = document.createElement("a"); 445 | github_link.target = "_blank"; 446 | github_link.rel = "MyDiscord author"; 447 | github_link.title = "MyDiscord - GitHub"; 448 | github_link.href = "https://github.com/justinoboyle/mydiscord"; 449 | github_link.className = social_link_class; 450 | github_link.innerHTML = ''; 451 | 452 | let discord_link = document.createElement("a"); 453 | discord_link.target = "_blank"; 454 | discord_link.rel = "myDiscord author"; 455 | discord_link.title = "myDiscord - Discord chat"; 456 | discord_link.href = "https://discord.gg/rN3WMWn"; 457 | discord_link.className = social_link_class; 458 | discord_link.innerHTML = ''; 459 | 460 | let ref = document.querySelector('.app .layers .layer+.layer .sidebar > div div:nth-child(20)'); 461 | ref.parentNode.insertBefore(button, ref.nextSibling); 462 | 463 | social_links.appendChild(github_link); 464 | social_links.appendChild(discord_link); 465 | ref.parentNode.appendChild(header); 466 | ref.parentNode.appendChild(social_links); 467 | 468 | let elements = document.querySelectorAll('.app .layers .layer+.layer .sidebar > div > div[class*=item]'); 469 | for (i = 0; i < elements.length; ++i) { 470 | let el = elements[i]; 471 | el.addEventListener("click", function(){ 472 | let a = document.querySelectorAll("." + selected_class.split(" ").join(".")); 473 | a[a.length - 1].className = unselected_class; 474 | this.className = selected_class; 475 | }); 476 | } 477 | 478 | } 479 | }, 100); 480 | 481 | function buildUi(){ 482 | /* Checking */ 483 | if(document.querySelector(".app .layers .layer+.layer .content-column") === null) return; 484 | 485 | /* Create elements */ 486 | let discord_container = document.querySelector(".app .layers .layer+.layer .content-column > div"); 487 | discord_container.className = "mydiscord-options"; 488 | 489 | let container = document.createElement("div"); 490 | container.className = "flex-vertical"; 491 | 492 | let title = document.createElement("h2"); 493 | title.className = "settings-title"; 494 | title.innerHTML = "MyDiscord"; 495 | 496 | let option_ga = getCheckbox("Google Analytics", "MyDiscord sends stats about your utilisation of myDiscord to help us improve. You can disable it or just leave it turned on.", !global.noAnalyze, function(){ 497 | global.noAnalyze = !global.noAnalyze; 498 | if(!global.noAnalyze && !ga){ 499 | initGa(); 500 | } 501 | }); 502 | 503 | /* Build */ 504 | discord_container.innerHTML = null; 505 | container.appendChild(title); 506 | container.appendChild(option_ga); 507 | 508 | /* Add users options */ 509 | if(customOptions.length != 0){ 510 | let heading = document.createElement("h5"); 511 | heading.className = "settings-heading"; 512 | heading.innerHTML = "Plugin options"; 513 | container.appendChild(heading); 514 | 515 | for(let k in customOptions) { 516 | container.appendChild(customOptions[k]); 517 | } 518 | } 519 | 520 | discord_container.appendChild(container); 521 | } 522 | })(); 523 | -------------------------------------------------------------------------------- /img/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinoboyle/mydiscord/0a4e3b2dcb8afff3b66a28d5893eed705627be40/img/welcome.png -------------------------------------------------------------------------------- /modal.js: -------------------------------------------------------------------------------- 1 | global.loadPlugin('https://raw.githubusercontent.com/justinoboyle/mydiscord/master/core.js'); 2 | // move to new name -------------------------------------------------------------------------------- /mydiscord/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinoboyle/mydiscord/0a4e3b2dcb8afff3b66a28d5893eed705627be40/mydiscord/__init__.py -------------------------------------------------------------------------------- /mydiscord/__main__.py: -------------------------------------------------------------------------------- 1 | from mydiscord.app import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /mydiscord/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import shutil 5 | import argparse 6 | import textwrap 7 | import subprocess 8 | import psutil 9 | import sys 10 | from collections import namedtuple 11 | from mydiscord.asar import Asar 12 | 13 | 14 | DiscordProcess = namedtuple('DiscordProcess', 'path exe processes') 15 | 16 | def discord_process_terminate(self): 17 | for process in self.processes: 18 | # terrible 19 | process.kill() 20 | 21 | def discord_process_launch(self): 22 | with open(os.devnull, 'w') as f: 23 | subprocess.Popen([os.path.join(self.path, self.exe)], stdout=f, stderr=subprocess.STDOUT) 24 | 25 | def discord_process_resources_path(self): 26 | if sys.platform == 'darwin': 27 | # OS X has a different resources path 28 | # Application directory is under <[EXE].app/Contents/MacOS/[EXE]> 29 | # where [EXE] is Discord Canary, Discord PTB, etc 30 | # Resources directory is under 31 | # So we need to fetch the folder based on the executable path. 32 | # Go two directories up and then go to Resources directory. 33 | return os.path.abspath(os.path.join(self.path, '..', 'Resources')) 34 | return os.path.join(self.path, 'resources') 35 | 36 | DiscordProcess.terminate = discord_process_terminate 37 | DiscordProcess.launch = discord_process_launch 38 | DiscordProcess.resources_path = property(discord_process_resources_path) 39 | 40 | def parse_args(): 41 | description = """\ 42 | Unpacks Discord and adds CSS hot-reloading and custom JavaScript support. 43 | 44 | Discord has to be open for this to work. When this tool is ran, 45 | Discord will close and then be relaunched when the tool completes. 46 | """ 47 | parser = argparse.ArgumentParser(description=description.strip()) 48 | parser.add_argument('--css', metavar='file', help='Location of the CSS file to watch') 49 | parser.add_argument('--js', metavar='file', help='Location of the JS file to inject') 50 | parser.add_argument('--revert', action='store_true', help='Reverts any changes made to Discord (does not delete CSS)') 51 | args = parser.parse_args() 52 | return args 53 | 54 | def discord_process(): 55 | executables = {} 56 | for proc in psutil.process_iter(): 57 | try: 58 | (path, exe) = os.path.split(proc.exe()) 59 | except psutil.AccessDenied: 60 | pass 61 | else: 62 | if exe.startswith('Discord') and not exe.endswith('Helper'): 63 | entry = executables.get(exe) 64 | 65 | if entry is None: 66 | entry = executables[exe] = DiscordProcess(path=path, exe=exe, processes=[]) 67 | 68 | entry.processes.append(proc) 69 | 70 | if len(executables) == 0: 71 | raise RuntimeError('Could not find Discord executable.') 72 | 73 | if len(executables) == 1: 74 | r = executables.popitem() 75 | print('Found {0.exe} under {0.path}'.format(r[1])) 76 | return r[1] 77 | 78 | lookup = list(executables) 79 | for index, exe in enumerate(lookup): 80 | print('%s: Found %s' % (index, exe)) 81 | 82 | while True: 83 | index = input("Discord executable to use (number): ") 84 | try: 85 | index = int(index) 86 | except ValueError as e: 87 | print('Invalid index passed') 88 | else: 89 | if index >= len(lookup) or index < 0: 90 | print('Index too big (or small)') 91 | else: 92 | key = lookup[index] 93 | return executables[key] 94 | 95 | def extract_asar(): 96 | try: 97 | with Asar.open('./app.asar') as a: 98 | try: 99 | a.extract('./app') 100 | except FileExistsError: 101 | answer = input('asar already extracted, overwrite? (Y/n): ') 102 | 103 | if answer.lower().startswith('n'): 104 | print('Exiting.') 105 | return False 106 | 107 | shutil.rmtree('./app') 108 | a.extract('./app') 109 | 110 | shutil.move('./app.asar', './original_app.asar') 111 | except FileNotFoundError as e: 112 | print('WARNING: app.asar not found') 113 | return True 114 | 115 | def main(): 116 | args = parse_args() 117 | try: 118 | discord = discord_process() 119 | except Exception as e: 120 | print(str(e)) 121 | return 122 | 123 | if args.css: 124 | args.css = os.path.abspath(args.css) 125 | else: 126 | args.css = os.path.join(discord.resources_path, 'discord-custom.css') 127 | 128 | if args.js: 129 | args.js = os.path.abspath(args.js) 130 | else: 131 | args.js = os.path.join(discord.resources_path, 'discord-custom.js') 132 | 133 | os.chdir(discord.resources_path) 134 | 135 | args.css = os.path.abspath(args.css) 136 | 137 | discord.terminate() 138 | 139 | if args.revert: 140 | try: 141 | shutil.rmtree('./app') 142 | shutil.move('./original_app.asar', './app.asar') 143 | except FileNotFoundError as e: 144 | # assume things are fine for now i guess 145 | print('No changes to revert.') 146 | else: 147 | print('Reverted changes, no more CSS hot-reload :(') 148 | else: 149 | if extract_asar(): 150 | if not os.path.exists(args.css): 151 | with open(args.css, 'w', encoding='utf-8') as f: 152 | f.write('/* put your custom css here. */\n') 153 | if not os.path.exists(args.js): 154 | with open(args.js, 'w') as f: 155 | f.write(textwrap.dedent("""\ 156 | /* 157 | * Hold Up! 158 | * Pasting anything in here could give attackers access to your Discord account. 159 | * Unless you understand exactly what you are doing, close this document and stay safe. 160 | */ 161 | 162 | // Make this array empty to not load the core plugin. (If you delete it, it will still load it.) I don't recommend removing this as it will remove all GUI functionality! 163 | global.plugins = [ 'https://raw.githubusercontent.com/justinoboyle/mydiscord/master/core.js' ]; 164 | 165 | if(global.config.plugins) 166 | for(let plugin of global.config.plugins) 167 | global.loadPlugin(plugin); 168 | 169 | // To load more plugins (below) -- don't recreate the array! **use global.loadPlugin(link)** 170 | 171 | // You probably don't actually need to touch this file if you're using the proper plugin installation system through core.js 172 | """)) 173 | 174 | css_injection_script = textwrap.dedent("""\ 175 | global.cssFile = '%s'; 176 | global.pluginFile = '%s'; 177 | window._fs = require("fs"); 178 | window._fileWatcher = null; 179 | window._styleTag = null; 180 | window._request = require('request'); 181 | 182 | global.config = {}; 183 | 184 | try { 185 | global.config = require(global.pluginFile + '.config.json') 186 | }catch(e) { 187 | // It doesn't exist, that's OK 188 | } 189 | 190 | global.saveConfig = () => { 191 | _fs.writeFile(global.pluginFile + '.config.json', JSON.stringify(global.config, null, 4), 'utf-8'); 192 | } 193 | saveConfig(); 194 | 195 | window.setupCSS = function(path) { 196 | var customCSS = window._fs.readFileSync(path, "utf-8"); 197 | if(window._styleTag === null) { 198 | window._styleTag = document.createElement("style"); 199 | document.head.appendChild(window._styleTag); 200 | } 201 | window._styleTag.innerHTML = customCSS; 202 | if(window._fileWatcher === null) { 203 | window._fileWatcher = window._fs.watch(path, { encoding: "utf-8" }, 204 | function(eventType, filename) { 205 | if(eventType === "change") { 206 | var changed = window._fs.readFileSync(path, "utf-8"); 207 | window._styleTag.innerHTML = changed; 208 | } 209 | } 210 | ); 211 | } 212 | }; 213 | 214 | window.tearDownCSS = function() { 215 | if(window._styleTag !== null) { window._styleTag.innerHTML = ""; } 216 | if(window._fileWatcher !== null) { window._fileWatcher.close(); window._fileWatcher = null; } 217 | }; 218 | 219 | window.applyAndWatchCSS = function(path) { 220 | window.tearDownCSS(); 221 | window.setupCSS(path); 222 | }; 223 | global.loadedPlugins = {}; 224 | global.loadPlugins = () => { 225 | for(let x of global.plugins) 226 | loadPlugin(x, false); 227 | } 228 | 229 | global.loadPlugin = (x, push = true) => { 230 | if(push) 231 | global.plugins.push(x); 232 | if(typeof(global._request) === "undefined") 233 | global._request = require('request'); 234 | if(!global.loadedPlugins[x]) 235 | global._request(x, function (error, response, body) { 236 | if (!error && response.statusCode == 200) { 237 | eval(body); 238 | } 239 | }) 240 | } 241 | 242 | window.runPluginFile = function(path) { 243 | try { 244 | _fs.readFile(path, 'utf-8', function(err, res) { 245 | if(err) 246 | return console.error(err); 247 | eval(res); 248 | if(typeof(global._request) === "undefined") 249 | global._request = require('request'); 250 | if(!global.plugins) 251 | global.plugins = [ 'https://raw.githubusercontent.com/justinoboyle/mydiscord/master/core.js' ]; 252 | global.loadPlugins(); 253 | }) 254 | }catch(e) { 255 | console.error(e); 256 | } 257 | } 258 | window.applyAndWatchCSS(global.cssFile); 259 | window.runPluginFile(global.pluginFile) 260 | """ % (args.css.replace('\\', '\\\\'), args.js.replace('\\', '\\\\'))) 261 | 262 | with open('./app/cssInjection.js', 'w', encoding='utf-8') as f: 263 | f.write(css_injection_script) 264 | 265 | css_injection_script_path = os.path.abspath('./app/cssInjection.js').replace('\\', '\\\\') 266 | 267 | css_reload_script = textwrap.dedent("""\ 268 | mainWindow.webContents.on('dom-ready', function () { 269 | mainWindow.webContents.executeJavaScript( 270 | _fs2.default.readFileSync('%s', 'utf-8') 271 | ); 272 | }); 273 | """ % css_injection_script_path) 274 | 275 | with open('./app/index.js', 'r', encoding='utf-8') as f: 276 | entire_thing = f.read() 277 | 278 | entire_thing = entire_thing.replace("mainWindow.webContents.on('dom-ready', function () {});", css_reload_script) 279 | 280 | with open('./app/index.js', 'w', encoding='utf-8') as f: 281 | f.write(entire_thing) 282 | 283 | print( 284 | '\nDone!\n' + 285 | '\nYou may now edit your CSS in %s,\n' % os.path.abspath(args.css) + 286 | "which will be reloaded whenever it's saved.\n" + 287 | 'You can also edit your JavaScript in %s\n,' % os.path.abspath(args.js) + 288 | "but you must reload (CMD/CTRL + R) Discord to re-run it\n" + 289 | "\n*Do not insert code that you do not understand, as it could steal your account!*\n" + 290 | '\nRelaunching Discord now...' 291 | ) 292 | 293 | discord.launch() 294 | 295 | 296 | if __name__ == '__main__': 297 | main() 298 | -------------------------------------------------------------------------------- /mydiscord/asar.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import json 4 | import struct 5 | import shutil 6 | 7 | 8 | def round_up(i, m): 9 | """Rounds up ``i`` to the next multiple of ``m``. 10 | 11 | ``m`` is assumed to be a power of two. 12 | """ 13 | return (i + m - 1) & ~(m - 1) 14 | 15 | 16 | class Asar: 17 | 18 | """Represents an asar file. 19 | 20 | You probably want to use the :meth:`.open` or :meth:`.from_path` 21 | class methods instead of creating an instance of this class. 22 | 23 | Attributes 24 | ---------- 25 | path : str 26 | Path of this asar file on disk. 27 | If :meth:`.from_path` is used, this is just 28 | the path given to it. 29 | fp : File-like object 30 | Contains the data for this asar file. 31 | header : dict 32 | Dictionary used for random file access. 33 | base_offset : int 34 | Indicates where the asar file header ends. 35 | """ 36 | 37 | def __init__(self, path, fp, header, base_offset): 38 | self.path = path 39 | self.fp = fp 40 | self.header = header 41 | self.base_offset = base_offset 42 | 43 | @classmethod 44 | def open(cls, path): 45 | """Decodes the asar file from the given ``path``. 46 | 47 | You should use the context manager interface here, 48 | to automatically close the file object when you're done with it, i.e. 49 | 50 | .. code-block:: python 51 | 52 | with Asar.open('./something.asar') as a: 53 | a.extract('./something_dir') 54 | 55 | Parameters 56 | ---------- 57 | path : str 58 | Path of the file to be decoded. 59 | """ 60 | fp = open(path, 'rb') 61 | 62 | # decode header 63 | # NOTE: we only really care about the last value here. 64 | data_size, header_size, header_object_size, header_string_size = struct.unpack('<4I', fp.read(16)) 65 | 66 | header_json = fp.read(header_string_size).decode('utf-8') 67 | 68 | return cls( 69 | path=path, 70 | fp=fp, 71 | header=json.loads(header_json), 72 | base_offset=round_up(16 + header_string_size, 4) 73 | ) 74 | 75 | @classmethod 76 | def from_path(cls, path): 77 | """Creates an asar file using the given ``path``. 78 | 79 | When this is used, the ``fp`` attribute of the returned instance 80 | will be a :class:`io.BytesIO` object, so it's not written to a file. 81 | You have to do something like: 82 | 83 | .. code-block:: python 84 | 85 | with Asar.from_path('./something_dir') as a: 86 | with open('./something.asar', 'wb') as f: 87 | a.fp.seek(0) # just making sure we're at the start of the file 88 | f.write(a.fp.read()) 89 | 90 | You cannot exclude files/folders from being packed yet. 91 | 92 | Parameters 93 | ---------- 94 | path : str 95 | Path to walk into, recursively, and pack 96 | into an asar file. 97 | """ 98 | offset = 0 99 | concatenated_files = b'' 100 | 101 | def _path_to_dict(path): 102 | nonlocal concatenated_files, offset 103 | result = {'files': {}} 104 | 105 | for f in os.scandir(path): 106 | if os.path.isdir(f.path): 107 | result['files'][f.name] = _path_to_dict(f.path) 108 | else: 109 | size = f.stat().st_size 110 | 111 | result['files'][f.name] = { 112 | 'size': size, 113 | 'offset': str(offset) 114 | } 115 | 116 | with open(f.path, 'rb') as fp: 117 | concatenated_files += fp.read() 118 | 119 | offset += size 120 | 121 | return result 122 | 123 | header = _path_to_dict(path) 124 | header_json = json.dumps(header, sort_keys=True, separators=(',', ':')).encode('utf-8') 125 | 126 | # TODO: using known constants here for now (laziness)... 127 | # we likely need to calc these, but as far as discord goes we haven't needed it. 128 | header_string_size = len(header_json) 129 | data_size = 4 # uint32 size 130 | aligned_size = round_up(header_string_size, data_size) 131 | header_size = aligned_size + 8 132 | header_object_size = aligned_size + data_size 133 | 134 | # pad remaining space with NULLs 135 | diff = aligned_size - header_string_size 136 | header_json = header_json + b'\0' * (diff) if diff else header_json 137 | 138 | fp = io.BytesIO() 139 | fp.write(struct.pack('<4I', data_size, header_size, header_object_size, header_string_size)) 140 | fp.write(header_json) 141 | fp.write(concatenated_files) 142 | 143 | return cls( 144 | path=path, 145 | fp=fp, 146 | header=header, 147 | base_offset=round_up(16 + header_string_size, 4) 148 | ) 149 | 150 | def _copy_unpacked_file(self, source, destination): 151 | """Copies an unpacked file to where the asar is extracted to. 152 | 153 | An example: 154 | 155 | . 156 | ├── test.asar 157 | └── test.asar.unpacked 158 | ├── abcd.png 159 | ├── efgh.jpg 160 | └── test_subdir 161 | └── xyz.wav 162 | 163 | If we are extracting ``test.asar`` to a folder called ``test_extracted``, 164 | not only the files concatenated in the asar will go there, but also 165 | the ones inside the ``*.unpacked`` folder too. 166 | 167 | That is, after extraction, the previous example will look like this: 168 | 169 | . 170 | ├── test.asar 171 | ├── test.asar.unpacked 172 | | └── ... 173 | └── test_extracted 174 | ├── whatever_was_inside_the_asar.js 175 | ├── junk.js 176 | ├── abcd.png 177 | ├── efgh.jpg 178 | └── test_subdir 179 | └── xyz.wav 180 | 181 | In the asar header, they will show up without an offset, and ``"unpacked": true``. 182 | 183 | Currently, if the expected directory doesn't already exist (or the file isn't there), 184 | a message is printed to stdout. It could be logged in a smarter way but that's a TODO. 185 | 186 | Parameters 187 | ---------- 188 | source : str 189 | Path of the file to locate and copy 190 | destination : str 191 | Destination folder to copy file into 192 | """ 193 | unpacked_dir = self.path + '.unpacked' 194 | if not os.path.isdir(unpacked_dir): 195 | print("Couldn't copy file {}, no extracted directory".format(source)) 196 | return 197 | 198 | src = os.path.join(unpacked_dir, source) 199 | if not os.path.exists(src): 200 | print("Couldn't copy file {}, doesn't exist".format(src)) 201 | return 202 | 203 | dest = os.path.join(destination, source) 204 | shutil.copyfile(src, dest) 205 | 206 | def _extract_file(self, source, info, destination): 207 | """Locates and writes to disk a given file in the asar archive. 208 | 209 | Parameters 210 | ---------- 211 | source : str 212 | Path of the file to write to disk 213 | info : dict 214 | Contains offset and size if applicable. 215 | If offset is not given, the file is assumed to be 216 | sitting outside of the asar, unpacked. 217 | destination : str 218 | Destination folder to write file into 219 | 220 | See Also 221 | -------- 222 | :meth:`._copy_unpacked_file` 223 | """ 224 | if 'offset' not in info: 225 | self._copy_unpacked_file(source, destination) 226 | return 227 | 228 | self.fp.seek(self.base_offset + int(info['offset'])) 229 | r = self.fp.read(int(info['size'])) 230 | 231 | dest = os.path.join(destination, source) 232 | with open(dest, 'wb') as f: 233 | f.write(r) 234 | 235 | def _extract_directory(self, source, files, destination): 236 | """Extracts all the files in a given directory. 237 | 238 | If a sub-directory is found, this calls itself as necessary. 239 | 240 | Parameters 241 | ---------- 242 | source : str 243 | Path of the directory 244 | files : dict 245 | Maps a file/folder name to another dictionary, 246 | containing either file information, 247 | or more files. 248 | destination : str 249 | Where the files in this folder should go to 250 | """ 251 | dest = os.path.normcase(os.path.join(destination, source)) 252 | 253 | if not os.path.exists(dest): 254 | os.makedirs(dest) 255 | 256 | for name, info in files.items(): 257 | item_path = os.path.join(source, name) 258 | 259 | if 'files' in info: 260 | self._extract_directory(item_path, info['files'], destination) 261 | continue 262 | 263 | self._extract_file(item_path, info, destination) 264 | 265 | def extract(self, path): 266 | """Extracts this asar file to ``path``. 267 | 268 | Parameters 269 | ---------- 270 | path : str 271 | Destination of extracted asar file. 272 | """ 273 | if os.path.exists(path): 274 | raise FileExistsError() 275 | 276 | self._extract_directory('.', self.header['files'], path) 277 | 278 | def __enter__(self): 279 | return self 280 | 281 | def __exit__(self, exc_type, exc_value, traceback): 282 | self.fp.close() 283 | -------------------------------------------------------------------------------- /mydiscord/discord.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Hold Up! 3 | * Pasting anything in here could give attackers access to your Discord account. 4 | * Unless you understand exactly what you are doing, close this document and stay safe. 5 | */ 6 | 7 | // Make this array empty to not load the core plugin. (If you delete it, it will still load it.) I don't recommend removing this as it will remove all GUI functionality! 8 | global.plugins = [ 'https://raw.githubusercontent.com/justinoboyle/mydiscord/master/core.js' ]; 9 | 10 | if(global.config.plugins) 11 | for(let plugin of global.config.plugins) 12 | global.loadPlugin(plugin); 13 | 14 | // To load more plugins (below) -- don't recreate the array! **use global.loadPlugin(link)** 15 | 16 | // You probably don't actually need to touch this file if you're using the proper plugin installation system through core.js 17 | -------------------------------------------------------------------------------- /mydiscord/discord.js.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "showsOnBoot": true 3 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | with open('requirements.txt') as f: 5 | requirements = f.read().splitlines() 6 | 7 | with open('README.md') as f: 8 | readme = f.read() 9 | 10 | 11 | setup( 12 | name='MyDiscord', 13 | author='justinoboyle', 14 | url='https://github.com/justinoboyle/MyDiscord', 15 | version='0.11.3', 16 | license='MIT', 17 | description='Adds custom CSS and JavaScript support to Discord. (Fork of BeautifulDiscord)', 18 | long_description=readme, 19 | packages=find_packages(), 20 | install_requires=requirements, 21 | include_package_data=True, 22 | entry_points={'console_scripts': ['mydiscord=mydiscord.app:main']} 23 | ) 24 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* Globals */ 2 | .flex{ 3 | display: flex; 4 | } 5 | /* END Globals */ 6 | 7 | /* Toast */ 8 | .toast { 9 | width: 100%; 10 | position: fixed; 11 | background-color: rgba(0,0,0,0.8); 12 | z-index: 100; 13 | bottom: 0px; 14 | padding: 10px 10px 10px 10px; 15 | color: white; 16 | webkit-transition: all 150ms ease-in ease-out; 17 | transition: all 150ms ease-in ease-out; 18 | opacity: 100%; 19 | } 20 | .toast-dying { 21 | opacity: 0%; 22 | bottom: -43px; 23 | webkit-transition: all 150ms ease-in ease-out; 24 | transition: all 150ms ease-in ease-out; 25 | } 26 | .toast-main { 27 | font-size: 20px; 28 | font-weight: 600; 29 | } 30 | .toast-subtext:before { 31 | font-size: 20px; 32 | font-weight: 200; 33 | content: " | " 34 | } 35 | .toast-subtext { 36 | font-size: 18px; 37 | font-weight: 300; 38 | } 39 | .toast-content { 40 | padding-right: 40px; 41 | } 42 | .toast-closeButton { 43 | right: 30px; 44 | position: absolute; 45 | bottom: 25%; 46 | } 47 | #app-mount { 48 | z-index: 1; 49 | } 50 | /* END Toast */ 51 | 52 | /* Settings */ 53 | .settings-title{ 54 | color: #f6f6f7; 55 | text-transform: uppercase; 56 | margin-bottom: 20px; 57 | font-weight: 600; 58 | line-height: 20px; 59 | font-size: 16px; 60 | } 61 | 62 | .settings-heading{ 63 | color: #b9bbbe; 64 | margin-bottom: 8px; 65 | letter-spacing: .5px; 66 | text-transform: uppercase; 67 | font-weight: 600; 68 | line-height: 16px; 69 | font-size: 12px; 70 | } 71 | 72 | .settings-container{ 73 | margin-bottom: 20px; 74 | } 75 | 76 | .settings-label{ 77 | color: #f6f6f7; 78 | margin-left: 0; 79 | margin-right: 10px; 80 | margin-top: 0; 81 | margin-bottom: 0; 82 | font-weight: 500; 83 | line-height: 24px; 84 | font-size: 16px; 85 | flex: 1 1 auto; 86 | } 87 | 88 | .settings-input-field .settings-label{ 89 | margin-bottom: 5px; 90 | } 91 | 92 | .settings-desc{ 93 | color: #72767d; 94 | font-size: 14px; 95 | line-height: 20px; 96 | font-weight: 500; 97 | margin-top: 4px; 98 | } 99 | 100 | .settings-checkbox-wrap{ 101 | margin-right: 0; 102 | margin-left: 10px; 103 | user-select: none; 104 | position: relative; 105 | width: 44px; 106 | height: 24px; 107 | display: block; 108 | } 109 | 110 | /* Checkboxes */ 111 | .settings-checkbox{ 112 | position: absolute; 113 | opacity: 0; 114 | cursor: pointer; 115 | width: 100%; 116 | height: 100%; 117 | z-index: 1; 118 | } 119 | .settings-checkbox-switch{ 120 | position: absolute; 121 | top: 0; 122 | right: 0; 123 | bottom: 0; 124 | left: 0; 125 | background: #72767d; 126 | border-radius: 14px; 127 | transition: background .15s ease-in-out,box-shadow .15s ease-in-out,border .15s ease-in-out; 128 | } 129 | .settings-checkbox-switch.settings-checkbox-checked{ 130 | background: #7289da; 131 | } 132 | .settings-checkbox-switch:before{ 133 | content: ""; 134 | display: block; 135 | width: 18px; 136 | height: 18px; 137 | position: absolute; 138 | top: 3px; 139 | left: 3px; 140 | bottom: 3px; 141 | background: #f6f6f7; 142 | border-radius: 10px; 143 | transition: all .15s ease; 144 | box-shadow: 0 3px 1px 0 rgba(0,0,0,.05), 0 2px 2px 0 rgba(0,0,0,.1), 0 3px 3px 0 rgba(0,0,0,.05); 145 | } 146 | .settings-checkbox-switch.settings-checkbox-checked:before{ 147 | -webkit-transform: translateX(20px); 148 | transform: translateX(20px); 149 | } 150 | /* Inputs */ 151 | .settings-input{ 152 | color: #f6f6f7; 153 | background-color: rgba(0,0,0,.1); 154 | border-color: rgba(0,0,0,.3); 155 | padding: 10px; 156 | height: 40px; 157 | box-sizing: border-box; 158 | width: 100%; 159 | border-width: 1px; 160 | border-style: solid; 161 | border-radius: 3px; 162 | outline: none; 163 | transition: background-color .15s ease,border .15s ease; 164 | font-size: 16px; 165 | } 166 | 167 | 168 | .settings-divider{ 169 | background-color: hsla(218,5%,47%,.3); 170 | width: 100%; 171 | height: 1px; 172 | margin-top: 20px; 173 | } 174 | 175 | .settings-social-mydiscord a{ 176 | margin-right: 6px; 177 | } 178 | 179 | .settings-logo-github{ 180 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAXCAYAAADgKtSgAAACDUlEQVRIiZ3UT4hOYRTH8c9co6mJWSj/JSZZWA0h2xMzG2Eto1B2LBRWklI2mvxbmkJYkpoY+fPepiwmMkaSQpL9pKQQyuLet+48817z57d53+f3nPO9nec852lrNBpqtAB96MU2rEY7/uALRvEYjyLieytAWw18P45hY92XK3qFCxFxczr4PFzFwRlAU13D4Yj42zSyymYnhucIVuYN53ne2Qo+qDhfeIe3M4S+LeOV+YPNjeax7MOtSsIuPMVOnFA08T2+oQvrFc09j/vYjqFKfn9E3G5rNBoLMY7uyuYmRaOmVUTI83wjxir2J/RkZSndSc6KmYD/E9+N3kxRelXjijs8G42aWunODFsS8yImZkOOiAlcSuwtGVYl5ofZgCv6mKxXZehIzPY5wtO8jgw/EnPpHOFLkvWPDJ8Tc/cc4Wne58zUm9GPPbOh5nm+B3sTezQzebJelr/3cMT0931lnudHcUfx6FU1lGEEb5qGYqh+4Yrizt8zdcjW4S5e4HIL8BhGMvzEydI8U653KBq9GD34miRPYCuW11R0KiJ+Nl/Fhxgo/9/EM6xVnP3mFvCvZcWtNBARw0x+co/jBtYontFNioHqqoGkRwHXIuJ4c5ElmwdwFhvwAK9xrgZezf2NsxFxqC6gqdMIPMF8LKuBNysaQl9EnE4D6kZ9BIsUU/e8Jua1oqHX1Zz/P4cAfD/4X0dQAAAAAElFTkSuQmCC") no-repeat; 181 | background-size: 19px 19px; 182 | width: 20px; 183 | height: 20px; 184 | display: inline-block; 185 | } 186 | 187 | .settings-logo-discord{ 188 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAQAAACROWYpAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHonAACAgwAA+mQAAIDSAAB2hgAA7OkAADmeAAAV/sZ+0zoAAAJISURBVHjanJQ/aBVBEMYnReIfAoqgxiIIgoiFhQGxCIitiDYaRLEQsRSJiLWFwvHAxChipSLayO+L2KTSQgvFSLSweir6iF1QsUwwDxmLt3e3u/caZbnlbm++mW9mZz5zwzCOscBXfaStttrx3nujfvtCmxsMYW7mhnFcLsdxhQeXq+9XOHumPT3wBK/kSgxiw/pP6QqXq+VmbrqoDolByiN2FTm7H8B0aBhQrX4uKcFM0mkSzk9ieAJWpwH5o9Xqu8tqykguJ41cEe/qhR4wzxIfeMciHd3WLX3K+IScJ+uCybXEUe1nnB0MYDJMmxhjjK2ajhNIIld3PIExRK91emDDGGQtpjcR8TJyWTBc3xmogQq7wgmnonrXtKtM7kSmARidrNNydlW9nAPtg8F0hDNsDtBDOlC642XVeXFkOa4V1oeY83LmZTKdlcs5HHhczgpWguW8Df6H9VuurjbINIPLaYUKHMnakxr8pKJ3Xc40hrEP10+NhhR2ZrSjDrsrwxhlG1HZQt13aaNMa7SSXVUZWXMyjGEKziUV36IpTmMydmcdFtF2PQxxLsh5r0ea0ax+6JtGMJn26lc+kulgLKmlcd1LRKCrEzrJXCwN/SL30Y9kiut/eW8rlplkSBMNSWhf0mIuQ7l5U8tK8Hl9TvWJZFU9tUxznjmop6la0hQdlzPF44YAYmzXclNqlSmXFmS8zmjT66prTFGooFChQgUFYQ+rxU0NYnoe+ETgrBlV6UhDU4zZuLf/FWxMy8X/gk1XdcXt7wAHaJTG0tD/pgAAAABJRU5ErkJggg==") no-repeat; 189 | background-size: 19px 19px; 190 | width: 20px; 191 | height: 20px; 192 | display: inline-block; 193 | } 194 | 195 | /* Modal */ 196 | .mydiscord-modal{ 197 | display: flex; 198 | position: absolute; 199 | top: 0; 200 | left: 0; 201 | height: 100%; 202 | width: 100%; 203 | padding-top: 60px; 204 | padding-bottom: 60px; 205 | z-index: 1000; 206 | box-sizing: border-box; 207 | min-height: 340px; 208 | animation: modalShow .1s; 209 | -webkit-box-orient: vertical; 210 | -webkit-box-direction: normal; 211 | flex-direction: column; 212 | pointer-events: auto; 213 | flex-direction: column; 214 | -webkit-box-pack: center; 215 | align-items: center; 216 | justify-content: center; 217 | } 218 | 219 | .mydiscord-modal-hide{ 220 | animation: modalHide .1s; 221 | } 222 | 223 | .mydiscord-modal-inner{ 224 | flex-direction: column; 225 | display: flex; 226 | background-color: #2f3136; 227 | box-shadow: 0 0 0 1px rgba(32,34,37,.6), 0 2px 10px 0 rgba(0,0,0,.2); 228 | width: 440px; 229 | max-height: 660px; 230 | min-height: 200px; 231 | position: relative; 232 | border-radius: 5px; 233 | } 234 | 235 | .mydiscord-modal-header{ 236 | transition: box-shadow .1s ease-out; 237 | word-wrap: break-word; 238 | position: relative; 239 | flex: 0 0 auto; 240 | padding: 20px; 241 | z-index: 1; 242 | overflow-x: hidden; 243 | } 244 | 245 | .mydiscord-modal-header h4{ 246 | color: #f6f6f7; 247 | text-transform: uppercase; 248 | letter-spacing: .3px; 249 | font-weight: 600; 250 | line-height: 20px; 251 | font-size: 16px; 252 | } 253 | 254 | .mydiscord-modal-scrollerWrap::-webkit-scrollbar, 255 | .mydiscord-modal-scroller::-webkit-scrollbar{ 256 | display: none; 257 | } 258 | 259 | .mydiscord-modal-scrollerWrap{ 260 | position: relative; 261 | min-height: 1px; 262 | height: 100%; 263 | display: flex; 264 | flex: 1; 265 | } 266 | 267 | .mydiscord-modal-scroller{ 268 | padding: 0 12px 0 20px; 269 | overflow-x: hidden; 270 | overflow-y: scroll; 271 | min-height: 1px; 272 | flex: 1; 273 | color: #f6f6f7; 274 | font-weight: 400; 275 | line-height: 24px; 276 | font-size 16px; 277 | margin-bottom: 20px; 278 | } 279 | 280 | .mydiscord-modal-footer{ 281 | background-color: rgba(32,34,37,.3); 282 | box-shadow: inset 0 1px 0 rgba(32,34,37,.6); 283 | border-radius: 0 0 5px 5px; 284 | position: relative; 285 | flex: 0 0 auto; 286 | padding: 20px; 287 | z-index: 1; 288 | overflow-x: hidden; 289 | } 290 | 291 | .mydiscord-modal-button-done{ 292 | border: none; 293 | border-radius: 3px; 294 | font-size: 14px; 295 | font-weight: 500; 296 | line-height: 16px; 297 | color: #fff; 298 | background-color: #7289da; 299 | transition: background-color .17s ease; 300 | cursor: pointer; 301 | min-width: 96px; 302 | min-height: 38px; 303 | } 304 | 305 | .mydiscord-modal-button-done:hover{ 306 | background-color: #677bc4; 307 | } 308 | 309 | .mydiscord-modal-button-inner{ 310 | display: inline; 311 | white-space: nowrap; 312 | text-overflow: ellipsis; 313 | overflow: hidden; 314 | } 315 | 316 | /* END GUI */ 317 | 318 | /* Keyframes */ 319 | @keyframes modalShow{ 320 | from{ 321 | opacity: 0; 322 | } 323 | to{ 324 | opacity: 1; 325 | } 326 | } 327 | 328 | @keyframes modalHide{ 329 | from{ 330 | opacity: 1; 331 | } 332 | to{ 333 | opacity: 0; 334 | } 335 | } 336 | --------------------------------------------------------------------------------