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(`
@@ -37,26 +52,77 @@ export default class Modals {
-
+
`);
- 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
+}