Rework updater and add UI

This commit is contained in:
Zack Rauen 2022-10-03 04:40:18 -04:00
parent 70194a7114
commit 46c57567d0
13 changed files with 442 additions and 185 deletions

View File

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

View File

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

View File

@ -42,17 +42,20 @@ export default new class PluginManager extends AddonManager {
initialize() {
const errors = super.initialize();
this.setupFunctions();
Settings.registerPanel("plugins", Strings.Panels.plugins, {element: () => SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.togglePlugin.bind(this),
reload: this.reloadPlugin.bind(this),
refreshList: this.updatePluginList.bind(this),
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
prefix: this.prefix
})});
Settings.registerPanel("plugins", Strings.Panels.plugins, {
order: 3,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.plugins, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.togglePlugin.bind(this),
reload: this.reloadPlugin.bind(this),
refreshList: this.updatePluginList.bind(this),
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
prefix: this.prefix
})
});
return errors;
}

View File

@ -21,17 +21,20 @@ export default new class ThemeManager extends AddonManager {
initialize() {
const errors = super.initialize();
Settings.registerPanel("themes", Strings.Panels.themes, {element: () => SettingsRenderer.getAddonPanel(Strings.Panels.themes, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.toggleTheme.bind(this),
reload: this.reloadTheme.bind(this),
refreshList: this.updateThemeList.bind(this),
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
prefix: this.prefix
})});
Settings.registerPanel("themes", Strings.Panels.themes, {
order: 4,
element: () => SettingsRenderer.getAddonPanel(Strings.Panels.themes, this.addonList, this.state, {
type: this.prefix,
folder: this.addonFolder,
onChange: this.toggleTheme.bind(this),
reload: this.reloadTheme.bind(this),
refreshList: this.updateThemeList.bind(this),
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
prefix: this.prefix
})
});
return errors;
}

View File

@ -0,0 +1,206 @@
import request from "request";
import fileSystem from "fs";
import {Config} from "data";
import path from "path";
import Logger from "common/logger";
import IPC from "./ipc";
import Strings from "./strings";
import DataStore from "./datastore";
import Settings from "./settingsmanager";
import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager";
import WebpackModules from "./webpackmodules";
import Toasts from "../ui/toasts";
import Notices from "../ui/notices";
import Modals from "../ui/modals";
import UpdaterPanel from "../ui/updater";
import DiscordModules from "./discordmodules";
const React = DiscordModules.React;
const UserSettingsWindow = WebpackModules.getByProps("updateAccount");
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};
return acc;
};
export default class Updater {
static initialize() {
Settings.registerPanel("updates", "Updates", {
order: 1,
element: () => {
return React.createElement(UpdaterPanel, {
coreUpdater: CoreUpdater,
pluginUpdater: PluginUpdater,
themeUpdater: ThemeUpdater
});
}
});
CoreUpdater.initialize();
PluginUpdater.initialize();
ThemeUpdater.initialize();
}
}
export class CoreUpdater {
static hasUpdate = false;
static apiData = {};
static remoteVersion = "";
static async initialize() {
this.checkForUpdate();
}
static async checkForUpdate(showNotice = true) {
const resp = await fetch(`https://api.github.com/repos/BetterDiscord/BetterDiscord/releases/latest`,{
method: "GET",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "BetterDiscord Updater"
}
});
const data = await resp.json();
this.apiData = data;
const remoteVersion = data.tag_name.startsWith("v") ? data.tag_name.slice(1) : data.tag_name;
this.hasUpdate = remoteVersion > Config.version;
this.remoteVersion = remoteVersion;
if (!this.hasUpdate || !showNotice) return;
const close = Notices.info(`BetterDiscord has a new update (v${remoteVersion})`, {
buttons: [{
label: "More Info",
onClick: () => {
close();
UserSettingsWindow?.open?.("updates");
}
}]
});
}
static async update() {
try {
const asar = this.apiData.assets.find(a => a.name === "betterdiscord.asar");
const buff = await new Promise((resolve, reject) =>
request(asar.url, {encoding: null, headers: {"User-Agent": "BetterDiscord Updater", "Accept": "application/octet-stream"}}, (err, resp, body) => {
if (err || resp.statusCode != 200) return reject(err || `${resp.statusCode} ${resp.statusMessage}`);
return resolve(body);
}));
const asarPath = path.join(DataStore.baseFolder, "betterdiscord.asar");
const fs = require("original-fs");
fs.writeFileSync(asarPath, buff);
this.hasUpdate = false;
Config.version = this.remoteVersion;
Modals.showConfirmationModal("Update Successful!", "BetterDiscord updated successfully. Discord needs to restart in order for it to take effect. Do you want to do this now?", {
confirmText: Strings.Modals.restartNow,
cancelText: Strings.Modals.restartLater,
danger: true,
onConfirm: () => IPC.relaunch()
});
}
catch (err) {
Logger.stacktrace("Updater", "Failed to update", err);
Modals.showConfirmationModal("Update Failed", "BetterDiscord failed to update. Please download the latest version of the installer from GitHub (https://github.com/BetterDiscord/Installer/releases/latest) and reinstall.", {
cancelText: null
});
}
}
}
class AddonUpdater {
constructor(type) {
this.manager = type === "plugins" ? PluginManager : ThemeManager;
this.type = type;
this.cache = {};
this.pending = [];
}
async initialize() {
await this.updateCache();
this.checkAll();
}
async updateCache() {
this.cache = {};
const addonData = await getJSON(route(this.type));
addonData.reduce(reducer, this.cache);
}
clearPending() {
this.pending.splice(0, this.pending.length);
}
checkAll(showNotice = true) {
for (const addon of this.manager.addonList) this.checkForUpdate(addon.filename, addon.version);
if (showNotice) this.showUpdateNotice();
}
checkForUpdate(filename, currentVersion) {
if (this.pending.includes(filename)) return;
const info = this.cache[path.basename(filename)];
if (!info) return;
const hasUpdate = info.version > currentVersion;
if (!hasUpdate) return;
this.pending.push(filename);
}
async updateAddon(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(this.manager.addonFolder), filename);
fileSystem.writeFile(file, body.toString(), () => {
Toasts.success(`${info.name} has been updated to version ${info.version}!`);
this.pending.splice(this.pending.indexOf(filename), 1);
});
});
}
showUpdateNotice() {
if (!this.pending.length) return;
const close = Notices.info(`BetterDiscord has found updates for ${this.pending.length} of your ${this.type}!`, {
buttons: [{
label: "More Info",
onClick: () => {
close();
UserSettingsWindow?.open?.("updates");
}
}]
});
}
}
export const PluginUpdater = new AddonUpdater("plugins");
export const ThemeUpdater = new AddonUpdater("themes");

View File

@ -31,7 +31,7 @@ export default class Module {
const ext = path.extname(file);
if (file === "package.json") {
const pkg = require(path.resolve(parent, file));
const pkg = __non_webpack_require__(path.resolve(parent, file));
if (!Reflect.has(pkg, "main")) continue;
return path.resolve(parent, pkg.main);

View File

@ -6,7 +6,7 @@ export default class NoResults extends React.Component {
return <div className={"bd-empty-results" + (this.props.className ? ` ${this.props.className}` : "")}>
<MagnifyingGlass />
<div className="bd-empty-results-text">
{DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
{this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
</div>
</div>;
}

View File

@ -0,0 +1,11 @@
import {React} from "modules";
export default class Checkmark extends React.Component {
render() {
const size = this.props.size || "24px";
return <svg viewBox="0 0 24 24" fill="#FFFFFF" className={this.props.className || ""} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</svg>;
}
}

View File

@ -83,7 +83,7 @@ export default new class SettingsRenderer {
element: () => this.buildSettingsPanel(collection.id, collection.name, collection.settings, Settings.state[collection.id], Settings.onSettingChange.bind(Settings, collection.id), collection.button ? collection.button : null)
});
}
for (const panel of Settings.panels.sort((a,b) => a.order > b.order)) {
for (const panel of Settings.panels.sort((a,b) => a.order > b.order ? 1 : -1)) {
if (panel.clickListener) panel.onClick = (event) => panel.clickListener(thisObject, event, returnValue);
if (!panel.className) panel.className = `bd-${panel.id}-tab`;
if (typeof(panel.label) !== "string") panel.label = panel.label.toString();

View File

@ -0,0 +1,53 @@
import {React} from "modules";
import Title from "./title";
import Divider from "../divider";
const baseClassName = "bd-settings-group";
export default class Drawer extends React.Component {
constructor(props) {
super(props);
if (this.props.button && this.props.collapsible) {
const original = this.props.button.onClick;
this.props.button.onClick = (event) => {
event.stopPropagation();
original(...arguments);
};
}
if (!this.props.hasOwnProperty("shown")) this.props.shown = true;
this.container = React.createRef();
this.state = {
collapsed: this.props.collapsible && !this.props.shown
};
this.toggleCollapse = this.toggleCollapse.bind(this);
}
toggleCollapse() {
const container = this.container.current;
const timeout = this.state.collapsed ? 300 : 1;
container.style.setProperty("height", container.scrollHeight + "px");
container.classList.add("animating");
this.setState({collapsed: !this.state.collapsed}, () => setTimeout(() => {
container.style.setProperty("height", "");
container.classList.remove("animating");
}, timeout));
if (this.props.onDrawerToggle) this.props.onDrawerToggle(this.state.collapsed);
}
render() {
const collapseClass = this.props.collapsible ? `collapsible ${this.state.collapsed ? "collapsed" : "expanded"}` : "";
const groupClass = `${baseClassName} ${collapseClass}`;
return <div className={groupClass}>
<Title text={this.props.name} collapsible={this.props.collapsible} onClick={this.toggleCollapse} button={this.props.button} isGroup={true} />
<div className="bd-settings-container" ref={this.container}>
{this.props.children}
</div>
{this.props.showDivider && <Divider />}
</div>;
}
}

View File

@ -1,5 +1,5 @@
import Logger from "common/logger";
import {React} from "modules";
import Drawer from "./drawer";
import Title from "./title";
import Divider from "../divider";
import Switch from "./components/switch";
@ -12,41 +12,12 @@ import Radio from "./components/radio";
import Keybind from "./components/keybind";
import Color from "./components/color";
const baseClassName = "bd-settings-group";
export default class Group extends React.Component {
constructor(props) {
super(props);
if (this.props.button && this.props.collapsible) {
const original = this.props.button.onClick;
this.props.button.onClick = (event) => {
event.stopPropagation();
original(...arguments);
};
}
if (!this.props.hasOwnProperty("shown")) this.props.shown = true;
this.container = React.createRef();
this.state = {
collapsed: this.props.collapsible && !this.props.shown
};
this.onChange = this.onChange.bind(this);
this.toggleCollapse = this.toggleCollapse.bind(this);
}
toggleCollapse() {
const container = this.container.current;
const timeout = this.state.collapsed ? 300 : 1;
container.style.setProperty("height", container.scrollHeight + "px");
container.classList.add("animating");
this.setState({collapsed: !this.state.collapsed}, () => setTimeout(() => {
container.style.setProperty("height", "");
container.classList.remove("animating");
}, timeout));
if (this.props.onDrawerToggle) this.props.onDrawerToggle(this.state.collapsed);
}
onChange(id, value) {
@ -58,35 +29,21 @@ export default class Group extends React.Component {
render() {
const {settings} = this.props;
const collapseClass = this.props.collapsible ? `collapsible ${this.state.collapsed ? "collapsed" : "expanded"}` : "";
const groupClass = `${baseClassName} ${collapseClass}`;
return <div className={groupClass}>
<Title text={this.props.name} collapsible={this.props.collapsible} onClick={this.toggleCollapse} button={this.props.button} isGroup={true} />
<div className="bd-settings-container" ref={this.container}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={this.onChange.bind(this, setting.id)} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</div>
{this.props.showDivider && <Divider />}
</div>;
return <Drawer collapsible={this.props.collapsible} name={this.props.name} button={this.props.button} shown={this.props.shown} onDrawerToggle={this.props.onDrawerToggle} showDivider={this.props.showDivider}>
{settings.filter(s => !s.hidden).map((setting) => {
let component = null;
if (setting.type == "dropdown") component = <Dropdown disabled={setting.disabled} id={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "number") component = <Number disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "switch") component = <Switch disabled={setting.disabled} id={setting.id} checked={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "text") component = <Textbox disabled={setting.disabled} id={setting.id} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "slider") component = <Slider disabled={setting.disabled} id={setting.id} min={setting.min} max={setting.max} step={setting.step} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "radio") component = <Radio disabled={setting.disabled} id={setting.id} name={setting.id} options={setting.options} value={setting.value} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "keybind") component = <Keybind disabled={setting.disabled} id={setting.id} value={setting.value} max={setting.max} onChange={this.onChange.bind(this, setting.id)} />;
if (setting.type == "color") component = <Color disabled={setting.disabled} id={setting.id} value={setting.value} defaultValue={setting.defaultValue} colors={setting.colors} onChange={this.onChange.bind(this, setting.id)} />;
if (!component) return null;
return <Item id={setting.id} inline={setting.type !== "radio"} key={setting.id} name={setting.name} note={setting.note}>{component}</Item>;
})}
</Drawer>;
}
}
const originalRender = Group.prototype.render;
Object.defineProperty(Group.prototype, "render", {
enumerable: false,
configurable: false,
set: function() {Logger.warn("Group", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");},
get: () => originalRender
});
}

View File

@ -4,12 +4,23 @@ const className = "bd-settings-title";
const className2 = "bd-settings-title bd-settings-group-title";
export default class SettingsTitle extends React.Component {
constructor(props) {
super(props);
this.buttonClick = this.buttonClick.bind(this);
}
buttonClick(event) {
event.stopPropagation();
event.preventDefault();
this.props?.button?.onClick?.(event);
}
render() {
const baseClass = this.props.isGroup ? className2 : className;
const titleClass = this.props.className ? `${baseClass} ${this.props.className}` : baseClass;
return <h2 className={titleClass} onClick={() => {this.props.onClick && this.props.onClick();}}>
{this.props.text}
{this.props.button && <button className="bd-button bd-button-title" onClick={this.props.button.onClick}>{this.props.button.title}</button>}
{this.props.button && <button className="bd-button bd-button-title" onClick={this.buttonClick}>{this.props.button.title}</button>}
{this.props.otherChildren}
</h2>;
}

109
renderer/src/ui/updater.jsx Normal file
View File

@ -0,0 +1,109 @@
import {Config} from "data";
import {React} from "modules";
import Drawer from "./settings/drawer";
import SettingItem from "./settings/components/item";
import SettingsTitle from "./settings/title";
import Toasts from "./toasts";
import Checkmark from "./icons/check";
class CoreUpdaterPanel extends React.Component {
render() {
return <Drawer name="BetterDiscord" collapsible={true}>
<SettingItem name={`Core v${Config.version}`} note={this.props.hasUpdate ? `Version ${this.props.remoteVersion} now available!` : "No updates available."} inline={true} id={"core-updater"}>
{!this.props.hasUpdate && <div className="bd-filled-checkmark"><Checkmark /></div>}
{this.props.hasUpdate && <button className="bd-button">Update!</button>}
</SettingItem>
</Drawer>;
}
}
class NoUpdates extends React.Component {
render() {
return <div className="bd-empty-updates">
<Checkmark size="48px" />
{`All of your ${this.props.type} seem to be up to date!`}
</div>;
}
}
class AddonUpdaterPanel extends React.Component {
render() {
const filenames = this.props.pending;
return <Drawer name={this.props.type} collapsible={true} button={filenames.length ? {title: "Update All!", onClick: () => this.props.updateAll(this.props.type)} : null}>
{!filenames.length && <NoUpdates type={this.props.type} />}
{filenames.map(f => {
const info = this.props.updater.cache[f];
const addon = this.props.updater.manager.addonList.find(a => a.filename === f);
return <SettingItem name={`${addon.name} v${addon.version}`} note={`Version ${info.version} now available!`} inline={true} id={addon.name}>
<button className="bd-button" onClick={() => this.props.update(this.props.type, f)}>Update!</button>
</SettingItem>;
})}
</Drawer>;
}
}
export default class UpdaterPanel extends React.Component {
constructor(props) {
super(props);
this.state = {
hasCoreUpdate: this.props.coreUpdater.hasUpdate,
plugins: this.props.pluginUpdater.pending.slice(0),
themes: this.props.themeUpdater.pending.slice(0)
};
this.checkForUpdates = this.checkForUpdates.bind(this);
this.updateAddon = this.updateAddon.bind(this);
this.updateAllAddons = this.updateAllAddons.bind(this);
}
async checkForUpdates() {
Toasts.info("Checking for updates!");
await this.checkCoreUpdate();
await this.checkAddons("plugins");
await this.checkAddons("themes");
Toasts.info("Finished checking for updates!");
}
async checkCoreUpdate() {
await this.props.coreUpdater.checkForUpdate(false);
this.setState({hasCoreUpdate: this.props.coreUpdater.hasUpdate});
}
async updateCore() {
await this.props.coreUpdater.update();
this.setState({hasCoreUpdate: false});
}
async checkAddons(type) {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater;
await updater.checkAll(false);
this.setState({[type]: updater.pending.slice(0)});
}
async updateAddon(type, filename) {
const updater = type === "plugins" ? this.props.pluginUpdater : this.props.themeUpdater;
await updater.updateAddon(filename);
this.setState(prev => {
prev[type].splice(prev[type].indexOf(filename), 1);
return prev;
});
}
async updateAllAddons(type) {
const toUpdate = this.state[type].slice(0);
for (const filename of toUpdate) {
await this.updateAddon(type, filename);
}
}
render() {
return [
<SettingsTitle text="Updates" button={{title: "Check For Updates!", onClick: this.checkForUpdates}} />,
<CoreUpdaterPanel remoteVersion={this.props.coreUpdater.remoteVersion} hasUpdate={this.state.hasCoreUpdate} />,
<AddonUpdaterPanel type="plugins" pending={this.state.plugins} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.pluginUpdater} />,
<AddonUpdaterPanel type="themes" pending={this.state.themes} update={this.updateAddon} updateAll={this.updateAllAddons} updater={this.props.themeUpdater} />,
];
}
}