1
0
Fork 1
mirror of https://github.com/Vendicated/Vencord.git synced 2025-01-10 09:56:24 +00:00
This commit is contained in:
Elvyra 2025-01-03 01:25:40 +01:00
parent 20ed7dc96b
commit 63eb6358fb
7 changed files with 277 additions and 61 deletions

View file

@ -220,6 +220,15 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
}
}
export function migrateSettingsToArrays(pluginName: string, settings: Array<string>, stringSeparator: string = ",") {
for (const setting of settings) {
if (typeof SettingsStore.plain.plugins[pluginName][setting] !== "string") continue;
logger.info(`Migrating setting ${setting} from ${pluginName} to list`);
if (SettingsStore.plain.plugins[pluginName][setting] === "") SettingsStore.plain.plugins[pluginName][setting] = [];
else SettingsStore.plain.plugins[pluginName][setting] = SettingsStore.plain.plugins[pluginName][setting].split(stringSeparator);
}
}
export function definePluginSettings<
Def extends SettingsDefinition,
Checks extends SettingsChecks<Def>,

View file

@ -43,7 +43,8 @@ import {
SettingNumericComponent,
SettingSelectComponent,
SettingSliderComponent,
SettingTextComponent
SettingTextComponent,
SettingListComponent,
} from "./components";
import { openContributorModal } from "./ContributorModal";
import { GithubButton, WebsiteButton } from "./LinkIconButton";
@ -81,7 +82,11 @@ const Components: Record<OptionType, React.ComponentType<ISettingElementProps<an
[OptionType.BOOLEAN]: SettingBooleanComponent,
[OptionType.SELECT]: SettingSelectComponent,
[OptionType.SLIDER]: SettingSliderComponent,
[OptionType.COMPONENT]: SettingCustomComponent
[OptionType.COMPONENT]: SettingCustomComponent,
/* [OptionType.LIST]: SettingListComponent,
[OptionType.USERS]: SettingListComponent,
[OptionType.CHANNELS]: SettingListComponent,
[OptionType.GUILDS]: SettingListComponent */
};
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {

View file

@ -0,0 +1,66 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Margins } from "@utils/margins";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionSelect } from "@utils/types";
import { Forms, useState, useEffect, ChannelStore, UserStore, GuildStore } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingListComponent({ option, pluginSettings, definedSettings, onChange, onError, id }: ISettingElementProps<PluginOptionSelect>) {
const [error, setError] = useState<string | null>(null);
const [items, setItems] = useState<string[]>([]);
const [newItem, setNewItem] = useState<string>("");
const addItem = () => {
if (newItem.trim() !== "") {
setItems([...items, newItem]);
setNewItem("");
}
};
const removeItem = (index: number) => {
setItems(items.filter((_, i) => i !== index));
};
useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue) {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Forms.FormTitle>{wordsToTitle(wordsFromCamel(id))}</Forms.FormTitle>
<Forms.FormText className={Margins.bottom16} type="description">{option.description}</Forms.FormText>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View file

@ -33,6 +33,7 @@ export interface ISettingElementProps<T extends PluginOptionBase> {
export * from "../../Badge";
export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent";
export * from "./SettingListComponent";
export * from "./SettingNumericComponent";
export * from "./SettingSelectComponent";
export * from "./SettingSliderComponent";

View file

@ -0,0 +1,121 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Menu, useState } from "@webpack/common";
function createContextMenu(name: "Guild" | "User" | "Channel", value: any) {
return (
<Menu.MenuItem
id="vc-plugin-settings"
label="Plugins"
>
{renderRegisteredPlugins(name, value)}
</Menu.MenuItem>
);
}
function renderRegisteredPlugins(name: "Guild" | "User" | "Channel", value: any) {
const type = name === "Guild" ? OptionType.GUILDS : name === "User" ? OptionType.USERS : OptionType.CHANNELS;
const plugins = registeredPlugins[type];
const [checkedItems, setCheckedItems] = useState<Record<string, boolean>>({});
const handleCheckboxClick = (plugin: string, setting: string) => {
const key = `${plugin}-${setting}`;
setCheckedItems(prevState => ({
...prevState,
[key]: !prevState[key]
}));
// @ts-ignore (cannot be undefined because settings have to exist for this to be called in the first place)
const s = Vencord.Plugins.plugins[plugin].settings.store[setting];
Vencord.Plugins.plugins[plugin].settings.store[setting] = s.includes(value.id)
? s.filter(id => id !== value.id)
: [...s, value.id];
};
return Object.keys(plugins).map(plugin => (
<Menu.MenuItem
id={`vc-plugin-settings-${plugin}`}
label={plugin}
>
{plugins[plugin].map(setting => (
<Menu.MenuCheckboxItem
id={`vc-plugin-settings-${plugin}-${setting}`}
// @ts-ignore
label={Vencord.Plugins.plugins[plugin].settings.def[setting].popoutText ?? setting}
action={() => handleCheckboxClick(plugin, setting)}
checked={checkedItems[`${plugin}-${setting}`]}
/>
))}
</Menu.MenuItem>
));
}
function MakeContextCallback(name: "Guild" | "User" | "Channel"): NavContextMenuPatchCallback {
return (children, props) => {
const value = props[name.toLowerCase()];
if (!value) return;
if (props.label === getIntlMessage("CHANNEL_ACTIONS_MENU_LABEL")) return; // random shit like notification settings
const lastChild = children.at(-1);
if (lastChild?.key === "developer-actions") {
const p = lastChild.props;
if (!Array.isArray(p.children))
p.children = [p.children];
children = p.children;
}
children.splice(-1, 0,
createContextMenu(name, value)
);
};
}
// {type: {plugin: [setting, setting, setting]}}
const registeredPlugins: Record<OptionType.USERS | OptionType.GUILDS | OptionType.CHANNELS, Record<string, Array<string>>> = {
[OptionType.USERS]: {},
[OptionType.GUILDS]: {},
[OptionType.CHANNELS]: {}
};
// TODO find a better name
export default definePlugin({
name: "SettingListsAPI",
description: "API to automatically add context menus for settings",
authors: [Devs.Elvyra],
contextMenus: {
"channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"),
"guild-context": MakeContextCallback("Guild"),
"user-context": MakeContextCallback("User")
},
start() {
for (const plugin of Object.values(Vencord.Plugins.plugins)) {
if (!Vencord.Plugins.isPluginEnabled(plugin.name) || !plugin.settings) continue;
const settings = plugin.settings.def;
for (const settingKey of Object.keys(settings)) {
const setting = settings[settingKey];
if (setting.type === OptionType.USERS || setting.type === OptionType.GUILDS || setting.type === OptionType.CHANNELS) {
if (!registeredPlugins[setting.type][plugin.name]) {
registeredPlugins[setting.type][plugin.name] = [];
}
registeredPlugins[setting.type][plugin.name].push(settingKey);
}
}
}
}
});

View file

@ -20,7 +20,7 @@ import "./messageLogger.css";
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { updateMessage } from "@api/MessageUpdater";
import { Settings } from "@api/Settings";
import { definePluginSettings, migrateSettingsToArrays, Settings } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
@ -36,6 +36,66 @@ import overlayStyle from "./deleteStyleOverlay.css?managed";
import textStyle from "./deleteStyleText.css?managed";
import { openHistoryModal } from "./HistoryModal";
migrateSettingsToArrays("MessageLogger", ["ignoreChannels", "ignoreGuilds", "ignoreUsers"]);
const settings = definePluginSettings({
deleteStyle: {
type: OptionType.SELECT,
description: "The style of deleted messages",
options: [
{ label: "Red text", value: "text", default: true },
{ label: "Red overlay", value: "overlay" }
],
onChange: () => addDeleteStyle()
},
logDeletes: {
type: OptionType.BOOLEAN,
description: "Whether to log deleted messages",
default: true,
},
collapseDeleted: {
type: OptionType.BOOLEAN,
description: "Whether to collapse deleted messages, similar to blocked messages",
default: false
},
logEdits: {
type: OptionType.BOOLEAN,
description: "Whether to log edited messages",
default: true,
},
inlineEdits: {
type: OptionType.BOOLEAN,
description: "Whether to display edit history as part of message content",
default: true
},
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots",
default: false
},
ignoreSelf: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by yourself",
default: false
},
ignoreUsers: {
type: OptionType.USERS,
description: "List of users to ignore",
popoutText: "Ignore deleted messages from this user",
},
ignoreChannels: {
type: OptionType.CHANNELS,
description: "List of channel IDs to ignore",
popoutText: "Ignore deleted messages in this channel"
},
ignoreGuilds: {
type: OptionType.GUILDS,
description: "List of guild IDs to ignore",
popoutText: "Ignore deleted messages in this guild",
},
});
interface MLMessage extends Message {
deleted?: boolean;
editHistory?: { timestamp: Date; content: string; }[];
@ -145,7 +205,7 @@ export default definePlugin({
name: "MessageLogger",
description: "Temporarily logs deleted and edited messages.",
authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi],
dependencies: ["MessageUpdaterAPI"],
dependencies: ["MessageUpdaterAPI", "SettingListsAPI"],
contextMenus: {
"message": patchMessageContextMenu,
@ -192,63 +252,7 @@ export default definePlugin({
};
},
options: {
deleteStyle: {
type: OptionType.SELECT,
description: "The style of deleted messages",
default: "text",
options: [
{ label: "Red text", value: "text", default: true },
{ label: "Red overlay", value: "overlay" }
],
onChange: () => addDeleteStyle()
},
logDeletes: {
type: OptionType.BOOLEAN,
description: "Whether to log deleted messages",
default: true,
},
collapseDeleted: {
type: OptionType.BOOLEAN,
description: "Whether to collapse deleted messages, similar to blocked messages",
default: false
},
logEdits: {
type: OptionType.BOOLEAN,
description: "Whether to log edited messages",
default: true,
},
inlineEdits: {
type: OptionType.BOOLEAN,
description: "Whether to display edit history as part of message content",
default: true
},
ignoreBots: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by bots",
default: false
},
ignoreSelf: {
type: OptionType.BOOLEAN,
description: "Whether to ignore messages by yourself",
default: false
},
ignoreUsers: {
type: OptionType.STRING,
description: "Comma-separated list of user IDs to ignore",
default: ""
},
ignoreChannels: {
type: OptionType.STRING,
description: "Comma-separated list of channel IDs to ignore",
default: ""
},
ignoreGuilds: {
type: OptionType.STRING,
description: "Comma-separated list of guild IDs to ignore",
default: ""
},
},
settings: settings,
handleDelete(cache: any, data: { ids: string[], id: string; mlDeleted?: boolean; }, isBulk: boolean) {
try {

View file

@ -167,6 +167,10 @@ export const enum OptionType {
SELECT,
SLIDER,
COMPONENT,
LIST,
USERS, // List of users
CHANNELS, // List of channels
GUILDS, // List of guilds
}
export type SettingsDefinition = Record<string, PluginSettingDef>;
@ -183,6 +187,7 @@ export type PluginSettingDef = (
| PluginSettingSliderDef
| PluginSettingComponentDef
| PluginSettingBigIntDef
| PluginSettingListDef
) & PluginSettingCommon;
export interface PluginSettingCommon {
@ -259,6 +264,11 @@ export interface PluginSettingSliderDef {
stickToMarkers?: boolean;
}
export interface PluginSettingListDef{
type: OptionType.LIST | OptionType.CHANNELS | OptionType.GUILDS | OptionType.USERS;
popoutText?: string;
}
export interface IPluginOptionComponentProps {
/**
* Run this when the value changes.