commit
f41b071727
|
@ -185,6 +185,8 @@ export default class AddonManager {
|
|||
addon.modified = stats.mtimeMs;
|
||||
addon.size = stats.size;
|
||||
addon.fileContent = fileContent;
|
||||
if (this.addonList.find(c => c.id == addon.id)) throw new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix);
|
||||
this.addonList.push(addon);
|
||||
return addon;
|
||||
}
|
||||
|
||||
|
@ -196,14 +198,22 @@ export default class AddonManager {
|
|||
addon = this.requireAddon(path.resolve(this.addonFolder, filename));
|
||||
}
|
||||
catch (e) {
|
||||
const partialAddon = this.addonList.find(c => c.filename == filename);
|
||||
if (partialAddon) {
|
||||
partialAddon.partial = true;
|
||||
this.state[partialAddon.id] = false;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
if (this.addonList.find(c => c.id == addon.id)) return new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix);
|
||||
|
||||
|
||||
const error = this.initializeAddon(addon);
|
||||
if (error) return error;
|
||||
if (error) {
|
||||
this.state[addon.id] = false;
|
||||
addon.partial = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
this.addonList.push(addon);
|
||||
if (shouldToast) Toasts.success(`${addon.name} v${addon.version} was loaded.`);
|
||||
this.emit("loaded", addon.id);
|
||||
|
||||
|
@ -248,7 +258,7 @@ export default class AddonManager {
|
|||
|
||||
enableAddon(idOrAddon) {
|
||||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||
if (!addon) return;
|
||||
if (!addon || addon.partial) return;
|
||||
if (this.state[addon.id]) return;
|
||||
this.state[addon.id] = true;
|
||||
this.startAddon(addon);
|
||||
|
@ -257,7 +267,7 @@ export default class AddonManager {
|
|||
|
||||
disableAddon(idOrAddon) {
|
||||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||
if (!addon) return;
|
||||
if (!addon || addon.partial) return;
|
||||
if (!this.state[addon.id]) return;
|
||||
this.state[addon.id] = false;
|
||||
this.stopAddon(addon);
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import IPC from "./ipc";
|
|||
import LoadingIcon from "../loadingicon";
|
||||
import Styles from "../styles/index.css";
|
||||
import Editor from "./editor";
|
||||
import AddonUpdater from "./addonupdater";
|
||||
|
||||
export default new class Core {
|
||||
async startup() {
|
||||
|
@ -64,6 +65,9 @@ export default new class Core {
|
|||
// const themeErrors = [];
|
||||
const themeErrors = ThemeManager.initialize();
|
||||
|
||||
Logger.log("Startup", "Initializing AddonUpdater");
|
||||
AddonUpdater.initialize();
|
||||
|
||||
Logger.log("Startup", "Removing Loading Icon");
|
||||
LoadingIcon.hide();
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ BdApi.showToast = function(content, options = {}) {
|
|||
* @param {object} options Options for the notice.
|
||||
* @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color.
|
||||
* @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text.
|
||||
* @param {number} [options.timeout=10000] Timeout until the notice is closed. Won't fire if it's set to 0;
|
||||
* @param {number} [options.timeout=0] Timeout until the notice is closed. Won't fire if it's set to 0;
|
||||
* @returns {function} A callback for closing the notice. Passing `true` as first parameter closes immediately without transitioning out.
|
||||
*/
|
||||
BdApi.showNotice = function (content, options = {}) {
|
||||
|
|
|
@ -187,7 +187,7 @@ export default new class PluginManager extends AddonManager {
|
|||
for (let i = 0; i < this.addonList.length; i++) {
|
||||
const plugin = this.addonList[i].instance;
|
||||
if (!this.state[this.addonList[i].id]) continue;
|
||||
if (typeof(plugin.onSwitch) === "function") {
|
||||
if (typeof(plugin?.onSwitch) === "function") {
|
||||
try {plugin.onSwitch();}
|
||||
catch (err) {Logger.stacktrace(this.name, `Unable to fire onSwitch for ${this.addonList[i].name} v${this.addonList[i].version}`, err);}
|
||||
}
|
||||
|
@ -198,7 +198,7 @@ export default new class PluginManager extends AddonManager {
|
|||
for (let i = 0; i < this.addonList.length; i++) {
|
||||
const plugin = this.addonList[i].instance;
|
||||
if (!this.state[this.addonList[i].id]) continue;
|
||||
if (typeof plugin.observer === "function") {
|
||||
if (typeof plugin?.observer === "function") {
|
||||
try {plugin.observer(mutation);}
|
||||
catch (err) {Logger.stacktrace(this.name, `Unable to fire observer for ${this.addonList[i].name} v${this.addonList[i].version}`, err);}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,10 @@
|
|||
fill: var(--header-primary);
|
||||
}
|
||||
|
||||
.disabled .bd-addon-header .bd-icon {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
.bd-title,
|
||||
.bd-name,
|
||||
.bd-meta {
|
||||
|
@ -106,6 +110,33 @@
|
|||
padding: 8px 16px 0 16px;
|
||||
}
|
||||
|
||||
.bd-description-wrap .banner {
|
||||
padding: 5px;
|
||||
border: 2px solid gray;
|
||||
background: #26191E;
|
||||
color: #ffffff;
|
||||
font-weight: 700px;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.banner.banner-danger {
|
||||
border-color: #F04747;
|
||||
background: #473C41;
|
||||
}
|
||||
|
||||
.banner .bd-icon {
|
||||
fill: #ffffff;
|
||||
margin-right: 5px;
|
||||
height: 16px !important;
|
||||
}
|
||||
|
||||
.banner-danger .bd-icon {
|
||||
fill: #F04747;
|
||||
}
|
||||
|
||||
.bd-addon-list .bd-description {
|
||||
word-break: break-word;
|
||||
margin-bottom: 5px;
|
||||
|
|
|
@ -108,3 +108,7 @@
|
|||
opacity: 0.5;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.bd-switch-disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import {React} from "modules";
|
||||
|
||||
export default class Error extends React.Component {
|
||||
render() {
|
||||
const size = this.props.size || "24px";
|
||||
return <svg viewBox="0 0 24 24" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick} className={this.props.className}>
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/>
|
||||
</svg>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import {WebpackModules} from "modules";
|
||||
import {Utilities, WebpackModules} from "modules";
|
||||
|
||||
export default class Notices {
|
||||
static get baseClass() {return this.__baseClass || (this.__baseClass = WebpackModules.getByProps("container", "base")?.base);}
|
||||
static get baseClass() {return this.__baseClass ??= WebpackModules.getByProps("container", "base")?.base;}
|
||||
static get errorPageClass() {return this.__errorPageClass ??= WebpackModules.getByProps("errorPage")?.errorPage;}
|
||||
|
||||
/** Shorthand for `type = "info"` for {@link module:Notices.show} */
|
||||
static info(content, options = {}) {return this.show(content, Object.assign({}, options, {type: "info"}));}
|
||||
|
@ -32,15 +33,16 @@ export default class Notices {
|
|||
* @param {object} options Options for the notice.
|
||||
* @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color.
|
||||
* @param {Array<{label: string, onClick: (immediately?: boolean = false) => void}>} [options.buttons] Buttons that should be added next to the notice text.
|
||||
* @param {number} [options.timeout=10000] Timeout until the toast is closed. Won't fire if it's set to 0;
|
||||
* @param {number} [options.timeout=0] Timeout until the toast is closed. Won't fire if it's set to 0;
|
||||
* @returns {(immediately?: boolean = false) => void}
|
||||
*/
|
||||
static show(content, options = {}) {
|
||||
const {type, buttons = [], timeout = 0} = options;
|
||||
const {type, buttons = [], timeout = 0, onClose = () => {}} = options;
|
||||
const haveContainer = this.ensureContainer();
|
||||
if (!haveContainer) return;
|
||||
|
||||
const closeNotification = function (immediately = false) {
|
||||
onClose?.();
|
||||
// Immediately remove the notice without adding the closing class.
|
||||
if (immediately) return noticeElement.remove();
|
||||
|
||||
|
@ -82,6 +84,14 @@ export default class Notices {
|
|||
});
|
||||
container.prepend(noticeContainer);
|
||||
|
||||
Utilities.onRemoved(container, async () => {
|
||||
if (!this.errorPageClass) return;
|
||||
|
||||
const element = await new Promise(res => Utilities.onAdded(`.${this.errorPageClass}`, res));
|
||||
|
||||
element.prepend(noticeContainer);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import WebIcon from "../icons/globe";
|
|||
import PatreonIcon from "../icons/patreon";
|
||||
import SupportIcon from "../icons/support";
|
||||
import ExtIcon from "../icons/extension";
|
||||
import ErrorIcon from "../icons/error";
|
||||
import ThemeIcon from "../icons/theme";
|
||||
import Modals from "../modals";
|
||||
import Toasts from "../toasts";
|
||||
|
@ -174,9 +175,12 @@ export default class AddonCard extends React.Component {
|
|||
<div className="bd-addon-header">
|
||||
{this.props.type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
|
||||
<div className="bd-title">{this.buildTitle(name, version, {name: author, id: this.props.addon.authorId, link: this.props.addon.authorLink})}</div>
|
||||
<Switch checked={this.props.enabled} onChange={this.onChange} />
|
||||
<Switch disabled={this.props.disabled} checked={this.props.enabled} onChange={this.onChange} />
|
||||
</div>
|
||||
<div className="bd-description-wrap">
|
||||
{this.props.disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${this.props.type}.`}</div>}
|
||||
<div className="bd-description">{SimpleMarkdown.parseToReact(description)}</div>
|
||||
</div>
|
||||
<div className="bd-description-wrap"><div className="bd-description">{SimpleMarkdown.parseToReact(description)}</div></div>
|
||||
{this.footer}
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -150,7 +150,7 @@ export default class AddonList extends React.Component {
|
|||
const renderedCards = sortedAddons.map(addon => {
|
||||
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
|
||||
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
|
||||
return <ErrorBoundary><AddonCard type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
|
||||
return <ErrorBoundary><AddonCard disabled={addon.partial} type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
|
||||
});
|
||||
|
||||
const hasAddonsInstalled = this.props.addonList.length !== 0;
|
||||
|
|
Loading…
Reference in New Issue