Merge branch 'main' into development
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class BdApi {
|
||||
get ContextMenu() {return ContextMenuAPI;}
|
||||
Components = {
|
||||
get Tooltip() {return DiscordModules.Tooltip;}
|
||||
}
|
||||
};
|
||||
Net = {fetch};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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");},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -165,6 +165,8 @@
|
||||
}
|
||||
|
||||
.bd-settings-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--header-primary, #FFFFFF);
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user