diff --git a/renderer/src/modules/pluginapi.js b/renderer/src/modules/pluginapi.js index cc8f5f0a..b490f0eb 100644 --- a/renderer/src/modules/pluginapi.js +++ b/renderer/src/modules/pluginapi.js @@ -5,6 +5,7 @@ import DiscordModules from "./discordmodules"; import DataStore from "./datastore"; import DOMManager from "./dommanager"; import Toasts from "../ui/toasts"; +import Notices from "../ui/notices"; import Modals from "../ui/modals"; import PluginManager from "./pluginmanager"; import ThemeManager from "./thememanager"; @@ -113,6 +114,19 @@ BdApi.showToast = function(content, options = {}) { Toasts.show(content, options); }; +/** + * Show a notice above discord's chat layer. + * @param {string|Node} content Content of the notice + * @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 notice is closed. Won't fire if it's set to 0; + * @returns {(immediately?: boolean = false) => void} + */ + BdApi.showNotice = function (content, options = {}) { + return Notices.show(content, options); +}; + // Finds module BdApi.findModule = function(filter) { return WebpackModules.getModule(filter); diff --git a/renderer/src/styles/ui/notices.css b/renderer/src/styles/ui/notices.css new file mode 100644 index 00000000..a12c7a45 --- /dev/null +++ b/renderer/src/styles/ui/notices.css @@ -0,0 +1,94 @@ +.bd-notice-success { + --color: #3ba55d; +} + +.bd-notice-error { + --color: #ED4245; +} + +.bd-notice-info { + --color: #4A8FE1; +} + +.bd-notice-warning { + --color: #FAA81A; +} + +.bd-notice-closing { + transition: height 400ms ease; + height: 0 !important; +} + +@keyframes bd-open-notice { + from { + height: 0; + } +} + +.bd-notice { + animation: bd-open-notice 400ms ease; + overflow: hidden; + height: 36px; + font-size: 14px; + line-height: 36px; + font-weight: 500; + text-align: center; + position: relative; + padding-left: 4px; + padding-right: 28px; + z-index: 101; + flex-shrink: 0; + flex-grow: 0; + box-shadow: var(--elevation-low); + color: #fff; + background: var(--color, var(--brand-experiment-600, #3C45A5)); +} + +.bd-notice:first-child { + border-radius: 8px 0 0; +} + +.bd-notice-close { + position: absolute; + top: 0; + right: 0; + width: 36px; + height: 36px; + background: url(https://discord.com/assets/7731c77d99babca1a8faec204d98c380.svg) no-repeat; + background-position: 50% 55%; + background-size: 10px 10px; + opacity: .5; + transition: opacity .2s; + cursor: pointer; + -webkit-app-region: no-drag; +} + +.bd-notice-button { + font-size: 14px; + font-weight: 500; + position: relative; + top: 6px; + border: 1px solid; + color: #fff; + border-radius: 3px; + height: 24px; + padding: 0 10px; + box-sizing: border-box; + display: inline-block; + vertical-align: top; + margin-left: 10px; + line-height: 22px; + transition: background-color .2s ease,color .2s ease,border-color .2s ease; + -webkit-app-region: no-drag; + border-color: #fff; + background: transparent; +} + +.bd-notice-button:hover { + color: var(--color, var(--background-mobile-primary)); + background: #fff; +} + +.bd-notice-close:hover { + opacity: 1; +} \ No newline at end of file diff --git a/renderer/src/ui/notices.js b/renderer/src/ui/notices.js new file mode 100644 index 00000000..0fe6941e --- /dev/null +++ b/renderer/src/ui/notices.js @@ -0,0 +1,87 @@ +import {WebpackModules} from "modules"; + +export default class Notices { + static get baseClass() {return this.__baseClass || (this.__baseClass = WebpackModules.getByProps("container", "base")?.base);} + + /** Shorthand for `type = "info"` for {@link module:Notices.show} */ + static info(content, options = {}) {return this.show(content, Object.assign({}, options, {type: "info"}));} + /** Shorthand for `type = "warning"` for {@link module:Notices.show} */ + static warn(content, options = {}) {return this.show(content, Object.assign({}, options, {type: "warning"}));} + /** Shorthand for `type = "error"` for {@link module:Notices.show} */ + static error(content, options = {}) {return this.show(content, Object.assign({}, options, {type: "error"}));} + /** Shorthand for `type = "success"` for {@link module:Notices.show} */ + static success(content, options = {}) {return this.show(content, Object.assign({}, options, {type: "success"}));} + + static createElement(type, options = {}, ...children) { + const element = document.createElement(type); + Object.assign(element, options); + const filteredChildren = children.filter((n) => n); + + if (filteredChildren.length > 0) element.append(...filteredChildren); + + return element; + } + + static joinClassNames(...classNames) { + return classNames.filter((n) => n).join(" "); + } + + /** + * Show a notice above discord's chat layer. + * @param {string} content Content of the notice + * @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; + * @returns {(immediately?: boolean = false) => void} + */ + static show(content, options = {}) { + const {type, buttons = [], timeout = 0} = options; + const haveContainer = this.ensureContainer(); + if (!haveContainer) return; + + const closeNotification = function (immediately = false) { + // Immediately remove the notice without adding the closing class. + if (immediately) return noticeElement.remove(); + + noticeElement.classList.add("bd-notice-closing"); + setTimeout(() => {noticeElement.remove();}, 300); + }; + + const noticeElement = this.createElement("div", { + className: this.joinClassNames("bd-notice", type && `bd-notice-${type}`), + }, this.createElement("div", { + className: "bd-notice-close", + onclick: closeNotification.bind(null, false) + }), this.createElement("span", { + className: "bd-notice-content" + }, content), ...buttons.map((button) => { + if (!button || !button.label || typeof(button.onClick) !== "function") return null; + + return this.createElement("button", { + className: "bd-notice-button", + onclick: button.onClick.bind(null, closeNotification) + }, button.label); + })); + + document.getElementById("bd-notices").appendChild(noticeElement); + + if (timeout > 0) { + setTimeout(closeNotification, timeout); + } + + return closeNotification; + } + + static ensureContainer() { + if (document.getElementById("bd-notices")) return true; + const container = document.querySelector(`.${this.baseClass}`); + if (!container) return false; + const noticeContainer = this.createElement("div", { + id: "bd-notices" + }); + container.prepend(noticeContainer); + + return true; + } +} \ No newline at end of file