first refactor of emote module

This commit is contained in:
Zack Rauen 2019-06-03 16:25:08 -04:00
parent b64f35fa88
commit 6bed40f21f
24 changed files with 1098 additions and 930 deletions

1194
js/main.js

File diff suppressed because it is too large Load Diff

2
js/main.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,9 @@ export default new class TwentyFourHour extends Builtin {
}
disabled() {
if (this.cancel24Hour) this.cancel24Hour();
if (!this.cancel24Hour) return;
this.cancel24Hour();
delete this.cancel24Hour;
}
inject24Hour() {

View File

@ -7,4 +7,6 @@ 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 EmoteMenu} from "./emotemenu";
export {default as EmoteAutocaps} from "./emoteautocaps";
export {default as EmoteModule} from "./emotes";

View File

@ -9,13 +9,6 @@ export default new class ClassNormalizer extends Builtin {
get category() {return "Modules";}
get name() {return "ClassNormalizer";}
disabled() {
if (!this.hasPatched) return;
this.unpatchClassModules(WebpackModules.getModules(this.moduleFilter.bind(this)));
this.revertElement(document.querySelector("#app-mount"));
this.hasPatched = false;
}
enabled() {
if (this.hasPatched) return;
this.patchClassModules(WebpackModules.getModules(this.moduleFilter.bind(this)));
@ -23,6 +16,13 @@ export default new class ClassNormalizer extends Builtin {
this.hasPatched = true;
}
disabled() {
if (!this.hasPatched) return;
this.unpatchClassModules(WebpackModules.getModules(this.moduleFilter.bind(this)));
this.revertElement(document.querySelector("#app-mount"));
this.hasPatched = false;
}
patchClassModules(modules) {
for (const module of modules) {
this.patchClassModule(normalizedPrefix, module);

View File

@ -13,7 +13,9 @@ export default new class ColoredText extends Builtin {
}
disabled() {
if (this.cancelColoredText) this.cancelColoredText();
if (!this.cancelColoredText) return;
this.cancelColoredText();
delete this.cancelColoredText;
}
injectColoredText() {

View File

@ -0,0 +1,40 @@
import Builtin from "../structs/builtin";
import {Emotes} from "data";
import {Utilities} from "modules";
export default new class EmoteAutocaps extends Builtin {
get name() {return "EmoteAutocapitalize";}
get category() {return "Modules";}
get id() {return "bda-es-4";}
enabled() {
$("body").off(".bdac");
$("body").on("keyup.bdac change.bdac paste.bdac", $(".channelTextArea-1LDbYG textarea:first"), () => {
const text = $(".channelTextArea-1LDbYG textarea:first").val();
if (text == undefined) return;
const lastWord = text.split(" ").pop();
if (lastWord.length > 3) {
if (lastWord == "danSgame") return;
const ret = this.capitalize(lastWord.toLowerCase());
if (ret !== null && ret !== undefined) {
Utilities.insertText(Utilities.getTextArea()[0], text.replace(lastWord, ret));
}
}
});
}
disabled() {
$("body").off(".bdac");
}
capitalize(value) {
const res = Emotes.TwitchGlobal;
for (const p in res) {
if (res.hasOwnProperty(p) && value == (p + "").toLowerCase()) {
return p;
}
}
}
};

View File

@ -1,5 +1,5 @@
import Builtin, {onSettingChange} from "../structs/builtin";
import {SettingsCookie, Emotes} from "data";
import {SettingsCookie, Emotes, State} from "data";
import {DataStore, Utilities, Events} from "modules";
const headerHTML = `<div id="bda-qem">
@ -62,6 +62,7 @@ export default new class EmoteMenu extends Builtin {
this.observer = new MutationObserver(mutations => {for (const mutation of mutations) this.observe(mutation);});
this.enableHideEmojis = this.enableHideEmojis.bind(this);
this.disableHideEmojis = this.disableHideEmojis.bind(this);
this.updateTwitchEmotes = this.updateTwitchEmotes.bind(this);
}
initialize() {
@ -72,10 +73,6 @@ export default new class EmoteMenu extends Builtin {
}
async enabled() {
await new Promise(resolve => {
Events.on("emotes-loaded", resolve);
});
this.updateTwitchEmotes();
this.log("Starting to observe");
this.observer.observe(document.getElementById("app-mount"), {
childList: true,
@ -83,14 +80,26 @@ export default new class EmoteMenu extends Builtin {
});
this.hideEmojiCancel = onSettingChange(this.category, this.hideEmojisID, this.enableHideEmojis, this.disableHideEmojis);
if (this.hideEmojis) this.enableHideEmojis();
// await this.waitForEmotes();
// this.updateTwitchEmotes();
if (State.emotesLoaded) this.updateTwitchEmotes();
Events.on("emotes-loaded", this.updateTwitchEmotes);
}
disabled() {
Events.off("emotes-loaded", this.updateTwitchEmotes);
this.observer.disconnect();
this.disableHideEmojis();
if (this.hideEmojiCancel) this.hideEmojiCancel();
}
async waitForEmotes() {
if (State.emotesLoaded) return;
return new Promise(resolve => {
Events.on("emotes-loaded", resolve);
});
}
enableHideEmojis() {
$(".emojiPicker-3m1S-j").addClass("bda-qme-hidden");
}

241
src/builtins/emotes.js Normal file
View File

@ -0,0 +1,241 @@
import Builtin, {onSettingChange} from "../structs/builtin";
import {Config, SettingsCookie, Emotes, EmoteBlacklist, EmoteInfo, EmoteModifiers, EmoteOverrides, State} from "data";
import {Utilities, WebpackModules, DataStore, DiscordModules, Events} from "modules";
import BDEmote from "../ui/emote";
import {Toasts} from "ui";
const bdEmoteSettingIDs = {
TwitchGlobal: "bda-es-7",
TwitchSubscriber: "bda-es-7",
BTTV: "bda-es-2",
FrankerFaceZ: "bda-es-1",
BTTV2: "bda-es-2"
};
export default new class EmoteModule extends Builtin {
get name() {return "Emotes";}
get category() {return "Modules";}
get id() {return "";}
get categories() { return Object.keys(bdEmoteSettingIDs).filter(k => SettingsCookie[bdEmoteSettingIDs[k]]); }
get MessageContentComponent() {return WebpackModules.getModule(m => m.defaultProps && m.defaultProps.hasOwnProperty("disableButtons"));}
async initialize() {
super.initialize();
await this.getBlacklist();
await this.loadEmoteData(EmoteInfo);
while (!this.MessageContentComponent) await new Promise(resolve => setTimeout(resolve, 100));
this.patchMessageContent();
}
disabled() {
if (this.cancelEmoteRender) return;
this.cancelEmoteRender();
delete this.cancelEmoteRender;
}
patchMessageContent() {
if (this.cancelEmoteRender) return;
this.cancelEmoteRender = Utilities.monkeyPatch(this.MessageContentComponent.prototype, "render", {after: ({returnValue}) => {
Utilities.monkeyPatch(returnValue.props, "children", {silent: true, after: ({returnValue}) => {
if (this.categories.length == 0) return;
const markup = returnValue.props.children[1];
if (!markup.props.children) return;
const nodes = markup.props.children[1];
if (!nodes || !nodes.length) return;
for (let n = 0; n < nodes.length; n++) {
const node = nodes[n];
if (typeof(node) !== "string") continue;
const words = node.split(/([^\s]+)([\s]|$)/g);
for (let c = 0, clen = this.categories.length; c < clen; c++) {
for (let w = 0, wlen = words.length; w < wlen; w++) {
const emote = words[w];
const emoteSplit = emote.split(":");
const emoteName = emoteSplit[0];
let emoteModifier = emoteSplit[1] ? emoteSplit[1] : "";
let emoteOverride = emoteModifier.slice(0);
if (emoteName.length < 4 || EmoteBlacklist.includes(emoteName)) continue;
if (!EmoteModifiers.includes(emoteModifier) || !SettingsCookie["bda-es-8"]) emoteModifier = "";
if (!EmoteOverrides.includes(emoteOverride)) emoteOverride = "";
else emoteModifier = emoteOverride;
let current = this.categories[c];
if (emoteOverride === "twitch") {
if (Emotes.TwitchGlobal[emoteName]) current = "TwitchGlobal";
else if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "bttv") {
if (Emotes.BTTV[emoteName]) current = "BTTV";
else if (Emotes.BTTV2[emoteName]) current = "BTTV2";
}
else if (emoteOverride === "ffz") {
if (Emotes.FrankerFaceZ[emoteName]) current = "FrankerFaceZ";
}
if (!Emotes[current][emoteName] || !SettingsCookie[bdEmoteSettingIDs[current]]) continue;
const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
if (!results) continue;
const pre = nodes[n].substring(0, results.index + results[1].length);
const post = nodes[n].substring(results.index + results[0].length - results[2].length);
nodes[n] = pre;
const emoteComponent = DiscordModules.React.createElement(BDEmote, {name: emoteName, url: Emotes[current][emoteName], modifier: emoteModifier});
nodes.splice(n + 1, 0, post);
nodes.splice(n + 1, 0, emoteComponent);
}
}
}
const onlyEmotes = nodes.every(r => {
if (typeof(r) == "string" && r.replace(/\s*/, "") == "") return true;
else if (r.type && r.type.name == "BDEmote") return true;
else if (r.props && r.props.children && r.props.children.props && r.props.children.props.emojiName) return true;
return false;
});
if (!onlyEmotes) return;
for (const node of nodes) {
if (typeof(node) != "object") continue;
if (node.type.name == "BDEmote") node.props.jumboable = true;
else if (node.props && node.props.children && node.props.children.props && node.props.children.props.emojiName) node.props.children.props.jumboable = true;
}
}});
}});
}
async loadEmoteData(emoteInfo) {
const _fs = require("fs");
const emoteFile = "emote_data.json";
const file = Config.dataPath + emoteFile;
const exists = _fs.existsSync(file);
if (exists && this.isCacheValid()) {
Toasts.show("Loading emotes from cache.", {type: "info"});
Utilities.log("Emotes", "Loading emotes from local cache.");
const data = await new Promise(resolve => {
_fs.readFile(file, "utf8", (err, data) => {
Utilities.log("Emotes", "Emotes loaded from cache.");
if (err) data = {};
resolve(data);
});
});
let isValid = Utilities.testJSON(data);
if (isValid) Object.assign(Emotes, JSON.parse(data));
for (const e in emoteInfo) {
isValid = Object.keys(Emotes[emoteInfo[e].variable]).length > 0;
}
if (isValid) {
Toasts.show("Emotes successfully loaded.", {type: "success"});
State.emotesLoaded = true;
Events.dispatch("emotes-loaded");
return;
}
Utilities.log("Emotes", "Cache was corrupt, downloading...");
_fs.unlinkSync(file);
}
if (!SettingsCookie["fork-es-3"]) return;
Toasts.show("Downloading emotes in the background do not reload.", {type: "info"});
for (const e in emoteInfo) {
await new Promise(r => setTimeout(r, 1000));
const data = await this.downloadEmotes(emoteInfo[e]);
Emotes[emoteInfo[e].variable] = data;
}
Toasts.show("All emotes successfully downloaded.", {type: "success"});
try { _fs.writeFileSync(file, JSON.stringify(Emotes), "utf8"); }
catch (err) { Utilities.err("Emotes", "Could not save emote data.", err); }
State.emotesLoaded = true;
Events.dispatch("emotes-loaded");
}
downloadEmotes(emoteMeta) {
const request = require("request");
const options = {
url: emoteMeta.url,
timeout: emoteMeta.timeout ? emoteMeta.timeout : 5000
};
Utilities.log("Emotes", `Downloading: ${emoteMeta.variable} (${emoteMeta.url})`);
return new Promise((resolve, reject) => {
request(options, (error, response, body) => {
if (error) {
Utilities.err("Emotes", "Could not download " + emoteMeta.variable, error);
if (emoteMeta.backup) {
emoteMeta.url = emoteMeta.backup;
emoteMeta.backup = null;
if (emoteMeta.backupParser) emoteMeta.parser = emoteMeta.backupParser;
return resolve(this.downloadEmotes(emoteMeta));
}
return reject({});
}
let parsedData = {};
try {
parsedData = JSON.parse(body);
}
catch (err) {
Utilities.err("Emotes", "Could not download " + emoteMeta.variable, err);
if (emoteMeta.backup) {
emoteMeta.url = emoteMeta.backup;
emoteMeta.backup = null;
if (emoteMeta.backupParser) emoteMeta.parser = emoteMeta.backupParser;
return resolve(this.downloadEmotes(emoteMeta));
}
return reject({});
}
if (typeof(emoteMeta.parser) === "function") parsedData = emoteMeta.parser(parsedData);
for (const emote in parsedData) {
if (emote.length < 4 || EmoteBlacklist.includes(emote)) {
delete parsedData[emote];
continue;
}
parsedData[emote] = emoteMeta.getEmoteURL(parsedData[emote]);
}
resolve(parsedData);
Utilities.log("Emotes", "Downloaded: " + emoteMeta.variable);
});
});
}
getBlacklist() {
return new Promise(resolve => {
$.getJSON(`https://rauenzi.github.io/BetterDiscordApp/data/emotefilter.json`, function (data) {
resolve(EmoteBlacklist.push(...data.blacklist));
});
});
}
isCacheValid() {
const cacheLength = DataStore.getBDData("emoteCacheDays") || DataStore.setBDData("emoteCacheDays", 7) || 7;
const cacheDate = new Date(DataStore.getBDData("emoteCacheDate") || null);
const currentDate = new Date();
const daysBetween = Math.round(Math.abs((currentDate.getTime() - cacheDate.getTime()) / (24 * 60 * 60 * 1000)));
if (daysBetween > cacheLength) {
DataStore.setBDData("emoteCacheDate", currentDate.toJSON());
return false;
}
return true;
}
clearEmoteData() {
const _fs = require("fs");
const emoteFile = "emote_data.json";
const file = Config.dataPath + emoteFile;
const exists = _fs.existsSync(file);
if (exists) _fs.unlinkSync(file);
DataStore.setBDData("emoteCacheDate", (new Date()).toJSON());
for (const category in Emotes) Object.assign(Emotes, {[category]: {}});
}
};

View File

@ -1,3 +1,4 @@
import State from "./state";
import SettingsInfo from "./settings";
import SettingsCookie from "./cookies/settingscookie";
import Config from "./config";
@ -7,5 +8,8 @@ import Themes from "./themes";
import Plugins from "./plugins";
import Emotes from "./emotes/emotes";
import EmoteBlacklist from "./emotes/blacklist";
import EmoteInfo from "./emotes/info";
import EmoteModifiers from "./emotes/modifiers";
import EmoteOverrides from "./emotes/overrides"
export {SettingsInfo, SettingsCookie, Config, PluginCookie, ThemeCookie, Themes, Plugins, Emotes, EmoteBlacklist};
export {State, SettingsInfo, SettingsCookie, Config, PluginCookie, ThemeCookie, Themes, Plugins, Emotes, EmoteBlacklist, EmoteInfo, EmoteModifiers, EmoteOverrides};

42
src/data/emotes/info.js Normal file
View File

@ -0,0 +1,42 @@
export default {
TwitchGlobal: {
url: "https://twitchemotes.com/api_cache/v3/global.json",
backup: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_twitch_global.json`,
variable: "TwitchGlobal",
getEmoteURL: (e) => `https://static-cdn.jtvnw.net/emoticons/v1/${e.id}/1.0`,
getOldData: (url, name) => { return {id: url.match(/\/([0-9]+)\//)[1], code: name, emoticon_set: 0, description: null}; }
},
TwitchSubscriber: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_twitch_subscriber.json`,
variable: "TwitchSubscriber",
getEmoteURL: (e) => `https://static-cdn.jtvnw.net/emoticons/v1/${e}/1.0`,
getOldData: (url) => url.match(/\/([0-9]+)\//)[1]
},
FrankerFaceZ: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_ffz.json`,
variable: "FrankerFaceZ",
getEmoteURL: (e) => `https://cdn.frankerfacez.com/emoticon/${e}/1`,
getOldData: (url) => url.match(/\/([0-9]+)\//)[1]
},
BTTV: {
url: "https://api.betterttv.net/emotes",
variable: "BTTV",
parser: (data) => {
const emotes = {};
for (let e = 0, len = data.emotes.length; e < len; e++) {
const emote = data.emotes[e];
emotes[emote.regex] = emote.url;
}
return emotes;
},
getEmoteURL: (e) => `${e}`,
getOldData: (url) => url
},
BTTV2: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_bttv.json`,
variable: "BTTV2",
oldVariable: "emotesBTTV2",
getEmoteURL: (e) => `https://cdn.betterttv.net/emote/${e}/1x`,
getOldData: (url) => url.match(/emote\/(.+)\//)[1]
}
};

View File

@ -0,0 +1 @@
export default ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];

View File

@ -0,0 +1 @@
export default ["twitch", "bttv", "ffz"];

3
src/data/state.js Normal file
View File

@ -0,0 +1,3 @@
export default {
emotesLoaded: false
};

View File

@ -1,14 +1,13 @@
import BDV2 from "./bdv2";
import Utilities from "./utilities";
import {Config, SettingsCookie} from "data";
import EmoteModule from "./emotes";
// import EmoteModule from "./emotes";
// import QuickEmoteMenu from "../builtins/emotemenu";
import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager";
import SettingsPanel from "./settingspanel";
import * as Builtins from "builtins";
import {Modals} from "ui";
import Events from "./emitter";
function Core() {
}
@ -34,12 +33,12 @@ Core.prototype.init = async function() {
Utilities.log("Startup", "Initializing Settings");
SettingsPanel.initialize();
Utilities.log("Startup", "Initializing EmoteModule");
window.emotePromise = EmoteModule.init().then(() => {
EmoteModule.initialized = true;
Utilities.log("Startup", "Initializing QuickEmoteMenu");
Events.dispatch("emotes-loaded");
// QuickEmoteMenu.init();
});
// window.emotePromise = EmoteModule.init().then(() => {
// EmoteModule.initialized = true;
// Utilities.log("Startup", "Initializing QuickEmoteMenu");
// Events.dispatch("emotes-loaded");
// // QuickEmoteMenu.init();
// });
this.injectExternals();
@ -58,7 +57,7 @@ Core.prototype.init = async function() {
$("#customcss").detach().appendTo(document.head);
// PublicServers.initialize();
EmoteModule.autoCapitalize();
// EmoteModule.autoCapitalize();
Utilities.log("Startup", "Removing Loading Icon");
document.getElementsByClassName("bd-loaderv2")[0].remove();
@ -66,10 +65,8 @@ Core.prototype.init = async function() {
this.initObserver();
// Show loading errors
if (SettingsCookie["fork-ps-1"]) {
Utilities.log("Startup", "Collecting Startup Errors");
Modals.showContentErrors({plugins: pluginErrors, themes: themeErrors});
}
Utilities.log("Startup", "Collecting Startup Errors");
Modals.showContentErrors({plugins: pluginErrors, themes: themeErrors});
};
Core.prototype.checkForGuilds = function() {

View File

@ -1,329 +0,0 @@
import {Config, SettingsCookie, Emotes, EmoteBlacklist} from "data";
import Utilities from "./utilities";
import BDV2 from "./bdv2";
import BDEmote from "../ui/emote";
import BdApi from "./pluginapi";
import DataStore from "./datastore";
import {DiscordModules} from "./webpackmodules";
import {Toasts} from "ui";
const bdEmoteSettingIDs = {
TwitchGlobal: "bda-es-7",
TwitchSubscriber: "bda-es-7",
BTTV: "bda-es-2",
FrankerFaceZ: "bda-es-1",
BTTV2: "bda-es-2"
};
function EmoteModule() {
Object.defineProperty(this, "categories", {
get: function() {
const cats = [];
for (const current in bdEmoteSettingIDs) {
if (SettingsCookie[bdEmoteSettingIDs[current]]) cats.push(current);
}
return cats;
}
});
}
EmoteModule.prototype.init = async function () {
this.modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];
this.overrides = ["twitch", "bttv", "ffz"];
const emoteInfo = {
TwitchGlobal: {
url: "https://twitchemotes.com/api_cache/v3/global.json",
backup: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_twitch_global.json`,
variable: "TwitchGlobal",
oldVariable: "emotesTwitch",
getEmoteURL: (e) => `https://static-cdn.jtvnw.net/emoticons/v1/${e.id}/1.0`,
getOldData: (url, name) => { return {id: url.match(/\/([0-9]+)\//)[1], code: name, emoticon_set: 0, description: null}; }
},
TwitchSubscriber: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_twitch_subscriber.json`,
variable: "TwitchSubscriber",
oldVariable: "subEmotesTwitch",
getEmoteURL: (e) => `https://static-cdn.jtvnw.net/emoticons/v1/${e}/1.0`,
getOldData: (url) => url.match(/\/([0-9]+)\//)[1]
},
FrankerFaceZ: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_ffz.json`,
variable: "FrankerFaceZ",
oldVariable: "emotesFfz",
getEmoteURL: (e) => `https://cdn.frankerfacez.com/emoticon/${e}/1`,
getOldData: (url) => url.match(/\/([0-9]+)\//)[1]
},
BTTV: {
url: "https://api.betterttv.net/emotes",
variable: "BTTV",
oldVariable: "emotesBTTV",
parser: (data) => {
const emotes = {};
for (let e = 0, len = data.emotes.length; e < len; e++) {
const emote = data.emotes[e];
emotes[emote.regex] = emote.url;
}
return emotes;
},
getEmoteURL: (e) => `${e}`,
getOldData: (url) => url
},
BTTV2: {
url: `https://rauenzi.github.io/BetterDiscordApp/data/emotedata_bttv.json`,
variable: "BTTV2",
oldVariable: "emotesBTTV2",
getEmoteURL: (e) => `https://cdn.betterttv.net/emote/${e}/1x`,
getOldData: (url) => url.match(/emote\/(.+)\//)[1]
}
};
await this.getBlacklist();
await this.loadEmoteData(emoteInfo);
while (!BDV2.MessageContentComponent) await new Promise(resolve => setTimeout(resolve, 100));
if (this.cancelEmoteRender) return;
this.cancelEmoteRender = Utilities.monkeyPatch(BDV2.MessageContentComponent.prototype, "render", {after: ({returnValue}) => {
Utilities.monkeyPatch(returnValue.props, "children", {silent: true, after: ({returnValue}) => {
if (this.categories.length == 0) return;
const markup = returnValue.props.children[1];
if (!markup.props.children) return;
const nodes = markup.props.children[1];
if (!nodes || !nodes.length) return;
for (let n = 0; n < nodes.length; n++) {
const node = nodes[n];
if (typeof(node) !== "string") continue;
const words = node.split(/([^\s]+)([\s]|$)/g);
for (let c = 0, clen = this.categories.length; c < clen; c++) {
for (let w = 0, wlen = words.length; w < wlen; w++) {
const emote = words[w];
const emoteSplit = emote.split(":");
const emoteName = emoteSplit[0];
let emoteModifier = emoteSplit[1] ? emoteSplit[1] : "";
let emoteOverride = emoteModifier.slice(0);
if (emoteName.length < 4 || EmoteBlacklist.includes(emoteName)) continue;
if (!this.modifiers.includes(emoteModifier) || !SettingsCookie["bda-es-8"]) emoteModifier = "";
if (!this.overrides.includes(emoteOverride)) emoteOverride = "";
else emoteModifier = emoteOverride;
let current = this.categories[c];
if (emoteOverride === "twitch") {
if (Emotes.TwitchGlobal[emoteName]) current = "TwitchGlobal";
else if (Emotes.TwitchSubscriber[emoteName]) current = "TwitchSubscriber";
}
else if (emoteOverride === "bttv") {
if (Emotes.BTTV[emoteName]) current = "BTTV";
else if (Emotes.BTTV2[emoteName]) current = "BTTV2";
}
else if (emoteOverride === "ffz") {
if (Emotes.FrankerFaceZ[emoteName]) current = "FrankerFaceZ";
}
if (!Emotes[current][emoteName] || !SettingsCookie[bdEmoteSettingIDs[current]]) continue;
const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
if (!results) continue;
const pre = nodes[n].substring(0, results.index + results[1].length);
const post = nodes[n].substring(results.index + results[0].length - results[2].length);
nodes[n] = pre;
const emoteComponent = DiscordModules.React.createElement(BDEmote, {name: emoteName, url: Emotes[current][emoteName], modifier: emoteModifier});
nodes.splice(n + 1, 0, post);
nodes.splice(n + 1, 0, emoteComponent);
}
}
}
const onlyEmotes = nodes.every(r => {
if (typeof(r) == "string" && r.replace(/\s*/, "") == "") return true;
else if (r.type && r.type.name == "BDEmote") return true;
else if (r.props && r.props.children && r.props.children.props && r.props.children.props.emojiName) return true;
return false;
});
if (!onlyEmotes) return;
for (const node of nodes) {
if (typeof(node) != "object") continue;
if (node.type.name == "BDEmote") node.props.jumboable = true;
else if (node.props && node.props.children && node.props.children.props && node.props.children.props.emojiName) node.props.children.props.jumboable = true;
}
}});
}});
};
EmoteModule.prototype.disable = function() {
this.disableAutoCapitalize();
if (this.cancelEmoteRender) return;
this.cancelEmoteRender();
this.cancelEmoteRender = null;
};
EmoteModule.prototype.clearEmoteData = async function() {
const _fs = require("fs");
const emoteFile = "emote_data.json";
const file = Config.dataPath + emoteFile;
const exists = _fs.existsSync(file);
if (exists) _fs.unlinkSync(file);
DataStore.setBDData("emoteCacheDate", (new Date()).toJSON());
Object.assign(Emotes, {
TwitchGlobal: {},
TwitchSubscriber: {},
BTTV: {},
FrankerFaceZ: {},
BTTV2: {}
});
};
EmoteModule.prototype.isCacheValid = function() {
const cacheLength = DataStore.getBDData("emoteCacheDays") || DataStore.setBDData("emoteCacheDays", 7) || 7;
const cacheDate = new Date(DataStore.getBDData("emoteCacheDate") || null);
const currentDate = new Date();
const daysBetween = Math.round(Math.abs((currentDate.getTime() - cacheDate.getTime()) / (24 * 60 * 60 * 1000)));
if (daysBetween > cacheLength) {
DataStore.setBDData("emoteCacheDate", currentDate.toJSON());
return false;
}
return true;
};
EmoteModule.prototype.loadEmoteData = async function(emoteInfo) {
const _fs = require("fs");
const emoteFile = "emote_data.json";
const file = Config.dataPath + emoteFile;
const exists = _fs.existsSync(file);
if (exists && this.isCacheValid()) {
if (SettingsCookie["fork-ps-2"]) Toasts.show("Loading emotes from cache.", {type: "info"});
Utilities.log("Emotes", "Loading emotes from local cache.");
const data = await new Promise(resolve => {
_fs.readFile(file, "utf8", (err, data) => {
Utilities.log("Emotes", "Emotes loaded from cache.");
if (err) data = {};
resolve(data);
});
});
let isValid = Utilities.testJSON(data);
if (isValid) Object.assign(Emotes, JSON.parse(data));
for (const e in emoteInfo) {
isValid = Object.keys(Emotes[emoteInfo[e].variable]).length > 0;
}
if (isValid) {
if (SettingsCookie["fork-ps-2"]) Toasts.show("Emotes successfully loaded.", {type: "success"});
return;
}
Utilities.log("Emotes", "Cache was corrupt, downloading...");
_fs.unlinkSync(file);
}
if (!SettingsCookie["fork-es-3"]) return;
if (SettingsCookie["fork-ps-2"]) Toasts.show("Downloading emotes in the background do not reload.", {type: "info"});
for (const e in emoteInfo) {
await new Promise(r => setTimeout(r, 1000));
const data = await this.downloadEmotes(emoteInfo[e]);
Emotes[emoteInfo[e].variable] = data;
}
if (SettingsCookie["fork-ps-2"]) Toasts.show("All emotes successfully downloaded.", {type: "success"});
try { _fs.writeFileSync(file, JSON.stringify(Emotes), "utf8"); }
catch (err) { Utilities.err("Emotes", "Could not save emote data.", err); }
};
EmoteModule.prototype.downloadEmotes = function(emoteMeta) {
const request = require("request");
const options = {
url: emoteMeta.url,
timeout: emoteMeta.timeout ? emoteMeta.timeout : 5000
};
Utilities.log("Emotes", `Downloading: ${emoteMeta.variable} (${emoteMeta.url})`);
return new Promise((resolve, reject) => {
request(options, (error, response, body) => {
if (error) {
Utilities.err("Emotes", "Could not download " + emoteMeta.variable, error);
if (emoteMeta.backup) {
emoteMeta.url = emoteMeta.backup;
emoteMeta.backup = null;
if (emoteMeta.backupParser) emoteMeta.parser = emoteMeta.backupParser;
return resolve(this.downloadEmotes(emoteMeta));
}
return reject({});
}
let parsedData = {};
try {
parsedData = JSON.parse(body);
}
catch (err) {
Utilities.err("Emotes", "Could not download " + emoteMeta.variable, err);
if (emoteMeta.backup) {
emoteMeta.url = emoteMeta.backup;
emoteMeta.backup = null;
if (emoteMeta.backupParser) emoteMeta.parser = emoteMeta.backupParser;
return resolve(this.downloadEmotes(emoteMeta));
}
return reject({});
}
if (typeof(emoteMeta.parser) === "function") parsedData = emoteMeta.parser(parsedData);
for (const emote in parsedData) {
if (emote.length < 4 || EmoteBlacklist.includes(emote)) {
delete parsedData[emote];
continue;
}
parsedData[emote] = emoteMeta.getEmoteURL(parsedData[emote]);
}
resolve(parsedData);
Utilities.log("Emotes", "Downloaded: " + emoteMeta.variable);
});
});
};
EmoteModule.prototype.getBlacklist = function () {
return new Promise(resolve => {
$.getJSON(`https://rauenzi.github.io/BetterDiscordApp/data/emotefilter.json`, function (data) {
resolve(EmoteBlacklist.push(...data.blacklist));
});
});
};
EmoteModule.prototype.autoCapitalize = function () {
if (!SettingsCookie["bda-es-4"] || this.autoCapitalizeActive) return;
$("body").on("keyup.bdac change.bdac paste.bdac", $(".channelTextArea-1LDbYG textarea:first"), () => {
const text = $(".channelTextArea-1LDbYG textarea:first").val();
if (text == undefined) return;
const lastWord = text.split(" ").pop();
if (lastWord.length > 3) {
if (lastWord == "danSgame") return;
const ret = this.capitalize(lastWord.toLowerCase());
if (ret !== null && ret !== undefined) {
Utilities.insertText(Utilities.getTextArea()[0], text.replace(lastWord, ret));
}
}
});
this.autoCapitalizeActive = true;
};
EmoteModule.prototype.capitalize = function (value) {
const res = Emotes.TwitchGlobal;
for (const p in res) {
if (res.hasOwnProperty(p) && value == (p + "").toLowerCase()) {
return p;
}
}
};
EmoteModule.prototype.disableAutoCapitalize = function() {
this.autoCapitalizeActive = false;
$("body").off(".bdac");
};
export default new EmoteModule();

View File

@ -11,7 +11,7 @@ import ContentManager from "./contentmanager";
import DataStore from "./datastore";
// import DevMode from "./devmode";
import Events from "./emitter";
import EmoteModule from "./emotes";
// import EmoteModule from "./emotes";
import PluginManager from "./pluginmanager";
// import PublicServers from "./publicservers";
import ThemeManager from "./thememanager";
@ -20,7 +20,7 @@ export const React = DiscordModules.React;
export const ReactDOM = DiscordModules.ReactDOM;
export {BDV2, BdApi, Core, ContentManager, DataStore,
Events, EmoteModule, PluginManager, /*PublicServers,*/ ThemeManager,
Events, PluginManager, /*PublicServers,*/ ThemeManager,
Utilities, WebpackModules, DiscordModules};

View File

@ -100,7 +100,16 @@ BdApi.showConfirmationModal = function (title, content, options = {}) {
return Modals.showConfirmationModal(title, content, options);
};
//Show toast alert
/**
* This shows a toast similar to android towards the bottom of the screen.
*
* @param {string} content The string to show in the toast.
* @param {object} options Options object. Optional parameter.
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: ""
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: true
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: 3000
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the bd setting
*/
BdApi.showToast = function(content, options = {}) {
Toasts.show(content, options);
};

View File

@ -33,7 +33,7 @@ PluginModule.prototype.loadPlugins = function () {
if (PluginCookie[name]) {
try {
plugin.start();
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${plugin.getName()} v${plugin.getVersion()} has started.`);
Toasts.show(`${plugin.getName()} v${plugin.getVersion()} has started.`);
}
catch (err) {
PluginCookie[name] = false;
@ -52,10 +52,10 @@ PluginModule.prototype.loadPlugins = function () {
PluginModule.prototype.startPlugin = function(plugin, reload = false) {
try {
Plugins[plugin].plugin.start();
if (SettingsCookie["fork-ps-2"] && !reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} has started.`);
if (!reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} has started.`);
}
catch (err) {
if (SettingsCookie["fork-ps-2"] && !reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} could not be started.`, {type: "error"});
if (!reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} could not be started.`, {type: "error"});
PluginCookie[plugin] = false;
this.savePluginData();
Utilities.err("Plugins", plugin + " could not be started.", err);
@ -65,10 +65,10 @@ PluginModule.prototype.startPlugin = function(plugin, reload = false) {
PluginModule.prototype.stopPlugin = function(plugin, reload = false) {
try {
Plugins[plugin].plugin.stop();
if (SettingsCookie["fork-ps-2"] && !reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} has stopped.`);
if (!reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} has stopped.`);
}
catch (err) {
if (SettingsCookie["fork-ps-2"] && !reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} could not be stopped.`, {type: "error"});
if (!reload) Toasts.show(`${Plugins[plugin].plugin.getName()} v${Plugins[plugin].plugin.getVersion()} could not be stopped.`, {type: "error"});
Utilities.err("Plugins", Plugins[plugin].plugin.getName() + " could not be stopped.", err);
}
};
@ -95,15 +95,15 @@ PluginModule.prototype.togglePlugin = function (plugin) {
PluginModule.prototype.loadPlugin = function(filename) {
const error = ContentManager.loadContent(filename, "plugin");
if (error) {
if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({plugins: [error]});
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${filename} could not be loaded.`, {type: "error"});
Modals.showContentErrors({plugins: [error]});
Toasts.show(`${filename} could not be loaded.`, {type: "error"});
return Utilities.err("ContentManager", `${filename} could not be loaded.`, error);
}
const plugin = Object.values(Plugins).find(p => p.filename == filename).plugin;
try { if (plugin.load && typeof(plugin.load) == "function") plugin.load();}
catch (err) {if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({plugins: [err]});}
catch (err) {Modals.showContentErrors({plugins: [err]});}
Utilities.log("ContentManager", `${plugin.getName()} v${plugin.getVersion()} was loaded.`);
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${plugin.getName()} v${plugin.getVersion()} was loaded.`, {type: "success"});
Toasts.show(`${plugin.getName()} v${plugin.getVersion()} was loaded.`, {type: "success"});
Emitter.dispatch("plugin-loaded", plugin.getName());
};
@ -115,12 +115,12 @@ PluginModule.prototype.unloadPlugin = function(filenameOrName) {
const error = ContentManager.unloadContent(Plugins[plugin].filename, "plugin");
delete Plugins[plugin];
if (error) {
if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({plugins: [error]});
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${plugin} could not be unloaded. It may have not been loaded yet.`, {type: "error"});
Modals.showContentErrors({plugins: [error]});
Toasts.show(`${plugin} could not be unloaded. It may have not been loaded yet.`, {type: "error"});
return Utilities.err("ContentManager", `${plugin} could not be unloaded. It may have not been loaded yet.`, error);
}
Utilities.log("ContentManager", `${plugin} was unloaded.`);
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${plugin} was unloaded.`, {type: "success"});
Toasts.show(`${plugin} was unloaded.`, {type: "success"});
Emitter.dispatch("plugin-unloaded", plugin);
};
@ -132,14 +132,14 @@ PluginModule.prototype.reloadPlugin = function(filenameOrName) {
if (enabled) this.stopPlugin(plugin, true);
const error = ContentManager.reloadContent(Plugins[plugin].filename, "plugin");
if (error) {
if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({plugins: [error]});
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${plugin} could not be reloaded.`, {type: "error"});
Modals.showContentErrors({plugins: [error]});
Toasts.show(`${plugin} could not be reloaded.`, {type: "error"});
return Utilities.err("ContentManager", `${plugin} could not be reloaded.`, error);
}
if (Plugins[plugin].plugin.load && typeof(Plugins[plugin].plugin.load) == "function") Plugins[plugin].plugin.load();
if (enabled) this.startPlugin(plugin, true);
Utilities.log("ContentManager", `${plugin} v${Plugins[plugin].plugin.getVersion()} was reloaded.`);
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${plugin} v${Plugins[plugin].plugin.getVersion()} was reloaded.`, {type: "success"});
Toasts.show(`${plugin} v${Plugins[plugin].plugin.getVersion()} was reloaded.`, {type: "success"});
Emitter.dispatch("plugin-reloaded", plugin);
};

View File

@ -2,7 +2,7 @@ import {SettingsCookie} from "data";
import DataStore from "./datastore";
import ContentManager from "./contentmanager";
import BdApi from "./pluginapi";
import EmoteModule from "./emotes";
// import EmoteModule from "./emotes";
import Events from "./emitter";
import WebpackModules from "./webpackmodules";
@ -83,10 +83,10 @@ export default new class SettingsPanel {
SettingsCookie[id] = enabled;
if (id == "bda-es-4") {
if (enabled) EmoteModule.autoCapitalize();
else EmoteModule.disableAutoCapitalize();
}
// if (id == "bda-es-4") {
// if (enabled) EmoteModule.autoCapitalize();
// else EmoteModule.disableAutoCapitalize();
// }
if (id == "fork-ps-5") {
if (enabled) {
@ -109,7 +109,7 @@ export default new class SettingsPanel {
}
initializeSettings() {
if (SettingsCookie["bda-es-4"]) EmoteModule.autoCapitalize();
// if (SettingsCookie["bda-es-4"]) EmoteModule.autoCapitalize();
if (SettingsCookie["fork-ps-5"]) {
ContentManager.watchContent("plugin");

View File

@ -31,14 +31,14 @@ ThemeModule.prototype.enableTheme = function(theme, reload = false) {
ThemeCookie[theme] = true;
this.saveThemeData();
$("head").append($("<style>", {id: Utilities.escapeID(theme), text: unescape(Themes[theme].css)}));
if (SettingsCookie["fork-ps-2"] && !reload) Toasts.show(`${Themes[theme].name} v${Themes[theme].version} has been applied.`);
if (!reload) Toasts.show(`${Themes[theme].name} v${Themes[theme].version} has been applied.`);
};
ThemeModule.prototype.disableTheme = function(theme, reload = false) {
ThemeCookie[theme] = false;
this.saveThemeData();
$(`#${Utilities.escapeID(Themes[theme].name)}`).remove();
if (SettingsCookie["fork-ps-2"] && !reload) Toasts.show(`${Themes[theme].name} v${Themes[theme].version} has been disabled.`);
if (!reload) Toasts.show(`${Themes[theme].name} v${Themes[theme].version} has been disabled.`);
};
ThemeModule.prototype.toggleTheme = function(theme) {
@ -49,13 +49,13 @@ ThemeModule.prototype.toggleTheme = function(theme) {
ThemeModule.prototype.loadTheme = function(filename) {
const error = ContentManager.loadContent(filename, "theme");
if (error) {
if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({themes: [error]});
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${filename} could not be loaded. It may not have been loaded.`, {type: "error"});
Modals.showContentErrors({themes: [error]});
Toasts.show(`${filename} could not be loaded. It may not have been loaded.`, {type: "error"});
return Utilities.err("ContentManager", `${filename} could not be loaded.`, error);
}
const theme = Object.values(Themes).find(p => p.filename == filename);
Utilities.log("ContentManager", `${theme.name} v${theme.version} was loaded.`);
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${theme.name} v${theme.version} was loaded.`, {type: "success"});
Toasts.show(`${theme.name} v${theme.version} was loaded.`, {type: "success"});
Emitter.dispatch("theme-loaded", theme.name);
};
@ -67,12 +67,12 @@ ThemeModule.prototype.unloadTheme = function(filenameOrName) {
const error = ContentManager.unloadContent(Themes[theme].filename, "theme");
delete Themes[theme];
if (error) {
if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({themes: [error]});
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${theme} could not be unloaded. It may have not been loaded yet.`, {type: "error"});
Modals.showContentErrors({themes: [error]});
Toasts.show(`${theme} could not be unloaded. It may have not been loaded yet.`, {type: "error"});
return Utilities.err("ContentManager", `${theme} could not be unloaded. It may have not been loaded yet.`, error);
}
Utilities.log("ContentManager", `${theme} was unloaded.`);
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${theme} was unloaded.`, {type: "success"});
Toasts.show(`${theme} was unloaded.`, {type: "success"});
Emitter.dispatch("theme-unloaded", theme);
};
@ -83,12 +83,12 @@ ThemeModule.prototype.reloadTheme = function(filenameOrName) {
const error = ContentManager.reloadContent(Themes[theme].filename, "theme");
if (ThemeCookie[theme]) this.disableTheme(theme, true), this.enableTheme(theme, true);
if (error) {
if (SettingsCookie["fork-ps-1"]) Modals.showContentErrors({themes: [error]});
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${theme} could not be reloaded.`, {type: "error"});
Modals.showContentErrors({themes: [error]});
Toasts.show(`${theme} could not be reloaded.`, {type: "error"});
return Utilities.err("ContentManager", `${theme} could not be reloaded.`, error);
}
Utilities.log("ContentManager", `${theme} v${Themes[theme].version} was reloaded.`);
if (SettingsCookie["fork-ps-2"]) Toasts.show(`${theme} v${Themes[theme].version} was reloaded.`, {type: "success"});
Toasts.show(`${theme} v${Themes[theme].version} was reloaded.`, {type: "success"});
Emitter.dispatch("theme-reloaded", theme);
};

View File

@ -1,7 +1,10 @@
import {SettingsCookie} from "data";
import {Utilities, WebpackModules, React} from "modules";
export default class Modals {
static get shouldShowContentErrors() {return SettingsCookie["fork-ps-1"];}
static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get AlertModal() {return WebpackModules.getByPrototypes("handleCancel", "handleSubmit", "handleMinorConfirm");}
static get TextElement() {return WebpackModules.getByProps("Sizes", "Weights");}
@ -86,7 +89,7 @@ export default class Modals {
}
static showContentErrors({plugins: pluginErrors = [], themes: themeErrors = []}) {
if (!pluginErrors || !themeErrors) return;
if (!pluginErrors || !themeErrors || !this.shouldShowContentErrors) return;
if (!pluginErrors.length && !themeErrors.length) return;
const modal = $(`<div class="bd-modal-wrapper theme-dark">
<div class="bd-backdrop backdrop-1wrmKB"></div>

View File

@ -1,5 +1,5 @@
import {SettingsInfo, SettingsCookie, Plugins, Themes} from "data";
import {React, ReactDOM, Utilities, ContentManager, Events, EmoteModule, PluginManager, ThemeManager} from "modules";
import {React, ReactDOM, Utilities, ContentManager, Events, PluginManager, ThemeManager} from "modules";
import Sidebar from "./sidebar";
import Scroller from "../scroller";
import List from "../list";
@ -117,7 +117,7 @@ export default class V2_SettingsPanel {
contentColumn: true, fade: true, dark: true, children: [
React.createElement(SettingsPanel, {key: "espanel", title: "Emote Settings", onChange: this.onChange, settings: this.emoteSettings, button: {
title: "Clear Emote Cache",
onClick: () => { EmoteModule.clearEmoteData(); EmoteModule.init(); }
onClick: () => { Events.dispatch("emotes-clear"); /*EmoteModule.clearEmoteData(); EmoteModule.init();*/ }
}}),
React.createElement(Tools, {key: "tools"})
]});

View File

@ -1,3 +1,4 @@
import {SettingsCookie} from "data";
import {WebpackModules} from "modules";
const channelsClass = WebpackModules.getByProps("channels").channels.split(" ")[0];
@ -5,6 +6,8 @@ const membersWrapClass = WebpackModules.getByProps("membersWrap").membersWrap.sp
export default class Toasts {
static get shouldShowToasts() {return SettingsCookie["fork-ps-2"];}
/** Shorthand for `type = "success"` for {@link module:Toasts.show} */
static async success(content, options = {}) {return this.show(content, Object.assign(options, {type: "success"}));}
@ -25,13 +28,15 @@ export default class Toasts {
*
* @param {string} content The string to show in the toast.
* @param {object} options Options object. Optional parameter.
* @param {string} options.type Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: ""
* @param {boolean} options.icon Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: true
* @param {number} options.timeout Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: 3000
* @param {string} [options.type=""] Changes the type of the toast stylistically and semantically. Choices: "", "info", "success", "danger"/"error", "warning"/"warn". Default: ""
* @param {boolean} [options.icon=true] Determines whether the icon should show corresponding to the type. A toast without type will always have no icon. Default: true
* @param {number} [options.timeout=3000] Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: 3000
* @param {boolean} [options.forceShow=false] Whether to force showing the toast and ignore the bd setting
*/
static show(content, options = {}) {
const {type = "", icon = true, timeout = 3000, forceShow = false} = options;
if (!this.shouldShowToasts && !forceShow) return;
this.ensureContainer();
const {type = "", icon = true, timeout = 3000} = options;
const toastElem = document.createElement("div");
toastElem.classList.add("bd-toast");
if (type) toastElem.classList.add("toast-" + type);