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/core.js b/renderer/src/modules/core.js index b64018f1..283b76a8 100644 --- a/renderer/src/modules/core.js +++ b/renderer/src/modules/core.js @@ -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(); diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index 441f939a..1e7c2c65 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -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; } diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js index 3e28feb1..5240eb85 100644 --- a/renderer/src/modules/thememanager.js +++ b/renderer/src/modules/thememanager.js @@ -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; } diff --git a/renderer/src/modules/updater.js b/renderer/src/modules/updater.js new file mode 100644 index 00000000..b550d5ec --- /dev/null +++ b/renderer/src/modules/updater.js @@ -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"); \ No newline at end of file diff --git a/renderer/src/polyfill/module.js b/renderer/src/polyfill/module.js index c03d67d9..9bf8a6fa 100644 --- a/renderer/src/polyfill/module.js +++ b/renderer/src/polyfill/module.js @@ -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); diff --git a/renderer/src/ui/blankslates/noresults.jsx b/renderer/src/ui/blankslates/noresults.jsx index c30cc156..e9d796eb 100644 --- a/renderer/src/ui/blankslates/noresults.jsx +++ b/renderer/src/ui/blankslates/noresults.jsx @@ -6,7 +6,7 @@ export default class NoResults extends React.Component { return
- {DiscordModules.Strings.SEARCH_NO_RESULTS || ""} + {this.props.text || DiscordModules.Strings.SEARCH_NO_RESULTS || ""}
; } diff --git a/renderer/src/ui/icons/check.jsx b/renderer/src/ui/icons/check.jsx new file mode 100644 index 00000000..c84fd0fc --- /dev/null +++ b/renderer/src/ui/icons/check.jsx @@ -0,0 +1,11 @@ +import {React} from "modules"; + +export default class Checkmark extends React.Component { + render() { + const size = this.props.size || "24px"; + return + + + ; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings.js b/renderer/src/ui/settings.js index 77b9af42..8b56da87 100644 --- a/renderer/src/ui/settings.js +++ b/renderer/src/ui/settings.js @@ -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(); diff --git a/renderer/src/ui/settings/drawer.jsx b/renderer/src/ui/settings/drawer.jsx new file mode 100644 index 00000000..ac2857fb --- /dev/null +++ b/renderer/src/ui/settings/drawer.jsx @@ -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="bd-settings-container" ref={this.container}> + {this.props.children} + </div> + {this.props.showDivider && <Divider />} + </div>; + } +} \ No newline at end of file diff --git a/renderer/src/ui/settings/group.jsx b/renderer/src/ui/settings/group.jsx index 6d401e91..fba8c810 100644 --- a/renderer/src/ui/settings/group.jsx +++ b/renderer/src/ui/settings/group.jsx @@ -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 -}); \ No newline at end of file +} \ No newline at end of file diff --git a/renderer/src/ui/settings/title.jsx b/renderer/src/ui/settings/title.jsx index 53f33a03..b318631c 100644 --- a/renderer/src/ui/settings/title.jsx +++ b/renderer/src/ui/settings/title.jsx @@ -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>; } diff --git a/renderer/src/ui/updater.jsx b/renderer/src/ui/updater.jsx new file mode 100644 index 00000000..5c64e495 --- /dev/null +++ b/renderer/src/ui/updater.jsx @@ -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} />, + ]; + } +} \ No newline at end of file