diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index 358b6551..4c945c82 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -201,7 +201,7 @@ export default class AddonManager { const addon = __non_webpack_require__(path.resolve(this.addonFolder, filename)); } catch (error) { - return new AddonError(filename, filename, Strings.Addons.compileError, {message: error.message, stack: error.stack}); + return new AddonError(filename, filename, Strings.Addons.compileError, {message: error.message, stack: error.stack}, this.prefix); } const addon = __non_webpack_require__(path.resolve(this.addonFolder, filename)); @@ -209,7 +209,7 @@ export default class AddonManager { // await Promise.resolve(addon); // addon = __non_webpack_require__(path.resolve(this.addonFolder, filename)); // console.log(addon); - 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})); + 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; diff --git a/renderer/src/modules/core.js b/renderer/src/modules/core.js index 3b801b15..d1e58d59 100644 --- a/renderer/src/modules/core.js +++ b/renderer/src/modules/core.js @@ -153,7 +153,7 @@ export default new class Core { 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: "" + cancelText: null }); } } diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index f0584649..cc895a47 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -66,9 +66,10 @@ export default new class PluginManager extends AddonManager { unloadPlugin(idOrFileOrAddon) {return this.unloadAddon(idOrFileOrAddon);} loadPlugin(filename) {return this.loadAddon(filename);} - loadAddon(filename) { + loadAddon(filename, shouldCTE = true) { const error = super.loadAddon(filename); - if (error) Modals.showAddonErrors({plugins: [error]}); + if (error && shouldCTE) Modals.showAddonErrors({plugins: [error]}); + return error; } reloadPlugin(idOrFileOrAddon) { @@ -79,7 +80,7 @@ export default new class PluginManager extends AddonManager { /* Overrides */ initializeAddon(addon) { - if (!addon.exports) return new AddonError(addon.name, addon.filename, "Plugin had no exports", {message: "Plugin had no exports or no name property.", stack: ""}); + if (!addon.exports) return new AddonError(addon.name, addon.filename, "Plugin had no exports", {message: "Plugin had no exports or no name property.", stack: ""}, this.prefix); try { const PluginClass = addon.exports; const thePlugin = new PluginClass(); @@ -93,10 +94,10 @@ export default new class PluginManager extends AddonManager { } catch (error) { this.state[addon.id] = false; - return new AddonError(addon.name, addon.filename, "load() could not be fired.", {message: error.message, stack: error.stack}); + return new AddonError(addon.name, addon.filename, "load() could not be fired.", {message: error.message, stack: error.stack}, this.prefix); } } - catch (error) {return new AddonError(addon.name, addon.filename, "Could not be constructed.", {message: error.message, stack: error.stack});} + catch (error) {return new AddonError(addon.name, addon.filename, "Could not be constructed.", {message: error.message, stack: error.stack}, this.prefix);} } getFileModification(module, fileContent, meta) { @@ -138,7 +139,7 @@ export default new class PluginManager extends AddonManager { this.state[addon.id] = false; Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version})); Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err); - return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}); + return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix); } this.emit("started", addon.id); Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version})); @@ -155,7 +156,7 @@ export default new class PluginManager extends AddonManager { this.state[addon.id] = false; Toasts.error(Strings.Addons.couldNotStop.format({name: addon.name, version: addon.version})); Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err); - return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "stop()"}), {message: err.message, stack: err.stack}); + return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "stop()"}), {message: err.message, stack: err.stack}, this.prefix); } this.emit("stopped", addon.id); Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version})); diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js index f9a6de1e..32dd1e2b 100644 --- a/renderer/src/modules/thememanager.js +++ b/renderer/src/modules/thememanager.js @@ -47,9 +47,10 @@ export default new class ThemeManager extends AddonManager { loadTheme(filename) {return this.loadAddon(filename);} reloadTheme(idOrFileOrAddon) {return this.reloadAddon(idOrFileOrAddon);} - loadAddon(filename) { + loadAddon(filename, shouldCTE = true) { const error = super.loadAddon(filename); - if (error) Modals.showAddonErrors({themes: [error]}); + if (error && shouldCTE) Modals.showAddonErrors({themes: [error]}); + return error; } /* Overrides */ diff --git a/renderer/src/structs/addonerror.js b/renderer/src/structs/addonerror.js index 65b01670..a3b8a89f 100644 --- a/renderer/src/structs/addonerror.js +++ b/renderer/src/structs/addonerror.js @@ -1,8 +1,9 @@ export default class AddonError extends Error { - constructor(name, filename, message, error) { + constructor(name, filename, message, error, type) { super(message); this.name = name; this.file = filename; this.error = error; + this.type = type; } } \ No newline at end of file diff --git a/renderer/src/structs/builtin.js b/renderer/src/structs/builtin.js index 432d41fc..f3c6358c 100644 --- a/renderer/src/structs/builtin.js +++ b/renderer/src/structs/builtin.js @@ -90,6 +90,10 @@ export default class BuiltinModule { return Patcher.before(this.name, object, func, callback); } + instead(object, func, callback) { + return Patcher.instead(this.name, object, func, callback); + } + after(object, func, callback) { return Patcher.after(this.name, object, func, callback); } diff --git a/renderer/src/styles/ui/addonerror.css b/renderer/src/styles/ui/addonerror.css new file mode 100644 index 00000000..a970aba7 --- /dev/null +++ b/renderer/src/styles/ui/addonerror.css @@ -0,0 +1,44 @@ +.be-modal .tab-bar.TOP .tab-bar-item:nth-child(1) { + margin-left: 20px; +} + +.bd-addon-error { + margin: 10px; + border-radius: 4px; + overflow: hidden; +} + +.bd-addon-error-header { + display: flex; + padding: 10px; + background: rgba(0, 0, 0, 0.2); + align-items: center; +} + +.bd-addon-error-message { + font-weight: 700; +} + +.bd-addon-error-body { + padding: 10px; + background: var(--background-mobile-secondary); +} + +.bd-addon-error-header svg { + margin-right: 7px; +} + +.bd-addon-error-stack-header svg { + float: right; + transform: rotate(90deg); + transition: 0.4s; + color: #fff; +} + +.bd-addon-error-stack.opened svg { + transform: rotate(0deg); +} + +.bd-addon-error-stack-header { + color: #b9bbbe; +} diff --git a/renderer/src/styles/ui/modals.css b/renderer/src/styles/ui/modals.css index 2381f50a..af1d9f78 100644 --- a/renderer/src/styles/ui/modals.css +++ b/renderer/src/styles/ui/modals.css @@ -32,6 +32,14 @@ transform: translateZ(0); } +.bd-modal { + border-radius: 3px; + overflow: hidden; + display: flex; + flex-direction: column; + flex: 1; +} + .bd-modal-wrapper.closing .bd-backdrop { animation: bd-backdrop-closing 200ms linear; animation-fill-mode: forwards; @@ -66,7 +74,7 @@ transform: scale(1); } -.bd-modal-wrapper .bd-modal-inner { +/* .bd-modal .bd-modal-inner { display: flex; contain: layout; flex-direction: column; @@ -79,6 +87,14 @@ min-height: 200px; width: 440px; user-select: text; +} */ + +.bd-modal .bd-modal-inner { + display: flex; + flex-direction: column; + flex: 1; + max-height: 660px; + overflow-y: auto; } .bd-modal-wrapper .bd-content-modal .bd-modal-inner { @@ -86,7 +102,7 @@ width: 700px; } -.bd-modal-wrapper .header { +.bd-modal .header { background-color: #35393e; box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.2); padding: 12px 20px; @@ -97,7 +113,7 @@ line-height: 19px; } -.bd-modal-wrapper .bd-modal-body { +.bd-modal .bd-modal-body { background-color: #36393f; color: #fff; overflow: hidden; @@ -108,22 +124,22 @@ position: relative; } -.bd-modal-wrapper .scroller { +.bd-modal .scroller { padding: 10px; overflow-y: auto; } -.bd-modal-wrapper .bd-content-modal .bd-modal-body { +.bd-modal .bd-content-modal .bd-modal-body { padding: 0; } -.bd-modal-wrapper .footer { +.bd-modal .footer { display: flex; justify-content: flex-end; padding: 10px 20px; } -.bd-modal-wrapper .footer button { +.bd-modal .footer button { min-height: 32px; min-width: 60px; align-items: center; @@ -136,7 +152,7 @@ user-select: none; } -.bd-modal-wrapper .tab-bar-container { +.bd-modal .tab-bar-container { align-items: center; border-bottom: 0; background: rgba(0, 0, 0, 0.2); @@ -147,7 +163,7 @@ margin-bottom: 15px; } -.bd-modal-wrapper .tab-bar.TOP { +.bd-modal .tab-bar.TOP { margin: 0; border: 0; display: flex; @@ -156,30 +172,30 @@ align-items: center; } -.bd-modal-wrapper .tab-bar-container .tab-bar-item { - margin: 0 15px; - padding: 15px 0; - color: #fff; - opacity: 0.5; - transition: opacity 200ms ease; - border-bottom: 2px solid transparent; -} - -.bd-modal-wrapper .tab-bar-container .tab-bar-item:hover { - border-color: #fff; +.bd-modal .tab-bar-container .tab-bar-item { + margin: 10px; + padding: 7px 10px; + border-radius: 5px; + opacity: 0.7; cursor: pointer; } -.bd-modal-wrapper .tab-bar-container .tab-bar-item.selected { +.bd-modal .tab-bar-item:not(.selected):hover { + background: var(--background-primary); +} + +.bd-modal .tab-bar.TOP .tab-bar-item:nth-child(1) { + margin-left: 20px; +} + +.bd-modal .tab-bar-container .tab-bar-item.selected { opacity: 1; - border-color: #fff; + background: #36393f; + border-radius: 5px; + font-weight: 600; } -.bd-modal-wrapper .tab-bar.TOP .tab-bar-item + .tab-bar-item { - margin: 0; -} - -.bd-modal-wrapper .table-header { +.bd-modal .table-header { display: flex; justify-content: space-between; color: #fff; @@ -190,23 +206,23 @@ font-size: 14px; } -.bd-modal-wrapper .table-column { +.bd-modal .table-column { width: 25%; word-wrap: break-word; } -.bd-modal-wrapper .table-column.column-error { +.bd-modal .table-column.column-error { width: 50%; } -.bd-modal-wrapper .errors { +.bd-modal .errors { display: flex; flex-direction: column; font-size: 14px; padding: 0 5px; } -.bd-modal-wrapper .error { +.bd-modal .error { display: flex; color: #fff; border-bottom: 1px solid rgba(255, 255, 255, 0.25); @@ -214,12 +230,12 @@ align-items: center; } -.bd-modal-wrapper .error-link { +.bd-modal .error-link { color: #3e82e5; font-weight: 500; } -.bd-modal-wrapper .bd-content-modal .scroller { +.bd-modal .bd-content-modal .scroller { padding-top: 0; } diff --git a/renderer/src/ui/addonerrormodal.jsx b/renderer/src/ui/addonerrormodal.jsx new file mode 100644 index 00000000..7a703dc2 --- /dev/null +++ b/renderer/src/ui/addonerrormodal.jsx @@ -0,0 +1,122 @@ +import {React, Strings, WebpackModules} from "modules"; +import DownArrow from "./icons/downarrow"; +import Extension from "./icons/extension"; +import ThemeIcon from "./icons/theme"; + +const Parser = Object(WebpackModules.getByProps("defaultRules", "parse")).defaultRules; + +const joinClassNames = (...classNames) => classNames.filter(e => e).join(" "); + +class Collapse extends React.Component { + constructor(props) { + super(props); + this.state = {opened: false}; + } + + toggle() { + if (!this.props.error.stack) return; + this.setState({opened: !this.state.opened}); + } + + render() { + const title = this.props.error.error ? this.props.error.message : this.props.error.message; + const stack = this.props.error.error && this.props.error.error.stack; + + return
+
{this.toggle();}} className="bd-addon-error-stack-header"> + {!this.state.opened && title} + {stack + ? <> + + {this.state.opened &&
{Parser ? Parser.codeBlock.react({content: stack, lang: "js"}, null, {}) : stack}
} + + : null} +
+
; + } +} + +export default class AddonErrorModal extends React.Component { + constructor(props) { + super(props); + + const tabs = this.getTabs(); + + this.state = { + selectedTab: tabs[0].id + }; + } + + mergeErrors(errors1 = [], errors2 = []) { + const list = []; + const allErrors = [...errors2, ...errors1]; + for (const error of allErrors) { + if (list.find(e => e.file === error.file)) continue; + list.push(error); + } + return list; + } + + refreshTabs(pluginErrors, themeErrors) { + this._tabs = null; + this.props.pluginErrors = this.mergeErrors(this.props.pluginErrors, pluginErrors); + this.props.themeErrors = this.mergeErrors(this.props.themeErrors, themeErrors); + this.forceUpdate(); + } + + generateTab(id, errors) { + return { + id: id, + name: Strings.Panels[id], + errors: errors + }; + } + + getTabs() { + return this._tabs || (this._tabs = [ + this.props.pluginErrors.length && this.generateTab("plugins", this.props.pluginErrors), + this.props.themeErrors.length && this.generateTab("themes", this.props.themeErrors) + ].filter(e => e)); + } + + renderError(err) { + return
+
+ {err.type == "plugin" ? : } +
{err.name} - {err.message}
+
+
+ +
+
; + } + + switchToTab(id) { + this.setState({selectedTab: id}); + } + + render() { + const selectedTab = this.getTabs().find(e => this.state.selectedTab === e.id); + const tabs = this.getTabs(); + return
+
+
{Strings.Modals.addonErrors}
+
+
+
+ {tabs.map(tab =>
{this.switchToTab(tab.id);}} className={joinClassNames("tab-bar-item", tab.id === selectedTab.id && "selected")}>{tab.name}
)} +
+
+
+
+ {selectedTab.errors.map(error => this.renderError(error))} +
+
+
+
+ +
+
+
; + } +} \ No newline at end of file diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index 7e601b46..ce05433d 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -2,6 +2,7 @@ import {Config} from "data"; import Logger from "common/logger"; import {WebpackModules, React, Settings, Strings, DOM, DiscordModules} from "modules"; import FormattableString from "../structs/string"; +import AddonErrorModal from "./addonerrormodal"; import ErrorBoundary from "./errorboundary"; export default class Modals { @@ -53,7 +54,7 @@ export default class Modals { } static alert(title, content) { - this.showConfirmationModal(title, content, {cancelText: ""}); + this.showConfirmationModal(title, content, {cancelText: null}); } /** @@ -97,78 +98,21 @@ export default class Modals { static showAddonErrors({plugins: pluginErrors = [], themes: themeErrors = []}) { if (!pluginErrors || !themeErrors || !this.shouldShowAddonErrors) return; if (!pluginErrors.length && !themeErrors.length) return; - const modal = DOM.createElement(`
-
- -
`); - - const generateTab = function(errors) { - const container = DOM.createElement(`
`); - for (const err of errors) { - const error = DOM.createElement(`
-
${err.name ? err.name : err.file}
-
${err.message}
- -
`); - container.append(error); - if (err.error) { - error.querySelectorAll("a").forEach(el => el.addEventListener("click", (e) => { - e.preventDefault(); - Logger.stacktrace("AddonError", `Error details for ${err.name ? err.name : err.file}.`, err.error); - })); - } - } - return container; - }; - - const tabs = [generateTab(pluginErrors), generateTab(themeErrors)]; - - modal.querySelectorAll(".tab-bar-item").forEach(el => el.addEventListener("click", (e) => { - e.preventDefault(); - const selected = modal.querySelector(".tab-bar-item.selected"); - if (selected) DOM.removeClass(selected, "selected"); - DOM.addClass(e.target, "selected"); - const scroller = modal.querySelector(".scroller"); - scroller.innerHTML = ""; - scroller.append(tabs[DOM.index(e.target)]); - })); - - modal.querySelector(".footer button").addEventListener("click", () => { - DOM.addClass(modal, "closing"); - setTimeout(() => {modal.remove();}, 300); - }); - modal.querySelector(".bd-backdrop").addEventListener("click", () => { - DOM.addClass(modal, "closing"); - setTimeout(() => {modal.remove();}, 300); - }); - DOM.query("#app-mount").append(modal); - if (pluginErrors.length) modal.querySelector(".tab-bar-item").click(); - else modal.querySelectorAll(".tab-bar-item")[1].click(); + this.addonErrorsRef = React.createRef(); + this.ModalActions.openModal(props => React.createElement(this.ModalComponents.ModalRoot, Object.assign(props, { + size: "medium", + children: React.createElement(AddonErrorModal, { + ref: this.addonErrorsRef, + pluginErrors: Array.isArray(pluginErrors) ? pluginErrors : [], + themeErrors: Array.isArray(themeErrors) ? themeErrors : [], + onClose: props.onClose + }) + }))); } static showChangelogModal(options = {}) {