diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 64e56fa3..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-language: node_js
-
-node_js:
-- "node"
-
-git:
- autocrlf: true
-
-branches:
- only:
- - development
-
-install:
- - npm ci
-
-script:
- - npm run lint-prod
- - npm run test-prod
- - npm run deploy
-
-before_deploy: "echo 'node_modules' > .gitignore"
-deploy:
- provider: pages
- skip_cleanup: true
- github_token: $TRAVIS_ACCESS
- keep_history: true
- local_dir: .
- name: BetterDiscord Deployment
- target_branch: gh-pages-development
- on:
- branch: development
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9346be4..c4ce4b5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,46 @@
This changelog starts with the restructured 1.0.0 release that happened after context isolation changes. The changelogs here should more-or-less mirror the ones that get shown in the client but probably with less formatting and pizzazz.
+## 1.8.0
+
+### Added
+- Proper updater system with UI.
+- Tooltip component for plugins.
+- Highly expanded plugin API.
+
+### Removed
+
+### Changed
+- Reverted how internal webpack module searches are performed.
+- New options for webpack searches.
+
+### Fixed
+- Fixed many issues regarding memory leaks and out-of-memory errors!
+- Fixed a major issue where webpack searches would iterate by default.
+- Fixed an issue with `byStrings` and `combine` filters in the API.
+- Fixed an issue where searching for multiple modules could yield the same module multiple times.
+- Fixed an issue where misnamed addon files could prevent startup.
+- Fixed an issue where the `request` module would not follow redirects.
+- Fixed an issue where certain modals could crash the client.
+- Fixed an issue where toasts would not show on the crash screen.
+
+## 1.7.0
+
+### Added
+- Polyfill for certain node modules.
+
+### Removed
+- Proxy protection for certain modules.
+
+### Changed
+- Changed how internal webpack module searches are performed.
+- New location for public servers button.
+- Switch to pnpm with workspaces.
+- Improved startup errors.
+
+### Fixed
+- Fixed several issues for Discord's internal changes.
+
## 1.6.3
### Added
diff --git a/package.json b/package.json
index cd8e8dad..46f41493 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "betterdiscord",
- "version": "1.7.0",
+ "version": "1.8.0",
"description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js",
"scripts": {
@@ -14,7 +14,7 @@
"lint": "eslint --ext .js common/ && pnpm --filter injector lint && pnpm --filter preload lint && pnpm --filter renderer lint-js",
"test": "mocha --require @babel/register --recursive \"./tests/renderer/*.js\"",
"dist": "pnpm run build-prod && node scripts/pack.js",
- "api": "jsdoc -X renderer/src/modules/pluginapi.js > jsdoc-ast.json"
+ "api": "jsdoc -X -r renderer/src/modules/api/ > jsdoc-ast.json"
},
"devDependencies": {
"asar": "^3.2.0",
diff --git a/preload/src/api/filesystem.js b/preload/src/api/filesystem.js
index a5829ed6..69f0b006 100644
--- a/preload/src/api/filesystem.js
+++ b/preload/src/api/filesystem.js
@@ -44,6 +44,14 @@ export function renameSync(oldPath, newPath) {
return fs.renameSync(oldPath, newPath);
}
+export function rm(pathToFile) {
+ return fs.rmSync(pathToFile);
+}
+
+export function rmSync(pathToFile) {
+ return fs.rmSync(pathToFile);
+}
+
export function unlinkSync(fileToDelete) {
return fs.unlinkSync(fileToDelete);
}
diff --git a/renderer/src/builtins/customcss.js b/renderer/src/builtins/customcss.js
index f2e4a8da..6f830f1c 100644
--- a/renderer/src/builtins/customcss.js
+++ b/renderer/src/builtins/customcss.js
@@ -39,7 +39,7 @@ export default new class CustomCSS extends Builtin {
if (this.isDetached) return;
if (this.nativeOpen) return this.openNative();
else if (this.startDetached) return this.openDetached(this.savedCss);
- const settingsView = Utilities.findInRenderTree(thisObject._reactInternals, m => m && m.onSetSection, {walkable: ["child", "memoizedProps", "props", "children"]});
+ const settingsView = Utilities.findInTree(thisObject._reactInternals, m => m && m.onSetSection, {walkable: ["child", "memoizedProps", "props", "children"]});
if (settingsView && settingsView.onSetSection) settingsView.onSetSection(this.id);
}
});
diff --git a/renderer/src/builtins/developer/debuglogs.js b/renderer/src/builtins/developer/debuglogs.js
index 03404328..d10a58bb 100644
--- a/renderer/src/builtins/developer/debuglogs.js
+++ b/renderer/src/builtins/developer/debuglogs.js
@@ -2,7 +2,6 @@ const fs = require("fs");
const path = require("path");
import Builtin from "../../structs/builtin";
import DataStore from "../../modules/datastore";
-import Utilities from "../../modules/utilities";
const timestamp = () => new Date().toISOString().replace("T", " ").replace("Z", "");
@@ -18,6 +17,11 @@ const getCircularReplacer = () => {
};
};
+const occurrences = (source, substring) => {
+ const regex = new RegExp(substring, "g");
+ return (source.match(regex) || []).length;
+};
+
export default new class DebugLogs extends Builtin {
get name() {return "DebugLogs";}
get category() {return "developer";}
@@ -45,7 +49,7 @@ export default new class DebugLogs extends Builtin {
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof(arg) === "string") {
- const styleCount = Utilities.occurrences(arg, "%c");
+ const styleCount = occurrences(arg, "%c");
sanitized.push(arg.replace(/%c/g, ""));
if (styleCount > 0) i += styleCount;
}
diff --git a/renderer/src/builtins/emotes/emotemenu.js b/renderer/src/builtins/emotes/emotemenu.js
index 0ff37072..f1c37bfd 100644
--- a/renderer/src/builtins/emotes/emotemenu.js
+++ b/renderer/src/builtins/emotes/emotemenu.js
@@ -20,7 +20,7 @@ export default new class EmoteMenu extends Builtin {
enabled() {
this.after(EmojiPicker, "type", (_, __, returnValue) => {
- const originalChildren = Utilities.getNestedProp(returnValue, "props.children.props.children");
+ const originalChildren = returnValue?.props?.children?.props?.children;
if (!originalChildren || originalChildren.__patched) return;
const activePicker = useExpressionPickerStore((state) => state.activeView);
@@ -30,8 +30,8 @@ export default new class EmoteMenu extends Builtin {
// Attach a try {} catch {} because this might crash the user.
try {
- const head = Utilities.findInReactTree(childrenReturn, (e) => e?.role === "tablist")?.children;
- const body = Utilities.findInReactTree(childrenReturn, (e) => e?.[0]?.type === "nav");
+ const head = Utilities.findInTree(childrenReturn, (e) => e?.role === "tablist", {walkable: ["props", "children", "return", "stateNode"]})?.children;
+ const body = Utilities.findInTree(childrenReturn, (e) => e?.[0]?.type === "nav", {walkable: ["props", "children", "return", "stateNode"]});
if (!head || !body) return childrenReturn;
const isActive = activePicker == "bd-emotes";
diff --git a/renderer/src/builtins/emotes/emotes.js b/renderer/src/builtins/emotes/emotes.js
index a8e0bad7..1afef9f1 100644
--- a/renderer/src/builtins/emotes/emotes.js
+++ b/renderer/src/builtins/emotes/emotes.js
@@ -1,7 +1,7 @@
import Builtin from "../../structs/builtin";
import {EmoteConfig, Config} from "data";
-import {Utilities, WebpackModules, DataStore, DiscordModules, Events, Settings, Strings} from "modules";
+import {WebpackModules, DataStore, DiscordModules, Events, Settings, Strings} from "modules";
import BDEmote from "../../ui/emote";
import Modals from "../../ui/modals";
import Toasts from "../../ui/toasts";
@@ -24,6 +24,10 @@ const Emotes = {
FrankerFaceZ: {}
};
+const escape = (s) => {
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
+};
+
const blocklist = [];
const overrides = ["twitch", "subscriber", "bttv", "ffz"];
const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin", "3spin", "tr", "bl", "br", "shake", "shake2", "shake3", "flap"];
@@ -53,7 +57,7 @@ const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin",
getUrl(category, name) {return EmoteURLs[category].format({id: Emotes[category][name]});}
getCategory(category) {return Emotes[category];}
- getRemoteFile(category) {return Utilities.repoUrl(`assets/emotes/${category.toLowerCase()}.json`);}
+ getRemoteFile(category) {return `https://cdn.staticaly.com/gh/BetterDiscord/BetterDiscord/${Config.hash}/assets/emotes/${category.toLowerCase()}.json`;}
initialize() {
super.initialize();
@@ -154,7 +158,7 @@ const modifiers = ["flip", "spin", "pulse", "spin2", "spin3", "1spin", "2spin",
}
if (!Emotes[current][emoteName]) continue;
- const results = nodes[n].match(new RegExp(`([\\s]|^)${Utilities.escape(emoteModifier ? emoteName + ":" + emoteModifier : emoteName)}([\\s]|$)`));
+ const results = nodes[n].match(new RegExp(`([\\s]|^)${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);
diff --git a/renderer/src/builtins/general/publicservers.js b/renderer/src/builtins/general/publicservers.js
index 395acb4b..7dc3dfe5 100644
--- a/renderer/src/builtins/general/publicservers.js
+++ b/renderer/src/builtins/general/publicservers.js
@@ -1,5 +1,5 @@
import Builtin from "../../structs/builtin";
-import {DiscordModules, WebpackModules, Strings, DOM, React} from "modules";
+import {DiscordModules, WebpackModules, Strings, DOMManager, React, ReactDOM} from "modules";
import PublicServersMenu from "../../ui/publicservers/menu";
import Globe from "../../ui/icons/globe";
@@ -44,49 +44,87 @@ export default new class PublicServers extends Builtin {
get id() {return "publicServers";}
enabled() {
- // let target = null;
- // WebpackModules.getModule((_, m) => {
- // if (m.exports?.toString().includes("privateChannelIds")) {
- // target = m.exports;
- // }
- // });
- // if (!target || !target.Z) return;
- // const PrivateChannelListComponents = WebpackModules.getByProps("LinkButton");
- // this.after(target, "Z", (_, __, returnValue) => {
- // const destination = returnValue?.props?.children?.props?.children;
- // if (!destination || !Array.isArray(destination)) return;
- // if (destination.find(b => b?.props?.children?.props?.id === "public-server-button")) return;
+ const PrivateChannelList = WebpackModules.getModule(m => m?.toString().includes("privateChannelIds"), {defaultExport: false});
+ if (!PrivateChannelList || !PrivateChannelList.Z) return this.warn("Could not find PrivateChannelList", PrivateChannelList);
+ const PrivateChannelButton = WebpackModules.getModule(m => m?.prototype?.render?.toString().includes("linkButton"), {searchExports: true});
+ if (!PrivateChannelButton) return this.warn("Could not find PrivateChannelButton", PrivateChannelButton);
+
+ this.after(PrivateChannelList, "Z", (_, __, returnValue) => {
+ const destination = returnValue?.props?.children?.props?.children;
+ if (!destination || !Array.isArray(destination)) return;
+ if (destination.find(b => b?.props?.children?.props?.id === "public-servers-button")) return; // If it exists, don't try to add again
- // destination.push(
- // React.createElement(ErrorBoundary, null,
- // React.createElement(PrivateChannelListComponents.LinkButton,
- // {
- // id: "public-server-button",
- // onClick: () => this.openPublicServers(),
- // text: "Public Servers",
- // icon: () => React.createElement(Globe, {color: "currentColor"})
- // }
- // )
- // )
- // );
- // });
+ destination.push(
+ React.createElement(ErrorBoundary, null,
+ React.createElement(PrivateChannelButton,
+ {
+ id: "public-servers-button",
+ onClick: () => this.openPublicServers(),
+ text: "Public Servers",
+ icon: () => React.createElement(Globe, {color: "currentColor"})
+ }
+ )
+ )
+ );
+ });
+
+ /**
+ * On being first enabled, we have no way of forceUpdating the list,
+ * so clone and modify an existing button and add it to the end
+ * of the button list.
+ */
+ const header = document.querySelector(`[class*="privateChannelsHeaderContainer-"]`);
+ if (!header) return; // No known element
+ const oldButton = header.previousElementSibling;
+ if (!oldButton.className.includes("channel-")) return; // Not what we expected to be there
+
+ // Clone existing button and set click handler
+ const newButton = oldButton.cloneNode(true);
+ newButton.addEventListener("click", (event) => {
+ event.stopImmediatePropagation();
+ event.stopPropagation();
+ event.preventDefault();
+ this.openPublicServers();
+ });
+
+ // Remove existing route and id
+ const aSlot = newButton.querySelector("a");
+ aSlot.href = "";
+ aSlot.dataset.listItemId = "public-servers";
+
+ // Remove any badges
+ const badge = newButton.querySelector(`[class*="premiumTrial"]`);
+ badge?.remove?.();
+
+ // Render our icon in the avatar slot
+ const avatarSlot = newButton.querySelector(`[class*="avatar-"]`);
+ avatarSlot.replaceChildren();
+ ReactDOM.render(React.createElement(Globe, {color: "currentColor"}), avatarSlot);
+ DOMManager.onRemoved(avatarSlot, () => ReactDOM.unmountComponentAtNode(avatarSlot));
+
+ // Replace the existing name
+ const nameSlot = newButton.querySelector(`[class*="name-"]`);
+ nameSlot.textContent = "Public Servers";
+
+ // Insert before the header, end of the list
+ header.parentNode.insertBefore(newButton, header);
}
disabled() {
- // this.unpatchAll();
+ this.unpatchAll();
// DOM.query("#bd-pub-li").remove();
}
async _appendButton() {
await new Promise(r => setTimeout(r, 1000));
- const existing = DOM.query("#bd-pub-li");
+ const existing = document.querySelector("#bd-pub-li");
if (existing) return;
- const guilds = DOM.query(`.${DiscordModules.GuildClasses.guilds} .${DiscordModules.GuildClasses.listItem}`);
+ const guilds = document.querySelector(`.${DiscordModules.GuildClasses.guilds} .${DiscordModules.GuildClasses.listItem}`);
if (!guilds) return;
- DOM.after(guilds, this.button);
+ guilds.parentNode.insertBefore(this.button, guilds.nextSibling);
}
openPublicServers() {
@@ -94,8 +132,8 @@ export default new class PublicServers extends Builtin {
}
get button() {
- const btn = DOM.createElement(`
`);
- const label = DOM.createElement(`
${Strings.PublicServers.button}
`);
+ const btn = DOMManager.parseHTML(`
`);
+ const label = DOMManager.parseHTML(`
${Strings.PublicServers.button}
`);
label.addEventListener("click", () => {this.openPublicServers();});
btn.append(label);
return btn;
diff --git a/renderer/src/data/changelog.js b/renderer/src/data/changelog.js
index 8569a382..cec42bff 100644
--- a/renderer/src/data/changelog.js
+++ b/renderer/src/data/changelog.js
@@ -1,23 +1,31 @@
// fixed, improved, added, progress
export default {
- description: "BetterDiscord is alive! At least... _sorta_.",
+ description: "Big improvements have been made!",
changes: [
{
- title: "Known Issues",
+ title: "What's New?",
type: "improved",
items: [
- "**Many many plugins are either completely broken or missing functionality.** Please refer to the respective developers for ETAs.",
- "The Twitch Emote system is completely broken, and there is no ETA on being fixed.",
- "The Public Servers module is also broken with no ETA for a fix.",
+ "BetterDiscord now has a built-in update system to help update broken plugins and themes.",
+ "New API options for plugin developers.",
+ "`Patcher` now works with the configurable getters.",
+ "A new tooltip component for use in plugins.",
+ "The plugin API now includes context menu capabilties.",
+ "Public servers button has found a new home on your Discord homepage above the DM list."
]
},
{
- title: "Important News!",
+ title: "Bug Fixes",
type: "fixed",
items: [
- "Due to recent and upcoming changes, BetterDiscord is going to go through a rewrite.",
- "There is no ETA or timeline for this rewrite.",
- "We will continue to try and __maintain__ this version of BetterDiscord without adding new features."
+ "Fixed many issues regarding memory leaks and out-of-memory errors!",
+ "Fixed a major issue where webpack searches would iterate by default.",
+ "Fixed an issue with `byStrings` and `combine` filters in the API.",
+ "Fixed an issue where searching for multiple modules could yield the same module multiple times.",
+ "Fixed an issue where misnamed addon files could prevent startup.",
+ "Fixed an issue where the `request` module would not follow redirects.",
+ "Fixed an issue where certain modals could crash the client.",
+ "Fixed an issue where toasts would not show on the crash screen."
]
}
]
diff --git a/renderer/src/data/settings.js b/renderer/src/data/settings.js
index 5fb700ea..0209d474 100644
--- a/renderer/src/data/settings.js
+++ b/renderer/src/data/settings.js
@@ -70,5 +70,31 @@ export default [
{type: "switch", id: "inspectElement", value: false, enableWith: "devTools"},
{type: "switch", id: "devToolsWarning", value: false, enableWith: "devTools"},
]
- }
+ },
+ // {
+ // type: "category",
+ // id: "debug",
+ // name: "Debug",
+ // collapsible: true,
+ // shown: true,
+ // settings: [
+ // {name: "Text test", note: "Just testing it", type: "text", id: "texttest", value: ""},
+ // {name: "Slider test", note: "Just testing it", type: "slider", id: "slidertest", value: 30, min: 20, max: 50, step: 10},
+ // {
+ // name: "Radio test",
+ // note: "Just testing it",
+ // type: "radio",
+ // id: "radiotest",
+ // value: "test",
+ // options: [
+ // {name: "First", value: 30, description: "little hint"},
+ // {name: "IDK", value: "test", description: "who cares"},
+ // {name: "Something", value: 666, description: "something else"},
+ // {name: "Last", value: "last", description: "nothing more to add"}
+ // ]
+ // },
+ // {name: "Keybind test", note: "Just testing it", type: "keybind", id: "keybindtest", value: ["Control", "H"]},
+ // {name: "Color test", note: "Just testing it", type: "color", id: "colortest", value: "#ff0000", defaultValue: "#ffffff"},
+ // ]
+ // }
];
\ No newline at end of file
diff --git a/renderer/src/index.js b/renderer/src/index.js
index 21208310..674b2d6d 100644
--- a/renderer/src/index.js
+++ b/renderer/src/index.js
@@ -2,11 +2,15 @@ import require from "./polyfill"; // eslint-disable-line no-unused-vars
import secure from "./secure";
import LoadingIcon from "./loadingicon";
import BetterDiscord from "./modules/core";
-import BdApi from "./modules/pluginapi";
+import BdApi from "./modules/api/index";
// Perform some setup
secure();
-window.BdApi = BdApi;
+Object.defineProperty(window, "BdApi", {
+ value: BdApi,
+ writable: false,
+ configurable: false
+});
window.global = window;
// Add loading icon at the bottom right
diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js
index 7eed3965..245c0a76 100644
--- a/renderer/src/modules/addonmanager.js
+++ b/renderer/src/modules/addonmanager.js
@@ -1,4 +1,3 @@
-import Utilities from "./utilities";
import Logger from "common/logger";
import Settings from "./settingsmanager";
import Events from "./emitter";
@@ -138,9 +137,14 @@ export default class AddonManager {
parseOldMeta(fileContent, filename) {
const meta = fileContent.split("\n")[0];
const metaData = meta.substring(meta.lastIndexOf("//META") + 6, meta.lastIndexOf("*//"));
- const parsed = Utilities.testJSON(metaData);
- if (!parsed) throw new AddonError(filename, filename, Strings.Addons.metaError, {message: "", stack: meta}, this.prefix);
- if (!parsed.name) throw new AddonError(filename, filename, Strings.Addons.missingNameData, {message: "", stack: meta}, this.prefix);
+ let parsed = null;
+ try {
+ parsed = JSON.parse(metaData);
+ }
+ catch (err) {
+ throw new AddonError(filename, filename, Strings.Addons.metaError, err, this.prefix);
+ }
+ if (!parsed || !parsed.name) throw new AddonError(filename, filename, Strings.Addons.missingNameData, {message: "", stack: meta}, this.prefix);
parsed.format = "json";
return parsed;
}
@@ -202,6 +206,7 @@ export default class AddonManager {
if (partialAddon) {
partialAddon.partial = true;
this.state[partialAddon.id] = false;
+ this.emit("loaded", partialAddon);
}
return e;
}
@@ -211,11 +216,12 @@ export default class AddonManager {
if (error) {
this.state[addon.id] = false;
addon.partial = true;
+ this.emit("loaded", addon);
return error;
}
if (shouldToast) Toasts.success(`${addon.name} v${addon.version} was loaded.`);
- this.emit("loaded", addon.id);
+ this.emit("loaded", addon);
if (!this.state[addon.id]) return this.state[addon.id] = false;
return this.startAddon(addon);
@@ -228,7 +234,7 @@ export default class AddonManager {
if (this.state[addon.id]) isReload ? this.stopAddon(addon) : this.disableAddon(addon);
this.addonList.splice(this.addonList.indexOf(addon), 1);
- this.emit("unloaded", addon.id);
+ this.emit("unloaded", addon);
if (shouldToast) Toasts.success(`${addon.name} was unloaded.`);
return true;
}
diff --git a/renderer/src/modules/addonupdater.js b/renderer/src/modules/addonupdater.js
deleted file mode 100644
index 91114cf6..00000000
--- a/renderer/src/modules/addonupdater.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import request from "request";
-import fileSystem from "fs";
-import {Config} from "data";
-import path from "path";
-
-import PluginManager from "./pluginmanager";
-import ThemeManager from "./thememanager";
-
-import Toasts from "../ui/toasts";
-import Notices from "../ui/notices";
-import Logger from "common/logger";
-
-const base = "https://api.betterdiscord.app/v2/store/";
-const route = r => `${base}${r}`;
-const redirect = addonId => `https://betterdiscord.app/gh-redirect?id=${addonId}`;
-
-const getJSON = url => {
- return new Promise(resolve => {
- request(url, (error, _, body) => {
- if (error) return resolve([]);
- resolve(JSON.parse(body));
- });
- });
-};
-
-const reducer = (acc, addon) => {
- if (addon.version === "Unknown") return acc;
- acc[addon.file_name] = {name: addon.name, version: addon.version, id: addon.id, type: addon.type};
- return acc;
-};
-
-export default class AddonUpdater {
-
- static async initialize() {
- this.cache = {};
- this.shown = false;
- this.pending = [];
-
- const pluginData = await getJSON(route("plugins"));
- const themeData = await getJSON(route("themes"));
-
- pluginData.reduce(reducer, this.cache);
- themeData.reduce(reducer, this.cache);
-
- for (const addon of PluginManager.addonList) this.checkForUpdate(addon.filename, addon.version);
- for (const addon of ThemeManager.addonList) this.checkForUpdate(addon.filename, addon.version);
-
- this.showUpdateNotice();
- }
-
- static clearPending() {
- this.pending.splice(0, this.pending.length);
- }
-
- static async checkForUpdate(filename, currentVersion) {
- const info = this.cache[path.basename(filename)];
- if (!info) return;
- const hasUpdate = info.version > currentVersion;
- if (!hasUpdate) return;
- this.pending.push(filename);
- }
-
- static async updatePlugin(filename) {
- const info = this.cache[filename];
- request(redirect(info.id), (error, _, body) => {
- if (error) {
- Logger.stacktrace("AddonUpdater", `Failed to download body for ${info.id}:`, error);
- return;
- }
-
- const file = path.join(path.resolve(Config.dataPath, info.type + "s"), filename);
- fileSystem.writeFile(file, body.toString(), () => {
- Toasts.success(`${info.name} has been updated to version ${info.version}!`);
- });
- });
- }
-
- static showUpdateNotice() {
- if (this.shown || !this.pending.length) return;
- this.shown = true;
- const close = Notices.info(`BetterDiscord has found updates for ${this.pending.length} of your plugins and themes!`, {
- timeout: 0,
- buttons: [{
- label: "Update Now",
- onClick: async () => {
- for (const name of this.pending) await this.updatePlugin(name);
- close();
- }
- }],
- onClose: () => {
- this.shown = false;
- this.clearPending();
- }
- });
- }
-}
diff --git a/renderer/src/modules/api/addonapi.js b/renderer/src/modules/api/addonapi.js
new file mode 100644
index 00000000..295551ab
--- /dev/null
+++ b/renderer/src/modules/api/addonapi.js
@@ -0,0 +1,64 @@
+/**
+ * `AddonAPI` is a utility class for working with plugins and themes. Instances are accessible through the {@link BdApi}.
+ * @name AddonAPI
+ */
+ class AddonAPI {
+ #manager;
+
+ constructor(manager) {this.#manager = manager;}
+
+ /**
+ * The path to the addon folder.
+ * @type string
+ */
+ get folder() {return this.#manager.addonFolder;}
+
+ /**
+ * Determines if a particular adon is enabled.
+ * @param {string} idOrFile Addon id or filename.
+ * @returns {boolean}
+ */
+ isEnabled(idOrFile) {return this.#manager.isEnabled(idOrFile);}
+
+ /**
+ * Enables the given addon.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ enable(idOrAddon) {return this.#manager.enableAddon(idOrAddon);}
+
+ /**
+ * Disables the given addon.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ disable(idOrAddon) {return this.#manager.disableAddon(idOrAddon);}
+
+ /**
+ * Toggles if a particular addon is enabled.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ toggle(idOrAddon) {return this.#manager.toggleAddon(idOrAddon);}
+
+ /**
+ * Reloads if a particular addon is enabled.
+ * @param {string} idOrFile Addon id or filename.
+ */
+ reload(idOrFileOrAddon) {return this.#manager.reloadAddon(idOrFileOrAddon);}
+
+ /**
+ * Gets a particular addon.
+ * @param {string} idOrFile Addon id or filename.
+ * @returns {object} Addon instance
+ */
+ get(idOrFile) {return this.#manager.getAddon(idOrFile);}
+
+ /**
+ * Gets all addons of this type.
+ * @returns {Array