Allow partials, add temp updater

This commit is contained in:
Zack Rauen 2022-09-29 01:04:22 -04:00
parent feb3bb792f
commit 18bb2886cd
10 changed files with 186 additions and 11 deletions

View File

@ -185,6 +185,8 @@ export default class AddonManager {
addon.modified = stats.mtimeMs; addon.modified = stats.mtimeMs;
addon.size = stats.size; addon.size = stats.size;
addon.fileContent = fileContent; 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; return addon;
} }
@ -196,14 +198,22 @@ export default class AddonManager {
addon = this.requireAddon(path.resolve(this.addonFolder, filename)); addon = this.requireAddon(path.resolve(this.addonFolder, filename));
} }
catch (e) { catch (e) {
const partialAddon = this.addonList.find(c => c.filename == filename);
if (partialAddon) {
partialAddon.partial = true;
this.state[partialAddon.id] = false;
}
return e; 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); 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.`); if (shouldToast) Toasts.success(`${addon.name} v${addon.version} was loaded.`);
this.emit("loaded", addon.id); this.emit("loaded", addon.id);
@ -248,7 +258,7 @@ export default class AddonManager {
enableAddon(idOrAddon) { enableAddon(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : 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; if (this.state[addon.id]) return;
this.state[addon.id] = true; this.state[addon.id] = true;
this.startAddon(addon); this.startAddon(addon);
@ -257,7 +267,7 @@ export default class AddonManager {
disableAddon(idOrAddon) { disableAddon(idOrAddon) {
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : 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; if (!this.state[addon.id]) return;
this.state[addon.id] = false; this.state[addon.id] = false;
this.stopAddon(addon); this.stopAddon(addon);

View File

@ -0,0 +1,109 @@
import {Config} from "data";
import fileSystem from "fs";
import path from "path";
import request from "request";
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 raceTimeout = (delay, reason) => new Promise((_, rej) => setTimeout(() => rej(reason), delay));
// const timeout = (promise, delay, reason) => Promise.race([promise, raceTimeout(delay, reason)]);
const getJSON = url => {
return new Promise(resolve => {
request(url, (error, response, 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 = [];
Logger.info("AddonUpdater", "Before get api");
const pluginData = await getJSON(route("plugins"));
const themeData = await getJSON(route("themes"));
Logger.info("AddonUpdater", "After get api");
this.temp = {pluginData, themeData};
pluginData.reduce(reducer, this.cache);
themeData.reduce(reducer, this.cache);
Logger.info("AddonUpdater", "going to check lists");
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) {
Logger.info("AddonUpdater", "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), (err, response) => {
if (err) return;
if (!response.headers.location) return; // expected redirect
request(response.headers.location, (error, _, body) => {
if (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() {
Logger.info("AddonUpdater", "showUpdateNotice", this.shown, this.pending);
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();
}
});
}
}
window.updater = AddonUpdater;

View File

@ -16,6 +16,7 @@ import IPC from "./ipc";
import LoadingIcon from "../loadingicon"; import LoadingIcon from "../loadingicon";
import Styles from "../styles/index.css"; import Styles from "../styles/index.css";
import Editor from "./editor"; import Editor from "./editor";
import AddonUpdater from "./addonupdater";
export default new class Core { export default new class Core {
async startup() { async startup() {
@ -64,6 +65,9 @@ export default new class Core {
// const themeErrors = []; // const themeErrors = [];
const themeErrors = ThemeManager.initialize(); const themeErrors = ThemeManager.initialize();
Logger.log("Startup", "Initializing AddonUpdater");
AddonUpdater.initialize();
Logger.log("Startup", "Removing Loading Icon"); Logger.log("Startup", "Removing Loading Icon");
LoadingIcon.hide(); LoadingIcon.hide();

View File

@ -187,7 +187,7 @@ export default new class PluginManager extends AddonManager {
for (let i = 0; i < this.addonList.length; i++) { for (let i = 0; i < this.addonList.length; i++) {
const plugin = this.addonList[i].instance; const plugin = this.addonList[i].instance;
if (!this.state[this.addonList[i].id]) continue; if (!this.state[this.addonList[i].id]) continue;
if (typeof(plugin.onSwitch) === "function") { if (typeof(plugin?.onSwitch) === "function") {
try {plugin.onSwitch();} try {plugin.onSwitch();}
catch (err) {Logger.stacktrace(this.name, `Unable to fire onSwitch for ${this.addonList[i].name} v${this.addonList[i].version}`, err);} 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++) { for (let i = 0; i < this.addonList.length; i++) {
const plugin = this.addonList[i].instance; const plugin = this.addonList[i].instance;
if (!this.state[this.addonList[i].id]) continue; if (!this.state[this.addonList[i].id]) continue;
if (typeof plugin.observer === "function") { if (typeof plugin?.observer === "function") {
try {plugin.observer(mutation);} 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);} catch (err) {Logger.stacktrace(this.name, `Unable to fire observer for ${this.addonList[i].name} v${this.addonList[i].version}`, err);}
} }

View File

@ -63,6 +63,10 @@
fill: var(--header-primary); fill: var(--header-primary);
} }
.disabled .bd-addon-header .bd-icon {
fill: red;
}
.bd-title, .bd-title,
.bd-name, .bd-name,
.bd-meta { .bd-meta {
@ -106,6 +110,33 @@
padding: 8px 16px 0 16px; padding: 8px 16px 0 16px;
} }
.bd-description-wrap .banner {
padding: 5px;
border-left: 5px solid gray;
background: #26191E;
color: #C13A3A;
border-radius: 5px;
font-size: 16px;
display: flex;
align-items: center;
}
.banner.banner-danger {
border-left-color: #C13A3A;
background: #26191E;
color: #C13A3A;
}
.banner .bd-icon {
fill: red;
margin-right: 5px;
height: 16px !important;
}
.banner-danger .bd-icon {
fill: red;
}
.bd-addon-list .bd-description { .bd-addon-list .bd-description {
word-break: break-word; word-break: break-word;
margin-bottom: 5px; margin-bottom: 5px;

View File

@ -107,4 +107,8 @@
.bd-switch-disabled { .bd-switch-disabled {
opacity: 0.5; opacity: 0.5;
filter: grayscale(1); filter: grayscale(1);
}
.bd-switch-disabled input {
cursor: not-allowed;
} }

View File

@ -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>;
}
}

View File

@ -36,11 +36,12 @@ export default class Notices {
* @returns {(immediately?: boolean = false) => void} * @returns {(immediately?: boolean = false) => void}
*/ */
static show(content, options = {}) { static show(content, options = {}) {
const {type, buttons = [], timeout = 0} = options; const {type, buttons = [], timeout = 0, onClose = () => {}} = options;
const haveContainer = this.ensureContainer(); const haveContainer = this.ensureContainer();
if (!haveContainer) return; if (!haveContainer) return;
const closeNotification = function (immediately = false) { const closeNotification = function (immediately = false) {
onClose?.();
// Immediately remove the notice without adding the closing class. // Immediately remove the notice without adding the closing class.
if (immediately) return noticeElement.remove(); if (immediately) return noticeElement.remove();

View File

@ -12,6 +12,7 @@ import WebIcon from "../icons/globe";
import PatreonIcon from "../icons/patreon"; import PatreonIcon from "../icons/patreon";
import SupportIcon from "../icons/support"; import SupportIcon from "../icons/support";
import ExtIcon from "../icons/extension"; import ExtIcon from "../icons/extension";
import ErrorIcon from "../icons/error";
import ThemeIcon from "../icons/theme"; import ThemeIcon from "../icons/theme";
import Modals from "../modals"; import Modals from "../modals";
import Toasts from "../toasts"; import Toasts from "../toasts";
@ -174,9 +175,12 @@ export default class AddonCard extends React.Component {
<div className="bd-addon-header"> <div className="bd-addon-header">
{this.props.type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />} {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> <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>
<div className="bd-description-wrap"><div className="bd-description">{SimpleMarkdown.parseToReact(description)}</div></div>
{this.footer} {this.footer}
</div>; </div>;
} }

View File

@ -150,7 +150,7 @@ export default class AddonList extends React.Component {
const renderedCards = sortedAddons.map(addon => { const renderedCards = sortedAddons.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); 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; const hasAddonsInstalled = this.props.addonList.length !== 0;