diff --git a/client/src/index.js b/client/src/index.js index 15d3844d..1256e4e2 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -8,7 +8,7 @@ * LICENSE file in the root directory of this source tree. */ -import { DOM, BdUI, BdMenu, Modals, Reflection } from 'ui'; +import { DOM, BdUI, BdMenu, Modals, Reflection, Toasts } from 'ui'; import BdCss from './styles/index.scss'; import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi } from 'modules'; import { ClientLogger as Logger, ClientIPC, Utils, IterableWeakCollections, IterableWeakMap, IterableWeakSet } from 'common'; @@ -27,7 +27,7 @@ class BetterDiscord { Logger.log('main', 'BetterDiscord starting'); this._bd = { - DOM, BdUI, BdMenu, Modals, Reflection, + DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js index 410eb527..5f7cbfbb 100644 --- a/client/src/modules/pluginapi.js +++ b/client/src/modules/pluginapi.js @@ -10,7 +10,7 @@ import { EmoteModule } from 'builtin'; import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'; -import { BdMenu, Modals, DOM, DOMObserver, Reflection, VueInjector } from 'ui'; +import { BdMenu, Modals, DOM, DOMObserver, Reflection, VueInjector, Toasts } from 'ui'; import * as CommonComponents from 'commoncomponents'; import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common'; import Settings from './settings'; @@ -305,6 +305,36 @@ export default class PluginApi { }); } + + /** + * Toasts + */ + showToast(message, options = {}) { + return Toasts.push(message, options); + } + showSuccessToast(message, options = {}) { + return Toasts.success(message, options); + } + showInfoToast(message, options = {}) { + return Toasts.info(message, options); + } + showErrorToast(message, options = {}) { + return Toasts.error(message, options); + } + showWarningToast(message, options = {}) { + return Toasts.warning(message, options); + } + get Toasts() { + return { + push: this.showToast.bind(this), + success: this.showSuccessToast.bind(this), + error: this.showErrorToast.bind(this), + info: this.showInfoToast.bind(this), + warning: this.showWarningToast.bind(this) + }; + } + + /** * Emotes */ diff --git a/client/src/modules/settings.js b/client/src/modules/settings.js index 9b539df2..403df8cc 100644 --- a/client/src/modules/settings.js +++ b/client/src/modules/settings.js @@ -8,6 +8,7 @@ * LICENSE file in the root directory of this source tree. */ +import { Toasts } from 'ui'; import { EmoteModule } from 'builtin'; import { SettingsSet } from 'structs'; import { FileUtils, ClientLogger as Logger } from 'common'; @@ -28,6 +29,7 @@ export default new class Settings { Logger.log('Settings', [`${set.id}/${category.id}/${setting.id} was changed from`, old_value, 'to', value]); Events.emit('setting-updated', event); Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event); + Toasts.success(`${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`); // Just for debugging purposes remove in prod }); set.on('settings-updated', async event => { diff --git a/client/src/styles/partials/animations.scss b/client/src/styles/partials/animations.scss index e4597d42..1287340a 100644 --- a/client/src/styles/partials/animations.scss +++ b/client/src/styles/partials/animations.scss @@ -49,6 +49,30 @@ } } +@keyframes bd-toast-up { + 0% { + transform: translateY(10px); + opacity: 0; + } + + 100% { + transform: translateY(0%); + opacity: 1; + } +} + +@keyframes bd-toast-down { + 0% { + transform: translateY(0%); + opacity: 1; + } + + 100% { + transform: translateY(10px); + opacity: 0; + } +} + @keyframes bd-fade-out { 0% { opacity: 1; diff --git a/client/src/styles/partials/index.scss b/client/src/styles/partials/index.scss index 0adc254d..c45015c8 100644 --- a/client/src/styles/partials/index.scss +++ b/client/src/styles/partials/index.scss @@ -13,3 +13,4 @@ @import './helpers.scss'; @import './misc.scss'; @import './emotes.scss'; +@import './toasts.scss'; diff --git a/client/src/styles/partials/toasts.scss b/client/src/styles/partials/toasts.scss new file mode 100644 index 00000000..0281240b --- /dev/null +++ b/client/src/styles/partials/toasts.scss @@ -0,0 +1,68 @@ +.bd-toasts { + display: flex; + position: fixed; + top: 0; + width: 700px; + left: 50%; + transform: translateX(-50%); + bottom: 100px; + flex-direction: column; + align-items: center; + justify-content: flex-end; + pointer-events: none; + z-index: 4000; + + .bd-toast { + position: relative; + animation: bd-toast-up 300ms ease; + background: #36393F; + padding: 10px; + border-radius: 5px; + box-shadow: 0 0 0 1px rgba(32,34,37,.6), 0 2px 10px 0 rgba(0,0,0,.2); + font-weight: 500; + color: #fff; + user-select: text; + font-size: 14px; + margin-top: 10px; + + &.bd-toast-error { + background: #f04747; + } + + &.bd-toast-info { + background: #4a90e2; + } + + &.bd-toast-warning { + background: #FFA600; + } + + &.bd-toast-success { + background: #43b581; + } + + &.bd-toast-has-icon { + padding-left: 30px; + } + } + + .bd-toast-icon { + position: absolute; + left: 5px; + top: 50%; + transform: translateY(-50%); + bottom: 0; + height: 20px; + width: 20px; + border-radius: 50%; + overflow: hidden; + + svg { + fill: white; + } + } + + .bd-toast.bd-toast-closing { + animation: bd-toast-down 300ms ease; + } +} diff --git a/client/src/ui/bdui.js b/client/src/ui/bdui.js index 7ff68d9c..4cf25833 100644 --- a/client/src/ui/bdui.js +++ b/client/src/ui/bdui.js @@ -12,7 +12,7 @@ import { Events, DiscordApi } from 'modules'; import { remote } from 'electron'; import DOM from './dom'; import Vue from './vue'; -import { BdSettingsWrapper, BdModals } from './components'; +import { BdSettingsWrapper, BdModals, BdToasts } from './components'; export default class { @@ -43,8 +43,15 @@ export default class { static injectUi() { DOM.createElement('div', null, 'bd-settings').appendTo(DOM.bdBody); DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals); + DOM.createElement('div', null, 'bd-toasts').appendTo(DOM.bdToasts); DOM.createElement('bd-tooltips').appendTo(DOM.bdBody); + this.toasts = new Vue({ + el: '#bd-toasts', + components: { BdToasts }, + template: '' + }); + this.modals = new Vue({ el: '#bd-modals', components: { BdModals }, diff --git a/client/src/ui/components/BdToasts.vue b/client/src/ui/components/BdToasts.vue new file mode 100644 index 00000000..87397ea9 --- /dev/null +++ b/client/src/ui/components/BdToasts.vue @@ -0,0 +1,32 @@ +/** + * BetterDiscord Toasts Component + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + + + + diff --git a/client/src/ui/components/common/MaterialIcon.js b/client/src/ui/components/common/MaterialIcon.js index 94c2c27c..a6991924 100644 --- a/client/src/ui/components/common/MaterialIcon.js +++ b/client/src/ui/components/common/MaterialIcon.js @@ -14,3 +14,6 @@ export { default as MiExtension } from './materialicons/Extension.vue'; export { default as MiError } from './materialicons/Error.vue'; export { default as MiDiscord } from './materialicons/Discord.vue'; export { default as MiStar } from './materialicons/Star.vue'; +export { default as MiInfo } from './materialicons/Info.vue'; +export { default as MiWarning } from './materialicons/Warning.vue'; +export { default as MiSuccess } from './materialicons/Success.vue'; diff --git a/client/src/ui/components/common/Toast.vue b/client/src/ui/components/common/Toast.vue new file mode 100644 index 00000000..cf93ad70 --- /dev/null +++ b/client/src/ui/components/common/Toast.vue @@ -0,0 +1,46 @@ +/** + * BetterDiscord Toast Component + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + + + + diff --git a/client/src/ui/components/common/index.js b/client/src/ui/components/common/index.js index 74fce6b4..c0241497 100644 --- a/client/src/ui/components/common/index.js +++ b/client/src/ui/components/common/index.js @@ -4,6 +4,7 @@ export { default as FormButton } from './FormButton.vue'; export { default as ButtonGroup } from './ButtonGroup.vue'; export { default as Button } from './Button.vue'; export { default as Modal } from './Modal.vue'; +export { default as Toast } from './Toast.vue'; export * from './MaterialIcon'; export { default as RefreshBtn } from './RefreshBtn.vue'; diff --git a/client/src/ui/components/common/materialicons/Info.vue b/client/src/ui/components/common/materialicons/Info.vue new file mode 100644 index 00000000..c5cd80af --- /dev/null +++ b/client/src/ui/components/common/materialicons/Info.vue @@ -0,0 +1,28 @@ +/** + * BetterDiscord Material Design Icon + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * Material Design Icons + * Copyright (c) 2014 Google + * Apache 2.0 LICENSE + * https://www.apache.org/licenses/LICENSE-2.0.txt +*/ + + + diff --git a/client/src/ui/components/common/materialicons/Success.vue b/client/src/ui/components/common/materialicons/Success.vue new file mode 100644 index 00000000..15d5548e --- /dev/null +++ b/client/src/ui/components/common/materialicons/Success.vue @@ -0,0 +1,28 @@ +/** + * BetterDiscord Material Design Icon + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * Material Design Icons + * Copyright (c) 2014 Google + * Apache 2.0 LICENSE + * https://www.apache.org/licenses/LICENSE-2.0.txt +*/ + + + diff --git a/client/src/ui/components/common/materialicons/Warning.vue b/client/src/ui/components/common/materialicons/Warning.vue new file mode 100644 index 00000000..7660ec27 --- /dev/null +++ b/client/src/ui/components/common/materialicons/Warning.vue @@ -0,0 +1,28 @@ +/** + * BetterDiscord Material Design Icon + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * Material Design Icons + * Copyright (c) 2014 Google + * Apache 2.0 LICENSE + * https://www.apache.org/licenses/LICENSE-2.0.txt +*/ + + + diff --git a/client/src/ui/components/index.js b/client/src/ui/components/index.js index 8f8e4a8c..3a22b4d8 100644 --- a/client/src/ui/components/index.js +++ b/client/src/ui/components/index.js @@ -1,3 +1,4 @@ export { default as BdSettingsWrapper } from './BdSettingsWrapper.vue'; export { default as BdSettings } from './BdSettings.vue'; export { default as BdModals } from './BdModals.vue'; +export { default as BdToasts } from './BdToasts.vue'; diff --git a/client/src/ui/dom.js b/client/src/ui/dom.js index 23f16424..6646a3f7 100644 --- a/client/src/ui/dom.js +++ b/client/src/ui/dom.js @@ -184,6 +184,9 @@ export default class DOM { static get bdThemes() { return this.getElement('bd-themes') || this.createElement('bd-themes').appendTo(this.bdHead) } static get bdTooltips() { return this.getElement('bd-tooltips') || this.createElement('bd-tooltips').appendTo(this.bdBody) } static get bdModals() { return this.getElement('bd-modals') || this.createElement('bd-modals').appendTo(this.bdBody) } + static get bdToasts() { + return this.getElement('bd-toasts') || this.createElement('bd-toasts').appendTo(this.bdBody); + } static getElement(e) { if (e instanceof BdNode) return e.element; diff --git a/client/src/ui/toasts.js b/client/src/ui/toasts.js new file mode 100644 index 00000000..1b572a54 --- /dev/null +++ b/client/src/ui/toasts.js @@ -0,0 +1,75 @@ +/** + * BetterDiscord Toasts + * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks + * All rights reserved. + * https://betterdiscord.net + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. +*/ + +let toasts = 0; + +export default class Toasts { + + /** + * This shows a popup message at the bottom of the screen similar to Android Toasts. This is useful for small user feedback. + * + * @param {string} message The message to be displayed in the toast + * @param {Object} options Options object. Optional parameter. + * @param {string} options.type Changes the type of the toast stylistically and semantically. Choices: "basic", "info", "success", "error", "warning". Default: "basic" + * @param {string} options.icon URL to custom icon to show in the toast. Having this overrides the default icon for the toast type. + * @param {string|Object|Array} options.additionalClasses Additional classes to add to the toast element. Can be used to style it. Optional. + * @param {number} options.timeout Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: 3000 + * @returns {Promise} This promise resolves when the toast is removed from the DOM. + */ + static async push(message, options = {}) { + const {type = 'basic', icon, additionalClasses, timeout = 3000} = options; + const toast = {id: toasts++, message, type, icon, additionalClasses, closing: false}; + this.stack.push(toast); + await new Promise(resolve => setTimeout(resolve, timeout)); + toast.closing = true; + await new Promise(resolve => setTimeout(resolve, 300)); + this.stack.splice(this.stack.indexOf(toast), 1); + } + + /** + * This is a shortcut for `type = "success"` in {@link Toasts#push}. The parameters and options are the same. + */ + static async success(message, options = {}) { + options.type = 'success'; + return this.push(message, options); + } + + /** + * This is a shortcut for `type = "error"` in {@link Toasts#push}. The parameters and options are the same. + */ + static async error(message, options = {}) { + options.type = 'error'; + return this.push(message, options); + } + + /** + * This is a shortcut for `type = "info"` in {@link Toasts#push}. The parameters and options are the same. + */ + static async info(message, options = {}) { + options.type = 'info'; + return this.push(message, options); + } + + /** + * This is a shortcut for `type = "warning"` in {@link Toasts#push}. The parameters and options are the same. + */ + static async warning(message, options = {}) { + options.type = 'warning'; + return this.push(message, options); + } + + /** + * An array of active toasts. + */ + static get stack() { + return this._stack || (this._stack = []); + } + +} diff --git a/client/src/ui/ui.js b/client/src/ui/ui.js index 90892da4..95cb30ed 100644 --- a/client/src/ui/ui.js +++ b/client/src/ui/ui.js @@ -2,6 +2,7 @@ export { default as DOM, DOMObserver, DOMManip } from './dom'; export { default as BdUI } from './bdui'; export { default as BdMenu, BdMenuItems } from './bdmenu'; export { default as Modals } from './modals'; +export { default as Toasts } from './toasts'; export { default as VueInjector } from './vueinjector'; export { default as Reflection } from './reflection';