Allow partials, add temp updater
This commit is contained in:
parent
feb3bb792f
commit
18bb2886cd
|
@ -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);
|
||||||
|
|
|
@ -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;
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue