diff --git a/renderer/src/modules/core.js b/renderer/src/modules/core.js index 283b76a8..9dc9931d 100644 --- a/renderer/src/modules/core.js +++ b/renderer/src/modules/core.js @@ -52,6 +52,8 @@ export default new class Core { Logger.log("Startup", "Initializing Editor"); await Editor.initialize(); + Modals.initialize(); + Logger.log("Startup", "Initializing Builtins"); for (const module in Builtins) { Builtins[module].initialize(); @@ -148,4 +150,4 @@ export default new class Core { }); } } -}; \ No newline at end of file +}; diff --git a/renderer/src/styles/ui/modal.css b/renderer/src/styles/ui/modal.css new file mode 100644 index 00000000..1421e5c9 --- /dev/null +++ b/renderer/src/styles/ui/modal.css @@ -0,0 +1,73 @@ +.bd-modal-wrapper { + position: absolute; + z-index: 1000; + width: 100vw; + height: 100vh; +} + +.bd-backdrop { + width: 100%; + height: 100%; + background: rgba(0,0,0, .6); + position: absolute; +} + +.bd-modal { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 33%; +} + +.bd-modal-inner { + background: var(--background-primary); + border-radius: 4px; + overflow: hidden; + animation: bd-modal-open ease-out; + animation-duration: 300ms; +} + +.bd-modal-wrapper.closing .bd-modal-inner { + animation: bd-modal-close ease-in; + animation-duration: 300ms; +} + +.bd-modal .footer { + display: flex; + justify-content: flex-end; + padding: 15px; + background: var(--background-secondary); +} + +.bd-modal-body { + padding: 20px 15px; + padding-top: 0; +} + +.bd-modal .header { + padding: 15px; +} + +.bd-modal .title { + font-size: 22px; + color: #fff; + font-weight: 600; +} + +.bd-modal-body { + color: #fff; +} + +.bd-modal .footer .bd-button { + min-width: 80px; + height: 38px; +} + +@keyframes bd-modal-close { + to {transform: scale(0.7);} +} + +@keyframes bd-modal-open { + from {transform: scale(0.7);} +} diff --git a/renderer/src/ui/errorboundary.jsx b/renderer/src/ui/errorboundary.jsx index 5b696bb5..af27c9da 100644 --- a/renderer/src/ui/errorboundary.jsx +++ b/renderer/src/ui/errorboundary.jsx @@ -1,26 +1,27 @@ -import Logger from "common/logger"; -import {React, IPC} from "modules"; - -export default class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = {hasError: false}; - } - - componentDidCatch() { - this.setState({hasError: true}); - } - - render() { - if (this.state.hasError) return
IPC.openDevTools()} className="react-error">There was an unexpected Error. Click to open console for more details.
; - return this.props.children; - } -} - -const originalRender = ErrorBoundary.prototype.render; -Object.defineProperty(ErrorBoundary.prototype, "render", { - enumerable: false, - configurable: false, - set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");}, - get: () => originalRender -}); \ No newline at end of file +import Logger from "common/logger"; +import {React, IPC} from "modules"; + +export default class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = {hasError: false}; + } + + componentDidCatch(error) { + this.setState({hasError: true}); + if (typeof this.props.onError === "function") this.props.onError(error); + } + + render() { + if (this.state.hasError) return
IPC.openDevTools()} className="react-error">There was an unexpected Error. Click to open console for more details.
; + return this.props.children; + } +} + +const originalRender = ErrorBoundary.prototype.render; +Object.defineProperty(ErrorBoundary.prototype, "render", { + enumerable: false, + configurable: false, + set: function() {Logger.warn("ErrorBoundary", "Addon policy for plugins #5 https://github.com/BetterDiscord/BetterDiscord/wiki/Addon-Policies#plugins");}, + get: () => originalRender +}); diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index 674f522d..e68ca722 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -1,6 +1,6 @@ import {Config} from "data"; import Logger from "common/logger"; -import {WebpackModules, React, Settings, Strings, DOMManager, DiscordModules} from "modules"; +import {WebpackModules, React, ReactDOM, Settings, Strings, DOMManager, DiscordModules} from "modules"; import FormattableString from "../structs/string"; import AddonErrorModal from "./addonerrormodal"; import ErrorBoundary from "./errorboundary"; @@ -11,23 +11,38 @@ export default class Modals { static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");} static get ModalActions() { - return { + return this._ModalActions ??= { openModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback") && m?.toString().includes("Layer")), closeModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback()")) }; } - static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");} - static get ModalComponents() {return WebpackModules.getByProps("Header", "Footer");} - static get ModalRoot() {return WebpackModules.getModule(m => m?.toString().includes("ENTERING"));} - static get ModalClasses() {return WebpackModules.getByProps("modal", "content");} - static get FlexElements() {return WebpackModules.getByProps("Child", "Align");} - static get FormTitle() {return WebpackModules.getByProps("Tags", "Sizes");} - static get TextElement() {return WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);} - static get ConfirmationModal() {return WebpackModules.getModule(m => m?.toString()?.includes("confirmText"));} - static get Markdown() {return WebpackModules.find(m => m?.prototype?.render && m.rules);} - static get Buttons() {return WebpackModules.getByProps("BorderColors");} + static get ModalStack() {return this._ModalStack ??= WebpackModules.getByProps("push", "update", "pop", "popWithKey");} + static get ModalComponents() {return this._ModalComponents ??= WebpackModules.getByProps("Header", "Footer");} + static get ModalRoot() {return this._ModalRoot ??= WebpackModules.getModule(m => m?.toString().includes("ENTERING"));} + static get ModalClasses() {return this._ModalClasses ??= WebpackModules.getByProps("modal", "content");} + static get FlexElements() {return this._FlexElements ??= WebpackModules.getByProps("Child", "Align");} + static get FormTitle() {return this._FormTitle ??= WebpackModules.getByProps("Tags", "Sizes");} + static get TextElement() {return this._TextElement ??= WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);} + static get ConfirmationModal() {return this._ConfirmationModal ??= WebpackModules.getModule(m => m?.toString()?.includes(".confirmButtonColor"));} + static get Markdown() {return this._Markdown ??= WebpackModules.find(m => m?.prototype?.render && m.rules);} + static get Buttons() {return this._Buttons ??= WebpackModules.getByProps("BorderColors");} + static get ModalQueue() {return this._ModalQueue ??= [];} + + static get hasModalOpen() {return !!document.getElementsByClassName("bd-modal").length;} - static default(title, content) { + static async initialize() { + const names = ["ModalActions", "Markdown", "ModalRoot", "ModalComponents", "Buttons", "TextElement", "FlexElements"]; + + for (const name of names) { + const value = this[name]; + + if (!value) { + Logger.warn("Modals", `Missing ${name} module!`); + } + } + } + + static default(title, content, buttons = []) { const modal = DOMManager.parseHTML(`
-
- ${content} -
+
- +
`); - modal.querySelector(".footer button").addEventListener("click", () => { + + const handleClose = () => { modal.classList.add("closing"); - setTimeout(() => {modal.remove();}, 300); - }); - modal.querySelector(".bd-backdrop").addEventListener("click", () => { - modal.classList.add("closing"); - setTimeout(() => {modal.remove();}, 300); - }); - document.querySelector("#app-mount").append(modal); + setTimeout(() => { + modal.remove(); + + const next = this.ModalQueue.shift(); + if (!next) return; + + next(); + }, 300); + }; + + if (!buttons.length) { + buttons.push({ + label: Strings.Modals.okay, + action: handleClose + }); + } + + const buttonContainer = modal.querySelector(".footer"); + for (const button of buttons) { + const buttonEl = Object.assign(document.createElement("button"), { + onclick: (e) => { + try {button.action(e);} catch (error) {console.error(error);} + + handleClose(); + }, + type: "button", + className: "bd-button" + }); + + if (button.danger) buttonEl.classList.add("bd-button-danger") + + buttonEl.append(button.label); + buttonContainer.appendChild(buttonEl); + } + + if (Array.isArray(content) ? content.every(el => React.isValidElement(el)) : React.isValidElement(content)) { + const container = modal.querySelector(".scroller"); + + try { + ReactDOM.render(content, container); + } catch (error) { + container.append(DOMManager.parseHTML(`There was an unexpected error. Modal could not be rendered.`)); + } + + DOMManager.onRemoved(container, () => { + ReactDOM.unmountComponentAtNode(container); + }); + } else { + modal.querySelector(".scroller").append(content); + } + + modal.querySelector(".footer button").addEventListener("click", handleClose); + modal.querySelector(".bd-backdrop").addEventListener("click", handleClose); + + const handleOpen = () => document.getElementById("app-mount").append(modal); + + if (this.hasModalOpen) { + this.ModalQueue.push(handleOpen); + } else { + handleOpen(); + } } static alert(title, content) { @@ -80,25 +146,41 @@ export default class Modals { const Markdown = this.Markdown; const ConfirmationModal = this.ConfirmationModal; const ModalActions = this.ModalActions; + if (content instanceof FormattableString) content = content.toString(); - if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) return this.default(title, content); const emptyFunction = () => {}; const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options; + if (!this.ModalActions || !this.ConfirmationModal || !this.Markdown) return this.default(title, content, [ + confirmText && {label: confirmText, action: onConfirm}, + cancelText && {label: cancelText, action: onCancel, danger} + ].filter(Boolean)); + if (!Array.isArray(content)) content = [content]; content = content.map(c => typeof(c) === "string" ? React.createElement(Markdown, null, c) : c); - return ModalActions.openModal(props => { - return React.createElement(ConfirmationModal, Object.assign({ + let modalKey = ModalActions.openModal(props => { + return React.createElement(ErrorBoundary, { + onError: () => { + setTimeout(() => { + ModalActions.closeModal(modalKey); + this.default(title, content, [ + confirmText && {label: confirmText, action: onConfirm}, + cancelText && {label: cancelText, action: onCancel, danger} + ].filter(Boolean)); + }); + } + }, React.createElement(ConfirmationModal, Object.assign({ header: title, confirmButtonColor: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND, confirmText: confirmText, cancelText: cancelText, onConfirm: onConfirm, onCancel: onCancel - }, props), content); + }, props), React.createElement(ErrorBoundary, {}, content))); }, {modalKey: key}); + return modalKey; } static showAddonErrors({plugins: pluginErrors = [], themes: themeErrors = []}) { @@ -110,7 +192,7 @@ export default class Modals { } this.addonErrorsRef = React.createRef(); - this.ModalActions.openModal(props => React.createElement(this.ModalRoot, Object.assign(props, { + this.ModalActions.openModal(props => React.createElement(ErrorBoundary, null, React.createElement(this.ModalRoot, Object.assign(props, { size: "medium", className: "bd-error-modal", children: [ @@ -127,7 +209,7 @@ export default class Modals { className: "bd-button" }, Strings.Modals.okay)) ] - }))); + })))); } static showChangelogModal(options = {}) { @@ -179,14 +261,14 @@ export default class Modals { const originalRoot = OriginalModalClasses.root; if (originalRoot) OriginalModalClasses.root = `${originalRoot} bd-changelog-modal`; const key = ModalActions.openModal(props => { - return React.createElement(Changelog, Object.assign({ + return React.createElement(ErrorBoundary, null, React.createElement(Changelog, Object.assign({ className: `bd-changelog ${ChangelogClasses.container}`, selectable: true, onScroll: _ => _, onClose: _ => _, renderHeader: renderHeader, renderFooter: renderFooter, - }, props), changelogItems); + }, props), changelogItems)); }); const closeModal = ModalActions.closeModal; @@ -241,7 +323,7 @@ export default class Modals { }; return this.ModalActions.openModal(props => { - return React.createElement(modal, props); + return React.createElement(ErrorBoundary, null, React.createElement(modal, props)); }); } -} \ No newline at end of file +} diff --git a/renderer/src/ui/toasts.js b/renderer/src/ui/toasts.js index fee5f004..121c45ec 100644 --- a/renderer/src/ui/toasts.js +++ b/renderer/src/ui/toasts.js @@ -64,7 +64,7 @@ export default class Toasts { const form = container ? container.querySelector("form") : null; const left = container ? container.getBoundingClientRect().left : 310; const right = memberlist ? memberlist.getBoundingClientRect().left : 0; - const width = right ? right - container.getBoundingClientRect().left : container.offsetWidth; + const width = right ? right - container.getBoundingClientRect().left : (container?.offsetWidth ?? document.body.offsetWidth / 2); const bottom = form ? form.offsetHeight : 80; const toastWrapper = document.createElement("div"); toastWrapper.classList.add("bd-toasts"); @@ -73,4 +73,4 @@ export default class Toasts { toastWrapper.style.setProperty("bottom", bottom + "px"); DOMManager.bdBody.appendChild(toastWrapper); } -} \ No newline at end of file +}