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