Bring up to date + some reorganizing

This commit is contained in:
Zack Rauen 2020-07-16 01:42:56 -04:00
parent ca2aca700d
commit 0f49e4257b
45 changed files with 1862 additions and 594 deletions

View File

@ -78,7 +78,6 @@
"opener": "off",
"origin": "off",
"parent": "off",
"performance": "off",
"personalbar": "off",
"print": "off",
"prompt": "off",

41
TODO.md
View File

@ -1,37 +1,36 @@
# To-Do List
This list only reflects the items that have needed to be done since June 2019, there was a lot of progress/change before this point.
This list only reflects the items that have needed to be done since July 2020, there was a lot of progress/change before this point.
Note: The items listed here are not in any sort of priority order.
### In Progress
- Redo emotemenu
- Remove all jquery usage
### To Do (Remote Side)
- Use DOM in place of jQuery
- Use fetch/require in place of $.ajax
- Dependency loading (jquery, css, config file)
- Stop depending on injector giving config
- Fix floating window module
- Dummyproof public servers
### To Dummy/Crash Proof
- PublicServers react
- Floating window
### To Do (Injector)
- Update to new windowprefs location
- Remove dependency management
- Remove string script injection/communication with remote
### To Complete
### To Do (Meta)
- Update README (info, patrons)
- Add issue template
- Add gh funding
### Someday
- 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
- Modify old monkeyPatch to really use Patcher
- Repo browser
- Addon update system
- PublicServer button patch
- Redo devmode
- Rewrite emote auto caps
### Potential Ideas
- Rearchitect to not use remote files
- Modify CSP rather than entirely remove
### Done
- PublicServers React Rewrite
- Rewrite plugin/theme cards
- Addon list controls
- Use an actual patcher and not monkeyPatch
- Modify CSP rather than entirely remove or use privileged scheme

View File

@ -2322,3 +2322,35 @@ body .ace_closeButton:active {
/* =============== */
/* END DARK MODE */
.bd-chat-badge {
vertical-align: bottom;
line-height: 1.375rem;
display: inline-block;
height: 1.25rem;
}
.bd-member-badge {
height: 15px;
margin-left: 4px;
}
.bd-sidebar-header {
display: flex;
justify-content: space-between;
}
.bd-sidebar-header .bd-changelog-button {
height: 16px;
}
.bd-sidebar-header .bd-icon {
cursor: pointer;
fill: #72767d;
}
.bd-sidebar-header .bd-icon:hover {
fill: #fff;
}

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -596,6 +596,7 @@
"Jaffa",
"Jago",
"James",
"Jannik",
"Jazz",
"Jeanne",
"Jeremiah",

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {WebpackModules} from "modules";
const MessageContent = WebpackModules.getModule(m => m.default && m.default.displayName && m.default.displayName == "Message");

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
export default new class DarkMode extends Builtin {
get name() {return "DarkMode";}

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
export default new class MinimalMode extends Builtin {
get name() {return "MinimalMode";}

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
export default new class VoiceMode extends Builtin {
get name() {return "VoiceMode";}

View File

@ -1,16 +1,22 @@
// Export these two first because they add settings/panels
export {default as EmoteModule} from "./emotes";
export {default as CustomCSS} from "./customcss";
export {default as VoiceMode} from "./voicemode";
export {default as ClassNormalizer} from "./classnormalizer";
export {default as DeveloperMode} from "./developermode";
export {default as PublicServers} from "./publicservers";
export {default as DarkMode} from "./darkmode";
export {default as MinimalMode} from "./minimalmode";
export {default as TwentyFourHour} from "./24hour";
export {default as ColoredText} from "./coloredtext";
export {default as VoiceDisconnect} from "./voicedisconnect";
export {default as EmoteMenu} from "./emotemenu";
export {default as EmoteAutocaps} from "./emoteautocaps";
export {default as WindowPrefs} from "./windowprefs";
export {default as CustomCSS} from "./customcss";
export {default as WindowPrefs} from "./windowprefs";
export {default as TwentyFourHour} from "./general/24hour";
export {default as ClassNormalizer} from "./general/classnormalizer";
export {default as PublicServers} from "./general/publicservers";
export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as ColoredText} from "./appearance/coloredtext";
export {default as DarkMode} from "./appearance/darkmode";
export {default as MinimalMode} from "./appearance/minimalmode";
export {default as VoiceMode} from "./appearance/voicemode";
export {default as EmoteModule} from "./emotes/emotes";
export {default as EmoteMenu} from "./emotes/emotemenu";
export {default as EmoteAutocaps} from "./emotes/emoteautocaps";
export {default as CopySelector} from "./developer/copyselector";
export {default as Debugger} from "./developer/debugger";
export {default as ReactDevTools} from "./developer/reactdevtools";

View File

@ -0,0 +1,80 @@
import Builtin from "../../structs/builtin";
import {DOM, DiscordModules} from "modules";
export default new class DeveloperMode extends Builtin {
get name() {return "DeveloperMode";}
get category() {return "developer";}
get id() {return "developerMode";}
get selectorModeID() {return "copySelector";}
get selectorMode() {return this.get(this.selectorModeID);}
constructor() {
super();
this.copySelectorListener = this.copySelectorListener.bind(this);
}
enabled() {
document.addEventListener("contextmenu", this.copySelectorListener);
}
disabled() {
document.removeEventListener("contextmenu", this.copySelectorListener);
}
copySelectorListener(ctxEvent) {
ctxEvent.stopPropagation();
const selector = this.getSelector(ctxEvent.target);
function attach() {
let cm = DOM.query(".contextMenu-HLZMGh");
if (!cm) {
const container = DOM.query("#app-mount");
const cmWrap = DOM.createElement(`<div class="layer-v9HyYc da-layer">`);
cm = DOM.createElement(`<div class="contextMenu-HLZMGh da-contextMenu bd-context-menu"></div>`);
cmWrap.append(cm);
container.append(cmWrap);
cmWrap.style.top = ctxEvent.clientY + "px";
cmWrap.style.left = ctxEvent.clientX + "px";
cmWrap.style.zIndex = "1002";
const removeCM = function(removeEvent) {
if (removeEvent.keyCode && removeEvent.keyCode !== 27) return;
cmWrap.remove();
document.removeEventListener("click", removeCM);
document.removeEventListener("contextmenu", removeCM);
document.removeEventListener("keyup", removeCM);
};
document.addEventListener("click", removeCM);
document.addEventListener("contextmenu", removeCM);
document.addEventListener("keyup", removeCM);
}
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.addEventListener("click", () => {
DiscordModules.ElectronModule.copy(selector);
cm.style.display = "none";
});
cmg.append(cmi);
cm.append(cmg);
}
setImmediate(attach);
}
getSelector(element) {
if (element.id) return `#${element.id}`;
const rules = this.getRules(element);
const latestRule = rules[rules.length - 1];
if (latestRule) return latestRule.selectorText;
else if (element.classList.length) return `.${Array.from(element.classList).join(".")}`;
return `.${Array.from(element.parentElement.classList).join(".")}`;
}
getRules(element, css = element.ownerDocument.styleSheets) {
//if (window.getMatchedCSSRules) return window.getMatchedCSSRules(element);
const sheets = [...css].filter(s => !s.href || !s.href.includes("BetterDiscordApp"));
const rules = sheets.map(s => [...(s.cssRules || [])]).flat();
const elementRules = rules.filter(r => r && r.selectorText && element.matches(r.selectorText) && r.style.length && r.selectorText.split(", ").length < 8 && !r.selectorText.split(", ").includes("*"));
return elementRules;
}
};

View File

@ -0,0 +1,23 @@
import Builtin from "../../structs/builtin";
export default new class DeveloperMode extends Builtin {
get name() {return "Debugger";}
get category() {return "developer";}
get id() {return "debuggerHotkey";}
enabled() {
document.addEventListener("keydown", this.debugListener);
}
disabled() {
document.removeEventListener("keydown", this.debugListener);
}
debugListener(e) {
if (e.which === 119 || e.which == 118) { //F8
debugger; // eslint-disable-line no-debugger
e.preventDefault();
e.stopImmediatePropagation();
}
}
};

View File

@ -0,0 +1,57 @@
import Builtin from "../../structs/builtin";
import Modals from "../../ui/modals";
const electron = require("electron");
const fs = require("fs");
const path = require("path");
const BrowserWindow = electron.remote.BrowserWindow;
const webContents = electron.remote.getCurrentWebContents();
export default new class ReactDevTools extends Builtin {
get name() {return "ReactDevTools";}
get category() {return "developer";}
get id() {return "reactDevTools";}
initialize() {
super.initialize();
this.findExtension();
}
findExtension() {
let extensionPath = "";
if (process.platform === "win32") extensionPath = path.resolve(process.env.LOCALAPPDATA, "Google/Chrome/User Data");
else if (process.platform === "linux") extensionPath = path.resolve(process.env.HOME, ".config/google-chrome");
else if (process.platform === "darwin") extensionPath = path.resolve(process.env.HOME, "Library/Application Support/Google/Chrome");
else extensionPath = path.resolve(process.env.HOME, ".config/chromium");
extensionPath += "/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/";
if (fs.existsSync(extensionPath)) {
const versions = fs.readdirSync(extensionPath);
extensionPath = path.resolve(extensionPath, versions[versions.length - 1]);
}
this.extensionPath = extensionPath;
this.isExtensionInstalled = fs.existsSync(extensionPath);
this.listener = this.listener.bind(this);
}
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.");
setImmediate(() => webContents.on("devtools-opened", this.listener));
if (webContents.isDevToolsOpened()) this.listener();
}
disabled() {
webContents.removeListener("devtools-opened", this.listener);
}
listener() {
if (!this.isExtensionInstalled) return;
BrowserWindow.removeDevToolsExtension("React Developer Tools");
const didInstall = BrowserWindow.addDevToolsExtension(this.extensionPath);
if (didInstall) this.log("Successfully installed react devtools.");
else this.error("Couldn't find react devtools in chrome extensions!");
}
};

View File

@ -1,103 +0,0 @@
import Builtin from "../structs/builtin";
import {DiscordModules, Strings} from "modules";
export default new class DeveloperMode extends Builtin {
get name() {return "DeveloperMode";}
get category() {return "developer";}
get id() {return "developerMode";}
get selectorModeID() {return "copySelector";}
get selectorMode() {return this.get(this.selectorModeID);}
constructor() {
super();
this.enableSelectors = this.enableSelectors.bind(this);
this.disableSelectors = this.disableSelectors.bind(this);
}
enabled() {
$(document).on("keydown.bdDevmode", (e) => {
if (e.which === 119 || e.which == 118) {//F8
this.log("Debugger Activated");
debugger; // eslint-disable-line no-debugger
}
});
if (this.selectorMode) this.enableSelectors();
this.selectorCancel = this.registerSetting(this.selectorModeID, this.enableSelectors, this.disableSelectors);
}
disabled() {
$(document).off("keydown.bdDevmode");
if (this.selectorMode) this.disableSelectors();
if (this.selectorCancel) this.selectorCancel();
}
enableSelectors() {
$(document).on("contextmenu.bdDevmode", (e) => {
this.lastSelector = this.getSelector(e.toElement);
const attach = () => {
let cm = $(".contextMenu-HLZMGh");
if (cm.length <= 0) {
cm = $("<div class=\"contextMenu-HLZMGh bd-context-menu\"></div>");
cm.addClass($(".app, .app-2rEoOp").hasClass("theme-dark") ? "theme-dark" : "theme-light");
cm.appendTo(".app, .app-2rEoOp");
cm.css("top", e.clientY);
cm.css("left", e.clientX);
$(document).on("click.bdDevModeCtx", () => {
cm.remove();
$(document).off(".bdDevModeCtx");
});
$(document).on("contextmenu.bdDevModeCtx", () => {
cm.remove();
$(document).off(".bdDevModeCtx");
});
$(document).on("keyup.bdDevModeCtx", (event) => {
if (event.keyCode === 27) {
cm.remove();
$(document).off(".bdDevModeCtx");
}
});
}
const cmo = $("<div/>", {
"class": "itemGroup-1tL0uz"
});
const cmi = $("<div/>", {
"class": "item-1Yvehc",
"click": () => {
DiscordModules.ElectronModule.copy(this.lastSelector);
cm.hide();
}
}).append($("<span/>", {text: Strings.Collections.settings.developer.copySelector.name}));
cmo.append(cmi);
cm.append(cmo);
if (cm.hasClass("undefined")) cm.css("top", "-=" + cmo.outerHeight());
};
setImmediate(attach);
e.stopPropagation();
});
}
disableSelectors() {
$(document).off("contextmenu.bdDevmode");
$(document).off("contextmenu.bdDevModeCtx");
}
getRules(element, css = element.ownerDocument.styleSheets) {
// return [].concat(...[...css].map(s => [...s.cssRules || []])).filter(r => r && r.selectorText && element.matches(r.selectorText) && r.style.length && r.selectorText.split(", ").length < 8);
const sheets = [...css].filter(s => !s.href || !s.href.includes("BetterDiscordApp"));
const rules = sheets.map(s => [...(s.cssRules || [])]).flat();
const elementRules = rules.filter(r => r && r.selectorText && element.matches(r.selectorText) && r.style.length && r.selectorText.split(", ").length < 8 && !r.selectorText.split(", ").includes("*"));
return elementRules;
}
getSelector(element) {
if (element.id) return `#${element.id}`;
const rules = this.getRules(element);
const latestRule = rules[rules.length - 1];
if (latestRule) return latestRule.selectorText;
else if (element.classList.length) return `.${Array.from(element.classList).join(".")}`;
return `.${Array.from(element.parentElement.classList).join(".")}`;
}
};

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {Utilities} from "modules";

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {Utilities, Events} from "modules";
import EmoteModule from "./emotes";

View File

@ -1,10 +1,10 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {EmoteConfig} from "data";
import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings, Strings} from "modules";
import BDEmote from "../ui/emote";
import Toasts from "../ui/toasts";
import FormattableString from "../structs/string";
import BDEmote from "../../ui/emote";
import Toasts from "../../ui/toasts";
import FormattableString from "../../structs/string";
const request = require("request");
const EmoteURLs = {
@ -53,7 +53,6 @@ export default new class EmoteModule extends Builtin {
initialize() {
super.initialize();
window.emoteModule = this;
const storedFavorites = DataStore.getBDData("favoriteEmotes");
this.favoriteEmotes = storedFavorites || {};
this.addFavorite = this.addFavorite.bind(this);
@ -67,15 +66,12 @@ export default new class EmoteModule extends Builtin {
await this.getBlacklist();
await this.loadEmoteData();
// while (!this.MessageContentComponent) await new Promise(resolve => setTimeout(resolve, 100));
// this.patchMessageContent();
Events.on("emotes-favorite-added", this.addFavorite);
Events.on("emotes-favorite-removed", this.removeFavorite);
Events.on("setting-updated", this.onCategoryToggle);
}
disabled() {
console.log("DISABLED");
Events.off("setting-updated", this.onCategoryToggle);
Events.off("emotes-favorite-added", this.addFavorite);
Events.off("emotes-favorite-removed", this.removeFavorite);

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {DiscordModules} from "modules";
export default new class TwentyFourHour extends Builtin {

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {WebpackModules} from "modules";
const normalizedPrefix = "da";
@ -115,8 +115,25 @@ export default new class ClassNormalizer extends Builtin {
patchDOMMethods() {
const contains = DOMTokenList.prototype.contains;
DOMTokenList.prototype.contains = function(token) {
const tokens = token.split(" ");
return tokens.every(t => contains.call(this, t));
// const tokens = token.split(" ");
return Reflect.apply(contains, this, [token.split(" ")[0]]);
// return tokens.every(t => contains.call(this, t));
};
const add = DOMTokenList.prototype.add;
DOMTokenList.prototype.add = function(...tokens) {
for (let t = 0; t < tokens.length; t++) {
tokens[t] = tokens[t].split(" ")[0];
}
return Reflect.apply(add, this, tokens);
};
const remove = DOMTokenList.prototype.remove;
DOMTokenList.prototype.remove = function(...tokens) {
for (let t = 0; t < tokens.length; t++) {
tokens[t] = tokens[t].split(" ")[0];
}
return Reflect.apply(remove, this, tokens);
};
}

View File

@ -0,0 +1,54 @@
import Builtin from "../../structs/builtin";
import {DiscordModules, WebpackModules, Strings, DOM} from "modules";
import PublicServersMenu from "../../ui/publicservers/menu";
const LayerStack = WebpackModules.getByProps("pushLayer");
export default new class PublicServers extends Builtin {
get name() {return "PublicServers";}
get category() {return "general";}
get id() {return "publicServers";}
enabled() {
const GuildList = WebpackModules.find(m => m.default && m.default.displayName == "NavigableGuilds");
const GuildListOld = WebpackModules.findByDisplayName("Guilds");
if (!GuildList && !GuildListOld) this.warn("Can't find GuildList component");
this.guildPatch = this.after(GuildList ? GuildList : GuildListOld.prototype, GuildList ? "default" : "render", this._appendButton);
this._appendButton();
}
disabled() {
this.unpatchAll();
DOM.query("#bd-pub-li").remove();
}
_appendButton() {
const wrapper = DiscordModules.GuildClasses.wrapper.split(" ")[0];
const guilds = DOM.query(`.${wrapper} .scroller-2TZvBN >:first-child`);
DOM.after(guilds, this.button);
}
openPublicServers() {
LayerStack.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerStack.popLayer}));
}
get button() {
const btn = DOM.createElement(`<div id="bd-pub-li" class="${DiscordModules.GuildClasses.listItem}">`);
const label = DOM.createElement(`<div id="bd-pub-button" class="${"wrapper-25eVIn " + DiscordModules.GuildClasses.circleButtonMask}">${Strings.PublicServers.button}</div>`);
label.addEventListener("click", () => {this.openPublicServers();});
btn.append(label);
return btn;
// const btn = $("<div/>", {
// "class": DiscordModules.GuildClasses.listItem,
// "id": "bd-pub-li"
// }).append($("<div/>", {
// "class": "wrapper-25eVIn " + DiscordModules.GuildClasses.circleButtonMask,
// "text": Strings.PublicServers.button,
// "id": "bd-pub-button",
// "click": () => { this.openPublicServers(); }
// }));
// return btn;
}
};

View File

@ -1,4 +1,4 @@
import Builtin from "../structs/builtin";
import Builtin from "../../structs/builtin";
import {DiscordModules} from "modules";
export default new class DarkMode extends Builtin {

View File

@ -1,39 +0,0 @@
import Builtin from "../structs/builtin";
import {DiscordModules, WebpackModules, Strings} from "modules";
import PublicServersMenu from "../ui/publicservers/menu";
const LayerStack = WebpackModules.getByProps("pushLayer");
export default new class PublicServers extends Builtin {
get name() {return "PublicServers";}
get category() {return "general";}
get id() {return "publicServers";}
enabled() {
const wrapper = DiscordModules.GuildClasses.wrapper.split(" ")[0];
const guilds = $(`.${wrapper} .scroller-2FKFPG >:first-child`);
guilds.after(this.button);
}
disabled() {
$("#bd-pub-li").remove();
}
openPublicServers() {
LayerStack.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerStack.popLayer}));
}
get button() {
const btn = $("<div/>", {
"class": DiscordModules.GuildClasses.listItem,
"id": "bd-pub-li"
}).append($("<div/>", {
"class": "wrapper-25eVIn " + DiscordModules.GuildClasses.circleButtonMask,
"text": Strings.PublicServers.button,
"id": "bd-pub-button",
"click": () => { this.openPublicServers(); }
}));
return btn;
}
};

View File

@ -1,50 +1,52 @@
import Builtin from "../structs/builtin";
const fs = require("fs");
const path = require("path");
import Modals from "../ui/modals";
import {DataStore, Strings} from "modules";
export default new class WindowPrefs extends Builtin {
get name() {return "WindowPrefs";}
get category() {return "window";}
get id() {return "transparency";}
get WindowConfigFile() {
if (this._windowConfigFile) return this._windowConfigFile;
const electron = require("electron").remote.app;
const base = electron.getAppPath();
const roamingBase = electron.getPath("userData");
const roamingLocation = path.resolve(roamingBase, electron.getVersion(), "modules", "discord_desktop_core", "injector", "config.json");
const location = path.resolve(base, "..", "app", "config.json");
const realLocation = fs.existsSync(location) ? location : fs.existsSync(roamingLocation) ? roamingLocation : null;
if (!realLocation) return this._windowConfigFile = null;
return this._windowConfigFile = realLocation;
initialize() {
super.initialize();
this.prefs = DataStore.getData("windowprefs") || {};
}
enabled() {
this.setWindowPreference("transparent", true);
this.setWindowPreference("backgroundColor", null);
this.setWindowPreference("backgroundColor", "#00000000");
this.showModal(Strings.WindowPrefs.enabledInfo);
}
disabled() {
this.setWindowPreference("transparent", false);
this.setWindowPreference("backgroundColor", "#2f3136");
this.deleteWindowPreference("transparent");
this.deleteWindowPreference("backgroundColor");
this.showModal(Strings.WindowPrefs.disabledInfo);
}
getAllWindowPreferences() {
if (!this.WindowConfigFile) return {};
return __non_webpack_require__(this.WindowConfigFile);
showModal(info) {
Modals.showConfirmationModal(Strings.Modals.additionalInfo, info, {
confirmText: Strings.Modals.restartNow,
cancelText: Strings.Modals.restartLater,
onConfirm: () => {
const app = require("electron").remote.app;
app.relaunch();
app.exit();
}
});
}
getWindowPreference(key) {
if (!this.WindowConfigFile) return undefined;
return this.getAllWindowPreferences()[key];
return this.prefs[key];
}
setWindowPreference(key, value) {
if (!this.WindowConfigFile) return;
const prefs = this.getAllWindowPreferences();
prefs[key] = value;
delete require.cache[this.WindowConfigFile];
fs.writeFileSync(this.WindowConfigFile, JSON.stringify(prefs, null, 4));
this.prefs[key] = value;
DataStore.setData("windowprefs", this.prefs);
}
deleteWindowPreference(key) {
delete this.prefs[key];
DataStore.setData("windowprefs", this.prefs);
}
};

View File

@ -53,8 +53,9 @@ export default [
collapsible: true,
shown: false,
settings: [
{type: "switch", id: "developerMode", value: false},
{type: "switch", id: "copySelector", value: false, enableWith: "developerMode"}
{type: "switch", id: "debuggerHotkey", value: false},
{type: "switch", id: "copySelector", value: false},
{type: "switch", id: "reactDevTools", value: false}
]
},
{

View File

@ -110,13 +110,17 @@ export default {
},
developer: {
name: "Developer Settings",
developerMode: {
name: "Developer Mode",
debuggerHotkey: {
name: "Debugger Hotkey",
note: "Allows activating debugger when pressing F8"
},
copySelector: {
name: "Copy Selector",
note: "Adds a \"Copy Selector\" option to context menus when developer mode is active"
},
reactDevTools: {
name: "React Developer Tools",
note: "Injects your local installation of React Developer Tools into Discord"
}
},
window: {
@ -192,8 +196,9 @@ export default {
addonSettings: "Settings",
website: "Website",
source: "Source",
server: "Support Server",
invite: "Support Server",
donate: "Donate",
patreon: "Patreon",
name: "Name",
author: "Author",
version: "Version",
@ -244,12 +249,31 @@ export default {
name: "Name",
message: "Message",
error: "Error",
addonErrors: "Addon Errors"
addonErrors: "Addon Errors",
restartRequired: "Restart Required",
restartNow: "Restart Now",
restartLater: "Restart Later",
additionalInfo: "Additional Info"
},
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)",
incompatibleApp: "BandagedBD does not work with {{app}}. Please uninstall one of them.",
updateNow: "Update Now",
maybeLater: "Maybe Later",
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)"
}
};

View File

@ -1,4 +1,5 @@
import {Config} from "data";
import secure from "./secure";
import Core from "./modules/core";
import BdApi from "./modules/pluginapi";
import PluginManager from "./modules/pluginmanager";
@ -6,14 +7,15 @@ 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";
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
// proxyLocalStorage();
secure();
const loadingIcon = document.createElement("div");
loadingIcon.className = "bd-loaderv2";
loadingIcon.title = "BandagedBD is loading...";

View File

@ -5,6 +5,9 @@ import Utilities from "./utilities";
import Patcher from "./patcher";
import BDLogo from "../ui/icons/bdlogo";
const React = DiscordModules.React;
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
export default new class ComponentPatcher {
initialize() {
@ -12,29 +15,11 @@ export default new class ComponentPatcher {
Utilities.suppressErrors(this.patchGuildPills.bind(this), "BD Guild Pills Patch")();
Utilities.suppressErrors(this.patchGuildListItems.bind(this), "BD Guild List Items Patch")();
Utilities.suppressErrors(this.patchGuildSeparator.bind(this), "BD Guild Separator Patch")();
Utilities.suppressErrors(this.patchMessageHeader.bind(this), "BD Message Header Patch")();
Utilities.suppressErrors(this.patchMemberList.bind(this), "BD Member List Patch")();
}
patchSocial() {
if (this.socialPatch) return;
// const TabBar = WebpackModules.getByDisplayName("TabBar");
// const Anchor = WebpackModules.getByDisplayName("Anchor");
// if (!TabBar || !Anchor) return;
// this.socialPatch = Patcher.after("ThemeHelper", TabBar.prototype, "render", (_, __, returnValue) => {
// const children = returnValue.props.children;
// if (!children || !children.length) return;
// if (children[children.length - 2].type.displayName !== "Separator") return;
// if (!children[children.length - 1].type.toString().includes("socialLinks")) return;
// const original = children[children.length - 1].type;
// const newOne = function() {
// const returnVal = original(...arguments);
// returnVal.props.children.push(DiscordModules.React.createElement(Anchor, {className: "bd-social-link", href: "https://github.com/rauenzi/BetterDiscordApp", rel: "author", title: "BandagedBD", target: "_blank"},
// DiscordModules.React.createElement(BDLogo, {size: "16px", className: "bd-social-logo"})
// ));
// return returnVal;
// };
// children[children.length - 1].type = newOne;
// });
if (this.socialPatch) return;
const TabBar = WebpackModules.getByDisplayName("TabBar");
const Anchor = WebpackModules.getByDisplayName("Anchor");
@ -49,11 +34,9 @@ export default new class ComponentPatcher {
const newOne = function() {
const returnVal = original(...arguments);
returnVal.props.children.push(
// DiscordModules.React.createElement(TooltipWrap, {color: "black", side: "top", text: "BandagedBD"},
DiscordModules.React.createElement(Anchor, {className: "bd-social-link", href: "https://github.com/rauenzi/BetterDiscordApp", title: "BandagedBD", target: "_blank"},
DiscordModules.React.createElement(BDLogo, {size: "16px", className: "bd-social-logo"})
)
// )
DiscordModules.React.createElement(Anchor, {className: "bd-social-link", href: "https://twitter.com/BandagedBD", title: "BandagedBD", target: "_blank"},
DiscordModules.React.createElement(BDLogo, {size: "16px", className: "bd-social-logo"})
)
);
return returnVal;
};
@ -82,7 +65,7 @@ export default new class ComponentPatcher {
const reactInstance = Utilities.getReactInstance(document.querySelector(`.${listItemClass} .${blobClass}`).parentElement);
const GuildComponent = reactInstance.return.type;
if (!GuildComponent) return;
this.guildListItemsPatch = Patcher.after("ThemeHelper", GuildComponent.prototype, "render", (thisObject, _, returnValue) => {
this.guildListItemsPatch = Patcher.after("ComponentPatcher", GuildComponent.prototype, "render", (thisObject, _, returnValue) => {
if (!returnValue || !thisObject) return;
const guildData = thisObject.props;
returnValue.props.className += " bd-guild";
@ -100,7 +83,7 @@ export default new class ComponentPatcher {
if (this.guildPillPatch) return;
const guildPill = WebpackModules.getModule(m => m.default && !m.default.displayName && m.default.toString && m.default.toString().includes("translate3d"));
if (!guildPill) return;
this.guildPillPatch = Patcher.after("ThemeHelper", guildPill, "default", (_, args, returnValue) => {
this.guildPillPatch = Patcher.after("ComponentPatcher", guildPill, "default", (_, args, returnValue) => {
const props = args[0];
if (props.unread) returnValue.props.className += " bd-unread";
if (props.selected) returnValue.props.className += " bd-selected";
@ -119,11 +102,51 @@ export default new class ComponentPatcher {
returnValue.props.className += " bd-guild-separator";
return returnValue;
};
this.guildSeparatorPatch = Patcher.after("ThemeHelper", Guilds.prototype, "render", (_, __, returnValue) => {
this.guildSeparatorPatch = Patcher.after("ComponentPatcher", Guilds.prototype, "render", (_, __, returnValue) => {
const Separator = Utilities.findInReactTree(returnValue, m => m.type && !m.type.displayName && typeof(m.type) == "function" && Utilities.isEmpty(m.props));
if (!Separator) return;
Separator.type = GuildSeparator;
});
}
patchMessageHeader() {
if (this.messageHeaderPatch) return;
const MessageHeader = WebpackModules.getByProps("MessageTimestamp");
const Anchor = WebpackModules.find(m => m.displayName == "Anchor");
if (!Anchor || !MessageHeader || !MessageHeader.default) return;
this.messageHeaderPatch = Patcher.after("ComponentPatcher", MessageHeader, "default", (_, args, returnValue) => {
const author = Utilities.getNestedProp(args[0], "message.author");
const children = Utilities.getNestedProp(returnValue, "props.children.1.props.children.1.props.children");
if (!children || !author || !author.id || author.id !== "249746236008169473") return;
if (!Array.isArray(children)) return;
children.push(
React.createElement(Tooltip, {color: "black", position: "top", text: "BandagedBD Developer"},
props => React.createElement(Anchor, Object.assign({className: "bd-chat-badge", href: "https://github.com/rauenzi/BetterDiscordApp", title: "BandagedBD", target: "_blank"}, props),
React.createElement(BDLogo, {size: "16px", className: "bd-logo"})
)
)
);
});
}
patchMemberList() {
if (this.memberListPatch) return;
const MemberListItem = WebpackModules.findByDisplayName("MemberListItem");
const Anchor = WebpackModules.find(m => m.displayName == "Anchor");
if (!Anchor || !MemberListItem || !MemberListItem.prototype || !MemberListItem.prototype.renderDecorators) return;
this.memberListPatch = Patcher.after("ComponentPatcher", MemberListItem.prototype, "renderDecorators", (thisObject, args, returnValue) => {
const user = Utilities.getNestedProp(thisObject, "props.user");
const children = Utilities.getNestedProp(returnValue, "props.children");
if (!children || !user || !user.id || user.id !== "249746236008169473") return;
if (!Array.isArray(children)) return;
children.push(
React.createElement(Tooltip, {color: "black", position: "top", text: "BandagedBD Developer"},
props => React.createElement(Anchor, Object.assign({className: "bd-member-badge", href: "https://github.com/rauenzi/BetterDiscordApp", title: "BandagedBD", target: "_blank"}, props),
React.createElement(BDLogo, {size: "16px", className: "bd-logo"})
)
)
);
});
}
};

View File

@ -1,7 +1,7 @@
import LocaleManager from "./localemanager";
import Logger from "./logger";
import {Config} from "data";
import {Config, Changelog} from "data";
// import EmoteModule from "./emotes";
// import QuickEmoteMenu from "../builtins/emotemenu";
import DOMManager from "./dommanager";
@ -14,6 +14,7 @@ import ReactComponents from "./reactcomponents";
import DataStore from "./datastore";
import DiscordModules from "./discordmodules";
import ComponentPatcher from "./componentpatcher";
import Strings from "./strings";
const GuildClasses = DiscordModules.GuildClasses;
@ -26,31 +27,47 @@ Core.prototype.setConfig = function(config) {
};
Core.prototype.init = async function() {
if (Config.version < Config.minSupportedVersion) {
Modals.alert("Not Supported", "BetterDiscord v" + Config.version + " (your version)" + " is not supported by the latest js (" + Config.bbdVersion + ").<br><br> Please download the latest version from <a href='https://github.com/rauenzi/BetterDiscordApp/releases/latest' target='_blank'>GitHub</a>");
return;
}
if (window.ED) {
Modals.alert("Not Supported", "BandagedBD does not work with EnhancedDiscord. Please uninstall one of them.");
return;
}
if (window.WebSocket && window.WebSocket.name && window.WebSocket.name.includes("Patched")) {
Modals.alert("Not Supported", "BandagedBD does not work with Powercord. Please uninstall one of them.");
return;
}
// const latestLocalVersion = Config.updater ? Config.updater.LatestVersion : Config.latestVersion;
// if (latestLocalVersion > Config.version) {
// Modals.alert("Update Available", `
// An update for BandagedBD is available (${latestLocalVersion})! Please Reinstall!<br /><br />
// <a href='https://github.com/rauenzi/BetterDiscordApp/releases/latest' target='_blank'>Download Installer</a>
// `);
// }
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"}));
}
console.log(Config);
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();
@ -74,11 +91,11 @@ Core.prototype.init = async function() {
Logger.log("Startup", "Collecting Startup Errors");
Modals.showAddonErrors({plugins: pluginErrors, themes: themeErrors});
// const previousVersion = DataStore.getBDData("version");
// if (bbdVersion > previousVersion) {
// if (bbdChangelog) this.showChangelogModal(bbdChangelog);
// DataStore.setBDData("version", bbdVersion);
// }
const previousVersion = DataStore.getBDData("version");
if (Config.bbdVersion > previousVersion) {
this.showChangelogModal(Changelog);
DataStore.setBDData("version", Config.bbdVersion);
}
};
Core.prototype.waitForGuilds = function() {
@ -99,4 +116,90 @@ Core.prototype.waitForGuilds = function() {
});
};
Core.prototype.updateInjector = async function() {
const injectionPath = DataStore.injectionPath;
if (!injectionPath) return false;
const fs = require("fs");
const path = require("path");
const rmrf = require("rimraf");
const yauzl = require("yauzl");
const mkdirp = require("mkdirp");
const request = require("request");
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));
});
});
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();
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
// 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));
// 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;
};
export default new Core();

View File

@ -2,7 +2,7 @@ import {Config} from "data";
import Utilities from "./utilities";
const fs = require("fs");
const path = require("path");
const releaseChannel = DiscordNative.globals.releaseChannel;
const releaseChannel = DiscordNative.globals ? DiscordNative.globals.releaseChannel : DiscordNative.app ? DiscordNative.app.getReleaseChannel() : "stable";
// Schema
// =======================
@ -34,6 +34,18 @@ export default new class DataStore {
this.cacheData = Utilities.testJSON(fs.readFileSync(this.cacheFile).toString()) || {};
}
get injectionPath() {
if (this._injectionPath) return this._injectionPath;
const electron = require("electron").remote.app;
const base = electron.getAppPath();
const roamingBase = electron.getPath("userData");
const roamingLocation = path.resolve(roamingBase, electron.getVersion(), "modules", "discord_desktop_core", "injector");
const location = path.resolve(base, "..", "app");
const realLocation = fs.existsSync(location) ? location : fs.existsSync(roamingLocation) ? roamingLocation : null;
if (!realLocation) return this._injectionPath = null;
return this._injectionPath = realLocation;
}
get customCSS() {return this._customCSS || (this._customCSS = path.resolve(this.dataFolder, "custom.css"));}
get baseFolder() {return this._baseFolder || (this._baseFolder = path.resolve(Config.dataPath, "data"));}
get dataFolder() {return this._dataFolder || (this._dataFolder = path.resolve(this.baseFolder, `${releaseChannel}`));}
@ -44,7 +56,7 @@ export default new class DataStore {
_getFile(key) {
if (key == "settings" || key == "plugins" || key == "themes") return path.resolve(this.dataFolder, `${key}.json`);
if (key == "settings" || key == "plugins" || key == "themes" || key == "window") return path.resolve(this.dataFolder, `${key}.json`);
return path.resolve(this.dataFolder, `misc.json`);
}

View File

@ -146,9 +146,11 @@ export default Utilities.memoizeObject({
/* Commonly Used Classes */
get GuildClasses() {
const guildsWrapper = WebpackModules.getByProps("wrapper", "unreadMentionsBar");
const guildsWrapper = WebpackModules.getByProps("wrapper", "unreadMentionsBar");
const guilds = WebpackModules.getByProps("guildsError", "selected");
const pill = WebpackModules.getByProps("blobContainer");
return Object.assign({}, guildsWrapper, guilds, pill);
}
},
get LayerStack() {return WebpackModules.getByProps("pushLayer");}
});

753
src/modules/domtools.js Normal file
View File

@ -0,0 +1,753 @@
/**
* Copyright 2018 Zachary Rauen
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* From: https://github.com/rauenzi/BDPluginLibrary
*/
/**
* @interface
* @name Offset
* @property {number} top - Top offset of the target element.
* @property {number} right - Right offset of the target element.
* @property {number} bottom - Bottom offset of the target element.
* @property {number} left - Left offset of the target element.
* @property {number} height - Outer height of the target element.
* @property {number} width - Outer width of the target element.
*/
/**
* Function that automatically removes added listener.
* @callback module:DOMTools~CancelListener
*/
export default class DOMTools {
static escapeID(id) {
return id.replace(/^[^a-z]+|[^\w-]+/gi, "-");
}
/**
* Adds a style to the document.
* @param {string} id - identifier to use as the element id
* @param {string} css - css to add to the document
*/
static addStyle(id, css) {
document.head.append(DOMTools.createElement(`<style id="${id}">${css}</style>`));
}
/**
* Removes a style from the document.
* @param {string} id - original identifier used
*/
static removeStyle(id) {
const element = document.getElementById(id);
if (element) element.remove();
}
/**
* Adds/requires a remote script to be loaded
* @param {string} id - identifier to use for this script
* @param {string} url - url from which to load the script
* @returns {Promise} promise that resolves when the script is loaded
*/
static addScript(id, url) {
return new Promise(resolve => {
const script = document.createElement("script");
script.id = id;
script.src = url;
script.type = "text/javascript";
script.onload = resolve;
document.head.append(script);
});
}
/**
* Removes a remote script from the document.
* @param {string} id - original identifier used
*/
static removeScript(id) {
id = this.escapeID(id);
const element = document.getElementById(id);
if (element) element.remove();
}
// https://javascript.info/js-animation
static animate({timing = _ => _, update, duration}) {
const start = performance.now();
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
// calculate the current animation state
const progress = timing(timeFraction);
update(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
/**
* This is my shit version of not having to use `$` from jQuery. Meaning
* that you can pass a selector and it will automatically run {@link module:DOMTools.query}.
* It also means that you can pass a string of html and it will perform and return `parseHTML`.
* @see module:DOMTools.parseHTML
* @see module:DOMTools.query
* @param {string} selector - Selector to query or HTML to parse
* @returns {(DocumentFragment|NodeList|HTMLElement)} - Either the result of `parseHTML` or `query`
*/
static Q(selector) {
const element = this.parseHTML(selector);
const isHTML = element instanceof NodeList ? Array.from(element).some(n => n.nodeType === 1) : element.nodeType === 1;
if (isHTML) return element;
return this.query(selector);
}
/**
* Essentially a shorthand for `document.querySelector`. If the `baseElement` is not provided
* `document` is used by default.
* @param {string} selector - Selector to query
* @param {Element} [baseElement] - Element to base the query from
* @returns {(Element|null)} - The found element or null if not found
*/
static query(selector, baseElement) {
if (!baseElement) baseElement = document;
return baseElement.querySelector(selector);
}
/**
* Essentially a shorthand for `document.querySelectorAll`. If the `baseElement` is not provided
* `document` is used by default.
* @param {string} selector - Selector to query
* @param {Element} [baseElement] - Element to base the query from
* @returns {Array<Element>} - Array of all found elements
*/
static queryAll(selector, baseElement) {
if (!baseElement) baseElement = document;
return baseElement.querySelectorAll(selector);
}
/**
* Parses a string of HTML and returns the results. If the second parameter is true,
* the parsed HTML will be returned as a document fragment {@see https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment}.
* This is extremely useful if you have a list of elements at the top level, they can then be appended all at once to another node.
*
* If the second parameter is false, then the return value will be the list of parsed
* nodes and there were multiple top level nodes, otherwise the single node is returned.
* @param {string} html - HTML to be parsed
* @param {boolean} [fragment=false] - Whether or not the return should be the raw `DocumentFragment`
* @returns {(DocumentFragment|NodeList|HTMLElement)} - The result of HTML parsing
*/
static parseHTML(html, fragment = false) {
const template = document.createElement("template");
template.innerHTML = html;
const node = template.content.cloneNode(true);
if (fragment) return node;
return node.childNodes.length > 1 ? node.childNodes : node.childNodes[0];
}
/** Alternate name for {@link module:DOMTools.parseHTML} */
static createElement(html, fragment = false) {return this.parseHTML(html, fragment);}
/**
* Takes a string of html and escapes it using the brower's own escaping mechanism.
* @param {String} html - html to be escaped
*/
static escapeHTML(html) {
const textNode = document.createTextNode("");
const spanElement = document.createElement("span");
spanElement.append(textNode);
textNode.nodeValue = html;
return spanElement.innerHTML;
}
/**
* Adds a list of classes from the target element.
* @param {Element} element - Element to edit classes of
* @param {...string} classes - Names of classes to add
* @returns {Element} - `element` to allow for chaining
*/
static addClass(element, ...classes) {
classes = classes.flat().filter(c => c);
for (let c = 0; c < classes.length; c++) classes[c] = classes[c].toString().split(" ");
classes = classes.flat().filter(c => c);
element.classList.add(...classes);
return element;
}
/**
* Removes a list of classes from the target element.
* @param {Element} element - Element to edit classes of
* @param {...string} classes - Names of classes to remove
* @returns {Element} - `element` to allow for chaining
*/
static removeClass(element, ...classes) {
for (let c = 0; c < classes.length; c++) classes[c] = classes[c].toString().split(" ");
classes = classes.flat().filter(c => c);
element.classList.remove(...classes);
return element;
}
/**
* When only one argument is present: Toggle class value;
* i.e., if class exists then remove it and return false, if not, then add it and return true.
* When a second argument is present:
* If the second argument evaluates to true, add specified class value, and if it evaluates to false, remove it.
* @param {Element} element - Element to edit classes of
* @param {string} classname - Name of class to toggle
* @param {boolean} [indicator] - Optional indicator for if the class should be toggled
* @returns {Element} - `element` to allow for chaining
*/
static toggleClass(element, classname, indicator) {
classname = classname.toString().split(" ").filter(c => c);
if (typeof(indicator) !== "undefined") classname.forEach(c => element.classList.toggle(c, indicator));
else classname.forEach(c => element.classList.toggle(c));
return element;
}
/**
* Checks if an element has a specific class
* @param {Element} element - Element to edit classes of
* @param {string} classname - Name of class to check
* @returns {boolean} - `true` if the element has the class, `false` otherwise.
*/
static hasClass(element, classname) {
return classname.toString().split(" ").filter(c => c).every(c => element.classList.contains(c));
}
/**
* Replaces one class with another
* @param {Element} element - Element to edit classes of
* @param {string} oldName - Name of class to replace
* @param {string} newName - New name for the class
* @returns {Element} - `element` to allow for chaining
*/
static replaceClass(element, oldName, newName) {
element.classList.replace(oldName, newName);
return element;
}
/**
* Appends `thisNode` to `thatNode`
* @param {Node} thisNode - Node to be appended to another node
* @param {Node} thatNode - Node for `thisNode` to be appended to
* @returns {Node} - `thisNode` to allow for chaining
*/
static appendTo(thisNode, thatNode) {
if (typeof(thatNode) == "string") thatNode = this.query(thatNode);
if (!thatNode) return null;
thatNode.append(thisNode);
return thisNode;
}
/**
* Prepends `thisNode` to `thatNode`
* @param {Node} thisNode - Node to be prepended to another node
* @param {Node} thatNode - Node for `thisNode` to be prepended to
* @returns {Node} - `thisNode` to allow for chaining
*/
static prependTo(thisNode, thatNode) {
if (typeof(thatNode) == "string") thatNode = this.query(thatNode);
if (!thatNode) return null;
thatNode.prepend(thisNode);
return thisNode;
}
/**
* Insert after a specific element, similar to jQuery's `thisElement.insertAfter(otherElement)`.
* @param {Node} thisNode - The node to insert
* @param {Node} targetNode - Node to insert after in the tree
* @returns {Node} - `thisNode` to allow for chaining
*/
static insertAfter(thisNode, targetNode) {
targetNode.parentNode.insertBefore(thisNode, targetNode.nextSibling);
return thisNode;
}
/**
* Insert after a specific element, similar to jQuery's `thisElement.after(newElement)`.
* @param {Node} thisNode - The node to insert
* @param {Node} newNode - Node to insert after in the tree
* @returns {Node} - `thisNode` to allow for chaining
*/
static after(thisNode, newNode) {
thisNode.parentNode.insertBefore(newNode, thisNode.nextSibling);
return thisNode;
}
/**
* Gets the next sibling element that matches the selector.
* @param {Element} element - Element to get the next sibling of
* @param {string} [selector=""] - Optional selector
* @returns {Element} - The sibling element
*/
static next(element, selector = "") {
return selector ? element.querySelector("+ " + selector) : element.nextElementSibling;
}
/**
* Gets all subsequent siblings.
* @param {Element} element - Element to get next siblings of
* @returns {NodeList} - The list of siblings
*/
static nextAll(element) {
return element.querySelectorAll("~ *");
}
/**
* Gets the subsequent siblings until an element matches the selector.
* @param {Element} element - Element to get the following siblings of
* @param {string} selector - Selector to stop at
* @returns {Array<Element>} - The list of siblings
*/
static nextUntil(element, selector) {
const next = [];
while (element.nextElementSibling && !element.nextElementSibling.matches(selector)) next.push(element = element.nextElementSibling);
return next;
}
/**
* Gets the previous sibling element that matches the selector.
* @param {Element} element - Element to get the previous sibling of
* @param {string} [selector=""] - Optional selector
* @returns {Element} - The sibling element
*/
static previous(element, selector = "") {
const previous = element.previousElementSibling;
if (selector) return previous && previous.matches(selector) ? previous : null;
return previous;
}
/**
* Gets all preceeding siblings.
* @param {Element} element - Element to get preceeding siblings of
* @returns {NodeList} - The list of siblings
*/
static previousAll(element) {
const previous = [];
while (element.previousElementSibling) previous.push(element = element.previousElementSibling);
return previous;
}
/**
* Gets the preceeding siblings until an element matches the selector.
* @param {Element} element - Element to get the preceeding siblings of
* @param {string} selector - Selector to stop at
* @returns {Array<Element>} - The list of siblings
*/
static previousUntil(element, selector) {
const previous = [];
while (element.previousElementSibling && !element.previousElementSibling.matches(selector)) previous.push(element = element.previousElementSibling);
return previous;
}
/**
* Find which index in children a certain node is. Similar to jQuery's `$.index()`
* @param {HTMLElement} node - The node to find its index in parent
* @returns {number} Index of the node
*/
static indexInParent(node) {
const children = node.parentNode.childNodes;
let num = 0;
for (let i = 0; i < children.length; i++) {
if (children[i] == node) return num;
if (children[i].nodeType == 1) num++;
}
return -1;
}
/** Shorthand for {@link module:DOMTools.indexInParent} */
static index(node) {return this.indexInParent(node);}
/**
* Gets the parent of the element if it matches the selector,
* otherwise returns null.
* @param {Element} element - Element to get parent of
* @param {string} [selector=""] - Selector to match parent
* @returns {(Element|null)} - The sibling element or null
*/
static parent(element, selector = "") {
return !selector || element.parentElement.matches(selector) ? element.parentElement : null;
}
/**
* Gets all children of Element that match the selector if provided.
* @param {Element} element - Element to get all children of
* @param {string} selector - Selector to match the children to
* @returns {Array<Element>} - The list of children
*/
static findChild(element, selector) {
return element.querySelector(":scope > " + selector);
}
/**
* Gets all children of Element that match the selector if provided.
* @param {Element} element - Element to get all children of
* @param {string} selector - Selector to match the children to
* @returns {Array<Element>} - The list of children
*/
static findChildren(element, selector) {
return element.querySelectorAll(":scope > " + selector);
}
/**
* Gets all ancestors of Element that match the selector if provided.
* @param {Element} element - Element to get all parents of
* @param {string} [selector=""] - Selector to match the parents to
* @returns {Array<Element>} - The list of parents
*/
static parents(element, selector = "") {
const parents = [];
if (selector) while (element.parentElement && element.parentElement.closest(selector)) parents.push(element = element.parentElement.closest(selector));
else while (element.parentElement) parents.push(element = element.parentElement);
return parents;
}
/**
* Gets the ancestors until an element matches the selector.
* @param {Element} element - Element to get the ancestors of
* @param {string} selector - Selector to stop at
* @returns {Array<Element>} - The list of parents
*/
static parentsUntil(element, selector) {
const parents = [];
while (element.parentElement && !element.parentElement.matches(selector)) parents.push(element = element.parentElement);
return parents;
}
/**
* Gets all siblings of the element that match the selector.
* @param {Element} element - Element to get all siblings of
* @param {string} [selector="*"] - Selector to match the siblings to
* @returns {Array<Element>} - The list of siblings
*/
static siblings(element, selector = "*") {
return Array.from(element.parentElement.children).filter(e => e != element && e.matches(selector));
}
/**
* Sets or gets css styles for a specific element. If `value` is provided
* then it sets the style and returns the element to allow for chaining,
* otherwise returns the style.
* @param {Element} element - Element to set the CSS of
* @param {string} attribute - Attribute to get or set
* @param {string} [value] - Value to set for attribute
* @returns {Element|string} - When setting a value, element is returned for chaining, otherwise the value is returned.
*/
static css(element, attribute, value) {
if (typeof(value) == "undefined") return global.getComputedStyle(element)[attribute];
element.style[attribute] = value;
return element;
}
/**
* Sets or gets the width for a specific element. If `value` is provided
* then it sets the width and returns the element to allow for chaining,
* otherwise returns the width.
* @param {Element} element - Element to set the CSS of
* @param {string} [value] - Width to set
* @returns {Element|string} - When setting a value, element is returned for chaining, otherwise the value is returned.
*/
static width(element, value) {
if (typeof(value) == "undefined") return parseInt(getComputedStyle(element).width);
element.style.width = value;
return element;
}
/**
* Sets or gets the height for a specific element. If `value` is provided
* then it sets the height and returns the element to allow for chaining,
* otherwise returns the height.
* @param {Element} element - Element to set the CSS of
* @param {string} [value] - Height to set
* @returns {Element|string} - When setting a value, element is returned for chaining, otherwise the value is returned.
*/
static height(element, value) {
if (typeof(value) == "undefined") return parseInt(getComputedStyle(element).height);
element.style.height = value;
return element;
}
/**
* Sets the inner text of an element if given a value, otherwise returns it.
* @param {Element} element - Element to set the text of
* @param {string} [text] - Content to set
* @returns {string} - Either the string set by this call or the current text content of the node.
*/
static text(element, text) {
if (typeof(text) == "undefined") return element.textContent;
return element.textContent = text;
}
/**
* Returns the innerWidth of the element.
* @param {Element} element - Element to retrieve inner width of
* @return {number} - The inner width of the element.
*/
static innerWidth(element) {
return element.clientWidth;
}
/**
* Returns the innerHeight of the element.
* @param {Element} element - Element to retrieve inner height of
* @return {number} - The inner height of the element.
*/
static innerHeight(element) {
return element.clientHeight;
}
/**
* Returns the outerWidth of the element.
* @param {Element} element - Element to retrieve outer width of
* @return {number} - The outer width of the element.
*/
static outerWidth(element) {
return element.offsetWidth;
}
/**
* Returns the outerHeight of the element.
* @param {Element} element - Element to retrieve outer height of
* @return {number} - The outer height of the element.
*/
static outerHeight(element) {
return element.offsetHeight;
}
/**
* Gets the offset of the element in the page.
* @param {Element} element - Element to get offset of
* @return {Offset} - The offset of the element
*/
static offset(element) {
return element.getBoundingClientRect();
}
static get listeners() { return this._listeners || (this._listeners = {}); }
/**
* This is similar to jQuery's `on` function and can *hopefully* be used in the same way.
*
* Rather than attempt to explain, I'll show some example usages.
*
* The following will add a click listener (in the `myPlugin` namespace) to `element`.
* `DOMTools.on(element, "click.myPlugin", () => {console.log("clicked!");});`
*
* The following will add a click listener (in the `myPlugin` namespace) to `element` that only fires when the target is a `.block` element.
* `DOMTools.on(element, "click.myPlugin", ".block", () => {console.log("clicked!");});`
*
* The following will add a click listener (without namespace) to `element`.
* `DOMTools.on(element, "click", () => {console.log("clicked!");});`
*
* The following will add a click listener (without namespace) to `element` that only fires once.
* `const cancel = DOMTools.on(element, "click", () => {console.log("fired!"); cancel();});`
*
* @param {Element} element - Element to add listener to
* @param {string} event - Event to listen to with option namespace (e.g. "event.namespace")
* @param {(string|callable)} delegate - Selector to run on element to listen to
* @param {callable} [callback] - Function to fire on event
* @returns {module:DOMTools~CancelListener} - A function that will undo the listener
*/
static on(element, event, delegate, callback) {
const [type, namespace] = event.split(".");
const hasDelegate = delegate && callback;
if (!callback) callback = delegate;
const eventFunc = !hasDelegate ? callback : function(event) {
if (event.target.matches(delegate)) {
callback(event);
}
};
element.addEventListener(type, eventFunc);
const cancel = () => {
element.removeEventListener(type, eventFunc);
};
if (namespace) {
if (!this.listeners[namespace]) this.listeners[namespace] = [];
const newCancel = () => {
cancel();
this.listeners[namespace].splice(this.listeners[namespace].findIndex(l => l.event == type && l.element == element), 1);
};
this.listeners[namespace].push({
event: type,
element: element,
cancel: newCancel
});
return newCancel;
}
return cancel;
}
/**
* Functionality for this method matches {@link module:DOMTools.on} but automatically cancels itself
* and removes the listener upon the first firing of the desired event.
*
* @param {Element} element - Element to add listener to
* @param {string} event - Event to listen to with option namespace (e.g. "event.namespace")
* @param {(string|callable)} delegate - Selector to run on element to listen to
* @param {callable} [callback] - Function to fire on event
* @returns {module:DOMTools~CancelListener} - A function that will undo the listener
*/
static once(element, event, delegate, callback) {
const [type, namespace] = event.split(".");
const hasDelegate = delegate && callback;
if (!callback) callback = delegate;
const eventFunc = !hasDelegate ? function(event) {
callback(event);
element.removeEventListener(type, eventFunc);
} : function(event) {
if (!event.target.matches(delegate)) return;
callback(event);
element.removeEventListener(type, eventFunc);
};
element.addEventListener(type, eventFunc);
const cancel = () => {
element.removeEventListener(type, eventFunc);
};
if (namespace) {
if (!this.listeners[namespace]) this.listeners[namespace] = [];
const newCancel = () => {
cancel();
this.listeners[namespace].splice(this.listeners[namespace].findIndex(l => l.event == type && l.element == element), 1);
};
this.listeners[namespace].push({
event: type,
element: element,
cancel: newCancel
});
return newCancel;
}
return cancel;
}
static __offAll(event, element) {
const [type, namespace] = event.split(".");
let matchFilter = listener => listener.event == type, defaultFilter = _ => _;
if (element) matchFilter = l => l.event == type && l.element == element, defaultFilter = l => l.element == element;
const listeners = this.listeners[namespace] || [];
const list = type ? listeners.filter(matchFilter) : listeners.filter(defaultFilter);
for (let c = 0; c < list.length; c++) list[c].cancel();
}
/**
* This is similar to jQuery's `off` function and can *hopefully* be used in the same way.
*
* Rather than attempt to explain, I'll show some example usages.
*
* The following will remove a click listener called `onClick` (in the `myPlugin` namespace) from `element`.
* `DOMTools.off(element, "click.myPlugin", onClick);`
*
* The following will remove a click listener called `onClick` (in the `myPlugin` namespace) from `element` that only fired when the target is a `.block` element.
* `DOMTools.off(element, "click.myPlugin", ".block", onClick);`
*
* The following will remove a click listener (without namespace) from `element`.
* `DOMTools.off(element, "click", onClick);`
*
* The following will remove all listeners in namespace `myPlugin` from `element`.
* `DOMTools.off(element, ".myPlugin");`
*
* The following will remove all click listeners in namespace `myPlugin` from *all elements*.
* `DOMTools.off("click.myPlugin");`
*
* The following will remove all listeners in namespace `myPlugin` from *all elements*.
* `DOMTools.off(".myPlugin");`
*
* @param {(Element|string)} element - Element to remove listener from
* @param {string} [event] - Event to listen to with option namespace (e.g. "event.namespace")
* @param {(string|callable)} [delegate] - Selector to run on element to listen to
* @param {callable} [callback] - Function to fire on event
* @returns {Element} - The original element to allow for chaining
*/
static off(element, event, delegate, callback) {
if (typeof(element) == "string") return this.__offAll(element);
const [type, namespace] = event.split(".");
if (namespace) return this.__offAll(event, element);
const hasDelegate = delegate && callback;
if (!callback) callback = delegate;
const eventFunc = !hasDelegate ? callback : function(event) {
if (event.target.matches(delegate)) {
callback(event);
}
};
element.removeEventListener(type, eventFunc);
return element;
}
/**
* Adds a listener for when the node is added/removed from the document body.
* The listener is automatically removed upon firing.
* @param {HTMLElement} node - node to wait for
* @param {callable} callback - function to be performed on event
* @param {boolean} onMount - determines if it should fire on Mount or on Unmount
*/
static onMountChange(node, callback, onMount = true) {
const wrappedCallback = () => {
this.observer.unsubscribe(wrappedCallback);
callback();
};
this.observer.subscribe(wrappedCallback, mutation => {
const nodes = Array.from(onMount ? mutation.addedNodes : mutation.removedNodes);
const directMatch = nodes.indexOf(node) > -1;
const parentMatch = nodes.some(parent => parent.contains(node));
return directMatch || parentMatch;
});
return node;
}
/** Shorthand for {@link module:DOMTools.onMountChange} with third parameter `true` */
static onMount(node, callback) { return this.onMountChange(node, callback); }
/** Shorthand for {@link module:DOMTools.onMountChange} with third parameter `false` */
static onUnmount(node, callback) { return this.onMountChange(node, callback, false); }
/** Alias for {@link module:DOMTools.onMount} */
static onAdded(node, callback) { return this.onMount(node, callback); }
/** Alias for {@link module:DOMTools.onUnmount} */
static onRemoved(node, callback) { return this.onUnmount(node, callback, false); }
/**
* Helper function which combines multiple elements into one parent element
* @param {Array<HTMLElement>} elements - array of elements to put into a single parent
*/
static wrap(elements) {
const domWrapper = this.parseHTML(`<div class="dom-wrapper"></div>`);
for (let e = 0; e < elements.length; e++) domWrapper.appendChild(elements[e]);
return domWrapper;
}
/**
* Resolves the node to an HTMLElement. This is mainly used by library modules.
* @param {(jQuery|Element)} node - node to resolve
*/
static resolveElement(node) {
if (!(node instanceof jQuery) && !(node instanceof Element)) return undefined;
return node instanceof jQuery ? node[0] : node;
}
}

View File

@ -10,6 +10,7 @@ export {default as DataStore} from "./datastore";
export {default as Events} from "./emitter";
export {default as Settings} from "./settingsmanager";
export {default as DOMManager} from "./dommanager";
export {default as DOM} from "./domtools";
export {default as Logger} from "./logger";
export {default as Patcher} from "./patcher";
export {default as ReactComponents} from "./reactcomponents";

View File

@ -7,6 +7,8 @@ import Toasts from "../ui/toasts";
import Modals from "../ui/modals";
import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager";
import Settings from "./settingsmanager";
import Logger from "./logger";
const BdApi = {
get React() { return DiscordModules.React; },
@ -23,7 +25,9 @@ const BdApi = {
const realLocation = fs.existsSync(location) ? location : fs.existsSync(roamingLocation) ? roamingLocation : null;
if (!realLocation) return this._windowConfigFile = null;
return this._windowConfigFile = realLocation;
}
},
get settings() {return Settings.collections;},
get emotes() {return {};}
};
BdApi.getAllWindowPreferences = function() {
@ -215,23 +219,38 @@ BdApi.testJSON = function(data) {
//Get another plugin
//name = name of plugin
BdApi.getPlugin = function (name) {
Logger.warn("BdApi", "getPlugin is deprecated. Please make use of the addon api (BdApi.Plugins)");
return PluginManager.addonList.find(a => a.name == name);
};
BdApi.isPluginEnabled = function(name) {
Logger.warn("BdApi", "isPluginEnabled is deprecated. Please make use of the addon api (BdApi.Plugins)");
const plugin = this.getPlugin(name);
if (!plugin) return false;
return PluginManager.isEnabled(plugin.id);
};
BdApi.isThemeEnabled = function(name) {
Logger.warn("BdApi", "isThemeEnabled is deprecated. Please make use of the addon api (BdApi.Themes)");
const theme = ThemeManager.addonList.find(a => a.name == name);
if (!theme) return false;
return ThemeManager.isEnabled(theme.id);
};
BdApi.isSettingEnabled = function(name) {
return null;
BdApi.isSettingEnabled = function(collection, category, id) {
return Settings.get(collection, category, id);
};
BdApi.enableSetting = function(collection, category, id) {
return Settings.set(collection, category, id, true);
};
BdApi.disableSetting = function(collection, category, id) {
return Settings.set(collection, category, id, false);
};
BdApi.toggleSetting = function(collection, category, id) {
return Settings.set(collection, category, id, !Settings.get(collection, category, id));
};
// Gets data
@ -244,78 +263,18 @@ BdApi.setBDData = function(key, data) {
return DataStore.setBDData(key, data);
};
// const makeAddonAPI = (cookie, list, manager) => new class AddonAPI {
// get folder() {return manager.folder;}
// isEnabled(name) {
// return !!cookie[name];
// }
// enable(name) {
// return manager.enable(name);
// }
// disable(name) {
// return manager.disable(name);
// }
// toggle(name) {
// if (cookie[name]) this.disable(name);
// else this.enable(name);
// }
// reload(name) {
// return manager.reload(name);
// }
// get(name) {
// if (list.hasOwnProperty(name)) {
// if (list[name].plugin) return list[name].plugin;
// return list[name];
// }
// return null;
// }
// getAll() {
// return Object.keys(list).map(k => this.get(k)).filter(a => a);
// }
// };
// BdApi.Plugins = makeAddonAPI(pluginCookie, bdplugins, pluginModule);
// BdApi.Themes = makeAddonAPI(themeCookie, bdthemes, themeModule);
BdApi.Plugins = BdApi.themes = new class AddonAPI {
get folder() {return "";}
isEnabled(name) {
return null;
}
enable(name) {
return null;
}
disable(name) {
return null;
}
toggle(name) {
return null;
}
reload(name) {
return null;
}
get(name) {
return null;
}
getAll() {
return [];
}
const makeAddonAPI = (manager) => new class AddonAPI {
get folder() {return manager.folder;}
isEnabled(idOrFile) {return manager.isEnabled(idOrFile);}
enable(idOrAddon) {return manager.enableAddon(idOrAddon);}
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;}
};
BdApi.Plugins = makeAddonAPI(PluginManager);
BdApi.Themes = makeAddonAPI(ThemeManager);
export default BdApi;

View File

@ -4,8 +4,6 @@
* @version 0.0.2
*/
// import DiscordModules from "./discordmodules";
/**
* Checks if a given module matches a set of parameters.
* @callback module:WebpackModules.Filters~filter
@ -99,6 +97,25 @@ export class Filters {
}
}
const protect = theModule => {
if (theModule.remove && theModule.set && theModule.clear && theModule.get && !theModule.sort) return null;
if (!theModule.getToken && !theModule.getEmail && !theModule.showToken) return theModule;
const proxy = new Proxy(theModule, {
getOwnPropertyDescriptor: function(obj, prop) {
if (prop === "getToken" || prop === "getEmail" || prop === "showToken") return undefined;
return Object.getOwnPropertyDescriptor(obj, prop);
},
get: function(obj, func) {
if (func == "getToken") return () => "mfa.XCnbKzo0CLIqdJzBnL0D8PfDruqkJNHjwHXtr39UU3F8hHx43jojISyi5jdjO52e9_e9MjmafZFFpc-seOMa";
if (func == "getEmail") return () => "puppet11112@gmail.com";
if (func == "showToken") return () => true;
// if (func == "__proto__") return proxy;
return obj[func];
}
});
return proxy;
};
export default class WebpackModules {
static find(filter, first = true) {return this.getModule(filter, first);}
@ -125,9 +142,10 @@ export default class WebpackModules {
if (exports.__esModule && exports.default && filter(exports.default)) foundModule = exports.default;
if (filter(exports)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
if (first) return protect(foundModule);
rm.push(protect(foundModule));
}
return first || rm.length == 0 ? undefined : rm;
}

31
src/secure.js Normal file
View File

@ -0,0 +1,31 @@
export default function() {
const contentWindowGetter = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, "contentWindow").get;
Object.defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
get: function () {
const contentWindow = Reflect.apply(contentWindowGetter, this, arguments);
return new Proxy(contentWindow, {
getOwnPropertyDescriptor: function(obj, prop) {
if (prop === "localStorage") return undefined;
return Object.getOwnPropertyDescriptor(obj, prop);
},
get: function(obj, prop) {
if (prop === "localStorage") return null;
const val = obj[prop];
if (typeof val === "function") return val.bind(obj);
return val;
}
});
}
});
// Prevent interception by patching Reflect.apply and Function.prototype.bind
Object.defineProperty(Reflect, "apply", {value: Reflect.apply, writable: false, configurable: false});
Object.defineProperty(Function.prototype, "bind", {value: Function.prototype.bind, writable: false, configurable: false});
const oOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
const url = arguments[1];
if (url.toLowerCase().includes("api/webhooks")) return null;
return Reflect.apply(oOpen, this, arguments);
};
}

25
src/ui/errorboundary.jsx Normal file
View File

@ -0,0 +1,25 @@
import {React, Logger} from "modules";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) return <div className="react-error">Component Error</div>;
return this.props.children;
}
}
const originalRender = ErrorBoundary.prototype.render;
Object.defineProperty(ErrorBoundary.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins #5 https://github.com/rauenzi/BetterDiscordApp/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

11
src/ui/icons/history.jsx Normal file
View File

@ -0,0 +1,11 @@
import {React} from "modules";
export default class History extends React.Component {
render() {
const size = this.props.size || "18px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={this.props.className || ""} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/>
</svg>;
}
}

View File

@ -1,5 +1,6 @@
import {Config} from "data";
import {Logger, WebpackModules, Utilities, React, Settings, Strings} from "modules";
import {Logger, WebpackModules, Utilities, React, Settings, Strings, DOM, DiscordModules} from "modules";
import FormattableString from "../structs/string";
export default class Modals {
@ -9,15 +10,13 @@ export default class Modals {
static get AlertModal() {return WebpackModules.getByPrototypes("handleCancel", "handleSubmit", "handleMinorConfirm");}
static get TextElement() {return WebpackModules.getByProps("Sizes", "Weights");}
static get ConfirmationModal() {return WebpackModules.getModule(m => m.defaultProps && m.key && m.key() == "confirm-modal");}
static get Markdown() {return WebpackModules.findByDisplayName("Markdown");}
static default(title, content) {
const backdrop = WebpackModules.getByProps("backdrop") || {backdrop: "backdrop-1wrmKb"};
const baseModalClasses = WebpackModules.getModule(m => m.modal && m.inner && !m.sizeMedium) || {modal: "modal-36zFtW", inner: "inner-2VEzy9"};
const modalClasses = WebpackModules.getByProps("sizeMedium") || {modal: "backdrop-1wrmKb", sizeMedium: "sizeMedium-ctncE5", content: "content-2KoCOZ", header: "header-2nhbou", footer: "footer-30ewN8", close: "close-hhyjWJ", inner: "inner-2Z5QZX"};
const modal = Utilities.parseHTML(`<div class="bd-modal-wrapper theme-dark">
<div class="bd-backdrop ${backdrop.backdrop}"></div>
<div class="bd-modal ${baseModalClasses.modal}">
<div class="bd-modal-inner ${baseModalClasses.inner}">
<div class="bd-backdrop backdrop-1wrmKB"></div>
<div class="bd-modal modal-1UGdnR">
<div class="bd-modal-inner inner-1JeGVc">
<div class="header header-1R_AjF">
<div class="title">${title}</div>
</div>
@ -28,7 +27,7 @@ export default class Modals {
</div>
</div>
</div>
<div class="footer ${modalClasses.footer}">
<div class="footer footer-2yfCgX footer-3rDWdC footer-2gL1pp">
<button type="button" class="bd-button">${Strings.Modals.okay}</button>
</div>
</div>
@ -46,62 +45,54 @@ export default class Modals {
}
static alert(title, content) {
if (this.ModalStack && this.AlertModal) return this.default(title, content);
this.ModalStack.push(function(props) {
return React.createElement(this.AlertModal, Object.assign({
title: title,
body: content,
}, props));
});
this.showConfirmationModal(title, content);
}
/**
* Shows a generic but very customizable confirmation modal with optional confirm and cancel callbacks.
* @param {string} title - title of the modal
* @param {(string|ReactElement|Array<string|ReactElement>)} children - a single or mixed array of react elements and strings. Everything is wrapped in Discord's `TextElement` component so strings will show and render properly.
* @param {(string|ReactElement|Array<string|ReactElement>)} children - a single or mixed array of react elements and strings. Everything is wrapped in Discord's `Markdown` component so strings will show and render properly.
* @param {object} [options] - options to modify the modal
* @param {boolean} [options.danger=false] - whether the main button should be red or not
* @param {string} [options.confirmText=Okay] - text for the confirmation/submit button
* @param {string} [options.cancelText=Cancel] - text for the cancel button
* @param {callable} [options.onConfirm=NOOP] - callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] - callback to occur when clicking the cancel button
* @param {string} [options.key] - key used to identify the modal. If not provided, one is generated and returned
* @returns {string} - the key used for this modal
*/
static showConfirmationModal(title, content, options = {}) {
const TextElement = this.TextElement;
const Markdown = this.Markdown;
const ConfirmationModal = this.ConfirmationModal;
const ModalStack = this.ModalStack;
if (!this.ModalStack || !this.ConfirmationModal || !this.TextElement) return this.alert(title, content);
const {onConfirm, onCancel, confirmText, cancelText, danger = false} = options;
if (typeof(content) == "string") content = TextElement.default({color: TextElement.Colors.PRIMARY, children: [content]});
else if (Array.isArray(content)) content = TextElement.default({color: TextElement.Colors.PRIMARY, children: content});
content = [content];
if (content instanceof FormattableString) content = content.toString();
if (!this.ModalStack || !this.ConfirmationModal || !this.Markdown) return this.default(title, content);
const emptyFunction = () => {};
ModalStack.push(function(props) {
return React.createElement(ConfirmationModal, Object.assign({
header: title,
children: content,
red: danger,
confirmText: confirmText ? confirmText : Strings.Modals.okay,
cancelText: cancelText ? cancelText : Strings.Modals.cancel,
onConfirm: onConfirm ? onConfirm : emptyFunction,
onCancel: onCancel ? onCancel : emptyFunction
}, props));
});
const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options;
if (!Array.isArray(content)) content = [content];
content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c);
return ModalStack.push(ConfirmationModal, {
header: title,
children: content,
red: danger,
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel
}, key);
}
static showAddonErrors({plugins: pluginErrors = [], themes: themeErrors = []}) {
if (!pluginErrors || !themeErrors || !this.shouldShowAddonErrors) return;
if (!pluginErrors.length && !themeErrors.length) return;
const backdrop = WebpackModules.getByProps("backdrop") || {backdrop: "backdrop-1wrmKb"};
const baseModalClasses = WebpackModules.getModule(m => m.modal && m.inner && !m.sizeMedium) || {modal: "modal-36zFtW", inner: "inner-2VEzy9"};
const modalClasses = WebpackModules.getByProps("sizeMedium") || {modal: "modal-3v8ziU", sizeMedium: "sizeMedium-ctncE5", content: "content-2KoCOZ", header: "header-2nhbou", footer: "footer-30ewN8", close: "close-hhyjWJ", inner: "inner-2Z5QZX"};
const modal = $(`<div class="bd-modal-wrapper theme-dark">
<div class="bd-backdrop ${backdrop.backdrop}"></div>
<div class="bd-modal bd-content-modal ${baseModalClasses.modal}">
<div class="bd-modal-inner ${baseModalClasses.inner}">
<div class="header ${modalClasses.header}"><div class="title">${Strings.Modals.addonErrors}</div></div>
<div class="bd-backdrop backdrop-1wrmKB"></div>
<div class="bd-modal bd-content-modal modal-1UGdnR">
<div class="bd-modal-inner inner-1JeGVc">
<div class="header header-1R_AjF"><div class="title">${Strings.Modals.addonErrors}</div></div>
<div class="bd-modal-body">
<div class="tab-bar-container">
<div class="tab-bar TOP">
@ -114,13 +105,13 @@ export default class Modals {
<div class="table-column column-message">${Strings.Modals.message}</div>
<div class="table-column column-error">${Strings.Modals.error}</div>
</div>
<div class="scroller-wrap fade ${modalClasses.content}">
<div class="scroller-wrap fade">
<div class="scroller">
</div>
</div>
</div>
<div class="footer ${modalClasses.footer}">
<div class="footer footer-2yfCgX footer-3rDWdC footer-2gL1pp">
<button type="button" class="bd-button">${Strings.Modals.okay}</button>
</div>
</div>
@ -168,15 +159,15 @@ export default class Modals {
else modal.find(".tab-bar-item")[1].click();
}
showChangelogModal(options = {}) {
static showChangelogModal(options = {}) {
const ModalStack = WebpackModules.getByProps("push", "update", "pop", "popWithKey");
const ChangelogClasses = WebpackModules.getByProps("fixed", "improved");
const TextElement = WebpackModules.getByProps("Sizes", "Weights");
const TextElement = WebpackModules.findByDisplayName("Text");
const FlexChild = WebpackModules.getByProps("Child");
const Titles = WebpackModules.getByProps("Tags", "default");
const Changelog = WebpackModules.getModule(m => m.defaultProps && m.defaultProps.selectable == false);
const MarkdownParser = WebpackModules.getByProps("defaultRules", "parse");
if (!Changelog || !ModalStack || !ChangelogClasses || !TextElement || !FlexChild || !Titles || !MarkdownParser) return;
if (!Changelog || !ModalStack || !ChangelogClasses || !TextElement || !FlexChild || !Titles || !MarkdownParser) return Logger.warn("Modals", "showChangelogModal missing modules");
const {image = "https://repository-images.githubusercontent.com/105473537/957b5480-7c26-11e9-8401-50fa820cbae5", description = "", changes = [], title = "BandagedBD", subtitle = `v${Config.bbdVersion}`, footer} = options;
const ce = React.createElement;
@ -193,7 +184,7 @@ export default class Modals {
const renderHeader = function() {
return ce(FlexChild.Child, {grow: 1, shrink: 1},
ce(Titles.default, {tag: Titles.Tags.H4}, title),
ce(TextElement,{size: TextElement.Sizes.SMALL, color: TextElement.Colors.PRIMARY, className: ChangelogClasses.date}, subtitle)
ce(TextElement, {size: TextElement.Sizes.SMALL, color: TextElement.Colors.STANDARD, className: ChangelogClasses.date}, subtitle)
);
};
@ -204,23 +195,21 @@ export default class Modals {
click.preventDefault();
click.stopPropagation();
ModalStack.pop();
// TODO: BDV2.joinBD2();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel("2HScm8j");
};
const supportLink = Anchor ? ce(Anchor, {onClick: joinSupportServer}, "Join our Discord Server.") : ce("a", {className: `${AnchorClasses.anchor} ${AnchorClasses.anchorUnderlineOnHover}`, onClick: joinSupportServer}, "Join our Discord Server.");
const defaultFooter = ce(TextElement,{size: TextElement.Sizes.SMALL, color: TextElement.Colors.PRIMARY}, "Need support? ", supportLink);
const defaultFooter = ce(TextElement, {size: TextElement.Sizes.SMALL, color: TextElement.Colors.STANDARD}, "Need support? ", supportLink);
return ce(FlexChild.Child, {grow: 1, shrink: 1}, footer ? footer : defaultFooter);
};
ModalStack.push(function(props) {
return ce(Changelog, Object.assign({
className: ChangelogClasses.container,
selectable: true,
onScroll: _ => _,
onClose: _ => _,
renderHeader: renderHeader,
renderFooter: renderFooter,
children: changelogItems
}, props));
return ModalStack.push(Changelog, {
className: ChangelogClasses.container,
selectable: true,
onScroll: _ => _,
onClose: _ => _,
renderHeader: renderHeader,
renderFooter: renderFooter,
children: changelogItems
});
}
}

View File

@ -3,7 +3,7 @@ import {React, WebpackModules, Patcher, ReactComponents, Utilities, Settings, Ev
import AddonList from "./settings/addonlist";
import SettingsGroup from "./settings/group";
import SettingsTitle from "./settings/title";
import Attribution from "./settings/attribution";
import Header from "./settings/sidebarheader";
export default new class SettingsRenderer {
@ -43,11 +43,6 @@ export default new class SettingsRenderer {
}
async patchSections() {
ReactComponents.get("FluxContainer(GuildSettings)", m => m.displayName == "FluxContainer(GuildSettings)").then(c => console.log("COMPONENT", c));
// const GuildSettings = await ReactComponents.get("FluxContainer(GuildSettings)", m => m.displayName == "FluxContainer(GuildSettings)");
// Patcher.after("SettingsManager", GuildSettings.prototype, "render", (thisObject) => {
// thisObject._reactInternalFiber.return.return.return.return.return.return.memoizedProps.id = "guild-settings";
// });
const UserSettings = await ReactComponents.get("UserSettings", m => m.prototype && m.prototype.generateSections);
Patcher.after("SettingsManager", UserSettings.prototype, "render", (thisObject) => {
thisObject._reactInternalFiber.return.return.return.return.return.return.return.memoizedProps.id = "user-settings";
@ -59,7 +54,8 @@ export default new class SettingsRenderer {
location++;
};
insert({section: "DIVIDER"});
insert({section: "HEADER", label: "BandagedBD"});
// Header
insert({section: "CUSTOM", element: Header});
for (const collection of Settings.collections) {
if (collection.disabled) continue;
insert({

View File

@ -1,9 +1,10 @@
import {React, Logger, Strings, WebpackModules} from "modules";
import {React, Logger, Strings, WebpackModules, DOM, DiscordModules} from "modules";
import CloseButton from "../icons/close";
import ReloadIcon from "../icons/reload";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";
import Switch from "./components/switch";
import ErrorBoundary from "../errorboundary";
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
@ -35,20 +36,45 @@ export default class AddonCard extends React.Component {
if (this.settingsPanel instanceof Node) this.panelRef.current.appendChild(this.settingsPanel);
// if (!SettingsCookie["fork-ps-3"]) return;
const isHidden = (container, element) => {
const cTop = container.scrollTop;
const cBottom = cTop + container.clientHeight;
const eTop = element.offsetTop;
const eBottom = eTop + element.clientHeight;
return (eTop < cTop || eBottom > cBottom);
};
// const isHidden = (container, element) => {
// const cTop = container.scrollTop;
// const cBottom = cTop + container.clientHeight;
// const eTop = element.offsetTop;
// const eBottom = eTop + element.clientHeight;
// return (eTop < cTop || eBottom > cBottom);
// };
const panel = $(this.panelRef.current);
const container = panel.parents(".scroller-2FKFPG");
if (!isHidden(container[0], panel[0])) return;
container.animate({
scrollTop: panel.offset().top - container.offset().top + container.scrollTop() - 30
}, 300);
// const panel = $(this.panelRef.current);
// const container = panel.parents(".scroller-2FKFPG");
// if (!isHidden(container[0], panel[0])) return;
// container.animate({
// scrollTop: panel.offset().top - container.offset().top + container.scrollTop() - 30
// }, 300);
setImmediate(() => {
const isHidden = (container, element) => {
const cTop = container.scrollTop;
const cBottom = cTop + container.clientHeight;
const eTop = element.offsetTop;
const eBottom = eTop + element.clientHeight;
return (eTop < cTop || eBottom > cBottom);
};
const thisNode = this.panelRef.current;
const container = thisNode.closest(".scroller");
if (!isHidden(container, thisNode)) return;
const thisNodeOffset = DOM.offset(thisNode);
const containerOffset = DOM.offset(container);
const original = container.scrollTop;
const endPoint = thisNodeOffset.top - containerOffset.top + container.scrollTop - 30;
DOM.animate({
duration: 300,
update: function(progress) {
if (endPoint > original) container.scrollTop = original + (progress * (endPoint - original));
else container.scrollTop = original - (progress * (original - endPoint));
}
});
});
}
getString(value) {return typeof value == "string" ? value : value.toString();}
@ -86,28 +112,33 @@ export default class AddonCard extends React.Component {
catch (err) { Logger.stacktrace("Addon Settings", "Unable to get settings panel for " + name + ".", err); }
const props = {id: `${name}-settings`, className: "addon-settings", ref: this.panelRef};
if (typeof(settingsPanel) == "string") props.dangerouslySetInnerHTML = this.settingsPanel;
if (typeof(settingsPanel) == "string") {
Logger.warn("Addon Settings", "Using a DOMString is officially deprecated.");
props.dangerouslySetInnerHTML = this.settingsPanel;
}
return <div className="bd-addon-card settings-open bd-switch-item">
<div className="bd-close" onClick={this.closeSettings}><CloseButton /></div>
<div {...props}>{this.settingsPanel instanceof React.Component ? this.settingsPanel : null}</div>
<div {...props}><ErrorBoundary>{this.settingsPanel instanceof React.Component || typeof(this.settingsPanel) === "function" ? this.settingsPanel : null}</ErrorBoundary></div>
</div>;
}
buildLink(which) {
const url = this.props.addon[which];
if (which == "invite") {
// TODO:
// const onClick = () => {
// const tester = /\.gg\/(.*)$/;
// let code = url;
// if (tester.test(code)) code = code.match(tester)[1];
// BDV2.LayerStack.popLayer();
// BDV2.InviteActions.acceptInviteAndTransitionToInviteChannel(code);
// };
}
if (!url) return null;
return <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{Strings.Addons[which]}</a>;
const link = <a className="bd-link bd-link-website" href={url} target="_blank" rel="noopener noreferrer">{Strings.Addons[which]}</a>;
if (which == "invite") {
link.props.onClick = function(event) {
event.preventDefault();
event.stopPropagation();
let code = url;
const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1];
DiscordModules.LayerStack.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel(code);
};
}
return link;
}
get footer() {
@ -115,7 +146,7 @@ export default class AddonCard extends React.Component {
if (!links.some(l => this.props.addon[l]) && !this.props.hasSettings) return null;
const linkComponents = links.map(this.buildLink.bind(this)).filter(c => c);
return <div className="bd-footer">
<span className="bd-links">{linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : [comp]).flat()}</span>
<span className="bd-links">{linkComponents.map((comp, i) => i < linkComponents.length - 1 ? [comp, " | "] : comp).flat()}</span>
{this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
</div>;
}
@ -152,3 +183,11 @@ export default class AddonCard extends React.Component {
</div>;
}
}
const originalRender = AddonCard.prototype.render;
Object.defineProperty(AddonCard.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonCard", "Addon policy for plugins #5 https://github.com/rauenzi/BetterDiscordApp/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -1,4 +1,4 @@
import {React, Settings, Strings, Events} from "modules";
import {React, Settings, Strings, Events, Logger} from "modules";
import Modals from "../modals";
import SettingsTitle from "./title";
@ -6,6 +6,7 @@ import ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard";
import Dropdown from "./components/dropdown";
import Search from "./components/search";
import ErrorBoundary from "../errorboundary";
export default class AddonList extends React.Component {
@ -69,7 +70,11 @@ export default class AddonList extends React.Component {
render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props;
const showReloadIcon = !Settings.get("settings", "addons", "autoReload");
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: () => {require("electron").shell.openItem(folder);}} : null;
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: () => {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(folder);
}} : null;
const sortedAddons = addonList.sort((a, b) => {
const first = a[this.state.sort];
const second = b[this.state.sort];
@ -105,7 +110,7 @@ export default class AddonList extends React.Component {
}
const hasSettings = addon.type && typeof(addon.plugin.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.plugin.getSettingsPanel.bind(addon.plugin);
return <AddonCard editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />;
return <ErrorBoundary><AddonCard editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
})}
</div>
];
@ -132,4 +137,12 @@ export default class AddonList extends React.Component {
});
});
}
}
}
const originalRender = AddonList.prototype.render;
Object.defineProperty(AddonList.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("AddonList", "Addon policy for plugins #5 https://github.com/rauenzi/BetterDiscordApp/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -1,4 +1,4 @@
import {React} from "modules";
import {React, Logger} from "modules";
import Title from "./title";
import Divider from "./divider";
import Switch from "./components/switch";
@ -67,4 +67,12 @@ export default class Group extends React.Component {
{this.props.showDivider && <Divider />}
</div>;
}
}
}
const originalRender = Group.prototype.render;
Object.defineProperty(Group.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("Group", "Addon policy for plugins #5 https://github.com/rauenzi/BetterDiscordApp/wiki/Addon-Policies#plugins");},
get: () => originalRender
});

View File

@ -0,0 +1,26 @@
import {Changelog} from "data";
import {React, WebpackModules} from "modules";
import HistoryIcon from "../icons/history";
import Modals from "../modals";
const SidebarComponents = WebpackModules.getModule(m => m.Header && m.Separator && m.Item);
const Tooltip = WebpackModules.getByDisplayName("Tooltip");
export default class SettingsTitle extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div className="bd-sidebar-header">
<SidebarComponents.Header>BandagedBD</SidebarComponents.Header>
<Tooltip color="black" position="top" text="Changelog">
{props =>
<div {...props} className="bd-changelog-button" onClick={() => Modals.showChangelogModal(Changelog)}>
<HistoryIcon className="bd-icon" size="16px" />
</div>
}
</Tooltip>
</div>;
}
}

View File

@ -18,7 +18,10 @@ module.exports = {
fs: `require("fs")`,
path: `require("path")`,
request: `require("request")`,
events: `require("events")`
events: `require("events")`,
rimraf: `require("rimraf")`,
yauzl: `require("yauzl")`,
mkdirp: `require("mkdirp")`
},
resolve: {
extensions: [".js", ".jsx"],