From 18bb2886cd0da053bf15896189dee626fb5088c3 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Thu, 29 Sep 2022 01:04:22 -0400 Subject: [PATCH 1/2] Allow partials, add temp updater --- renderer/src/modules/addonmanager.js | 20 +++-- renderer/src/modules/addonupdater.js | 109 +++++++++++++++++++++++++ renderer/src/modules/core.js | 4 + renderer/src/modules/pluginmanager.js | 4 +- renderer/src/styles/ui/addonlist.css | 31 +++++++ renderer/src/styles/ui/switch.css | 4 + renderer/src/ui/icons/error.jsx | 12 +++ renderer/src/ui/notices.js | 3 +- renderer/src/ui/settings/addoncard.jsx | 8 +- renderer/src/ui/settings/addonlist.jsx | 2 +- 10 files changed, 186 insertions(+), 11 deletions(-) create mode 100644 renderer/src/modules/addonupdater.js create mode 100644 renderer/src/ui/icons/error.jsx diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index e9a45eff..7eed3965 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -185,6 +185,8 @@ export default class AddonManager { addon.modified = stats.mtimeMs; addon.size = stats.size; 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; } @@ -196,14 +198,22 @@ export default class AddonManager { addon = this.requireAddon(path.resolve(this.addonFolder, filename)); } catch (e) { + const partialAddon = this.addonList.find(c => c.filename == filename); + if (partialAddon) { + partialAddon.partial = true; + this.state[partialAddon.id] = false; + } 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); - 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.`); this.emit("loaded", addon.id); @@ -248,7 +258,7 @@ export default class AddonManager { enableAddon(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; this.state[addon.id] = true; this.startAddon(addon); @@ -257,7 +267,7 @@ export default class AddonManager { disableAddon(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; this.state[addon.id] = false; this.stopAddon(addon); diff --git a/renderer/src/modules/addonupdater.js b/renderer/src/modules/addonupdater.js new file mode 100644 index 00000000..147e6130 --- /dev/null +++ b/renderer/src/modules/addonupdater.js @@ -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; \ No newline at end of file diff --git a/renderer/src/modules/core.js b/renderer/src/modules/core.js index 98889a35..b64018f1 100644 --- a/renderer/src/modules/core.js +++ b/renderer/src/modules/core.js @@ -16,6 +16,7 @@ import IPC from "./ipc"; import LoadingIcon from "../loadingicon"; import Styles from "../styles/index.css"; import Editor from "./editor"; +import AddonUpdater from "./addonupdater"; export default new class Core { async startup() { @@ -64,6 +65,9 @@ export default new class Core { // const themeErrors = []; const themeErrors = ThemeManager.initialize(); + Logger.log("Startup", "Initializing AddonUpdater"); + AddonUpdater.initialize(); + Logger.log("Startup", "Removing Loading Icon"); LoadingIcon.hide(); diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index 451938f4..441f939a 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -187,7 +187,7 @@ export default new class PluginManager extends AddonManager { for (let i = 0; i < this.addonList.length; i++) { const plugin = this.addonList[i].instance; if (!this.state[this.addonList[i].id]) continue; - if (typeof(plugin.onSwitch) === "function") { + if (typeof(plugin?.onSwitch) === "function") { try {plugin.onSwitch();} 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++) { const plugin = this.addonList[i].instance; if (!this.state[this.addonList[i].id]) continue; - if (typeof plugin.observer === "function") { + if (typeof plugin?.observer === "function") { 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);} } diff --git a/renderer/src/styles/ui/addonlist.css b/renderer/src/styles/ui/addonlist.css index 3ded72ad..8dfeb747 100644 --- a/renderer/src/styles/ui/addonlist.css +++ b/renderer/src/styles/ui/addonlist.css @@ -63,6 +63,10 @@ fill: var(--header-primary); } +.disabled .bd-addon-header .bd-icon { + fill: red; +} + .bd-title, .bd-name, .bd-meta { @@ -106,6 +110,33 @@ 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 { word-break: break-word; margin-bottom: 5px; diff --git a/renderer/src/styles/ui/switch.css b/renderer/src/styles/ui/switch.css index a94e4e51..9ad196c0 100644 --- a/renderer/src/styles/ui/switch.css +++ b/renderer/src/styles/ui/switch.css @@ -107,4 +107,8 @@ .bd-switch-disabled { opacity: 0.5; filter: grayscale(1); +} + +.bd-switch-disabled input { + cursor: not-allowed; } \ No newline at end of file diff --git a/renderer/src/ui/icons/error.jsx b/renderer/src/ui/icons/error.jsx new file mode 100644 index 00000000..eb1bf767 --- /dev/null +++ b/renderer/src/ui/icons/error.jsx @@ -0,0 +1,12 @@ +import {React} from "modules"; + +export default class Error extends React.Component { + render() { + const size = this.props.size || "24px"; + return + + + ; + } +} + diff --git a/renderer/src/ui/notices.js b/renderer/src/ui/notices.js index 0fe6941e..3585d274 100644 --- a/renderer/src/ui/notices.js +++ b/renderer/src/ui/notices.js @@ -36,11 +36,12 @@ export default class Notices { * @returns {(immediately?: boolean = false) => void} */ static show(content, options = {}) { - const {type, buttons = [], timeout = 0} = options; + const {type, buttons = [], timeout = 0, onClose = () => {}} = options; const haveContainer = this.ensureContainer(); if (!haveContainer) return; const closeNotification = function (immediately = false) { + onClose?.(); // Immediately remove the notice without adding the closing class. if (immediately) return noticeElement.remove(); diff --git a/renderer/src/ui/settings/addoncard.jsx b/renderer/src/ui/settings/addoncard.jsx index e7f35d04..98d14150 100644 --- a/renderer/src/ui/settings/addoncard.jsx +++ b/renderer/src/ui/settings/addoncard.jsx @@ -12,6 +12,7 @@ import WebIcon from "../icons/globe"; import PatreonIcon from "../icons/patreon"; import SupportIcon from "../icons/support"; import ExtIcon from "../icons/extension"; +import ErrorIcon from "../icons/error"; import ThemeIcon from "../icons/theme"; import Modals from "../modals"; import Toasts from "../toasts"; @@ -174,9 +175,12 @@ export default class AddonCard extends React.Component {
{this.props.type === "plugin" ? : }
{this.buildTitle(name, version, {name: author, id: this.props.addon.authorId, link: this.props.addon.authorLink})}
- + +
+
+ {this.props.disabled &&
{`An error was encountered while trying to load this ${this.props.type}.`}
} +
{SimpleMarkdown.parseToReact(description)}
-
{SimpleMarkdown.parseToReact(description)}
{this.footer} ; } diff --git a/renderer/src/ui/settings/addonlist.jsx b/renderer/src/ui/settings/addonlist.jsx index eecc93bc..213c7275 100644 --- a/renderer/src/ui/settings/addonlist.jsx +++ b/renderer/src/ui/settings/addonlist.jsx @@ -150,7 +150,7 @@ export default class AddonList extends React.Component { const renderedCards = sortedAddons.map(addon => { const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); - return ; + return ; }); const hasAddonsInstalled = this.props.addonList.length !== 0; From 91e4cd5eb16a050efb80739596b5d31a814b1596 Mon Sep 17 00:00:00 2001 From: Strencher <46447572+Strencher@users.noreply.github.com> Date: Fri, 30 Sep 2022 01:00:52 +0200 Subject: [PATCH 2/2] Several Fixes - Fix error banner style - Cleanup updater code - Make notice api show in crash screens Co-authored-by: Zack Rauen --- renderer/src/modules/addonupdater.js | 43 ++++++++++------------------ renderer/src/modules/pluginapi.js | 4 +-- renderer/src/styles/ui/addonlist.css | 16 +++++------ renderer/src/ui/notices.js | 17 ++++++++--- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/renderer/src/modules/addonupdater.js b/renderer/src/modules/addonupdater.js index 147e6130..91114cf6 100644 --- a/renderer/src/modules/addonupdater.js +++ b/renderer/src/modules/addonupdater.js @@ -1,26 +1,22 @@ -import {Config} from "data"; -import fileSystem from "fs"; -import path from "path"; 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"; - +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) => { + request(url, (error, _, body) => { if (error) return resolve([]); resolve(JSON.parse(body)); }); @@ -40,19 +36,15 @@ export default class AddonUpdater { 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(); } @@ -61,7 +53,6 @@ export default class AddonUpdater { } 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; @@ -71,22 +62,20 @@ export default class AddonUpdater { 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}!`); - }); - }); + 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() { - 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!`, { @@ -105,5 +94,3 @@ export default class AddonUpdater { }); } } - -window.updater = AddonUpdater; \ No newline at end of file diff --git a/renderer/src/modules/pluginapi.js b/renderer/src/modules/pluginapi.js index e9583b51..0e6b6dc7 100644 --- a/renderer/src/modules/pluginapi.js +++ b/renderer/src/modules/pluginapi.js @@ -155,7 +155,7 @@ BdApi.showToast = function(content, options = {}) { * @param {object} options Options for the notice. * @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color. * @param {Array<{label: string, onClick: function}>} [options.buttons] Buttons that should be added next to the notice text. - * @param {number} [options.timeout=10000] Timeout until the notice is closed. Won't fire if it's set to 0; + * @param {number} [options.timeout=0] Timeout until the notice is closed. Won't fire if it's set to 0; * @returns {function} A callback for closing the notice. Passing `true` as first parameter closes immediately without transitioning out. */ BdApi.showNotice = function (content, options = {}) { @@ -681,4 +681,4 @@ Object.freeze(BdApi.Patcher); Object.freeze(BdApi.Webpack); Object.freeze(BdApi.Webpack.Filters); -export default BdApi; \ No newline at end of file +export default BdApi; diff --git a/renderer/src/styles/ui/addonlist.css b/renderer/src/styles/ui/addonlist.css index 8dfeb747..2ad67c29 100644 --- a/renderer/src/styles/ui/addonlist.css +++ b/renderer/src/styles/ui/addonlist.css @@ -112,9 +112,10 @@ .bd-description-wrap .banner { padding: 5px; - border-left: 5px solid gray; + border: 2px solid gray; background: #26191E; - color: #C13A3A; + color: #ffffff; + font-weight: 700px; border-radius: 5px; font-size: 16px; display: flex; @@ -122,19 +123,18 @@ } .banner.banner-danger { - border-left-color: #C13A3A; - background: #26191E; - color: #C13A3A; + border-color: #F04747; + background: #473C41; } .banner .bd-icon { - fill: red; + fill: #ffffff; margin-right: 5px; height: 16px !important; } .banner-danger .bd-icon { - fill: red; + fill: #F04747; } .bd-addon-list .bd-description { @@ -315,4 +315,4 @@ .bd-addon-list .bd-footer .bd-links .bd-addon-button { height: 24px; -} \ No newline at end of file +} diff --git a/renderer/src/ui/notices.js b/renderer/src/ui/notices.js index 3585d274..b914ceab 100644 --- a/renderer/src/ui/notices.js +++ b/renderer/src/ui/notices.js @@ -1,7 +1,8 @@ -import {WebpackModules} from "modules"; +import {Utilities, WebpackModules} from "modules"; export default class Notices { - static get baseClass() {return this.__baseClass || (this.__baseClass = WebpackModules.getByProps("container", "base")?.base);} + static get baseClass() {return this.__baseClass ??= WebpackModules.getByProps("container", "base")?.base;} + static get errorPageClass() {return this.__errorPageClass ??= WebpackModules.getByProps("errorPage")?.errorPage;} /** Shorthand for `type = "info"` for {@link module:Notices.show} */ static info(content, options = {}) {return this.show(content, Object.assign({}, options, {type: "info"}));} @@ -32,7 +33,7 @@ export default class Notices { * @param {object} options Options for the notice. * @param {string} [options.type="info" | "error" | "warning" | "success"] Type for the notice. Will affect the color. * @param {Array<{label: string, onClick: (immediately?: boolean = false) => void}>} [options.buttons] Buttons that should be added next to the notice text. - * @param {number} [options.timeout=10000] Timeout until the toast is closed. Won't fire if it's set to 0; + * @param {number} [options.timeout=0] Timeout until the toast is closed. Won't fire if it's set to 0; * @returns {(immediately?: boolean = false) => void} */ static show(content, options = {}) { @@ -83,6 +84,14 @@ export default class Notices { }); container.prepend(noticeContainer); + Utilities.onRemoved(container, async () => { + if (!this.errorPageClass) return; + + const element = await new Promise(res => Utilities.onAdded(`.${this.errorPageClass}`, res)); + + element.prepend(noticeContainer); + }); + return true; } -} \ No newline at end of file +}