bugfixes and drawer states

- Separate loading icon
- Add dependency injection
- Separate module patch
- Make Core a class
- DOMManager no longer waits for init
- monkeyPatch now uses patcher
- AddonManagers now have a getAddon function
- Alert modal no longer has cancel option
- Settings drawers remember their states
- Remove unused modules
- Add more UI strings to locale managed module
This commit is contained in:
Zack Rauen 2020-07-18 19:01:49 -04:00
parent 8cfa4e4001
commit 8f2ece3678
23 changed files with 508 additions and 386 deletions

View File

@ -5,14 +5,13 @@ This list only reflects the items that have needed to be done since July 2020, t
Note: The items listed here are not in any sort of priority order. Note: The items listed here are not in any sort of priority order.
### To Do (Remote Side) ### To Do (Remote Side)
- Dependency loading (jquery, css, config file) - Transition code
- Stop depending on injector giving config
- Abstract out more UI strings
### To Do (Injector) ### To Do (Injector)
- Update to new windowprefs location - Update to new windowprefs location
- Remove dependency management - Remove dependency management
- Remove string script injection/communication with remote - Remove string script injection/communication with remote
- expand ipc/config commands
### To Do (Meta) ### To Do (Meta)
- Update README (info, patrons) - Update README (info, patrons)
@ -20,14 +19,12 @@ Note: The items listed here are not in any sort of priority order.
- Add gh funding - Add gh funding
### Someday ### Someday
- Switch from /css and /js to /release
- Move old utilities to BdApi - Move old utilities to BdApi
- Component patcher (also does additional classes, etc) - Component patcher (also does additional classes, etc)
- Plugin Class - Plugin Class
- New Plugin API - New Plugin API
- Require patch - Require patch
- Backwards compatibility module (with deprecation notices)
- Modify old monkeyPatch to really use Patcher
- Repo browser - Repo browser
- Addon update system - Addon update system
- Rewrite emote auto caps
- Modify CSP rather than entirely remove or use privileged scheme - Modify CSP rather than entirely remove or use privileged scheme

View File

@ -1,29 +1,3 @@
/* BEGIN V2 LOADER */
/* =============== */
.bd-loaderv2 {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+IDwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyBoZWlnaHQ9IjEwMCUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiB3aWR0aD0iMTAwJSIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIj4gICAgPG1ldGFkYXRhIC8+ICAgIDxkZWZzPiAgICAgICAgPGZpbHRlciBpZD0ic2hhZG93MSI+ICAgICAgICA8ZmVEcm9wU2hhZG93IGR4PSIyMCIgZHk9IjAiIHN0ZERldmlhdGlvbj0iMjAiIGZsb29kLWNvbG9yPSJyZ2JhKDAsMCwwLDAuMzUpIi8+ICAgICAgICA8L2ZpbHRlcj4gICAgICAgIDxmaWx0ZXIgaWQ9InNoYWRvdzIiPiAgICAgICAgPGZlRHJvcFNoYWRvdyBkeD0iMTUiIGR5PSIwIiBzdGREZXZpYXRpb249IjIwIiBmbG9vZC1jb2xvcj0icmdiYSgyNTUsMjU1LDI1NSwwLjE1KSIvPiAgICAgICAgPC9maWx0ZXI+ICAgICAgICA8ZmlsdGVyIGlkPSJzaGFkb3czIj4gICAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjEwIiBkeT0iMCIgc3RkRGV2aWF0aW9uPSIyMCIgZmxvb2QtY29sb3I9InJnYmEoMCwwLDAsMC4zNSkiLz4gICAgICAgIDwvZmlsdGVyPiAgICA8L2RlZnM+ICAgIDxnPiAgICAgICAgPHBhdGggc3R5bGU9ImZpbHRlcjogdXJsKCNzaGFkb3czKSIgZD0iTTExOTUuNDQrMTM1LjQ0MkwxMTk1LjQ0KzEzNS40NDJMOTk3LjYrMTM2LjQ0MkMxMDI0LjIrMTQ5Ljc0MisxMTcwLjM0KzE2My41NDIrMTE5My42NCsxNzkuNzQyQzEyNjQuMzQrMjI4Ljg0MisxMzE5Ljc0KzI5MS4yNDIrMTM1OC4yNCszNjUuMDQyQzEzOTguMTQrNDQxLjY0MisxNDE5Ljc0KzUzMC42NDIrMTQyMi41NCs2MjkuNjQyTDE0MjIuNTQrNjMwLjg0MkwxNDIyLjU0KzYzMi4wNDJDMTQyMi41NCs3NzMuMTQyKzE0MjIuNTQrMTIyOC4xNCsxNDIyLjU0KzEzNjkuMTRMMTQyMi41NCsxMzcwLjM0TDE0MjIuNTQrMTM3MS41NEMxNDE5Ljg0KzE0NzAuNTQrMTM5OC4yNCsxNTU5LjU0KzEzNTguMjQrMTYzNi4xNEMxMzE5Ljc0KzE3MDkuOTQrMTI2NC40NCsxNzcyLjM0KzExOTMuNjQrMTgyMS40NEMxMTcxLjA0KzE4MzcuMTQrMTAyNS43KzE4NTAuNTQrMTAwMCsxODYzLjU0TDExOTMuNTQrMTg2NC41NEMxNTM5Ljc0KzE4NjYuNDQrMTg2NC41NCsxNjkzLjM0KzE4NjQuNTQrMTI5Ni42NEwxODY0LjU0KzcxNi45NDJDMTg2Ni40NCszMTIuNDQyKzE1NDEuNjQrMTM1LjQ0MisxMTk1LjQ0KzEzNS40NDJaIiBmaWxsPSIjMTcxNzE3IiBvcGFjaXR5PSIxIi8+ICAgICAgICA8cGF0aCBzdHlsZT0iZmlsdGVyOiB1cmwoI3NoYWRvdzIpIiBkPSJNMTY5NS41NCs2MzEuNDQyQzE2ODUuODQrMjc4LjA0MisxNDA5LjM0KzEzNS40NDIrMTA1Mi45NCsxMzUuNDQyTDM2MS43NCsxMzYuNDQyTDgwMy43NCs0OTAuNDQyTDEwNjAuNzQrNDkwLjQ0MkMxMzM1LjI0KzQ5MC40NDIrMTMzNS4yNCs4MzUuMzQyKzEwNjAuNzQrODM1LjM0MkwxMDYwLjc0KzExNjQuODRDMTE1MC4yMisxMTY0Ljg0KzEyMTAuNTMrMTIwMS40OCsxMjQxLjY4KzEyNTAuODdDMTMwNi4wNysxMzUzKzEyNDUuNzYrMTUwOS42NCsxMDYwLjc0KzE1MDkuNjRMMzYxLjc0KzE4NjMuNTRMMTA1Mi45NCsxODY0LjU0QzE0MDkuMjQrMTg2NC41NCsxNjg1Ljc0KzE3MjEuOTQrMTY5NS41NCsxMzY4LjU0QzE2OTUuNTQrMTIwNS45NCsxNjUxLjA0KzEwODQuNDQrMTU3Mi42NCs5OTkuOTQyQzE2NTEuMDQrOTE1LjU0MisxNjk1LjU0Kzc5NC4wNDIrMTY5NS41NCs2MzEuNDQyWiIgZmlsbD0iIzNFODJFNSIgb3BhY2l0eT0iMSIvPiAgICAgICAgPHBhdGggc3R5bGU9ImZpbHRlcjogdXJsKCNzaGFkb3cxKSIgZD0iTTE0NjkuMjUrNjMxLjQ0MkMxNDU5LjU1KzI3OC4wNDIrMTE4My4wNSsxMzUuNDQyKzgyNi42NSsxMzUuNDQyTDEzNS40NSsxMzUuNDQyTDEzNS40NSsxMDA0QzEzNS40NSsxMDA0KzEzNS40MjcrMTI1NS4yMSszNTUuNjI2KzEyNTUuMjFDNTc1LjgyNSsxMjU1LjIxKzU3NS44NDgrMTAwNCs1NzUuODQ4KzEwMDRMNTc3LjQ1KzQ5MC40NDJMODM0LjQ1KzQ5MC40NDJDMTEwOC45NSs0OTAuNDQyKzExMDguOTUrODM1LjM0Mis4MzQuNDUrODM1LjM0Mkw2NjQuNjUrODM1LjM0Mkw2NjQuNjUrMTE2NC44NEw4MzQuNDUrMTE2NC44NEM5MjMuOTMyKzExNjQuODQrOTg0LjI0NCsxMjAxLjQ4KzEwMTUuMzkrMTI1MC44N0MxMDc5Ljc4KzEzNTMrMTAxOS40NysxNTA5LjY0KzgzNC40NSsxNTA5LjY0TDEzNS40NSsxNTA5LjY0TDEzNS40NSsxODY0LjU0TDgyNi42NSsxODY0LjU0QzExODIuOTUrMTg2NC41NCsxNDU5LjQ1KzE3MjEuOTQrMTQ2OS4yNSsxMzY4LjU0QzE0NjkuMjUrMTIwNS45NCsxNDI0Ljc1KzEwODQuNDQrMTM0Ni4zNSs5OTkuOTQyQzE0MjQuNzUrOTE1LjU0MisxNDY5LjI1Kzc5NC4wNDIrMTQ2OS4yNSs2MzEuNDQyWiIgZmlsbD0iI0ZGRkZGRiIgb3BhY2l0eT0iMSIvPiAgICA8L2c+PC9zdmc+);
}
.bd-loaderv2 {
position: fixed;
bottom:5px;
right:5px;
z-index: 2147483647;
display: block;
width: 20px;
height: 20px;
background-size: 100% 100%;
animation: bd-loaderv2-animation 1.5s ease-in-out infinite;
}
@keyframes bd-loaderv2-animation {
0% { opacity: 0.05; }
50% { opacity: 0.6; }
100% { opacity: 0.05; }
}
/* =============== */
/* END V2 LOADER */
.bd-settings-group.collapsible .bd-settings-title { .bd-settings-group.collapsible .bd-settings-title {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
js/main.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import Builtin from "../../structs/builtin"; import Builtin from "../../structs/builtin";
import {DOM, DiscordModules} from "modules"; import {DOM, DiscordModules, Strings} from "modules";
export default new class DeveloperMode extends Builtin { export default new class DeveloperMode extends Builtin {
get name() {return "DeveloperMode";} get name() {return "DeveloperMode";}
@ -49,7 +49,7 @@ export default new class DeveloperMode extends Builtin {
const cmg = DOM.createElement(`<div class="itemGroup-1tL0uz da-itemGroup">`); const cmg = DOM.createElement(`<div class="itemGroup-1tL0uz da-itemGroup">`);
const cmi = DOM.createElement(`<div class="item-1Yvehc itemBase-tz5SeC da-item da-itemBase clickable-11uBi- da-clickable">`); const cmi = DOM.createElement(`<div class="item-1Yvehc itemBase-tz5SeC da-item da-itemBase clickable-11uBi- da-clickable">`);
cmi.append(DOM.createElement(`<div class="label-JWQiNe da-label">Copy Selector</div>`)); cmi.append(DOM.createElement(`<div class="label-JWQiNe da-label">${Strings.Developer.copySelector}</div>`));
cmi.addEventListener("click", () => { cmi.addEventListener("click", () => {
DiscordModules.ElectronModule.copy(selector); DiscordModules.ElectronModule.copy(selector);
cm.style.display = "none"; cm.style.display = "none";

View File

@ -1,6 +1,6 @@
import Builtin from "../../structs/builtin"; import Builtin from "../../structs/builtin";
import Modals from "../../ui/modals"; import Modals from "../../ui/modals";
import {Strings} from "modules";
const electron = require("electron"); const electron = require("electron");
const fs = require("fs"); const fs = require("fs");
@ -37,7 +37,7 @@ export default new class ReactDevTools extends Builtin {
enabled() { enabled() {
if (!this.isExtensionInstalled) this.findExtension(); if (!this.isExtensionInstalled) this.findExtension();
if (!this.isExtensionInstalled) return Modals.alert("Extension Not Found", "Unable to find the React Developer Tools extension on your PC. Please install the extension on your local Chrome installation."); if (!this.isExtensionInstalled) return Modals.alert(Strings.ReactDevTools.notFound, Strings.ReactDevTools.notFoundDetails);
setImmediate(() => webContents.on("devtools-opened", this.listener)); setImmediate(() => webContents.on("devtools-opened", this.listener));
if (webContents.isDevToolsOpened()) this.listener(); if (webContents.isDevToolsOpened()) this.listener();
} }

View File

@ -68,6 +68,7 @@ export default new class EmoteMenu extends Builtin {
} }
async enabled() { async enabled() {
// Temporary measure, so not using Strings/translation
return Modals.alert("Emote Menu Broken", "Emote Menu is currently broken, it is recommended to disable this until it is fixed."); return Modals.alert("Emote Menu Broken", "Emote Menu is currently broken, it is recommended to disable this until it is fixed.");
// this.log("Starting to observe"); // this.log("Starting to observe");
// this.observer.observe(document.getElementById("app-mount"), { // this.observer.observe(document.getElementById("app-mount"), {

View File

@ -211,15 +211,14 @@ export default {
confirmationText: "You have unsaved changes to {{name}}. Closing this window will lose all those changes.", confirmationText: "You have unsaved changes to {{name}}. Closing this window will lose all those changes.",
enabled: "{{name}} has been enabled.", enabled: "{{name}} has been enabled.",
disabled: "{{name}} has been disabled.", disabled: "{{name}} has been disabled.",
couldNotEnable: "{{name}} could not be enabled.",
couldNotDisable: "{{name}} could not be disabled.",
couldNotStart: "{{name}} could not be started.",
couldNotStop: "{{name}} could not be stopped.",
methodError: "{{method}} could not be fired.",
unknownAuthor: "Unknown Author", unknownAuthor: "Unknown Author",
noDescription: "Description not provided." noDescription: "Description not provided."
}, },
Emotes: {
loading: "Loading emotes in the background do not reload.",
loaded: "All emotes successfully loaded.",
clearEmotes: "Clear Emote Data",
favoriteAction: "Favorite!"
},
CustomCSS: { CustomCSS: {
confirmationText: "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.", confirmationText: "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",
update: "Update", update: "Update",
@ -229,6 +228,15 @@ export default {
settings: "Editor Settings", settings: "Editor Settings",
editorTitle: "Custom CSS Editor" editorTitle: "Custom CSS Editor"
}, },
Developer: {
copySelector: "Copy Selector"
},
Emotes: {
loading: "Loading emotes in the background do not reload.",
loaded: "All emotes successfully loaded.",
clearEmotes: "Clear Emote Data",
favoriteAction: "Favorite!"
},
PublicServers: { PublicServers: {
button: "public", button: "public",
join: "Join", join: "Join",
@ -259,16 +267,16 @@ export default {
restartLater: "Restart Later", restartLater: "Restart Later",
additionalInfo: "Additional Info" additionalInfo: "Additional Info"
}, },
ReactDevTools: {
notFound: "Extension Not Found",
notFoundDetails: "Unable to find the React Developer Tools extension on your PC. Please install the extension on your local Chrome installation."
},
Sorting: { Sorting: {
sortBy: "Sort By", sortBy: "Sort By",
order: "Order", order: "Order",
ascending: "Ascending", ascending: "Ascending",
descending: "Descending" descending: "Descending"
}, },
WindowPrefs: {
enabledInfo: "This option requires a transparent theme in order to work properly. On Windows this may break your aero snapping and maximizing.\n\nIn order to take effect, Discord needs to be restarted. Do you want to restart now?",
disabledInfo: "In order to take effect, Discord needs to be restarted. Do you want to restart now?"
},
Startup: { Startup: {
notSupported: "Not Supported", notSupported: "Not Supported",
versionMismatch: "BandagedBD Injector v{{injector}} is not supported by the latest remote (v{{remote}}).\n\nPlease download the latest version from [GitHub](https://github.com/rauenzi/BetterDiscordApp/releases/latest)", versionMismatch: "BandagedBD Injector v{{injector}} is not supported by the latest remote (v{{remote}}).\n\nPlease download the latest version from [GitHub](https://github.com/rauenzi/BetterDiscordApp/releases/latest)",
@ -278,6 +286,12 @@ export default {
updateAvailable: "Update Available", updateAvailable: "Update Available",
updateInfo: "There is an update available for BandagedBD's Injector ({{version}}).\n\nYou can either update and restart now, or later.", updateInfo: "There is an update available for BandagedBD's Injector ({{version}}).\n\nYou can either update and restart now, or later.",
updateFailed: "Could Not Update", updateFailed: "Could Not Update",
manualUpdate: "Unable to update automatically, please download the installer and reinstall normally.\n\n[Download Installer](https://github.com/rauenzi/BetterDiscordApp/releases/latest)" manualUpdate: "Unable to update automatically, please download the installer and reinstall normally.\n\n[Download Installer](https://github.com/rauenzi/BetterDiscordApp/releases/latest)",
jqueryFailed: "jQuery Failed To Load",
jqueryFailedDetails: "jQuery could not be loaded, and some plugins may not work properly. Proceed at your own risk."
},
WindowPrefs: {
enabledInfo: "This option requires a transparent theme in order to work properly. On Windows this may break your aero snapping and maximizing.\n\nIn order to take effect, Discord needs to be restarted. Do you want to restart now?",
disabledInfo: "In order to take effect, Discord needs to be restarted. Do you want to restart now?"
} }
}; };

View File

@ -1,51 +1,18 @@
// import {Config} from "data";
import secure from "./secure"; import secure from "./secure";
import patchModuleLoad from "./moduleloader";
import Core from "./modules/core"; import Core from "./modules/core";
import BdApi from "./modules/pluginapi"; import BdApi from "./modules/pluginapi";
// import PluginManager from "./modules/pluginmanager"; import LoadingIcon from "./loadingicon";
// import ThemeManager from "./modules/thememanager";
// import Events from "./modules/emitter";
// import Settings from "./modules/settingsmanager";
// import DataStore from "./modules/datastore";
// import EmoteModule from "./builtins/emotes/emotes";
// import DomManager from "./modules/dommanager";
// import Utilities from "./modules/utilities";
// import ReactComponents from "./modules/reactcomponents";
// import Strings from "./modules/strings";
// Perform some setup // Perform some setup
secure(); secure();
patchModuleLoad();
const loadingIcon = document.createElement("div");
loadingIcon.className = "bd-loaderv2";
loadingIcon.title = "BandagedBD is loading...";
document.body.appendChild(loadingIcon);
// window.Core = Core;
window.BdApi = BdApi; window.BdApi = BdApi;
// window.settings = SettingsInfo;
// window.settingsCookie = SettingsCookie;
// window.pluginCookie = PluginCookie;
// window.themeCookie = ThemeCookie;
// window.pluginModule = PluginManager;
// window.themeModule = ThemeManager;
// // window.bdthemes = Themes;
// // window.bdplugins = Plugins;
// window.bdEmotes = EmoteModule.Emotes;
// window.bemotes = EmoteModule.blacklist;
// // window.bdPluginStorage = bdPluginStorage;
// window.settingsModule = Settings;
// window.DataStore = DataStore;
// Add loading icon at the bottom right
LoadingIcon.show();
// window.DomManager = DomManager; // Backwards compatibility for now
// window.utils = Utilities;
// window.Components = ReactComponents;
// window.BDEvents = Events;
// window.bdConfig = Config;
// window.Strings = Strings;
export default class CoreWrapper { export default class CoreWrapper {
constructor(config) { constructor(config) {
Core.setConfig(config); Core.setConfig(config);
@ -54,39 +21,4 @@ export default class CoreWrapper {
init() { init() {
Core.init(); Core.init();
} }
} }
function patchModuleLoad() {
const namespace = "betterdiscord";
const prefix = `${namespace}/`;
const Module = require("module");
const load = Module._load;
// const resolveFilename = Module._resolveFilename;
Module._load = function(request) {
if (request === namespace || request.startsWith(prefix)) {
const requested = request.substr(prefix.length);
if (requested == "api") return BdApi;
}
return load.apply(this, arguments);
};
// Module._resolveFilename = function (request, parent, isMain) {
// if (request === "betterdiscord" || request.startsWith("betterdiscord/")) {
// const contentPath = PluginManager.getPluginPathByModule(parent);
// if (contentPath) return request;
// }
// return resolveFilename.apply(this, arguments);
// };
return function() {
Module._load = load;
};
}
patchModuleLoad();
// var settingsPanel, emoteModule, quickEmoteMenu, voiceMode,, dMode, publicServersModule;
// var bdConfig = null;

45
src/loadingicon.js Normal file
View File

@ -0,0 +1,45 @@
const css = `/* BEGIN V2 LOADER */
/* =============== */
#bd-loading-icon {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+IDwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyBoZWlnaHQ9IjEwMCUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7IiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWw6c3BhY2U9InByZXNlcnZlIiB3aWR0aD0iMTAwJSIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIj4gICAgPG1ldGFkYXRhIC8+ICAgIDxkZWZzPiAgICAgICAgPGZpbHRlciBpZD0ic2hhZG93MSI+ICAgICAgICA8ZmVEcm9wU2hhZG93IGR4PSIyMCIgZHk9IjAiIHN0ZERldmlhdGlvbj0iMjAiIGZsb29kLWNvbG9yPSJyZ2JhKDAsMCwwLDAuMzUpIi8+ICAgICAgICA8L2ZpbHRlcj4gICAgICAgIDxmaWx0ZXIgaWQ9InNoYWRvdzIiPiAgICAgICAgPGZlRHJvcFNoYWRvdyBkeD0iMTUiIGR5PSIwIiBzdGREZXZpYXRpb249IjIwIiBmbG9vZC1jb2xvcj0icmdiYSgyNTUsMjU1LDI1NSwwLjE1KSIvPiAgICAgICAgPC9maWx0ZXI+ICAgICAgICA8ZmlsdGVyIGlkPSJzaGFkb3czIj4gICAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjEwIiBkeT0iMCIgc3RkRGV2aWF0aW9uPSIyMCIgZmxvb2QtY29sb3I9InJnYmEoMCwwLDAsMC4zNSkiLz4gICAgICAgIDwvZmlsdGVyPiAgICA8L2RlZnM+ICAgIDxnPiAgICAgICAgPHBhdGggc3R5bGU9ImZpbHRlcjogdXJsKCNzaGFkb3czKSIgZD0iTTExOTUuNDQrMTM1LjQ0MkwxMTk1LjQ0KzEzNS40NDJMOTk3LjYrMTM2LjQ0MkMxMDI0LjIrMTQ5Ljc0MisxMTcwLjM0KzE2My41NDIrMTE5My42NCsxNzkuNzQyQzEyNjQuMzQrMjI4Ljg0MisxMzE5Ljc0KzI5MS4yNDIrMTM1OC4yNCszNjUuMDQyQzEzOTguMTQrNDQxLjY0MisxNDE5Ljc0KzUzMC42NDIrMTQyMi41NCs2MjkuNjQyTDE0MjIuNTQrNjMwLjg0MkwxNDIyLjU0KzYzMi4wNDJDMTQyMi41NCs3NzMuMTQyKzE0MjIuNTQrMTIyOC4xNCsxNDIyLjU0KzEzNjkuMTRMMTQyMi41NCsxMzcwLjM0TDE0MjIuNTQrMTM3MS41NEMxNDE5Ljg0KzE0NzAuNTQrMTM5OC4yNCsxNTU5LjU0KzEzNTguMjQrMTYzNi4xNEMxMzE5Ljc0KzE3MDkuOTQrMTI2NC40NCsxNzcyLjM0KzExOTMuNjQrMTgyMS40NEMxMTcxLjA0KzE4MzcuMTQrMTAyNS43KzE4NTAuNTQrMTAwMCsxODYzLjU0TDExOTMuNTQrMTg2NC41NEMxNTM5Ljc0KzE4NjYuNDQrMTg2NC41NCsxNjkzLjM0KzE4NjQuNTQrMTI5Ni42NEwxODY0LjU0KzcxNi45NDJDMTg2Ni40NCszMTIuNDQyKzE1NDEuNjQrMTM1LjQ0MisxMTk1LjQ0KzEzNS40NDJaIiBmaWxsPSIjMTcxNzE3IiBvcGFjaXR5PSIxIi8+ICAgICAgICA8cGF0aCBzdHlsZT0iZmlsdGVyOiB1cmwoI3NoYWRvdzIpIiBkPSJNMTY5NS41NCs2MzEuNDQyQzE2ODUuODQrMjc4LjA0MisxNDA5LjM0KzEzNS40NDIrMTA1Mi45NCsxMzUuNDQyTDM2MS43NCsxMzYuNDQyTDgwMy43NCs0OTAuNDQyTDEwNjAuNzQrNDkwLjQ0MkMxMzM1LjI0KzQ5MC40NDIrMTMzNS4yNCs4MzUuMzQyKzEwNjAuNzQrODM1LjM0MkwxMDYwLjc0KzExNjQuODRDMTE1MC4yMisxMTY0Ljg0KzEyMTAuNTMrMTIwMS40OCsxMjQxLjY4KzEyNTAuODdDMTMwNi4wNysxMzUzKzEyNDUuNzYrMTUwOS42NCsxMDYwLjc0KzE1MDkuNjRMMzYxLjc0KzE4NjMuNTRMMTA1Mi45NCsxODY0LjU0QzE0MDkuMjQrMTg2NC41NCsxNjg1Ljc0KzE3MjEuOTQrMTY5NS41NCsxMzY4LjU0QzE2OTUuNTQrMTIwNS45NCsxNjUxLjA0KzEwODQuNDQrMTU3Mi42NCs5OTkuOTQyQzE2NTEuMDQrOTE1LjU0MisxNjk1LjU0Kzc5NC4wNDIrMTY5NS41NCs2MzEuNDQyWiIgZmlsbD0iIzNFODJFNSIgb3BhY2l0eT0iMSIvPiAgICAgICAgPHBhdGggc3R5bGU9ImZpbHRlcjogdXJsKCNzaGFkb3cxKSIgZD0iTTE0NjkuMjUrNjMxLjQ0MkMxNDU5LjU1KzI3OC4wNDIrMTE4My4wNSsxMzUuNDQyKzgyNi42NSsxMzUuNDQyTDEzNS40NSsxMzUuNDQyTDEzNS40NSsxMDA0QzEzNS40NSsxMDA0KzEzNS40MjcrMTI1NS4yMSszNTUuNjI2KzEyNTUuMjFDNTc1LjgyNSsxMjU1LjIxKzU3NS44NDgrMTAwNCs1NzUuODQ4KzEwMDRMNTc3LjQ1KzQ5MC40NDJMODM0LjQ1KzQ5MC40NDJDMTEwOC45NSs0OTAuNDQyKzExMDguOTUrODM1LjM0Mis4MzQuNDUrODM1LjM0Mkw2NjQuNjUrODM1LjM0Mkw2NjQuNjUrMTE2NC44NEw4MzQuNDUrMTE2NC44NEM5MjMuOTMyKzExNjQuODQrOTg0LjI0NCsxMjAxLjQ4KzEwMTUuMzkrMTI1MC44N0MxMDc5Ljc4KzEzNTMrMTAxOS40NysxNTA5LjY0KzgzNC40NSsxNTA5LjY0TDEzNS40NSsxNTA5LjY0TDEzNS40NSsxODY0LjU0TDgyNi42NSsxODY0LjU0QzExODIuOTUrMTg2NC41NCsxNDU5LjQ1KzE3MjEuOTQrMTQ2OS4yNSsxMzY4LjU0QzE0NjkuMjUrMTIwNS45NCsxNDI0Ljc1KzEwODQuNDQrMTM0Ni4zNSs5OTkuOTQyQzE0MjQuNzUrOTE1LjU0MisxNDY5LjI1Kzc5NC4wNDIrMTQ2OS4yNSs2MzEuNDQyWiIgZmlsbD0iI0ZGRkZGRiIgb3BhY2l0eT0iMSIvPiAgICA8L2c+PC9zdmc+);
}
#bd-loading-icon {
position: fixed;
bottom:5px;
right:5px;
z-index: 2147483647;
display: block;
width: 20px;
height: 20px;
background-size: 100% 100%;
animation: bd-loading-animation 1.5s ease-in-out infinite;
}
@keyframes bd-loading-animation {
0% { opacity: 0.05; }
50% { opacity: 0.6; }
100% { opacity: 0.05; }
}
/* =============== */
/* END V2 LOADER */`;
const iconStyle = document.createElement("style");
iconStyle.textContent = css;
const loadingIcon = document.createElement("div");
loadingIcon.id = "bd-loading-icon";
loadingIcon.className = "bd-loaderv2";
loadingIcon.title = "BandagedBD is loading...";
export default class {
static show() {
document.body.appendChild(iconStyle);
document.body.appendChild(loadingIcon);
}
static hide() {
if (iconStyle) iconStyle.remove();
if (loadingIcon) loadingIcon.remove();
}
}

21
src/moduleloader.js Normal file
View File

@ -0,0 +1,21 @@
import BdApi from "./modules/pluginapi";
export default function() {
const namespace = "betterdiscord";
const prefix = `${namespace}/`;
const Module = require("module");
const load = Module._load;
Module._load = function(request) {
if (request === namespace || request.startsWith(prefix)) {
const requested = request.substr(prefix.length);
if (requested == "bdapi") return BdApi;
}
return load.apply(this, arguments);
};
return function() {
Module._load = load;
};
}

View File

@ -221,6 +221,10 @@ export default class AddonManager {
return this.state[addon.id]; return this.state[addon.id];
} }
getAddon(idOrFile) {
return this.addonList.find(c => c.id == idOrFile || c.filename == idOrFile);
}
enableAddon(idOrAddon) { enableAddon(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon; const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
if (!addon) return; if (!addon) return;

View File

@ -15,187 +15,236 @@ import DataStore from "./datastore";
import DiscordModules from "./discordmodules"; import DiscordModules from "./discordmodules";
import ComponentPatcher from "./componentpatcher"; import ComponentPatcher from "./componentpatcher";
import Strings from "./strings"; import Strings from "./strings";
import LoadingIcon from "../loadingicon";
import Utilities from "./utilities";
const {ipcRenderer} = require("electron");
const GuildClasses = DiscordModules.GuildClasses; const GuildClasses = DiscordModules.GuildClasses;
function Core() { export default new class Core {
} constructor() {
ipcRenderer.invoke("bd-config", "get").then(injectorConfig => {
Core.prototype.setConfig = function(config) { if (this.hasStarted) return;
Object.assign(Config, config); Object.assign(Config, injectorConfig);
}; this.init();
});
Core.prototype.init = async function() {
DataStore.initialize();
await LocaleManager.initialize();
if (Config.version < Config.minSupportedVersion) {
return Modals.alert(Strings.Startup.notSupported, Strings.Startup.versionMismatch.format({injector: Config.version, remote: Config.bbdVersion}));
} }
if (window.ED) { get dependencies() {
return Modals.alert(Strings.Startup.notSupported, Strings.Startup.incompatibleApp.format({app: "EnhancedDiscord"})); return [
} {
name: "jquery",
if (window.WebSocket && window.WebSocket.name && window.WebSocket.name.includes("Patched")) { type: "script",
return Modals.alert(Strings.Startup.notSupported, Strings.Startup.incompatibleApp.format({app: "Powercord"})); url: "//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js",
} backup: "//cdn.jsdelivr.net/gh/jquery/jquery@2.0.0/jquery.min.js",
local: null
const latestLocalVersion = Config.updater ? Config.updater.LatestVersion : Config.latestVersion; },
if (latestLocalVersion > Config.version) { {
Modals.showConfirmationModal(Strings.Startup.updateAvailable, Strings.Startup.updateInfo.format({version: latestLocalVersion}), { name: "bd-stylesheet",
confirmText: Strings.Startup.updateNow, type: "style",
cancelText: Strings.Startup.maybeLater, url: "//cdn.staticaly.com/gh/{{repo}}/BetterDiscordApp/{{hash}}/css/main{{minified}}.css",
onConfirm: async () => { backup: "//rauenzi.github.io/BetterDiscordApp/css/main{{minified}}.css",
const onUpdateFailed = () => {Modals.alert(Strings.Startup.updateFailed, Strings.Startup.manualUpdate);}; local: "{{localServer}}/BetterDiscordApp/css/main.css"
try {
const didUpdate = await this.updateInjector();
if (!didUpdate) return onUpdateFailed();
const app = require("electron").remote.app;
app.relaunch();
app.exit();
}
catch (err) {
onUpdateFailed();
}
} }
];
}
setConfig(config) {
if (this.hasStarted) return;
Object.assign(Config, config);
}
async init() {
if (this.hasStarted) return;
this.hasStarted = true;
// Load dependencies asynchronously if they don't exist
let dependencyPromise = new Promise(r => r());
if (!window.$ || !window.jQuery) dependencyPromise = this.loadDependencies();
DataStore.initialize();
await LocaleManager.initialize();
if (Config.version < Config.minSupportedVersion) return Modals.alert(Strings.Startup.notSupported, Strings.Startup.versionMismatch.format({injector: Config.version, remote: Config.bbdVersion}));
if (window.ED) return Modals.alert(Strings.Startup.notSupported, Strings.Startup.incompatibleApp.format({app: "EnhancedDiscord"}));
if (window.WebSocket && window.WebSocket.name && window.WebSocket.name.includes("Patched")) return Modals.alert(Strings.Startup.notSupported, Strings.Startup.incompatibleApp.format({app: "Powercord"}));
const latestLocalVersion = Config.updater ? Config.updater.LatestVersion : Config.latestVersion;
if (latestLocalVersion > Config.version) {
Modals.showConfirmationModal(Strings.Startup.updateAvailable, Strings.Startup.updateInfo.format({version: latestLocalVersion}), {
confirmText: Strings.Startup.updateNow,
cancelText: Strings.Startup.maybeLater,
onConfirm: async () => {
const onUpdateFailed = () => { Modals.alert(Strings.Startup.updateFailed, Strings.Startup.manualUpdate); };
try {
const didUpdate = await this.updateInjector();
if (!didUpdate) return onUpdateFailed();
const app = require("electron").remote.app;
app.relaunch();
app.exit();
}
catch (err) {
onUpdateFailed();
}
}
});
}
Logger.log("Startup", "Initializing Settings");
Settings.initialize();
// DOMManager.initialize();
await this.waitForGuilds();
ReactComponents.initialize();
ComponentPatcher.initialize();
for (const module in Builtins) Builtins[module].initialize();
await dependencyPromise;
Logger.log("Startup", "Loading Plugins");
const pluginErrors = PluginManager.initialize();
Logger.log("Startup", "Loading Themes");
const themeErrors = ThemeManager.initialize();
Logger.log("Startup", "Removing Loading Icon");
LoadingIcon.hide();
// Show loading errors
Logger.log("Startup", "Collecting Startup Errors");
Modals.showAddonErrors({plugins: pluginErrors, themes: themeErrors});
const previousVersion = DataStore.getBDData("version");
if (Config.bbdVersion > previousVersion) {
Modals.showChangelogModal(Changelog);
DataStore.setBDData("version", Config.bbdVersion);
}
}
waitForGuilds() {
let timesChecked = 0;
return new Promise(resolve => {
const checkForGuilds = function () {
timesChecked++;
if (document.readyState != "complete") setTimeout(checkForGuilds, 100);
const wrapper = GuildClasses.wrapper.split(" ")[0];
const guild = GuildClasses.listItem.split(" ")[0];
const blob = GuildClasses.blobContainer.split(" ")[0];
if (document.querySelectorAll(`.${wrapper} .${guild} .${blob}`).length > 0) return resolve(Config.deferLoaded = true);
else if (timesChecked >= 50) return resolve(Config.deferLoaded = true);
setTimeout(checkForGuilds, 100);
};
checkForGuilds();
}); });
} }
async loadDependencies() {
Logger.log("Startup", "Initializing Settings"); for (const data of this.dependencies) {
Settings.initialize(); const url = Utilities.formatString((Config.local && data.local != null) ? data.local : data.url, {repo: Config.repo, hash: Config.hash, minified: Config.minified ? ".min" : "", localServer: Config.localServer});
Logger.log(`Startup`, `Loading Resource (${url})`);
DOMManager.initialize(); const injector = (data.type == "script" ? DOMManager.injectScript : DOMManager.linkStyle).bind(DOMManager);
await this.waitForGuilds(); try {
ReactComponents.initialize(); await injector(data.name, url);
ComponentPatcher.initialize(); }
for (const module in Builtins) Builtins[module].initialize(); catch (err) {
const backup = Utilities.formatString(data.backup, {minified: Config.minified ? ".min" : ""});
Logger.log("Startup", "Loading Plugins"); Logger.stacktrace(`Startup`, `Could not load ${url}. Using backup ${backup}`, err);
const pluginErrors = PluginManager.initialize(); try {
await injector(data.name, backup);
Logger.log("Startup", "Loading Themes"); }
const themeErrors = ThemeManager.initialize(); catch (e) {
Logger.stacktrace(`Startup`, `Could not load ${url}. Using backup ${backup}`, err);
Logger.log("Startup", "Removing Loading Icon"); if (data.name === "jquery") Modals.alert(Strings.Startup.jqueryFailed, Strings.Startup.jqueryFailedDetails);
document.getElementsByClassName("bd-loaderv2")[0].remove(); }
}
// Show loading errors }
Logger.log("Startup", "Collecting Startup Errors");
Modals.showAddonErrors({plugins: pluginErrors, themes: themeErrors});
const previousVersion = DataStore.getBDData("version");
if (Config.bbdVersion > previousVersion) {
Modals.showChangelogModal(Changelog);
DataStore.setBDData("version", Config.bbdVersion);
} }
};
Core.prototype.waitForGuilds = function() { async updateInjector() {
let timesChecked = 0; const injectionPath = DataStore.injectionPath;
return new Promise(resolve => { if (!injectionPath) return false;
const checkForGuilds = function() {
timesChecked++;
if (document.readyState != "complete") setTimeout(checkForGuilds, 100);
const wrapper = GuildClasses.wrapper.split(" ")[0];
const guild = GuildClasses.listItem.split(" ")[0];
const blob = GuildClasses.blobContainer.split(" ")[0];
if (document.querySelectorAll(`.${wrapper} .${guild} .${blob}`).length > 0) return resolve(Config.deferLoaded = true);
else if (timesChecked >= 50) return resolve(Config.deferLoaded = true);
setTimeout(checkForGuilds, 100);
};
checkForGuilds(); const fs = require("fs");
}); const path = require("path");
}; const rmrf = require("rimraf");
const yauzl = require("yauzl");
const mkdirp = require("mkdirp");
const request = require("request");
Core.prototype.updateInjector = async function() { const parentPath = path.resolve(injectionPath, "..");
const injectionPath = DataStore.injectionPath; const folderName = path.basename(injectionPath);
if (!injectionPath) return false; const zipLink = "https://github.com/rauenzi/BetterDiscordApp/archive/injector.zip";
const savedZip = path.resolve(parentPath, "injector.zip");
const extractedFolder = path.resolve(parentPath, "BetterDiscordApp-injector");
const fs = require("fs"); // Download the injector zip file
const path = require("path"); Logger.log("InjectorUpdate", "Downloading " + zipLink);
const rmrf = require("rimraf"); let success = await new Promise(resolve => {
const yauzl = require("yauzl"); request.get({url: zipLink, encoding: null}, async (error, response, body) => {
const mkdirp = require("mkdirp"); if (error || response.statusCode !== 200) return resolve(false);
const request = require("request"); // Save a backup in case someone has their own copy
const alreadyExists = await new Promise(res => fs.exists(savedZip, res));
if (alreadyExists) await new Promise(res => fs.rename(savedZip, `${savedZip}.bak${Math.round(performance.now())}`, res));
const parentPath = path.resolve(injectionPath, ".."); Logger.log("InjectorUpdate", "Writing " + savedZip);
const folderName = path.basename(injectionPath); fs.writeFile(savedZip, body, err => resolve(!err));
const zipLink = "https://github.com/rauenzi/BetterDiscordApp/archive/injector.zip"; });
const savedZip = path.resolve(parentPath, "injector.zip");
const extractedFolder = path.resolve(parentPath, "BetterDiscordApp-injector");
// Download the injector zip file
Logger.log("InjectorUpdate", "Downloading " + zipLink);
let success = await new Promise(resolve => {
request.get({url: zipLink, encoding: null}, async (error, response, body) => {
if (error || response.statusCode !== 200) return resolve(false);
// Save a backup in case someone has their own copy
const alreadyExists = await new Promise(res => fs.exists(savedZip, res));
if (alreadyExists) await new Promise(res => fs.rename(savedZip, `${savedZip}.bak${Math.round(performance.now())}`, res));
Logger.log("InjectorUpdate", "Writing " + savedZip);
fs.writeFile(savedZip, body, err => resolve(!err));
}); });
}); if (!success) return success;
if (!success) return success;
// Check and delete rename extraction // Check and delete rename extraction
const alreadyExists = await new Promise(res => fs.exists(extractedFolder, res)); const alreadyExists = await new Promise(res => fs.exists(extractedFolder, res));
if (alreadyExists) await new Promise(res => fs.rename(extractedFolder, `${extractedFolder}.bak${Math.round(performance.now())}`, res)); if (alreadyExists) await new Promise(res => fs.rename(extractedFolder, `${extractedFolder}.bak${Math.round(performance.now())}`, res));
// Unzip the downloaded zip file
const zipfile = await new Promise(r => yauzl.open(savedZip, {lazyEntries: true}, (err, zip) => r(zip)));
zipfile.on("entry", function(entry) {
// Skip directories, they are handled with mkdirp
if (entry.fileName.endsWith("/")) return zipfile.readEntry();
Logger.log("InjectorUpdate", "Extracting " + entry.fileName); // Unzip the downloaded zip file
// Make any needed parent directories const zipfile = await new Promise(r => yauzl.open(savedZip, {lazyEntries: true}, (err, zip) => r(zip)));
const fullPath = path.resolve(parentPath, entry.fileName); zipfile.on("entry", function (entry) {
mkdirp.sync(path.dirname(fullPath)); // Skip directories, they are handled with mkdirp
zipfile.openReadStream(entry, function(err, readStream) { if (entry.fileName.endsWith("/")) return zipfile.readEntry();
if (err) return success = false;
readStream.on("end", function() {zipfile.readEntry();}); // Go to next file after this Logger.log("InjectorUpdate", "Extracting " + entry.fileName);
readStream.pipe(fs.createWriteStream(fullPath)); // Make any needed parent directories
const fullPath = path.resolve(parentPath, entry.fileName);
mkdirp.sync(path.dirname(fullPath));
zipfile.openReadStream(entry, function (err, readStream) {
if (err) return success = false;
readStream.on("end", function () { zipfile.readEntry(); }); // Go to next file after this
readStream.pipe(fs.createWriteStream(fullPath));
});
}); });
}); zipfile.readEntry(); // Start reading
zipfile.readEntry(); // Start reading
// Wait for the final file to finish
await new Promise(resolve => zipfile.once("end", resolve));
// Save a backup in case something goes wrong during final step // Wait for the final file to finish
const backupFolder = path.resolve(parentPath, `${folderName}.bak${Math.round(performance.now())}`); await new Promise(resolve => zipfile.once("end", resolve));
await new Promise(resolve => fs.rename(injectionPath, backupFolder, resolve));
// Rename the extracted folder to what it should be // Save a backup in case something goes wrong during final step
Logger.log("InjectorUpdate", `Renaming ${path.basename(extractedFolder)} to ${folderName}`); const backupFolder = path.resolve(parentPath, `${folderName}.bak${Math.round(performance.now())}`);
success = await new Promise(resolve => fs.rename(extractedFolder, injectionPath, err => resolve(!err))); await new Promise(resolve => fs.rename(injectionPath, backupFolder, resolve));
if (!success) {
Logger.err("InjectorUpdate", "Failed to rename the final directory"); // Rename the extracted folder to what it should be
Logger.log("InjectorUpdate", `Renaming ${path.basename(extractedFolder)} to ${folderName}`);
success = await new Promise(resolve => fs.rename(extractedFolder, injectionPath, err => resolve(!err)));
if (!success) {
Logger.err("InjectorUpdate", "Failed to rename the final directory");
return success;
}
// If rename had issues, delete what we tried to rename and restore backup
if (!success) {
Logger.err("InjectorUpdate", "Something went wrong... restoring backups.");
await new Promise(resolve => rmrf(extractedFolder, resolve));
await new Promise(resolve => fs.rename(backupFolder, injectionPath, resolve));
return success;
}
// If we've gotten to this point, everything should have gone smoothly.
// Cleanup the backup folder then remove the zip
await new Promise(resolve => rmrf(backupFolder, resolve));
await new Promise(resolve => fs.unlink(savedZip, resolve));
Logger.log("InjectorUpdate", "Injector Updated!");
return success; return success;
} }
};
// If rename had issues, delete what we tried to rename and restore backup
if (!success) {
Logger.err("InjectorUpdate", "Something went wrong... restoring backups.");
await new Promise(resolve => rmrf(extractedFolder, resolve));
await new Promise(resolve => fs.rename(backupFolder, injectionPath, resolve));
return success;
}
// If we've gotten to this point, everything should have gone smoothly.
// Cleanup the backup folder then remove the zip
await new Promise(resolve => rmrf(backupFolder, resolve));
await new Promise(resolve => fs.unlink(savedZip, resolve));
Logger.log("InjectorUpdate", "Injector Updated!");
return success;
};
export default new Core();

View File

@ -10,14 +10,14 @@ export default class DOMManager {
// static get bdModals() { return this.getElement("bd-modals") || this.createElement("bd-modals").appendTo(this.bdBody); } // static get bdModals() { return this.getElement("bd-modals") || this.createElement("bd-modals").appendTo(this.bdBody); }
// static get bdToasts() { return this.getElement("bd-toasts") || this.createElement("bd-toasts").appendTo(this.bdBody); } // static get bdToasts() { return this.getElement("bd-toasts") || this.createElement("bd-toasts").appendTo(this.bdBody); }
static initialize() { // static initialize() {
this.createElement("bd-head", {target: document.head}); // this.createElement("bd-head", {target: document.head});
this.createElement("bd-body", {target: document.body}); // this.createElement("bd-body", {target: document.body});
this.createElement("bd-scripts", {target: this.bdHead}); // this.createElement("bd-scripts", {target: this.bdHead});
this.createElement("bd-styles", {target: this.bdHead}); // this.createElement("bd-styles", {target: this.bdHead});
this.createElement("bd-themes", {target: this.bdHead}); // this.createElement("bd-themes", {target: this.bdHead});
this.createElement("style", {id: "customcss", target: this.bdHead}); // this.createElement("style", {id: "customcss", target: this.bdHead});
} // }
static escapeID(id) { static escapeID(id) {
return id.replace(/^[^a-z]+|[^\w-]+/gi, "-"); return id.replace(/^[^a-z]+|[^\w-]+/gi, "-");
@ -50,6 +50,21 @@ export default class DOMManager {
this.bdStyles.append(style); this.bdStyles.append(style);
} }
static unlinkStyle(id) {
return this.removeStyle(id);
}
static linkStyle(id, url) {
id = this.escapeID(id);
return new Promise(resolve => {
const link = this.getElement(`#${id}`, this.bdStyles) || this.createElement("link", {id});
link.rel = "stylesheet";
link.href = url;
link.onload = resolve;
this.bdStyles.append(link);
});
}
static removeTheme(id) { static removeTheme(id) {
id = this.escapeID(id); id = this.escapeID(id);
const exists = this.getElement(`#${id}`, this.bdThemes); const exists = this.getElement(`#${id}`, this.bdThemes);
@ -82,4 +97,11 @@ export default class DOMManager {
this.bdScripts.append(script); this.bdScripts.append(script);
}); });
} }
} }
DOMManager.createElement("bd-head", {target: document.head});
DOMManager.createElement("bd-body", {target: document.body});
DOMManager.createElement("bd-scripts", {target: DOMManager.bdHead});
DOMManager.createElement("bd-styles", {target: DOMManager.bdHead});
DOMManager.createElement("bd-themes", {target: DOMManager.bdHead});
DOMManager.createElement("style", {id: "customcss", target: DOMManager.bdHead});

View File

@ -10,6 +10,7 @@ import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager"; import ThemeManager from "./thememanager";
import Settings from "./settingsmanager"; import Settings from "./settingsmanager";
import Logger from "./logger"; import Logger from "./logger";
import Patcher from "./patcher";
const BdApi = { const BdApi = {
get React() { return DiscordModules.React; }, get React() { return DiscordModules.React; },
@ -148,43 +149,65 @@ BdApi.deleteData = function(pluginName, key) {
}; };
// Patches other functions // Patches other functions
// BdApi.monkeyPatch = function(what, methodName, options) {
// const {before, after, instead, once = false, silent = false, force = false} = options;
// const displayName = options.displayName || what.displayName || what.name || what.constructor.displayName || what.constructor.name;
// if (!silent) console.log("patch", methodName, "of", displayName); // eslint-disable-line no-console
// if (!what[methodName]) {
// if (force) what[methodName] = function() {};
// else return console.error(methodName, "does not exist for", displayName); // eslint-disable-line no-console
// }
// const origMethod = what[methodName];
// const cancel = () => {
// if (!silent) console.log("unpatch", methodName, "of", displayName); // eslint-disable-line no-console
// what[methodName] = origMethod;
// };
// what[methodName] = function() {
// const data = {
// thisObject: this,
// methodArguments: arguments,
// cancelPatch: cancel,
// originalMethod: origMethod,
// callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments)
// };
// if (instead) {
// const tempRet = Utilities.suppressErrors(instead, "`instead` callback of " + what[methodName].displayName)(data);
// if (tempRet !== undefined) data.returnValue = tempRet;
// }
// else {
// if (before) Utilities.suppressErrors(before, "`before` callback of " + what[methodName].displayName)(data);
// data.callOriginalMethod();
// if (after) Utilities.suppressErrors(after, "`after` callback of " + what[methodName].displayName)(data);
// }
// if (once) cancel();
// return data.returnValue;
// };
// what[methodName].__monkeyPatched = true;
// if (!what[methodName].__originalMethod) what[methodName].__originalMethod = origMethod;
// what[methodName].displayName = "patched " + (what[methodName].displayName || methodName);
// return cancel;
// };
BdApi.monkeyPatch = function(what, methodName, options) { BdApi.monkeyPatch = function(what, methodName, options) {
const {before, after, instead, once = false, silent = false, force = false} = options; const {before, after, instead, once = false} = options;
const displayName = options.displayName || what.displayName || what.name || what.constructor.displayName || what.constructor.name; const patchType = before ? "before" : after ? "after" : instead ? "instead" : "";
if (!silent) console.log("patch", methodName, "of", displayName); // eslint-disable-line no-console if (!patchType) return Logger.err("BdApi", "Must provide one of: after, before, instead");
if (!what[methodName]) { const originalMethod = what[methodName];
if (force) what[methodName] = function() {}; const data = {
else return console.error(methodName, "does not exist for", displayName); // eslint-disable-line no-console originalMethod: originalMethod,
} callOriginalMethod: () => data.originalMethod.apply(data.thisObject, data.methodArguments)
const origMethod = what[methodName];
const cancel = () => {
if (!silent) console.log("unpatch", methodName, "of", displayName); // eslint-disable-line no-console
what[methodName] = origMethod;
}; };
what[methodName] = function() { data.cancelPatch = Patcher[patchType]("BdApi", what, methodName, (thisObject, args, returnValue) => {
const data = { data.thisObject = thisObject;
thisObject: this, data.methodArguments = args;
methodArguments: arguments, data.returnValue = returnValue;
cancelPatch: cancel, try {
originalMethod: origMethod, Reflect.apply(options[patchType], null, [data]);
callOriginalMethod: () => data.returnValue = data.originalMethod.apply(data.thisObject, data.methodArguments) if (once) data.cancelPatch();
};
if (instead) {
const tempRet = Utilities.suppressErrors(instead, "`instead` callback of " + what[methodName].displayName)(data);
if (tempRet !== undefined) data.returnValue = tempRet;
} }
else { catch (err) {
if (before) Utilities.suppressErrors(before, "`before` callback of " + what[methodName].displayName)(data); // Logger.err("monkeyPatch", `Error in the ${patchType} of ${methodName}`);
data.callOriginalMethod();
if (after) Utilities.suppressErrors(after, "`after` callback of " + what[methodName].displayName)(data);
} }
if (once) cancel(); });
return data.returnValue;
};
what[methodName].__monkeyPatched = true;
if (!what[methodName].__originalMethod) what[methodName].__originalMethod = origMethod;
what[methodName].displayName = "patched " + (what[methodName].displayName || methodName);
return cancel;
}; };
// Event when element is removed // Event when element is removed
BdApi.onRemoved = function(node, callback) { BdApi.onRemoved = function(node, callback) {
@ -255,15 +278,30 @@ const makeAddonAPI = (manager) => new class AddonAPI {
disable(idOrAddon) {return manager.disableAddon(idOrAddon);} disable(idOrAddon) {return manager.disableAddon(idOrAddon);}
toggle(idOrAddon) {return manager.toggleAddon(idOrAddon);} toggle(idOrAddon) {return manager.toggleAddon(idOrAddon);}
reload(idOrFileOrAddon) {return manager.reloadAddon(idOrFileOrAddon);} reload(idOrFileOrAddon) {return manager.reloadAddon(idOrFileOrAddon);}
get(idOrFile) {return manager.addonList.find(c => c.id == idOrFile || c.filename == idOrFile);} get(idOrFile) {return manager.getAddon(idOrFile);}
getAll() {return manager.addonList;} getAll() {return manager.addonList.map(a => manager.getAddon(a.id));}
}; };
BdApi.Plugins = makeAddonAPI(PluginManager); BdApi.Plugins = makeAddonAPI(PluginManager);
BdApi.Themes = makeAddonAPI(ThemeManager); BdApi.Themes = makeAddonAPI(ThemeManager);
BdApi.Patcher = {
patch: (caller, moduleToPatch, functionName, callback, options = {}) => {
if (typeof(caller) !== "string") return Logger.err("BdApi.Patcher", "Parameter 0 of patch must be a string representing the caller");
if (options.type !== "before" && options.type !== "instead" && options.type !== "after") return Logger.err("BdApi.Patcher", "options.type must be one of: before, instead, after");
return Patcher.pushChildPatch(caller, moduleToPatch, functionName, callback, options);
},
before: (caller, moduleToPatch, functionName, callback, options = {}) => BdApi.Patcher.patch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "before"})),
instead: (caller, moduleToPatch, functionName, callback, options = {}) => BdApi.Patcher.patch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "instead"})),
after: (caller, moduleToPatch, functionName, callback, options = {}) => BdApi.Patcher.patch(caller, moduleToPatch, functionName, callback, Object.assign(options, {type: "after"})),
unpatchAll: (caller) => {
if (typeof(caller) !== "string") return Logger.err("BdApi.Patcher", "Parameter 0 of unpatchAll must be a string representing the caller");
return Patcher.unpatchAll(caller);
}
};
Object.freeze(BdApi); Object.freeze(BdApi);
Object.freeze(BdApi.Plugins); Object.freeze(BdApi.Plugins);
Object.freeze(BdApi.Themes); Object.freeze(BdApi.Themes);
Object.freeze(BdApi.Patcher);
export default BdApi; export default BdApi;

View File

@ -104,6 +104,7 @@ export default new class PluginManager extends AddonManager {
startAddon(id) {return this.startPlugin(id);} startAddon(id) {return this.startPlugin(id);}
stopAddon(id) {return this.stopPlugin(id);} stopAddon(id) {return this.stopPlugin(id);}
getAddon(id) {return this.getPlugin(id);}
startPlugin(idOrAddon) { startPlugin(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon; const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
@ -111,15 +112,15 @@ export default new class PluginManager extends AddonManager {
const plugin = addon.plugin; const plugin = addon.plugin;
try { try {
plugin.start(); plugin.start();
this.emit("started", addon.id);
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
} }
catch (err) { catch (err) {
this.state[addon.id] = false; this.state[addon.id] = false;
Toasts.error(`${addon.name} v${addon.version} could not be started.`); Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version}));
Logger.stacktrace(this.name, addon.name + " could not be started.", err); Logger.stacktrace(this.name, addon.name + " could not be started.", err);
return new AddonError(addon.name, addon.filename, "start() could not be fired.", {message: err.message, stack: err.stack}); return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack});
} }
this.emit("started", addon.id);
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
} }
stopPlugin(idOrAddon) { stopPlugin(idOrAddon) {
@ -128,15 +129,21 @@ export default new class PluginManager extends AddonManager {
const plugin = addon.plugin; const plugin = addon.plugin;
try { try {
plugin.stop(); plugin.stop();
this.emit("stopped", addon.id);
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
} }
catch (err) { catch (err) {
this.state[addon.id] = false; this.state[addon.id] = false;
Toasts.error(`${addon.name} v${addon.version} could not be stopped.`); Toasts.error(Strings.Addons.couldNotStop.format({name: addon.name, version: addon.version}));
Logger.stacktrace(this.name, addon.name + " could not be stopped.", err); Logger.stacktrace(this.name, addon.name + " could not be stopped.", err);
return new AddonError(addon.name, addon.filename, "stop() could not be fired.", {message: err.message, stack: err.stack}); return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "stop()"}), {message: err.message, stack: err.stack});
} }
this.emit("stopped", addon.id);
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
}
getPlugin(idOrFile) {
const addon = this.addonList.find(c => c.id == idOrFile || c.filename == idOrFile);
if (!addon) return;
return addon.plugin;
} }
setupFunctions() { setupFunctions() {

View File

@ -57,12 +57,14 @@ export default class BuiltinModule {
async enable() { async enable() {
this.log("Enabled"); this.log("Enabled");
await this.enabled(); try {await this.enabled();}
catch (e) {this.stacktrace("Could not be enabled", e);}
} }
async disable() { async disable() {
this.log("Disabled"); this.log("Disabled");
await this.disabled(); try {await this.disabled();}
catch (e) {this.stacktrace("Could not be disabled", e);}
} }
async enabled() {} async enabled() {}

View File

@ -46,7 +46,7 @@ export default class Modals {
} }
static alert(title, content) { static alert(title, content) {
this.showConfirmationModal(title, content); this.showConfirmationModal(title, content, {cancelText: ""});
} }
/** /**

View File

@ -1,4 +1,4 @@
import {React, WebpackModules, Patcher, ReactComponents, Utilities, Settings, Events} from "modules"; import {React, WebpackModules, Patcher, ReactComponents, Utilities, Settings, Events, DataStore} from "modules";
import AddonList from "./settings/addonlist"; import AddonList from "./settings/addonlist";
import SettingsGroup from "./settings/group"; import SettingsGroup from "./settings/group";
@ -12,6 +12,20 @@ export default new class SettingsRenderer {
Events.on("strings-updated", this.forceUpdate); Events.on("strings-updated", this.forceUpdate);
} }
onDrawerToggle(collection, group, state) {
const drawerStates = DataStore.getBDData("drawerStates") || {};
if (!drawerStates[collection]) drawerStates[collection] = {};
drawerStates[collection][group] = state;
DataStore.setBDData("drawerStates", drawerStates);
}
getDrawerState(collection, group, defaultValue) {
const drawerStates = DataStore.getBDData("drawerStates") || {};
if (!drawerStates[collection]) return defaultValue;
if (!drawerStates[collection].hasOwnProperty(group)) return defaultValue;
return drawerStates[collection][group];
}
onChange(onChange) { onChange(onChange) {
return (collection, category, id) => { return (collection, category, id) => {
const before = Settings.collections.length + Settings.panels.length; const before = Settings.collections.length + Settings.panels.length;
@ -21,16 +35,20 @@ export default new class SettingsRenderer {
}; };
} }
buildSettingsPanel(title, config, state, onChange, button = null) { buildSettingsPanel(id, title, config, state, onChange, button = null) {
config.forEach(section => { config.forEach(section => {
section.settings.forEach(item => item.value = state[section.id][item.id]); section.settings.forEach(item => item.value = state[section.id][item.id]);
}); });
return this.getSettingsPanel(title, config, this.onChange(onChange), button); return this.getSettingsPanel(id, title, config, this.onChange(onChange), button);
} }
getSettingsPanel(title, groups, onChange, button = null) { getSettingsPanel(id, title, groups, onChange, button = null) {
return [React.createElement(SettingsTitle, {text: title, button: button}), groups.map(section => { return [React.createElement(SettingsTitle, {text: title, button: button}), groups.map(section => {
return React.createElement(SettingsGroup, Object.assign({}, section, {onChange})); return React.createElement(SettingsGroup, Object.assign({}, section, {
onChange: onChange,
onDrawerToggle: state => this.onDrawerToggle(id, section.id, state),
shown: this.getDrawerState(id, section.id, section.hasOwnProperty("shown") ? section.shown : true)
}));
})]; })];
} }
@ -61,14 +79,13 @@ export default new class SettingsRenderer {
insert({ insert({
section: collection.name, section: collection.name,
label: collection.name, label: collection.name,
element: () => this.buildSettingsPanel(collection.name, collection.settings, Settings.state[collection.id], Settings.onSettingChange.bind(Settings, collection.id), collection.button ? collection.button : null) element: () => this.buildSettingsPanel(collection.id, collection.name, collection.settings, Settings.state[collection.id], Settings.onSettingChange.bind(Settings, collection.id), collection.button ? collection.button : null)
}); });
} }
for (const panel of Settings.panels.sort((a,b) => a.order > b.order)) { for (const panel of Settings.panels.sort((a,b) => a.order > b.order)) {
if (panel.clickListener) panel.onClick = (event) => panel.clickListener(thisObject, event, returnValue); if (panel.clickListener) panel.onClick = (event) => panel.clickListener(thisObject, event, returnValue);
insert(panel); insert(panel);
} }
// insert({section: "CUSTOM", element: Attribution});
}); });
this.forceUpdate(); this.forceUpdate();
} }

View File

@ -1,22 +0,0 @@
import {Config} from "data";
import {React, Strings} from "modules";
export default class BBDAttribution extends React.Component {
buildTitle(name, version, author) {
const title = Strings.Addons.title.split(/({{[A-Za-z]+}})/);
const nameIndex = title.findIndex(s => s == "{{name}}");
if (nameIndex) title[nameIndex] = name;
const versionIndex = title.findIndex(s => s == "{{version}}");
if (nameIndex) title[versionIndex] = version;
const authorIndex = title.findIndex(s => s == "{{author}}");
if (nameIndex) title[authorIndex] = author;
return title.flat();
}
render() {
return <div id="bbd-version">
{this.buildTitle("BBD", Config.bbdVersion, <a href="https://github.com/rauenzi" target="_blank" rel="noopener noreferrer">Zerebos</a>)}
</div>;
}
}

View File

@ -39,6 +39,7 @@ export default class Group extends React.Component {
container.style.setProperty("height", ""); container.style.setProperty("height", "");
container.classList.remove("animating"); container.classList.remove("animating");
}, timeout)); }, timeout));
if (this.props.onDrawerToggle) this.props.onDrawerToggle(this.state.collapsed);
} }
onChange(id, value) { onChange(id, value) {

View File

@ -1,4 +0,0 @@
// export {default as SettingsPanel} from "./settings/settings";
// export {default as PublicServersMenu} from "./publicservers/menu";
// export {default as Toasts} from "./toasts";
// export {default as Modals} from "./modals";