Merge branch 'main' into development

This commit is contained in:
2024-02-22 02:07:00 -05:00
42 changed files with 590 additions and 130 deletions
+1
View File
@@ -4,6 +4,7 @@ export {default as CustomCSS} from "./customcss";
export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as MediaKeys} from "./general/mediakeys";
export {default as BDContextMenu} from "./general/contextmenu";
// export {default as EmoteModule} from "./emotes/emotes";
// export {default as EmoteMenu} from "./emotes/emotemenu";
+23 -1
View File
@@ -3,6 +3,9 @@ import path from "path";
import Builtin from "@structs/builtin";
import DataStore from "@modules/datastore";
import Strings from "@modules/strings";
import Modals from "@ui/modals";
const timestamp = () => new Date().toISOString().replace("T", " ").replace("Z", "");
@@ -28,8 +31,9 @@ export default new class DebugLogs extends Builtin {
get category() {return "developer";}
get id() {return "debugLogs";}
enabled() {
async enabled() {
this.logFile = path.join(DataStore.dataFolder, "debug.log");
await this.checkFilesize();
this.stream = fs.createWriteStream(this.logFile, {flags: "a"});
this.stream.write(`\n\n================= Starting Debug Log (${timestamp()}) =================\n`);
for (const level of levels) {
@@ -62,4 +66,22 @@ export default new class DebugLogs extends Builtin {
}
return sanitized.join(" ");
}
async checkFilesize() {
try {
const stats = fs.statSync(this.logFile);
const mb = stats.size / (1024 * 1024);
if (mb < 100) return; // Under 100MB, all good
return new Promise(resolve => Modals.showConfirmationModal(Strings.Modals.additionalInfo, Strings.Modals.debuglog, {
confirmText: Strings.Modals.okay,
cancelText: Strings.Modals.cancel,
danger: true,
onConfirm: () => fs.rmSync(this.logFile),
onClose: resolve
}));
}
catch (e) {
this.error(e);
}
}
};
@@ -0,0 +1,95 @@
import Builtin from "@structs/builtin";
import Strings from "@modules/strings";
import Settings from "@modules/settingsmanager";
import Webpack from "@modules/webpackmodules";
import ContextMenuPatcher from "@modules/api/contextmenu";
import pluginManager from "@modules/pluginmanager";
import themeManager from "@modules/thememanager";
const ContextMenu = new ContextMenuPatcher();
const UserSettingsWindow = Webpack.getByProps("open", "updateAccount");
export default new class BDContextMenu extends Builtin {
get name() {return "BDContextMenu";}
get category() {return "general";}
get id() {return "bdContextMenu";}
constructor() {
super(...arguments);
this.callback = this.callback.bind(this);
}
enabled() {
this.patch = ContextMenu.patch("user-settings-cog", this.callback);
}
disabled() {
this.patch?.();
}
callback(retVal) {
const items = Settings.collections.map(c => this.buildCollectionMenu(c));
items.push({label: Strings.panels.updates, action: () => {this.openCategory("updates");}});
if (Settings.get("settings", "customcss", "customcss")) items.push({label: Strings.panels.customcss, action: () => {this.openCategory("customcss");}});
items.push(this.buildAddonMenu(Strings.panels.plugins, pluginManager));
items.push(this.buildAddonMenu(Strings.panels.themes, themeManager));
retVal?.props?.children?.props?.children?.[0].push(ContextMenu.buildItem({type: "separator"}));
retVal?.props?.children?.props?.children?.[0].push(ContextMenu.buildItem({type: "submenu", label: "BetterDiscord", items: items}));
}
buildCollectionMenu(collection) {
return {
type: "submenu",
label: collection.name,
action: () => {this.openCategory(collection.name);},
items: collection.settings.map(category => {
return {
type: "submenu",
label: category.name,
action: () => {this.openCategory(collection.name);},
items: category.settings.filter(s => s.type === "switch" && !s.hidden && s.id !== this.id).map(setting => {
return {
type: "toggle",
label: setting.name,
disabled: setting.disabled,
active: Settings.get(collection.id, category.id, setting.id),
action: () => Settings.set(collection.id, category.id, setting.id, !Settings.get(collection.id, category.id, setting.id))
};
})
};
})
};
}
/**
*
* @param {string} label
* @param {import("../../modules/addonmanager").default} manager
* @returns
*/
buildAddonMenu(label, manager) {
const names = manager.addonList.map(a => a.name || a.getName()).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
return {
type: "submenu",
label: label,
action: () => {this.openCategory(label.toLowerCase());},
items: names.map(name => {
return {
type: "toggle",
label: name,
disabled: manager.getAddon(name)?.partial ?? false,
active: manager.isEnabled(name),
action: () => {manager.toggleAddon(name);}
};
})
};
}
async openCategory(id) {
ContextMenu.close();
UserSettingsWindow?.open?.(id);
}
};
+7 -26
View File
@@ -1,35 +1,16 @@
// fixed, improved, added, progress
export default {
description: "There are some small but important fixes and changes in this update to keep things running smoothly!",
description: "This is a small but very important update to fix some key issues!",
changes: [
{
title: "What's New?",
type: "added",
items: [
"Keybinds will now show properly instead of `[object Undefined]`.",
"Update banners will now appear consistently when there are updates.",
"Translations should now actually load when a new locale is selected in Discord's settings."
]
},
{
title: "Translations",
type: "improved",
items: [
"Added a new Vietnamese translation thanks to Minato Isuki.",
"Improved Italian translation thanks to TheItalianTranslator.",
"Improved Chinese (traditional) translation thanks to Frost_koi.",
"Removed several outdated keys and strings.",
"Added multiple translated strings to UI where they were hardcoded."
]
},
{
title: "Technical Changes",
title: "What's Fixed?",
type: "fixed",
items: [
"The webpack hook now no longer prevents Discord modules from shadowing built-in functions. This was originally meant as a sanity check but now Discord actually does this intentionally which can lead to issues like the incorrectly displayed keybinds.",
"`BdApi.UI.showNotice` should work again in cases where it seemed not to unless you had addons with updates. This was a race condition versus the load order of class modules.",
"`BdApi.Net.fetch` now actually uses all the options passed to it, previously it failed to pass the options to the other process.",
"It also now supports all HTTP request types rather than just `POST`, `GET`, `DELETE`, and `PUT`."
"Spanish (LATAM) is now properly supported.",
"Future cases of unrecognized locales as well as locale fallback now works as intended and shouldn't cause loading issues.",
"Updated translations for Vietnamese locale.",
"Fixed an issue where certain actions (such as favoriting GIFs) caused unexpected lag.",
"Fixed some issues with general client lag."
]
}
]
+2 -1
View File
@@ -6,7 +6,8 @@ export default [
settings: [
{type: "switch", id: "voiceDisconnect", value: false},
{type: "switch", id: "showToasts", value: true},
{type: "switch", id: "mediaKeys", value: false}
{type: "switch", id: "mediaKeys", value: false},
{type: "switch", id: "bdContextMenu", value: true}
]
},
{
+34 -6
View File
@@ -1,6 +1,5 @@
import path from "path";
import fs from "fs";
import {shell} from "electron";
import Logger from "@common/logger";
@@ -11,13 +10,16 @@ import Events from "./emitter";
import DataStore from "./datastore";
import React from "./react";
import Strings from "./strings";
import ipc from "./ipc";
import AddonEditor from "@ui/misc/addoneditor";
import FloatingWindows from "@ui/floatingwindows";
import Toasts from "@ui/toasts";
const openItem = shell.openItem || shell.openPath;
// const SWITCH_ANIMATION_TIME = 250;
const openItem = ipc.openPath;
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
const escapedAtRegex = /^\\@/;
@@ -270,8 +272,21 @@ export default class AddonManager {
if (!addon || addon.partial) return;
if (this.state[addon.id]) return;
this.state[addon.id] = true;
this.startAddon(addon);
this.saveState();
this.emit("enabled", addon);
// setTimeout(() => {
this.startAddon(addon);
this.saveState();
// }, SWITCH_ANIMATION_TIME);
}
enableAllAddons() {
const originalSetting = Settings.get("settings", "general", "showToasts", false);
Settings.set("settings", "general", "showToasts", false);
for (let a = 0; a < this.addonList.length; a++) {
this.enableAddon(this.addonList[a]);
}
Settings.set("settings", "general", "showToasts", originalSetting);
this.emit("batch");
}
disableAddon(idOrAddon) {
@@ -279,8 +294,21 @@ export default class AddonManager {
if (!addon || addon.partial) return;
if (!this.state[addon.id]) return;
this.state[addon.id] = false;
this.stopAddon(addon);
this.saveState();
this.emit("disabled", addon);
// setTimeout(() => {
this.stopAddon(addon);
this.saveState();
// }, SWITCH_ANIMATION_TIME);
}
disableAllAddons() {
const originalSetting = Settings.get("settings", "general", "showToasts", false);
Settings.set("settings", "general", "showToasts", false);
for (let a = 0; a < this.addonList.length; a++) {
this.disableAddon(this.addonList[a]);
}
Settings.set("settings", "general", "showToasts", originalSetting);
this.emit("batch");
}
toggleAddon(id) {
+5 -2
View File
@@ -35,7 +35,8 @@ const ContextMenuActions = (() => {
}
startupComplete &&= typeof(out.closeContextMenu) === "function" && typeof(out.openContextMenu) === "function";
} catch (error) {
}
catch (error) {
startupComplete = false;
Logger.stacktrace("ContextMenu~Components", "Fatal startup error:", error);
@@ -222,6 +223,7 @@ class ContextMenu {
// This is done to make sure the UI actually displays the on/off correctly
if (type === "toggle") {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [active, doToggle] = React.useState(props.checked || false);
const originalAction = props.action;
props.checked = active;
@@ -330,7 +332,8 @@ Object.freeze(ContextMenu.prototype);
try {
MenuPatcher.initialize();
} catch (error) {
}
catch (error) {
Logger.error("ContextMenu~Patcher", "Fatal error:", error);
}
+3 -3
View File
@@ -80,13 +80,13 @@ export default function fetch(url, options = {}) {
ctx.onComplete(() => {
try {
const data = ctx.readData();
const resultData = ctx.readData();
const req = new FetchResponse({
method: options.method ?? "GET",
status: data.statusCode,
status: resultData.statusCode,
...options,
...data
...resultData
});
resolve(req);
+1 -1
View File
@@ -58,7 +58,7 @@ export default class BdApi {
get ContextMenu() {return ContextMenuAPI;}
Components = {
get Tooltip() {return DiscordModules.Tooltip;}
}
};
Net = {fetch};
}
+1
View File
@@ -50,6 +50,7 @@ const UI = {
* @param {string} [options.cancelText=Cancel] Text for the cancel button
* @param {callable} [options.onConfirm=NOOP] Callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] Callback to occur when clicking the cancel button
* @param {callable} [options.onClose=NOOP] Callback to occur when exiting the modal
* @returns {string} The key used for this modal.
*/
showConfirmationModal(title, content, options = {}) {
+4 -5
View File
@@ -4,11 +4,10 @@ import WebpackModules, {Filters} from "@modules/webpackmodules";
const getOptions = (args, defaultOptions = {}) => {
if (args.length > 1 &&
typeof(args[args.length - 1]) === "object" &&
!Array.isArray(args[args.length - 1]) &&
args[args.length - 1] !== null
) {
if (args.length > 1
&& typeof(args[args.length - 1]) === "object" // eslint-disable-line operator-linebreak
&& !Array.isArray(args[args.length - 1]) // eslint-disable-line operator-linebreak
&& args[args.length - 1] !== null) { // eslint-disable-line operator-linebreak
Object.assign(defaultOptions, args.pop());
}
+1 -1
View File
@@ -15,7 +15,7 @@ export default Utilities.memoizeObject({
get ChannelActions() {return WebpackModules.getByProps("selectChannel");},
get LocaleStore() {return WebpackModules.getByProps("locale", "initialize");},
get UserStore() {return WebpackModules.getByProps("getCurrentUser", "getUser");},
get InviteActions() {return WebpackModules.getByProps("acceptInvite");},
get InviteActions() {return WebpackModules.getByProps("createInvite");},
get SimpleMarkdown() {return WebpackModules.getByProps("parseBlock", "parseInline", "defaultOutput");},
get Strings() {return WebpackModules.getByProps("Messages").Messages;},
get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");},
+4
View File
@@ -60,4 +60,8 @@ export default new class IPCRenderer {
getSystemAccentColor() {
return ipc.invoke(IPCEvents.GET_ACCENT_COLOR);
}
openPath(path) {
return ipc.send(IPCEvents.OPEN_PATH, path);
}
};
+7 -11
View File
@@ -11,7 +11,6 @@ export default new class LocaleManager {
get defaultLocale() {return "en-US";}
constructor() {
this.locale = "";
this.strings = Utilities.extend({}, Locales[this.defaultLocale]);
}
@@ -21,16 +20,13 @@ export default new class LocaleManager {
}
setLocale() {
let newStrings;
if (this.discordLocale != this.defaultLocale) {
newStrings = Locales[this.discordLocale];
if (!newStrings) return this.setLocale(this.defaultLocale);
}
else {
newStrings = Locales[this.defaultLocale];
}
this.locale = this.discordLocale;
Utilities.extendTruthy(this.strings, newStrings);
// Reset to the default locale in case a language is incomplete
Utilities.extend(this.strings, Locales[this.defaultLocale]);
// Get the strings of the new language and extend if a translation exists
const newStrings = Locales[this.discordLocale];
if (newStrings) Utilities.extendTruthy(this.strings, newStrings);
Events.emit("strings-updated");
}
};
+1 -1
View File
@@ -50,7 +50,7 @@
}
static makeOverride(patch) {
return function () {
return function BDPatcher() {
let returnValue;
if (!patch.children || !patch.children.length) return patch.originalFunction.apply(this, arguments);
for (const superPatch of patch.children.filter(c => c.type === "before")) {
+3
View File
@@ -57,6 +57,8 @@ export default new class PluginManager extends AddonManager {
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
enableAll: this.enableAllAddons.bind(this),
disableAll: this.disableAllAddons.bind(this),
prefix: this.prefix
})
});
@@ -151,6 +153,7 @@ export default new class PluginManager extends AddonManager {
}
catch (err) {
this.state[addon.id] = false;
this.emit("disabled", addon);
Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version}));
Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err);
return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix);
+2
View File
@@ -35,6 +35,8 @@ export default new class ThemeManager extends AddonManager {
saveAddon: this.saveAddon.bind(this),
editAddon: this.editAddon.bind(this),
deleteAddon: this.deleteAddon.bind(this),
enableAll: this.enableAllAddons.bind(this),
disableAll: this.disableAllAddons.bind(this),
prefix: this.prefix
})
});
+7 -2
View File
@@ -190,7 +190,8 @@ export default class WebpackModules {
if (!modules.hasOwnProperty(index)) continue;
let module = null;
try {module = modules[index];} catch {continue;}
try {module = modules[index];}
catch {continue;}
const {exports} = module;
if (!exports || exports === window || exports === document.documentElement || exports[Symbol.toStringTag] === "DOMTokenList") continue;
@@ -199,7 +200,8 @@ export default class WebpackModules {
for (const key in exports) {
let foundModule = null;
let wrappedExport = null;
try {wrappedExport = exports[key];} catch {continue;}
try {wrappedExport = exports[key];}
catch {continue;}
if (!wrappedExport) continue;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
@@ -522,6 +524,9 @@ export default class WebpackModules {
catch (error) {
Logger.stacktrace("WebpackModules", "Could not patch pushed module", error);
}
finally{
require.m[moduleId] = originalModule;
}
};
Object.assign(modules[moduleId], originalModule, {
+6
View File
@@ -22,6 +22,12 @@ originalFs.writeFile = (path, data, options) => fs.writeFile(path, data, Object.
export const createRequire = function (path) {
return mod => {
// Ignore relative require attempts because Discord
// erroneously does this a lot apparently which
// causes us to do filesystem accesses in our default
// switch statement mainly used for absolute paths
if (typeof(mod) === "string" && mod.startsWith("./")) return;
if (deprecated.has(mod)) {
Logger.warn("Remote~Require", `The "${mod}" module is marked as deprecated. ${deprecated.get(mod)}`);
}
+14
View File
@@ -24,4 +24,18 @@
.bd-search-wrapper > svg {
margin-right: 2px;
fill: var(--interactive-normal);
}
.bd-search-wrapper > .bd-button {
margin-right: 2px;
background: none;
padding: 0;
}
.bd-search-wrapper > .bd-button > svg .fill {
fill: var(--interactive-normal);
}
.bd-search-wrapper > .bd-button:hover > svg .fill {
fill: var(--interactive-hover);
}
+16 -8
View File
@@ -212,7 +212,7 @@
flex-wrap: wrap;
}
.bd-addon-controls .bd-search {
.bd-settings-title .bd-search {
font-size: 13px;
margin: 0;
width: 200px;
@@ -261,35 +261,43 @@
margin-left: 10px;
}
.bd-addon-views .bd-view-button {
.bd-addon-controls .bd-button {
background-color: transparent;
padding: 3px 4px;
}
.bd-addon-views .bd-view-button svg {
.bd-addon-controls .bd-button svg {
fill: var(--interactive-normal);
}
.bd-addon-views .bd-view-button.selected svg {
.bd-addon-controls .bd-button.selected svg {
fill: #FFFFFF;
}
.bd-addon-views .bd-view-button:hover {
.bd-addon-controls .bd-button:hover {
background-color: var(--background-modifier-selected);
}
.bd-addon-views .bd-view-button:active {
.bd-addon-controls .bd-button:active {
background-color: var(--background-modifier-accent);
}
.bd-addon-views .bd-view-button.selected {
.bd-addon-controls .bd-button.selected {
background-color: #3E82E5;
}
.bd-addon-views .bd-view-button + .bd-view-button {
.bd-addon-controls .bd-button + .bd-button {
margin-left: 5px;
}
.bd-controls-basic .bd-button:active svg {
fill: #FFFFFF;
}
.bd-controls-basic .bd-button:active {
background-color: #3E82E5;
}
.bd-addon-list .bd-footer .bd-links,
.bd-addon-list .bd-footer .bd-links a,
.bd-addon-list .bd-footer .bd-addon-button {
+2
View File
@@ -165,6 +165,8 @@
}
.bd-settings-title {
display: flex;
justify-content: space-between;
color: var(--header-primary, #FFFFFF);
display: flex;
font-weight: 600;
+9
View File
@@ -0,0 +1,9 @@
import React from "@modules/react";
export default function FullScreen(props) {
const size = props.size || "20px";
return <svg className={props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}} onClick={props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>;
}
+6 -2
View File
@@ -158,6 +158,7 @@ export default class Modals {
* @param {string} [options.cancelText=Cancel] - text for the cancel button
* @param {callable} [options.onConfirm=NOOP] - callback to occur when clicking the submit button
* @param {callable} [options.onCancel=NOOP] - callback to occur when clicking the cancel button
* @param {callable} [options.onClose=NOOP] - callback to occur when exiting the modal
* @param {string} [options.key] - key used to identify the modal. If not provided, one is generated and returned
* @returns {string} - the key used for this modal
*/
@@ -167,7 +168,7 @@ export default class Modals {
if (content instanceof FormattableString) content = content.toString();
const emptyFunction = () => {};
const {onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options;
const {onClose = emptyFunction, onConfirm = emptyFunction, onCancel = emptyFunction, confirmText = Strings.Modals.okay, cancelText = Strings.Modals.cancel, danger = false, key = undefined} = options;
if (!this.ModalActions) {
return this.default(title, content, [
@@ -196,7 +197,10 @@ export default class Modals {
confirmText: confirmText,
cancelText: cancelText,
onConfirm: onConfirm,
onCancel: onCancel
onCancel: onCancel,
onCloseCallback: () => {
if (props?.transitionState === 1) onClose?.();
}
}, props), React.createElement(ErrorBoundary, {}, content)));
}, {modalKey: key});
return modalKey;
+21 -5
View File
@@ -3,6 +3,7 @@ import Logger from "@common/logger";
import SimpleMarkdown from "@structs/markdown";
import React from "@modules/react";
import Events from "@modules/emitter";
import Strings from "@modules/strings";
import WebpackModules from "@modules/webpackmodules";
import DiscordModules from "@modules/discordmodules";
@@ -25,7 +26,7 @@ import ExtIcon from "@ui/icons/extension";
import ErrorIcon from "@ui/icons/error";
import ThemeIcon from "@ui/icons/theme";
const {useState, useCallback, useMemo} = React;
const {useState, useCallback, useMemo, useEffect} = React;
const LinkIcons = {
@@ -88,12 +89,27 @@ function buildLink(type, url) {
return makeButton(Strings.Addons[type], link);
}
export default function AddonCard({addon, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) {
export default function AddonCard({addon, prefix, type, disabled, enabled: initialValue, onChange: parentChange, hasSettings, editAddon, deleteAddon, getSettingsPanel}) {
const [isEnabled, setEnabled] = useState(initialValue);
useEffect(() => {
const onEnabled = updated => {
if (addon.id === updated.id) setEnabled(true);
};
const onDisabled = updated => {
if (addon.id === updated.id) setEnabled(false);
};
Events.on(`${prefix}-enabled`, onEnabled);
Events.on(`${prefix}-disabled`, onDisabled);
return () => {
Events.off(`${prefix}-enabled`, onEnabled);
Events.off(`${prefix}-disabled`, onDisabled);
};
}, [prefix, addon]);
const onChange = useCallback(() => {
setEnabled(!isEnabled);
if (parentChange) parentChange(addon.id);
}, [addon.id, parentChange, isEnabled]);
}, [addon.id, parentChange]);
const showSettings = useCallback(() => {
if (!hasSettings || !isEnabled) return;
@@ -154,7 +170,7 @@ export default function AddonCard({addon, type, disabled, enabled: initialValue,
<div className="bd-addon-header">
{type === "plugin" ? <ExtIcon size="18px" className="bd-icon" /> : <ThemeIcon size="18px" className="bd-icon" />}
<div className="bd-title">{title}</div>
<Switch disabled={disabled} checked={isEnabled} onChange={onChange} />
<Switch internalState={false} disabled={disabled} checked={isEnabled} onChange={onChange} />
</div>
<div className="bd-description-wrap">
{disabled && <div className="banner banner-danger"><ErrorIcon className="bd-icon" />{`An error was encountered while trying to load this ${type}.`}</div>}
+43 -9
View File
@@ -3,6 +3,7 @@ import Strings from "@modules/strings";
import Events from "@modules/emitter";
import DataStore from "@modules/datastore";
import DiscordModules from "@modules/discordmodules";
import ipc from "@modules/ipc";
import Button from "../base/button";
import SettingsTitle from "./title";
@@ -15,6 +16,9 @@ import ErrorBoundary from "@ui/errorboundary";
import ListIcon from "@ui/icons/list";
import GridIcon from "@ui/icons/grid";
import FolderIcon from "@ui/icons/folder";
import CheckIcon from "@ui/icons/check";
import CloseIcon from "@ui/icons/close";
import NoResults from "@ui/blankslates/noresults";
import EmptyImage from "@ui/blankslates/emptyimage";
@@ -38,9 +42,7 @@ const buildDirectionOptions = () => [
function openFolder(folder) {
const shell = require("electron").shell;
const open = shell.openItem || shell.openPath;
open(folder);
ipc.openPath(folder);
}
function blankslate(type, onClick) {
@@ -50,6 +52,12 @@ function blankslate(type, onClick) {
</EmptyImage>;
}
function makeBasicButton(title, children, action) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => <button {...props} className="bd-button" onClick={action}>{children}</button>}
</DiscordModules.Tooltip>;
}
function makeControlButton(title, children, action, selected = false) {
return <DiscordModules.Tooltip color="primary" position="top" text={title}>
{(props) => {
@@ -83,8 +91,28 @@ function confirmDelete(addon) {
});
}
/**
* @param {function} action
* @param {string} type
* @returns
*/
function confirmEnable(action, type) {
/**
* @param {MouseEvent} event
*/
return function(event) {
if (event.shiftKey) return action();
Modals.showConfirmationModal(Strings.Modals.confirmAction, Strings.Addons.enableAllWarning.format({type: type.toLocaleLowerCase()}), {
confirmText: Strings.Modals.okay,
cancelText: Strings.Modals.cancel,
danger: true,
onConfirm: action,
});
};
}
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon}) {
export default function AddonList({prefix, type, title, folder, addonList, addonState, onChange, reload, editAddon, deleteAddon, enableAll, disableAll}) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState(getState.bind(null, type, "sort", "name"));
const [ascending, setAscending] = useState(getState.bind(null, type, "ascending", true));
@@ -127,7 +155,6 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
if (deleteAddon) deleteAddon(addon);
}, [addonList, deleteAddon]);
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: openFolder.bind(null, folder)} : null;
const renderedCards = useMemo(() => {
let sorted = addonList.sort((a, b) => {
const sortByEnabled = sort === "isEnabled";
@@ -156,18 +183,25 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} prefix={prefix} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, prefix, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps
const hasAddonsInstalled = addonList.length !== 0;
const isSearching = !!query;
const hasResults = renderedCards.length !== 0;
return [
<SettingsTitle key="title" text={title} button={button} />,
<SettingsTitle key="title" text={isSearching ? `${title} - ${Strings.Addons.results.format({count: `${renderedCards.length}`})}` : title}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: `${renderedCards.length} ${title}`})}...`} />
</SettingsTitle>,
<div className={"bd-controls bd-addon-controls"}>
<Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} />
{/* <Search onChange={search} placeholder={`${Strings.Addons.search.format({type: title})}...`} /> */}
<div className="bd-controls-basic">
{makeBasicButton(Strings.Addons.openFolder.format({type: title}), <FolderIcon />, openFolder.bind(null, folder))}
{makeBasicButton(Strings.Addons.enableAll, <CheckIcon size="20px" />, confirmEnable(enableAll, title))}
{makeBasicButton(Strings.Addons.disableAll, <CloseIcon size="20px" />, disableAll)}
</div>
<div className="bd-controls-advanced">
<div className="bd-addon-dropdowns">
<div className="bd-select-wrapper">
+17 -3
View File
@@ -1,20 +1,34 @@
import React from "@modules/react";
import Close from "@ui/icons/close";
import SearchIcon from "@ui/icons/search";
const {useState, useCallback} = React;
const {useState, useEffect, useCallback, useRef} = React;
export default function Search({onChange, className, onKeyDown, placeholder}) {
const input = useRef(null);
const [value, setValue] = useState("");
// focus search bar on page select
useEffect(()=>{
if (!input.current) return;
input.current.focus();
}, []);
const change = useCallback((e) => {
onChange?.(e);
setValue(e.target.value);
}, [onChange]);
const reset = useCallback(() => {
onChange?.({target: {value: ""}});
setValue("");
}, [onChange]);
return <div className={"bd-search-wrapper" + (className ? ` ${className}` : "")}>
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} />
<SearchIcon />
<input onChange={change} onKeyDown={onKeyDown} type="text" className="bd-search" placeholder={placeholder} maxLength="50" value={value} ref={input}/>
{!value && <SearchIcon />}
{value && <button className="bd-button" onClick={reset}><Close size="16px" /></button>}
</div>;
}
@@ -3,17 +3,18 @@ import React from "@modules/react";
const {useState, useCallback} = React;
export default function Switch({id, checked: initialValue, disabled, onChange}) {
export default function Switch({id, checked: initialValue, disabled, onChange, internalState = true}) {
const [checked, setChecked] = useState(initialValue);
const change = useCallback(() => {
onChange?.(!checked);
setChecked(!checked);
}, [checked, onChange]);
const isChecked = internalState ? checked : initialValue;
const enabledClass = disabled ? " bd-switch-disabled" : "";
const checkedClass = checked ? " bd-switch-checked" : "";
const checkedClass = isChecked ? " bd-switch-checked" : "";
return <div className={`bd-switch` + enabledClass + checkedClass}>
<input id={id} type="checkbox" disabled={disabled} checked={checked} onChange={change} />
<input id={id} type="checkbox" disabled={disabled} checked={isChecked} onChange={change} />
<div className="bd-switch-body">
<svg className="bd-switch-slider" viewBox="0 0 28 20" preserveAspectRatio="xMinYMid meet">
<rect className="bd-switch-handle" fill="white" x="4" y="0" height="20" width="20" rx="10"></rect>
+2 -2
View File
@@ -8,7 +8,7 @@ const {useCallback} = React;
const basicClass = "bd-settings-title";
const groupClass = "bd-settings-title bd-settings-group-title";
export default function SettingsTitle({isGroup, className, button, onClick, text, otherChildren}) {
export default function SettingsTitle({isGroup, className, button, onClick, text, children}) {
const click = useCallback((event) => {
event.stopPropagation();
event.preventDefault();
@@ -21,7 +21,7 @@ export default function SettingsTitle({isGroup, className, button, onClick, text
return <h2 className={titleClass} onClick={() => {onClick?.();}}>
{text}
{button && <Button className="bd-button-title" onClick={click} size={Button.Sizes.NONE}>{button.title}</Button>}
{otherChildren}
{children}
</h2>;
}