├── .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 |
--------------------------------------------------------------------------------