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.
### To Do (Remote Side)
- Dependency loading (jquery, css, config file)
- Stop depending on injector giving config
- Abstract out more UI strings
- Transition code
### To Do (Injector)
- Update to new windowprefs location
- Remove dependency management
- Remove string script injection/communication with remote
- expand ipc/config commands
### To Do (Meta)
- Update README (info, patrons)
@ -20,14 +19,12 @@ Note: The items listed here are not in any sort of priority order.
- Add gh funding
### Someday
- Switch from /css and /js to /release
- Move old utilities to BdApi
- Component patcher (also does additional classes, etc)
- Plugin Class
- New Plugin API
- Require patch
- Backwards compatibility module (with deprecation notices)
- Modify old monkeyPatch to really use Patcher
- Repo browser
- Addon update system
- Rewrite emote auto caps
- 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 {
display: flex;
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 {DOM, DiscordModules} from "modules";
import {DOM, DiscordModules, Strings} from "modules";
export default new class DeveloperMode extends Builtin {
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 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", () => {
DiscordModules.ElectronModule.copy(selector);
cm.style.display = "none";

View File

@ -1,6 +1,6 @@
import Builtin from "../../structs/builtin";
import Modals from "../../ui/modals";
import {Strings} from "modules";
const electron = require("electron");
const fs = require("fs");
@ -37,7 +37,7 @@ export default new class ReactDevTools extends Builtin {
enabled() {
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));
if (webContents.isDevToolsOpened()) this.listener();
}

View File

@ -68,6 +68,7 @@ export default new class EmoteMenu extends Builtin {
}
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.");
// this.log("Starting to observe");
// 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.",
enabled: "{{name}} has been enabled.",
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",
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: {
confirmationText: "You have unsaved changes to your Custom CSS. Closing this window will lose all those changes.",
update: "Update",
@ -229,6 +228,15 @@ export default {
settings: "Editor Settings",
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: {
button: "public",
join: "Join",
@ -259,16 +267,16 @@ export default {
restartLater: "Restart Later",
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: {
sortBy: "Sort By",
order: "Order",
ascending: "Ascending",
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: {
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)",
@ -278,6 +286,12 @@ export default {
updateAvailable: "Update Available",
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",
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 patchModuleLoad from "./moduleloader";
import Core from "./modules/core";
import BdApi from "./modules/pluginapi";
// import PluginManager from "./modules/pluginmanager";
// 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";
import LoadingIcon from "./loadingicon";
// Perform some setup
secure();
const loadingIcon = document.createElement("div");
loadingIcon.className = "bd-loaderv2";
loadingIcon.title = "BandagedBD is loading...";
document.body.appendChild(loadingIcon);
// window.Core = Core;
patchModuleLoad();
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;
// window.utils = Utilities;
// window.Components = ReactComponents;
// window.BDEvents = Events;
// window.bdConfig = Config;
// window.Strings = Strings;
// Backwards compatibility for now
export default class CoreWrapper {
constructor(config) {
Core.setConfig(config);
@ -54,39 +21,4 @@ export default class CoreWrapper {
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];
}
getAddon(idOrFile) {
return this.addonList.find(c => c.id == idOrFile || c.filename == idOrFile);
}
enableAddon(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
if (!addon) return;

View File

@ -15,187 +15,236 @@ import DataStore from "./datastore";
import DiscordModules from "./discordmodules";
import ComponentPatcher from "./componentpatcher";
import Strings from "./strings";
import LoadingIcon from "../loadingicon";
import Utilities from "./utilities";
const {ipcRenderer} = require("electron");
const GuildClasses = DiscordModules.GuildClasses;
function Core() {
}
Core.prototype.setConfig = function(config) {
Object.assign(Config, config);
};
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}));
export default new class Core {
constructor() {
ipcRenderer.invoke("bd-config", "get").then(injectorConfig => {
if (this.hasStarted) return;
Object.assign(Config, injectorConfig);
this.init();
});
}
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();
}
get dependencies() {
return [
{
name: "jquery",
type: "script",
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
},
{
name: "bd-stylesheet",
type: "style",
url: "//cdn.staticaly.com/gh/{{repo}}/BetterDiscordApp/{{hash}}/css/main{{minified}}.css",
backup: "//rauenzi.github.io/BetterDiscordApp/css/main{{minified}}.css",
local: "{{localServer}}/BetterDiscordApp/css/main.css"
}
];
}
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();
});
}
Logger.log("Startup", "Initializing Settings");
Settings.initialize();
DOMManager.initialize();
await this.waitForGuilds();
ReactComponents.initialize();
ComponentPatcher.initialize();
for (const module in Builtins) Builtins[module].initialize();
Logger.log("Startup", "Loading Plugins");
const pluginErrors = PluginManager.initialize();
Logger.log("Startup", "Loading Themes");
const themeErrors = ThemeManager.initialize();
Logger.log("Startup", "Removing Loading Icon");
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);
async loadDependencies() {
for (const data of this.dependencies) {
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})`);
const injector = (data.type == "script" ? DOMManager.injectScript : DOMManager.linkStyle).bind(DOMManager);
try {
await injector(data.name, url);
}
catch (err) {
const backup = Utilities.formatString(data.backup, {minified: Config.minified ? ".min" : ""});
Logger.stacktrace(`Startup`, `Could not load ${url}. Using backup ${backup}`, err);
try {
await injector(data.name, backup);
}
catch (e) {
Logger.stacktrace(`Startup`, `Could not load ${url}. Using backup ${backup}`, err);
if (data.name === "jquery") Modals.alert(Strings.Startup.jqueryFailed, Strings.Startup.jqueryFailedDetails);
}
}
}
}
};
Core.prototype.waitForGuilds = function() {
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);
};
async updateInjector() {
const injectionPath = DataStore.injectionPath;
if (!injectionPath) return false;
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 injectionPath = DataStore.injectionPath;
if (!injectionPath) return false;
const parentPath = path.resolve(injectionPath, "..");
const folderName = path.basename(injectionPath);
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");
const path = require("path");
const rmrf = require("rimraf");
const yauzl = require("yauzl");
const mkdirp = require("mkdirp");
const request = require("request");
// 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));
const parentPath = path.resolve(injectionPath, "..");
const folderName = path.basename(injectionPath);
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));
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
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));
// 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();
// Check and delete rename extraction
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));
Logger.log("InjectorUpdate", "Extracting " + entry.fileName);
// 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));
// 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);
// 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
const backupFolder = path.resolve(parentPath, `${folderName}.bak${Math.round(performance.now())}`);
await new Promise(resolve => fs.rename(injectionPath, backupFolder, resolve));
// Wait for the final file to finish
await new Promise(resolve => zipfile.once("end", resolve));
// 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");
// Save a backup in case something goes wrong during final step
const backupFolder = path.resolve(parentPath, `${folderName}.bak${Math.round(performance.now())}`);
await new Promise(resolve => fs.rename(injectionPath, backupFolder, resolve));
// 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;
}
// 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 bdToasts() { return this.getElement("bd-toasts") || this.createElement("bd-toasts").appendTo(this.bdBody); }
static initialize() {
this.createElement("bd-head", {target: document.head});
this.createElement("bd-body", {target: document.body});
this.createElement("bd-scripts", {target: this.bdHead});
this.createElement("bd-styles", {target: this.bdHead});
this.createElement("bd-themes", {target: this.bdHead});
this.createElement("style", {id: "customcss", target: this.bdHead});
}
// static initialize() {
// this.createElement("bd-head", {target: document.head});
// this.createElement("bd-body", {target: document.body});
// this.createElement("bd-scripts", {target: this.bdHead});
// this.createElement("bd-styles", {target: this.bdHead});
// this.createElement("bd-themes", {target: this.bdHead});
// this.createElement("style", {id: "customcss", target: this.bdHead});
// }
static escapeID(id) {
return id.replace(/^[^a-z]+|[^\w-]+/gi, "-");
@ -50,6 +50,21 @@ export default class DOMManager {
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) {
id = this.escapeID(id);
const exists = this.getElement(`#${id}`, this.bdThemes);
@ -82,4 +97,11 @@ export default class DOMManager {
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 Settings from "./settingsmanager";
import Logger from "./logger";
import Patcher from "./patcher";
const BdApi = {
get React() { return DiscordModules.React; },
@ -148,43 +149,65 @@ BdApi.deleteData = function(pluginName, key) {
};
// 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) {
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;
const {before, after, instead, once = false} = options;
const patchType = before ? "before" : after ? "after" : instead ? "instead" : "";
if (!patchType) return Logger.err("BdApi", "Must provide one of: after, before, instead");
const originalMethod = what[methodName];
const data = {
originalMethod: originalMethod,
callOriginalMethod: () => data.originalMethod.apply(data.thisObject, data.methodArguments)
};
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;
data.cancelPatch = Patcher[patchType]("BdApi", what, methodName, (thisObject, args, returnValue) => {
data.thisObject = thisObject;
data.methodArguments = args;
data.returnValue = returnValue;
try {
Reflect.apply(options[patchType], null, [data]);
if (once) data.cancelPatch();
}
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);
catch (err) {
// Logger.err("monkeyPatch", `Error in the ${patchType} of ${methodName}`);
}
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
BdApi.onRemoved = function(node, callback) {
@ -255,15 +278,30 @@ const makeAddonAPI = (manager) => new class AddonAPI {
disable(idOrAddon) {return manager.disableAddon(idOrAddon);}
toggle(idOrAddon) {return manager.toggleAddon(idOrAddon);}
reload(idOrFileOrAddon) {return manager.reloadAddon(idOrFileOrAddon);}
get(idOrFile) {return manager.addonList.find(c => c.id == idOrFile || c.filename == idOrFile);}
getAll() {return manager.addonList;}
get(idOrFile) {return manager.getAddon(idOrFile);}
getAll() {return manager.addonList.map(a => manager.getAddon(a.id));}
};
BdApi.Plugins = makeAddonAPI(PluginManager);
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.Plugins);
Object.freeze(BdApi.Themes);
Object.freeze(BdApi.Patcher);
export default BdApi;

View File

@ -104,6 +104,7 @@ export default new class PluginManager extends AddonManager {
startAddon(id) {return this.startPlugin(id);}
stopAddon(id) {return this.stopPlugin(id);}
getAddon(id) {return this.getPlugin(id);}
startPlugin(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;
try {
plugin.start();
this.emit("started", addon.id);
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
}
catch (err) {
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);
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) {
@ -128,15 +129,21 @@ export default new class PluginManager extends AddonManager {
const plugin = addon.plugin;
try {
plugin.stop();
this.emit("stopped", addon.id);
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
}
catch (err) {
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);
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() {

View File

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

View File

@ -46,7 +46,7 @@ export default class Modals {
}
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 SettingsGroup from "./settings/group";
@ -12,6 +12,20 @@ export default new class SettingsRenderer {
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) {
return (collection, category, id) => {
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 => {
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(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({
section: 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)) {
if (panel.clickListener) panel.onClick = (event) => panel.clickListener(thisObject, event, returnValue);
insert(panel);
}
// insert({section: "CUSTOM", element: Attribution});
});
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.classList.remove("animating");
}, timeout));
if (this.props.onDrawerToggle) this.props.onDrawerToggle(this.state.collapsed);
}
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";