diff --git a/.gitignore b/.gitignore index b258c97d..6264fedc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -dist/ -node_modules -.env \ No newline at end of file +dist/ +node_modules +.env +.idea/ \ No newline at end of file diff --git a/preload/src/api/electron.js b/preload/src/api/electron.js index f8c91558..50bfeac6 100644 --- a/preload/src/api/electron.js +++ b/preload/src/api/electron.js @@ -1,4 +1,4 @@ -import {ipcRenderer as IPC, shell} from "electron"; +import {ipcRenderer as IPC, shell, webUtils} from "electron"; export const ipcRenderer = { send: IPC.send.bind(IPC), @@ -9,4 +9,4 @@ export const ipcRenderer = { off: IPC.off.bind(IPC) }; -export {shell}; \ No newline at end of file +export {shell, webUtils}; \ No newline at end of file diff --git a/renderer/src/data/changelog.js b/renderer/src/data/changelog.js index 7d40a184..cba3b092 100644 --- a/renderer/src/data/changelog.js +++ b/renderer/src/data/changelog.js @@ -1,7 +1,12 @@ +import config from "./config"; + // fixed, improved, added, progress export default { + title: "BetterDiscord", + subtitle: `v${config.version}`, video: "https://www.youtube.com/embed/evyvq9eQTqA?si=opmzjGjUArT4VLrj&vq=hd720p&hd=1&rel=0&showinfo=0&mute=1&loop=1&autohide=1", - description: "A hotfix to get things going again. Plugins and Themes will take time to update.", + banner: "https://i.imgur.com/wuh5yMK.png", + blurb: "A hotfix to get things going again. Plugins and Themes will take time to update.", changes: [ { title: "Bugs Squashed", diff --git a/renderer/src/modules/api/index.js b/renderer/src/modules/api/index.js index 421c5a95..2a68654a 100644 --- a/renderer/src/modules/api/index.js +++ b/renderer/src/modules/api/index.js @@ -1,4 +1,4 @@ -import Logger from "@common/logger"; +import BDLogger from "@common/logger"; import PluginManager from "@modules/pluginmanager"; import ThemeManager from "@modules/thememanager"; @@ -15,6 +15,20 @@ import Webpack from "./webpack"; import * as Legacy from "./legacy"; import ContextMenu from "./contextmenu"; import fetch from "./fetch"; +import Logger from "./logger"; + +import ColorInput from "@ui/settings/components/color"; +import DropdownInput from "@ui/settings/components/dropdown"; +import SettingItem from "@ui/settings/components/item"; +import KeybindInput from "@ui/settings/components/keybind"; +import NumberInput from "@ui/settings/components/number"; +import RadioInput from "@ui/settings/components/radio"; +import SearchInput from "@ui/settings/components/search"; +import SliderInput from "@ui/settings/components/slider"; +import SwitchInput from "@ui/settings/components/switch"; +import TextInput from "@ui/settings/components/textbox"; +import SettingGroup from "@ui/settings/group"; +import ErrorBoundary from "@ui/errorboundary"; const bounded = new Map(); const PluginAPI = new AddonAPI(PluginManager); @@ -23,6 +37,7 @@ const PatcherAPI = new Patcher(); const DataAPI = new Data(); const DOMAPI = new DOM(); const ContextMenuAPI = new ContextMenu(); +const DefaultLogger = new Logger(); /** * `BdApi` is a globally (`window.BdApi`) accessible object for use by plugins and developers to make their lives easier. @@ -33,7 +48,7 @@ export default class BdApi { if (!pluginName) return BdApi; if (bounded.has(pluginName)) return bounded.get(pluginName); if (typeof(pluginName) !== "string") { - Logger.error("BdApi", "Plugin name not a string, returning generic API!"); + BDLogger.error("BdApi", "Plugin name not a string, returning generic API!"); return BdApi; } @@ -44,6 +59,7 @@ export default class BdApi { this.Patcher = new Patcher(pluginName); this.Data = new Data(pluginName); this.DOM = new DOM(pluginName); + this.Logger = new Logger(pluginName); bounded.set(pluginName, this); } @@ -57,7 +73,19 @@ export default class BdApi { get ReactUtils() {return ReactUtils;} get ContextMenu() {return ContextMenuAPI;} Components = { - get Tooltip() {return DiscordModules.Tooltip;} + get Tooltip() {return DiscordModules.Tooltip;}, + get ColorInput() {return ColorInput;}, + get DropdownInput() {return DropdownInput;}, + get SettingItem() {return SettingItem;}, + get KeybindInput() {return KeybindInput;}, + get NumberInput() {return NumberInput;}, + get RadioInput() {return RadioInput;}, + get SearchInput() {return SearchInput;}, + get SliderInput() {return SliderInput;}, + get SwitchInput() {return SwitchInput;}, + get TextInput() {return TextInput;}, + get SettingGroup() {return SettingGroup;}, + get ErrorBoundary() {return ErrorBoundary;}, }; Net = {fetch}; } @@ -125,12 +153,38 @@ BdApi.DOM = DOMAPI; */ BdApi.ContextMenu = ContextMenuAPI; +/** + * An set of react components plugins can make use of. + * @type Components + */ BdApi.Components = { - get Tooltip() {return DiscordModules.Tooltip;} + get Tooltip() {return DiscordModules.Tooltip;}, + get ColorInput() {return ColorInput;}, + get DropdownInput() {return DropdownInput;}, + get SettingItem() {return SettingItem;}, + get KeybindInput() {return KeybindInput;}, + get NumberInput() {return NumberInput;}, + get RadioInput() {return RadioInput;}, + get SearchInput() {return SearchInput;}, + get SliderInput() {return SliderInput;}, + get SwitchInput() {return SwitchInput;}, + get TextInput() {return TextInput;}, + get SettingGroup() {return SettingGroup;}, + get ErrorBoundary() {return ErrorBoundary;}, }; +/** + * An instance of {@link Net} for using network related tools. + * @type Net + */ BdApi.Net = {fetch}; +/** + * An instance of {@link Logger} for logging information. + * @type Logger + */ +BdApi.Logger = DefaultLogger; + Object.freeze(BdApi); Object.freeze(BdApi.Net); Object.freeze(BdApi.prototype); diff --git a/renderer/src/modules/api/logger.js b/renderer/src/modules/api/logger.js new file mode 100644 index 00000000..2d7c3169 --- /dev/null +++ b/renderer/src/modules/api/logger.js @@ -0,0 +1,130 @@ +/** + * Simple logger for the lib and plugins. + * + * @module Logger + * @version 0.1.0 + */ + +/* eslint-disable no-console */ + +/** + * List of logging types. + */ + +const LogTypes = { + error: "error", + debug: "debug", + log: "log", + warn: "warn", + info: "info" +}; + +const parseType = type => LogTypes[type] || "log"; + + +/** + * `Logger` is a helper class to log data in a nice and consistent way. An instance is available on {@link BdApi}. + * @type Logger + * @summary {@link Logger} is a simple utility for logging information. + * @name Logger + */ +class Logger { + + #pluginName = ""; + #nameStyle = "color: #3a71c1; font-weight: 700;"; + #messageStyle = ""; + + /** + * @param {string} pluginName - Name of the plugin + * @param {string} nameStyle - CSS to style the plugin name + * @param {string} messageStyle - CSS to style the main message + * @returns + */ + constructor(pluginName, nameStyle, messageStyle) { + if (!pluginName) return; + this.#pluginName = pluginName; + if (nameStyle) this.#nameStyle = nameStyle; + if (messageStyle) this.#messageStyle = messageStyle; + } + + /** + * Logs an error using a collapsed error group with stacktrace. + * + * @param {string} pluginName - Name of the calling module. + * @param {string} message - Message or error to have logged. + * @param {Error} error - Error object to log with the message. + */ + stacktrace(pluginName, message, error) { + if (this.#pluginName) { + error = message; + message = pluginName; + pluginName = this.#pluginName; + } + console.error(`%c[${pluginName}]%c ${message}\n\n%c`, this.#nameStyle, "color: red; font-weight: 700;", "color: red;", error); + } + + /** + * Logs an error message. + * + * @param {string} pluginName Name of the calling module + * @param {...any} message Messages to have logged. + */ + error(pluginName, ...message) {this.#_log(pluginName, message, "error");} + + /** + * Logs a warning message. + * + * @param {string} module - Name of the calling module. + * @param {...any} message - Messages to have logged. + */ + warn(pluginName, ...message) {this.#_log(pluginName, message, "warn");} + + /** + * Logs an informational message. + * + * @param {string} module - Name of the calling module. + * @param {...any} message - Messages to have logged. + */ + info(pluginName, ...message) {this.#_log(pluginName, message, "info");} + + /** + * Logs used for debugging purposes. + * + * @param {string} module - Name of the calling module. + * @param {...any} message - Messages to have logged. + */ + debug(pluginName, ...message) {this.#_log(pluginName, message, "debug");} + + /** + * Logs used for basic loggin. + * + * @param {string} module - Name of the calling module. + * @param {...any} message - Messages to have logged. + */ + log(pluginName, ...message) {this.#_log(pluginName, message);} + + /** + * Logs strings using different console levels and a module label. + * + * @param {string} module - Name of the calling module. + * @param {any|Array} message - Messages to have logged. + * @param {module:Logger.LogTypes} type - Type of log to use in console. + */ + #_log(pluginName, message, type = "log") { + type = parseType(type); + + // Normalize messages to be an array for later spreading + if (!Array.isArray(message)) message = message ? [message] : []; + + // If a name was set via constructor move the "name" to be part of the message + if (pluginName && this.#pluginName) message = [pluginName, ...message]; + + const displayName = this.#pluginName || pluginName; + console[type](`%c[${displayName}]%c`, this.#nameStyle, this.#messageStyle, ...message); + } +} + + +Object.freeze(Logger); +Object.freeze(Logger.prototype); +export default Logger; \ No newline at end of file diff --git a/renderer/src/modules/api/ui.js b/renderer/src/modules/api/ui.js index b4bac7b8..9ba2f8ab 100644 --- a/renderer/src/modules/api/ui.js +++ b/renderer/src/modules/api/ui.js @@ -4,6 +4,9 @@ import Modals from "@ui/modals"; import Toasts from "@ui/toasts"; import Notices from "@ui/notices"; import Tooltip from "@ui/tooltip"; +import Group, {buildSetting} from "@ui/settings/group"; +import React from "@modules/react"; +import ErrorBoundary from "@ui/errorboundary"; /** @@ -57,6 +60,34 @@ const UI = { return Modals.showConfirmationModal(title, content, options); }, + /** + * Shows a changelog modal in a similar style to Discord's. Customizable with images, videos, colored sections and supports markdown. + * + * The changes option is a array of objects that have this typing: + * ```ts + * interface Changes { + * title: string; + * type: "fixed" | "added" | "progress" | "changed"; + * items: Array; + * blurb?: string; + * } + * ``` + * + * @param {object} options Information to display in the modal + * @param {string} options.title Title to show in the modal header + * @param {string} options.subtitle Title to show below the main header + * @param {string} [options.blurb] Text to show in the body of the modal before the list of changes + * @param {string} [options.banner] URL to an image to display as the banner of the modal + * @param {string} [options.video] Youtube link or url of a video file to use as the banner + * @param {string} [options.poster] URL to use for the video freeze-frame poster + * @param {string|ReactElement|Array} [options.footer] What to show in the modal footer + * @param {Array} [options.changes] List of changes to show (see description for details) + * @returns {string} The key used for this modal. + */ + showChangelogModal(options) { + return Modals.showChangelogModal(options); + }, + /** * This shows a toast similar to android towards the bottom of the screen. * @@ -109,6 +140,77 @@ const UI = { if (data.error) throw new Error(data.error); return data; + }, + + /** + * Creates a single setting wrapped in a setting item that has a name and note. + * The shape of the object should match the props of the component you want to render, check the + * `BdApi.Components` section for details. Shown below are ones common to all setting types. + * @param {object} setting + * @param {string} setting.type One of: dropdown, number, switch, text, slider, radio, keybind, color, custom + * @param {string} setting.id Identifier to used for callbacks + * @param {string} setting.name Visual name to display + * @param {string} setting.note Visual description to display + * @param {any} setting.value Current value of the setting + * @param {ReactElement} [setting.children] Only used for "custom" type + * @param {CallableFunction} [setting.onChange] Callback when the value changes (only argument is new value) + * @param {boolean} [setting.disabled=false] Whether this setting is disabled + * @param {boolean} [setting.inline=true] Whether the input should render inline with the name (this is false by default for radio type) + * @returns A SettingItem with a an input as the child + */ + buildSettingItem(setting) { + return buildSetting(setting); + }, + + /** + * Creates a settings panel (react element) based on json-like data. + * + * The `settings` array here is an array of the same settings types described in `buildSetting` above. + * However, this API allows one additional setting "type" called `category`. This has the same properties + * as the Group React Component found under the `Components` API. + * + * `onChange` will always be given 3 arguments: category id, setting id, and setting value. In the case + * that you have settings on the "root" of the panel, the category id is `null`. Any `onChange` + * listeners attached to individual settings will fire before the panel-level change listener. + * + * `onDrawerToggle` is given 2 arguments: category id, and the current shown state. You can use this to + * save drawer states. + * + * `getDrawerState` is given 2 arguments: category id, and the default shown state. You can use this to + * recall a saved drawer state. + * + * @param {object} props + * @param {Array} props.settings Array of settings to show + * @param {CallableFunction} props.onChange Function called on every change + * @param {CallableFunction} [props.onDrawerToggle] Optionally used to save drawer states + * @param {CallableFunction} [props.getDrawerState] Optionially used to recall drawer states + * @returns React element usable for a settings panel + */ + buildSettingsPanel({settings, onChange, onDrawerToggle, getDrawerState}) { + if (!settings?.length) throw new Error("No settings provided!"); + + return React.createElement(ErrorBoundary, null, settings.map(setting => { + if (!setting.id || !setting.type) throw new Error(`Setting item missing id or type`); + + if (setting.type === "category") { + const shownByDefault = setting.hasOwnProperty("shown") ? setting.shown : true; + + return React.createElement(Group, { + ...setting, + onChange: onChange, + onDrawerToggle: state => onDrawerToggle?.(setting.id, state), + shown: getDrawerState?.(setting.id, shownByDefault) ?? shownByDefault + }); + } + + return buildSetting({ + ...setting, + onChange: value => { + setting?.onChange?.(value); + onChange(null, setting.id, value); + } + }); + })); } }; diff --git a/renderer/src/modules/api/utils.js b/renderer/src/modules/api/utils.js index 74fd7aa8..5f5977ea 100644 --- a/renderer/src/modules/api/utils.js +++ b/renderer/src/modules/api/utils.js @@ -1,4 +1,5 @@ import Utilities from "@modules/utilities"; +import {comparator} from "@structs/semver"; /** @@ -71,6 +72,27 @@ const Utils = { */ className() { return Utilities.className(...arguments); + }, + + /** + * Gets a nested value (if it exists) of an object safely. keyPath should be something like `key.key2.key3`. + * Numbers can be used for arrays as well like `key.key2.array.0.id`. + * @param {object} obj - object to get nested value from + * @param {string} keyPath - key path to the desired value + */ + getNestedValue(obj, keyPath) { + return Utilities.getNestedValue(obj, keyPath); + }, + + /** + * This works on semantic versioning e.g. "1.0.0". + * + * @param {string} currentVersion + * @param {string} newVersion + * @returns {number} 0 indicates equal, -1 indicates left hand greater, 1 indicates right hand greater + */ + semverCompare(currentVersion, newVersion) { + return comparator(currentVersion, newVersion); } }; diff --git a/renderer/src/modules/react.js b/renderer/src/modules/react.js index b5720fbd..f6162cd0 100644 --- a/renderer/src/modules/react.js +++ b/renderer/src/modules/react.js @@ -1,3 +1,5 @@ -import DiscordModules from "./discordmodules"; -export default DiscordModules.React; +import DiscordModules from "./discordmodules"; +/** @type {import("react")} */ +const React = DiscordModules.React; +export default React; export const ReactDOM = DiscordModules.ReactDOM; \ No newline at end of file diff --git a/renderer/src/modules/utilities.js b/renderer/src/modules/utilities.js index b1de98ba..1d008aff 100644 --- a/renderer/src/modules/utilities.js +++ b/renderer/src/modules/utilities.js @@ -216,4 +216,16 @@ export default class Utilities { return classes.join(" "); } + + /** + * Gets a nested value (if it exists) safely. Path should be something like `prop.prop2.prop3`. + * Numbers can be used for arrays as well like `prop.prop2.array.0.id`. + * @param {Object} obj - object to get nested value of + * @param {string} path - representation of the key path to obtain + */ + static getNestedValue(obj, path) { + return path.split(".").reduce(function(ob, prop) { + return ob && ob[prop]; + }, obj); + } } \ No newline at end of file diff --git a/renderer/src/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index 674642af..866720f7 100644 --- a/renderer/src/modules/webpackmodules.js +++ b/renderer/src/modules/webpackmodules.js @@ -119,8 +119,8 @@ export class Filters { * @returns {module:WebpackModules.Filters~filter} - Combinatory filter of all arguments */ static combine(...filters) { - return module => { - return filters.every(filter => filter(module)); + return (exports, module, id) => { + return filters.every(filter => filter(exports, module, id)); }; } } @@ -398,7 +398,7 @@ export default class WebpackModules { return new Promise((resolve) => { const cancel = () => this.removeListener(listener); - const listener = function(exports) { + const listener = function(exports, module, id) { if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") return; let foundModule = null; @@ -407,14 +407,14 @@ export default class WebpackModules { foundModule = null; const wrappedExport = exports[key]; if (!wrappedExport) continue; - if (wrappedFilter(wrappedExport)) foundModule = wrappedExport; + if (wrappedFilter(wrappedExport, module, id)) foundModule = wrappedExport; } } else { - if (exports.Z && wrappedFilter(exports.Z)) foundModule = defaultExport ? exports.Z : exports; - if (exports.ZP && wrappedFilter(exports.ZP)) foundModule = defaultExport ? exports.ZP : exports; - if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports; - if (wrappedFilter(exports)) foundModule = exports; + if (exports.Z && wrappedFilter(exports.Z, module, id)) foundModule = defaultExport ? exports.Z : exports; + if (exports.ZP && wrappedFilter(exports.ZP, module, id)) foundModule = defaultExport ? exports.ZP : exports; + if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, id)) foundModule = defaultExport ? exports.default : exports; + if (wrappedFilter(exports, module, id)) foundModule = exports; } @@ -515,7 +515,7 @@ export default class WebpackModules { const listeners = [...this.listeners]; for (let i = 0; i < listeners.length; i++) { - try {listeners[i](exports);} + try {listeners[i](exports, module, module.id);} catch (error) { Logger.stacktrace("WebpackModules", "Could not fire callback listener:", error); } @@ -525,7 +525,7 @@ export default class WebpackModules { Logger.stacktrace("WebpackModules", "Could not patch pushed module", error); } finally { - require.m[moduleId] = originalModule; + require.m[moduleId] = originalModule; } }; diff --git a/renderer/src/styles/ui/bdsettings.css b/renderer/src/styles/ui/bdsettings.css index 7662523c..15ca8320 100644 --- a/renderer/src/styles/ui/bdsettings.css +++ b/renderer/src/styles/ui/bdsettings.css @@ -48,6 +48,11 @@ transition: 150ms ease border-color; } +.bd-select.bd-select-disabled { + cursor: not-allowed; + opacity: 0.5; +} + .bd-select:hover, .bd-select.menu-open { border-color: var(--background-tertiary); @@ -123,7 +128,7 @@ .bd-setting-header label { font-weight: 500; - cursor: pointer; + /* cursor: pointer; */ overflow: hidden; word-wrap: break-word; font-size: 16px; diff --git a/renderer/src/styles/ui/colorpicker.css b/renderer/src/styles/ui/colorpicker.css index 24f240c0..3aac2fbf 100644 --- a/renderer/src/styles/ui/colorpicker.css +++ b/renderer/src/styles/ui/colorpicker.css @@ -1,5 +1,16 @@ .bd-color-picker-container { display: flex; + justify-content: center; +} + +.bd-color-picker-container.bd-color-picker-disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.bd-color-picker-container.bd-color-picker-disabled > .bd-color-picker-controls, +.bd-color-picker-container.bd-color-picker-disabled > .bd-color-picker-swatch { + pointer-events: none; } .bd-color-picker-controls { @@ -10,8 +21,8 @@ .bd-color-picker-default { cursor: pointer; - width: 72px; - height: 54px; + width: 75px; + height: 60px; border-radius: 4px; margin-right: 9px; display: flex; @@ -29,13 +40,14 @@ position: absolute; top: 5px; right: 5px; + pointer-events: none; } .bd-color-picker { outline: none; - width: 70px; + width: 75px; border: none; - height: 54px; + height: 60px; margin-top: 1px; border-radius: 4px; cursor: pointer; @@ -50,16 +62,35 @@ flex-wrap: wrap; align-content: flex-start; margin-left: 5px !important; - max-width: 340px; + max-width: 330px; } .bd-color-picker-swatch-item { cursor: pointer; border-radius: 4px; - width: 23px; - height: 23px; + width: 15px; + height: 15px; margin: 4px; display: flex; align-items: center; justify-content: center; +} + + +.bd-setting-item.inline .bd-color-picker-swatch { + max-width: 220px; + margin-top: 1px; +} + + +.bd-setting-item.inline .bd-color-picker-default, +.bd-setting-item.inline .bd-color-picker { + width: 50px; + height: 40px +} + +.bd-setting-item.inline .bd-color-picker-swatch-item { + height: 18px; + width: 18px; + margin: 2px 2px; } \ No newline at end of file diff --git a/renderer/src/styles/ui/file.css b/renderer/src/styles/ui/file.css new file mode 100644 index 00000000..4c3ebe6b --- /dev/null +++ b/renderer/src/styles/ui/file.css @@ -0,0 +1,69 @@ +.bd-setting-item.inline .bd-file-input-wrap { + max-width: 300px; +} + +.bd-file-input-wrap { + display: flex; + align-items: center; + gap: 5px; + position: relative; + min-width: 250px; + box-sizing: border-box; + border-radius: 3px; + background-color: hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .1); + border: 1px solid hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .3); + padding: 0 4px; + height: 40px; +} + +.bd-file-input { + flex: 1; + outline: none; + color: var(--text-normal); + font-size: 16px; + font-weight: 600; + width: 100%; + cursor: pointer; +} + +.bd-file-input::-webkit-file-upload-button { + height: 0; + width: 0; + padding: 0 !important; + margin: 0; + visibility: hidden; + user-select: none; + pointer-events: none; +} + +.bd-file-input-wrap .bd-file-input-browse { + padding: 7px 16px; +} + +.bd-file-input-wrap .bd-file-input-clear { + margin-left: 5px; + /* background: none!important; */ + opacity: 0.5; + padding-right: 4px!important; +} + +.bd-file-input-wrap .bd-file-input-clear:hover { + /* background: none; */ + opacity: 1; +} + +.bd-file-input-wrap .bd-file-input-clear svg { + width: 18px !important; + height: 18px !important; +} + +.bd-file-input-wrap.bd-file-input-disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.bd-file-input-wrap.bd-file-input-disabled .bd-file-input-browse, +.bd-file-input-wrap.bd-file-input-disabled .bd-file-input, +.bd-file-input-wrap.bd-file-input-disabled .bd-file-input-clear { + pointer-events: none; +} \ No newline at end of file diff --git a/renderer/src/styles/ui/keybind.css b/renderer/src/styles/ui/keybind.css index 89a630a5..c3659652 100644 --- a/renderer/src/styles/ui/keybind.css +++ b/renderer/src/styles/ui/keybind.css @@ -1,16 +1,29 @@ .bd-keybind-wrap { + display: flex; + align-items: center; + gap: 5px; position: relative; min-width: 250px; box-sizing: border-box; border-radius: 3px; background-color: hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .1); border: 1px solid hsla(0, calc(var(--saturation-factor, 1)*0%), 0%, .3); - padding: 10px; + padding: 0 4px; height: 40px; cursor: pointer; } +.bd-keybind-wrap.bd-keybind-disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.bd-keybind-wrap.bd-keybind-disabled .bd-keybind-controls { + pointer-events: none; +} + .bd-keybind-wrap input { + flex: 1; outline: none; border: none; pointer-events: none; @@ -18,7 +31,11 @@ background: none; font-size: 16px; text-transform: uppercase; - font-weight: 700; + font-weight: 600; +} + +.bd-keybind-wrap input::placeholder { + text-transform: capitalize; } .bd-keybind-wrap.recording { @@ -29,28 +46,23 @@ box-shadow: 0 0 6px hsla(359, calc(var(--saturation-factor, 1)*82.6%), 59.4%, .3); } -.bd-keybind-controls { - position: absolute; - right: 5px; - top: 3px; - display: flex; - align-items: center; - gap: 5px; +.bd-keybind-wrap.recording input { + color: hsl(359, calc(var(--saturation-factor, 1)*82.6%), 59.4%) } -.bd-keybind-controls .bd-keybind-record { - padding: 4px 8px; +.bd-keybind-wrap .bd-keybind-record { + padding: 3px 8px; } .bd-keybind-clear { margin-left: 5px; - background: none!important; + /* background: none!important; */ opacity: 0.5; padding-right: 4px!important; } .bd-keybind-clear:hover { - background: none; + /* background: none; */ opacity: 1; } diff --git a/renderer/src/styles/ui/modal.css b/renderer/src/styles/ui/modal.css index 5d05ee6e..a8d4f6dd 100644 --- a/renderer/src/styles/ui/modal.css +++ b/renderer/src/styles/ui/modal.css @@ -112,21 +112,25 @@ white-space: pre-wrap; word-wrap: break-word; width: 490px; - max-height: 800px; + max-height: min(800px, 60vh); } .bd-modal-medium { width: 600px; - max-height: 800px; + max-height: min(800px, 60vh); min-height: 400px; } .bd-modal-large { width: 800px; - max-height: 960px; + max-height: min(960px, 70vh); min-height: 400px; } +.bd-addon-modal { + min-height: 0; +} + .bd-modal-header, .bd-modal-footer { position: relative; diff --git a/renderer/src/styles/ui/number.css b/renderer/src/styles/ui/number.css index 60ca1f1a..6559deeb 100644 --- a/renderer/src/styles/ui/number.css +++ b/renderer/src/styles/ui/number.css @@ -1,16 +1,21 @@ -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - opacity: 0.5; -} - -.bd-number-input { - display: flex; - background-color: var(--deprecated-text-input-bg); - border: 1px solid var(--deprecated-text-input-border); - color: var(--text-normal); - font-size: 14px; - padding: 5px; - margin: 0; - border-radius: 3px; - width: 70px; +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + opacity: 0.5; +} + +.bd-number-input { + display: flex; + background-color: var(--deprecated-text-input-bg); + border: 1px solid var(--deprecated-text-input-border); + color: var(--text-normal); + font-size: 14px; + padding: 5px; + margin: 0; + border-radius: 3px; + width: 70px; +} + +.bd-number-input:disabled { + cursor: not-allowed; + opacity: 0.5; } \ No newline at end of file diff --git a/renderer/src/styles/ui/radio.css b/renderer/src/styles/ui/radio.css index bf23b332..ad47f66e 100644 --- a/renderer/src/styles/ui/radio.css +++ b/renderer/src/styles/ui/radio.css @@ -1,55 +1,65 @@ -.bd-radio-group { - min-width: 300px; -} - -.bd-radio-option { - display: flex; - align-items: center; - padding: 10px; - margin-bottom: 8px; - cursor: pointer; - user-select: none; - background-color: var(--background-secondary); - border-radius: 3px; - color: var(--interactive-normal); -} - -.bd-radio-option:hover { - background-color: var(--background-modifier-hover); -} - -.bd-radio-option.bd-radio-selected { - background-color: var(--background-modifier-selected); - color: var(--interactive-active); -} - -.bd-radio-option input { - position: absolute; - opacity: 0; - cursor: pointer; - height: 0; - width: 0; -} - -.bd-radio-icon { - margin-right: 10px; -} - -.bd-radio-label-wrap { - display: flex; - flex-direction: column; -} - -.bd-radio-label { - font-family: var(--font-primary); - font-size: 16px; - line-height: 20px; - font-weight: 500; -} - -.bd-radio-description { - font-family: var(--font-primary); - font-size: 14px; - line-height: 18px; - font-weight: 400; +.bd-radio-group { + min-width: 300px; +} + +.bd-radio-option { + display: flex; + align-items: center; + border-left: 3px solid transparent; + padding: 10px 10px 10px 7px; + margin-bottom: 8px; + cursor: pointer; + user-select: none; + background-color: var(--background-secondary); + border-radius: 3px; + color: var(--interactive-normal); +} + +.bd-radio-group.bd-radio-disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.bd-radio-group.bd-radio-disabled .bd-radio-option { + pointer-events: none; +} + +.bd-radio-option:hover { + background-color: var(--background-modifier-hover); +} + +.bd-radio-option.bd-radio-selected { + background-color: var(--background-modifier-selected); + color: var(--interactive-active); +} + +.bd-radio-option input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.bd-radio-icon { + margin-right: 10px; +} + +.bd-radio-label-wrap { + display: flex; + flex-direction: column; +} + +.bd-radio-label { + font-family: var(--font-primary); + font-size: 16px; + line-height: 20px; + font-weight: 500; +} + +.bd-radio-description { + font-family: var(--font-primary); + font-size: 14px; + line-height: 18px; + font-weight: 400; } \ No newline at end of file diff --git a/renderer/src/styles/ui/slider.css b/renderer/src/styles/ui/slider.css index 90c8e4c2..ad789e71 100644 --- a/renderer/src/styles/ui/slider.css +++ b/renderer/src/styles/ui/slider.css @@ -1,42 +1,121 @@ -.bd-slider-wrap { - display: flex; - color: var(--text-normal); - align-items: center; - } - - .bd-slider-label { - background: var(--brand-experiment); - font-weight: 700; - padding: 5px; - margin-right: 10px; - border-radius: 5px; - } - - .bd-slider-input { - /* -webkit-appearance: none; */ - height: 8px; - border-radius: 4px; - appearance: none; - min-width: 350px; - border-radius: 5px; - background: hsl(217,calc(var(--saturation-factor, 1)*7.6%),33.5%); - outline: none; - transition: opacity .2s; - background-image: linear-gradient(var(--brand-experiment), var(--brand-experiment)); - background-size: 70% 100%; - background-repeat: no-repeat; - } - - /* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */ - .bd-slider-input::-webkit-slider-thumb { - appearance: none; - width: 10px; - height: 24px; - top: 50%; - border-radius: 3px; - background-color: hsl(0,calc(var(--saturation-factor, 1)*0%),100%); - border: 1px solid hsl(210,calc(var(--saturation-factor, 1)*2.9%),86.7%); - -webkit-box-shadow: 0 3px 1px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05),0 2px 2px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.1),0 3px 3px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05); - box-shadow: 0 3px 1px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05),0 2px 2px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.1),0 3px 3px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05); - cursor: ew-resize; - } \ No newline at end of file +.bd-setting-item:not(.inline) .bd-slider-wrap { + margin-top: 10px; +} + +.bd-slider-wrap { + display: flex; + flex-direction: column; + justify-content: center; + color: var(--text-normal); + align-items: center; + position: relative; + margin: 0 10px; +} + +.bd-slider-wrap.bd-slider-markers { + padding-bottom: 10px; + margin-bottom: 10px; +} + + +.bd-slider-wrap.bd-slider-disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.bd-slider-wrap.bd-slider-disabled > .bd-slider-input, +.bd-slider-wrap.bd-slider-disabled > .bd-slider-label, +.bd-slider-wrap.bd-slider-disabled > .bd-slider-track, +.bd-slider-wrap.bd-slider-disabled > .bd-slider-marker-container, +.bd-slider-wrap.bd-slider-disabled > .bd-slider-input::-webkit-slider-thumb { + pointer-events: none; +} + +.bd-slider-label { + background: black; + font-weight: 700; + padding: 5px 7px; + border-radius: 5px; + opacity: 0; + pointer-events: none; + position: absolute; + top: -45px; +} + +.bd-slider-input:hover + .bd-slider-label { + opacity: 1; +} + +.bd-slider-input { + /* height: 8px; */ + appearance: none; + /* min-width: 350px; */ + /* border-radius: 5px; */ + outline: none; + pointer-events: none; + position: absolute; + background: none; + width: 100%; + z-index: 2; +} + + /* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */ +.bd-slider-input::-webkit-slider-thumb { + appearance: none; + width: 10px; + height: 24px; + top: 50%; + border-radius: 3px; + background-color: hsl(0,calc(var(--saturation-factor, 1)*0%),100%); + border: 1px solid hsl(210,calc(var(--saturation-factor, 1)*2.9%),86.7%); + -webkit-box-shadow: 0 3px 1px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05),0 2px 2px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.1),0 3px 3px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05); + box-shadow: 0 3px 1px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05),0 2px 2px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.1),0 3px 3px 0 hsla(0,calc(var(--saturation-factor, 1)*0%),0%,.05); + cursor: ew-resize; + /* z-index: 3; */ + pointer-events: all; +} + +.bd-slider-track { + height: 8px; + border-radius: 4px; + min-width: 350px; + border-radius: 5px; + background: hsl(217,calc(var(--saturation-factor, 1)*7.6%),33.5%); + transition: opacity .2s; + background-image: linear-gradient(#3E82E5, #3E82E5); + background-size: 70% 100%; + background-repeat: no-repeat; + cursor: pointer; + width: 100%; + z-index: 1; +} + +.bd-slider-marker-container { + display: flex; + width: 98%; + justify-content: space-between; + position: absolute; + bottom: 0; +} + +.bd-slider-marker { + position: absolute; + transform: translateX(-50%); + font-size: 12px; + cursor: pointer; +} + +.bd-slider-marker::before { + content: ""; + position: absolute; + width: 2px; + height: 24px; + background: rgba(255, 255, 255, 0.2); + top: -26px; + left: calc(50% - 1px); + z-index: -1; +} + +.bd-setting-item.inline:first-child:has(.bd-slider-wrap) { + padding-top: 50px; +} diff --git a/renderer/src/styles/ui/textbox.css b/renderer/src/styles/ui/textbox.css index a277a2e3..38266adf 100644 --- a/renderer/src/styles/ui/textbox.css +++ b/renderer/src/styles/ui/textbox.css @@ -1,11 +1,16 @@ -.bd-text-input { - min-width: 250px; - font-size: 16px; - box-sizing: border-box; - border-radius: 3px; - color: var(--text-normal); - background-color: var(--input-background); - border: none; - padding: 10px; - height: 40px; - } \ No newline at end of file +.bd-text-input { + min-width: 250px; + font-size: 16px; + box-sizing: border-box; + border-radius: 3px; + color: var(--text-normal); + background-color: var(--input-background); + border: none; + padding: 10px; + height: 40px; +} + +.bd-text-input:disabled { + cursor: not-allowed; + opacity: 0.5; +} \ No newline at end of file diff --git a/renderer/src/ui/customcss/editor.jsx b/renderer/src/ui/customcss/editor.jsx index 6062c965..647ad99c 100644 --- a/renderer/src/ui/customcss/editor.jsx +++ b/renderer/src/ui/customcss/editor.jsx @@ -24,7 +24,7 @@ function makeButton(button, value) { function makeSwitch(control) { return {control.label} - + ; } diff --git a/renderer/src/ui/modals.js b/renderer/src/ui/modals.js index e86d7f0a..06da2551 100644 --- a/renderer/src/ui/modals.js +++ b/renderer/src/ui/modals.js @@ -1,5 +1,3 @@ -import Config from "@data/config"; - import FormattableString from "@structs/string"; import Logger from "@common/logger"; @@ -221,8 +219,6 @@ export default class Modals { } static showChangelogModal(options = {}) { - options = Object.assign({image: "https://i.imgur.com/wuh5yMK.png", description: "", changes: [], title: "BetterDiscord", subtitle: `v${Config.version}`}, options); - const key = this.openModal(props => { return React.createElement(ErrorBoundary, null, React.createElement(ChangelogModal, Object.assign(options, props))); }); diff --git a/renderer/src/ui/modals/changelog.jsx b/renderer/src/ui/modals/changelog.jsx index ba6afcc1..5e532426 100644 --- a/renderer/src/ui/modals/changelog.jsx +++ b/renderer/src/ui/modals/changelog.jsx @@ -57,7 +57,7 @@ function Video({src, poster}) { } -export default function ChangelogModal({transitionState, footer, title, subtitle, onClose, video, poster, image, description, changes}) { +export default function ChangelogModal({transitionState, footer, title, subtitle, onClose, video, poster, banner, blurb, changes}) { const ChangelogHeader = useMemo(() =>
@@ -78,23 +78,27 @@ export default function ChangelogModal({transitionState, footer, title, subtitle , [footer]); const changelogItems = useMemo(() => { - const items = [video ?