Merge remote-tracking branch 'upstream/dev' into dev

This commit is contained in:
thororen1234 2026-05-04 18:20:36 -04:00
commit 6ec2a06023
No known key found for this signature in database
45 changed files with 980 additions and 363 deletions

View file

@ -136,6 +136,8 @@ export default defineConfig(
"use-isnan": "error",
"prefer-const": ["error", { destructuring: "all" }],
"prefer-spread": "error",
// These are old deprecated browser globals which may be used by mistake, e.g. `addEventListener(e => console.log(event))`
"no-restricted-globals": ["error", "event", "name"],
// Plugin Rules
"simple-import-sort/imports": "error",

View file

@ -331,6 +331,15 @@ declare enum PopoutAnimation {
type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center";
export interface PopoutProps {
position: PopoutPosition;
nudge: number;
isPositioned: boolean;
setPopoutRef(ref: any): void;
closePopout(): void;
updatePosition(): void;
}
export type Popout = ComponentType<{
children(
thing: {
@ -347,14 +356,8 @@ export type Popout = ComponentType<{
): ReactNode;
shouldShow?: boolean;
targetElementRef: RefObject<any>;
renderPopout(args: {
closePopout(): void;
isPositioned: boolean;
nudge: number;
position: PopoutPosition;
setPopoutRef(ref: any): void;
updatePosition(): void;
}): ReactNode;
renderPopout(props: PopoutProps): ReactNode;
preload?(): Promise<any>;
onRequestOpen?(): void;
onRequestClose?(): void;

View file

@ -41,8 +41,7 @@ import { popNotice, showNotice } from "./api/Notices";
import { NotificationData, showNotification } from "./api/Notifications";
import { initPluginManager, PMLogger, startAllPlugins } from "./api/PluginManager";
import { PlainSettings, Settings, SettingsStore } from "./api/Settings";
import { getCloudSettings, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync";
import { localStorage } from "./utils/localStorage";
import { areLocalSettingsDirty, getCloudSettings, getCloudSyncDirection, markLocalSettingsDirty, putCloudSettings, shouldCloudSync } from "./api/SettingsSync/cloudSync";
import { relaunch } from "./utils/native";
import { checkForUpdates, isOutdated as getIsOutdated, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
@ -53,11 +52,6 @@ if (IS_REPORTER) {
}
async function syncSettings() {
// Check if cloud auth exists for current user before attempting sync
if (localStorage.Vencord_cloudSyncDirection === undefined) {
// by default, sync bi-directionally
localStorage.Vencord_cloudSyncDirection = "both";
}
const hasCloudAuth = await dsGet("Vencord_cloudSecret");
if (!hasCloudAuth) {
if (Settings.cloud.authenticated) {
@ -93,9 +87,9 @@ async function syncSettings() {
if (
Settings.cloud.settingsSync && // if it's enabled
Settings.cloud.authenticated && // if cloud integrations are enabled
localStorage.Vencord_cloudSyncDirection !== "manual" // if we're not in manual mode
getCloudSyncDirection() !== "manual" // if we're not in manual mode
) {
if (localStorage.Vencord_settingsDirty && shouldCloudSync("push")) {
if (areLocalSettingsDirty() && shouldCloudSync("push")) {
await putCloudSettings();
} else if (shouldCloudSync("pull") && await getCloudSettings(false)) { // if we synchronized something (false means no sync)
// we show a notification here instead of allowing getCloudSettings() to show one to declutter the amount of
@ -118,7 +112,7 @@ async function syncSettings() {
}, 60_000);
SettingsStore.addGlobalChangeListener(() => {
localStorage.Vencord_settingsDirty = true;
markLocalSettingsDirty();
saveSettingsOnFrequentAction();
});
}

View file

@ -15,7 +15,7 @@
position: absolute;
z-index: 2147483647;
right: 1rem;
width: 25vw;
width: min(25vw, 400px);
min-height: 10vh;
}

View file

@ -32,7 +32,7 @@ import { traceFunction } from "@debug/Tracer";
import { Logger } from "@utils/Logger";
import { onlyOnce } from "@utils/onlyOnce";
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { DefinedSettings, Patch, Plugin, PluginDef, PluginSettingDef, ReporterTestable, StartAt } from "@utils/types";
import { FluxEvents } from "@vencord/discord-types";
import { FluxDispatcher } from "@webpack/common";
import { patches } from "@webpack/patcher";
@ -60,13 +60,32 @@ export function isPluginEnabled(p: string) {
Settings.plugins[p]?.enabled
) ?? false;
}
export function isPluginRequired(p: string) {
return (
Plugins[p]?.required
) ?? false;
}
export function isSettingHidden(settings: DefinedSettings, setting: PluginSettingDef) {
if (!("hidden" in setting)) return false;
return typeof setting.hidden === "function"
? setting.hidden.call(settings)
: Boolean(setting.hidden);
}
export function isSettingDisabled(settings: DefinedSettings, setting: PluginSettingDef) {
if (!("disabled" in setting)) return false;
return typeof setting.disabled === "function"
? setting.disabled.call(settings)
: Boolean(setting.disabled);
}
export function hasAnyVisibleSettings({ settings }: Plugin) {
return !!settings && Object.values(settings.def).some(s => !isSettingHidden(settings, s));
}
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {
// TODO: this causes crashes
if (pluginName === "Vesktop" && newPatch.find === ".STREAMING_AUTO_STREAMER_MODE,") return;
@ -416,22 +435,11 @@ export const initPluginManager = onlyOnce(function init() {
for (const p of pluginsValues) {
if (p.settings) {
p.options ??= {};
p.settings.pluginName = p.name;
for (const name in p.settings.def) {
const def = p.settings.def[name];
const checks = p.settings.checks?.[name];
p.options[name] = { ...def, ...checks };
}
}
if (p.options) {
for (const name in p.options) {
const opt = p.options[name];
if (opt.onChange != null) {
SettingsStore.addChangeListener(`plugins.${p.name}.${name}`, opt.onChange);
}
for (const [key, def] of Object.entries(p.settings.def)) {
if (def.onChange)
SettingsStore.addChangeListener(`plugins.${p.name}.${key}`, def.onChange);
}
}

View file

@ -178,7 +178,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
if (path.startsWith("plugins.")) {
const plugin = path.slice("plugins.".length);
if (plugin in plugins) {
const setting = plugins[plugin].options?.[key];
const setting = plugins[plugin].settings?.def[key];
if (!setting) return v;
if ("default" in setting)
@ -208,7 +208,7 @@ if (!IS_REPORTER) {
* Same as {@link Settings} but unproxied. You should treat this as readonly,
* as modifying properties on this will not save to disk or call settings
* listeners.
* WARNING: default values specified in plugin.options will not be ensured here. In other words,
* WARNING: default values specified in plugin.settings will not be ensured here. In other words,
* settings for which you specified a default value may be uninitialised. If you need proper
* handling for default values, use {@link Settings}
*/
@ -353,7 +353,13 @@ export function definePluginSettings<
Checks extends SettingsChecks<Def>,
PrivateSettings extends object = {}
>(def: Def, checks?: Checks) {
const definedSettings: DefinedSettings<Def, Checks, PrivateSettings> = {
if (checks) {
for (const [name, check] of Object.entries(checks)) {
Object.assign(def[name], check);
}
}
const definedSettings: DefinedSettings<Def, PrivateSettings> = {
get store() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
@ -368,11 +374,10 @@ export function definePluginSettings<
: [`plugins.${definedSettings.pluginName}.*`]
) as UseSettings<Settings>[]).plugins[definedSettings.pluginName] as any,
def,
checks: checks ?? {} as any,
pluginName: "",
withPrivateSettings<T extends object>() {
return this as DefinedSettings<Def, Checks, T>;
return this as DefinedSettings<Def, T>;
}
};

View file

@ -24,6 +24,14 @@ const API_VERSION_STORE_KEY = "Vencord_cloudApiVersions";
type ApiVersion = "v2" | "v1";
const SYNC_DIRECTION_KEY = "Vencord_cloudSyncDirection";
const SETTINGS_DIRTY_KEY = "Vencord_settingsDirty";
export const getCloudSyncDirection = () => localStorage.getItem(SYNC_DIRECTION_KEY) || "both";
export const setCloudSyncDirection = (direction: "push" | "pull" | "both" | "manual") => localStorage.setItem(SYNC_DIRECTION_KEY, direction);
export const areLocalSettingsDirty = () => localStorage.getItem(SETTINGS_DIRTY_KEY) === "true";
export const markLocalSettingsDirty = () => localStorage.setItem(SETTINGS_DIRTY_KEY, "true");
export const markLocalSettingsClean = () => localStorage.removeItem(SETTINGS_DIRTY_KEY);
async function loadApiVersionMap(): Promise<Record<string, ApiVersion>> {
return await DataStore.get<Record<string, ApiVersion>>(API_VERSION_STORE_KEY) ?? {};
}

View file

@ -5,13 +5,13 @@
*/
import { showNotice } from "@api/Notices";
import { isPluginEnabled, pluginRequiresRestart, startDependenciesRecursive, startPlugin, stopPlugin } from "@api/PluginManager";
import { hasAnyVisibleSettings, isPluginEnabled, pluginRequiresRestart, startDependenciesRecursive, startPlugin, stopPlugin } from "@api/PluginManager";
import { Settings } from "@api/Settings";
import { CogWheel, InfoIcon } from "@components/Icons";
import { AddonCard } from "@components/settings/AddonCard";
import { classNameFactory } from "@utils/css";
import { Logger } from "@utils/Logger";
import { OptionType, Plugin } from "@utils/types";
import { Plugin } from "@utils/types";
import { React, showToast, Toasts } from "@webpack/common";
import { PluginMeta } from "~plugins";
@ -146,7 +146,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on
onClick={() => openPluginModal(plugin, onRestartNeeded)}
className={cl("info-button")}
>
{plugin.settings?.def && Object.values(plugin.settings.def).some(s => s.type !== OptionType.CUSTOM && !s.hidden)
{hasAnyVisibleSettings(plugin)
? <CogWheel className={cl("info-icon")} />
: <InfoIcon className={cl("info-icon")} />
}

View file

@ -19,6 +19,7 @@
import "./PluginModal.css";
import { generateId } from "@api/Commands";
import { hasAnyVisibleSettings, isSettingHidden } from "@api/PluginManager";
import { useSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import { Button } from "@components/Button";
@ -86,7 +87,7 @@ function PluginTags({ tags }: { tags: PluginTag[]; }) {
export default function PluginModal({ plugin, onRestartNeeded, onClose, transitionState }: PluginModalProps) {
const pluginSettings = useSettings([`plugins.${plugin.name}.*`]).plugins[plugin.name];
const hasSettings = Boolean(pluginSettings && plugin.options && !isObjectEmpty(plugin.options));
const hasSettings = hasAnyVisibleSettings(plugin);
// avoid layout shift by showing dummy users while loading users
const fallbackAuthors = useMemo(() => [makeDummyUser({ username: "Loading...", id: "-1465912127305809920" })], []);
@ -114,14 +115,17 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
}
function renderSettings() {
if (!hasSettings || !plugin.options)
const { settings } = plugin;
if (!hasSettings || !settings)
return <Paragraph>There are no settings for this plugin.</Paragraph>;
const options = Object.entries(plugin.options).map(([key, setting]) => {
if (setting.type === OptionType.CUSTOM || setting.hidden) return null;
const options = Object.entries(settings.def).map(([key, setting]) => {
if (setting.type === OptionType.CUSTOM) return null;
if (isSettingHidden(settings, setting)) return null;
function onChange(newValue: any) {
const option = plugin.options?.[key];
const option = plugin.settings!.def[key];
if (!option || option.type === OptionType.CUSTOM) return;
pluginSettings[key] = newValue;
@ -134,10 +138,10 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
<ErrorBoundary noop key={key}>
<Component
id={key}
option={setting}
setting={setting}
onChange={debounce(onChange)}
pluginSettings={pluginSettings}
definedSettings={plugin.settings}
definedSettings={settings}
/>
</ErrorBoundary>
);

View file

@ -16,20 +16,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { isSettingDisabled } from "@api/PluginManager";
import { Switch } from "@components/Switch";
import { PluginOptionBoolean } from "@utils/types";
import { PluginSettingBooleanDef } from "@utils/types";
import { React, useState } from "@webpack/common";
import { resolveError, SettingProps, SettingsSection } from "./Common";
export function BooleanSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionBoolean>) {
const def = pluginSettings[id] ?? option.default;
export function BooleanSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingBooleanDef>) {
const def = pluginSettings[id] ?? setting.default;
const [state, setState] = useState(def ?? false);
const [error, setError] = useState<string | null>(null);
function handleChange(newValue: boolean): void {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
setState(newValue);
setError(resolveError(isValid));
@ -40,11 +41,11 @@ export function BooleanSetting({ option, pluginSettings, definedSettings, id, on
}
return (
<SettingsSection tag="label" name={id} description={option.description} error={error} inlineSetting>
<SettingsSection tag="label" name={id} description={setting.description} error={error} inlineSetting>
<Switch
checked={state}
onChange={handleChange}
disabled={option.disabled?.call(definedSettings) ?? false}
disabled={isSettingDisabled(definedSettings, setting)}
/>
</SettingsSection>
);

View file

@ -8,24 +8,24 @@ import { BaseText } from "@components/BaseText";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { DefinedSettings, PluginOptionBase } from "@utils/types";
import { DefinedSettings, PluginSettingDefCommon } from "@utils/types";
import { PropsWithChildren } from "react";
export const cl = classNameFactory("vc-plugins-setting-");
interface SettingBaseProps<T> {
option: T;
setting: T;
onChange(newValue: any): void;
pluginSettings: {
[setting: string]: any;
enabled: boolean;
};
id: string;
definedSettings?: DefinedSettings;
definedSettings: DefinedSettings;
}
export type SettingProps<T extends PluginOptionBase> = SettingBaseProps<T>;
export type ComponentSettingProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = SettingBaseProps<T>;
export type SettingProps<T extends PluginSettingDefCommon> = SettingBaseProps<T>;
export type ComponentSettingProps<T extends Omit<PluginSettingDefCommon, "description" | "placeholder">> = SettingBaseProps<T>;
export function resolveError(isValidResult: boolean | string) {
if (typeof isValidResult === "string") return isValidResult;

View file

@ -16,10 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionComponent } from "@utils/types";
import { PluginSettingComponentDef } from "@utils/types";
import { ComponentSettingProps } from "./Common";
export function ComponentSetting({ option, onChange }: ComponentSettingProps<PluginOptionComponent>) {
return option.component({ setValue: onChange, option });
export function ComponentSetting({ setting, onChange }: ComponentSettingProps<PluginSettingComponentDef>) {
return setting.component({ setValue: onChange, option: setting });
}

View file

@ -16,24 +16,25 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { OptionType, PluginOptionNumber } from "@utils/types";
import { isSettingDisabled } from "@api/PluginManager";
import { OptionType, PluginSettingBigIntDef, PluginSettingNumberDef } from "@utils/types";
import { React, TextInput, useState } from "@webpack/common";
import { resolveError, SettingProps, SettingsSection } from "./Common";
const MAX_SAFE_NUMBER = BigInt(Number.MAX_SAFE_INTEGER);
export function NumberSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionNumber>) {
export function NumberSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingNumberDef | PluginSettingBigIntDef>) {
function serialize(value: any) {
if (option.type === OptionType.BIGINT) return BigInt(value);
if (setting.type === OptionType.BIGINT) return BigInt(value);
return Number(value);
}
const [state, setState] = useState<any>(`${pluginSettings[id] ?? option.default ?? 0}`);
const [state, setState] = useState<any>(`${pluginSettings[id] ?? setting.default ?? 0}`);
const [error, setError] = useState<string | null>(null);
function handleChange(newValue: any) {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
setError(resolveError(isValid));
@ -41,7 +42,7 @@ export function NumberSetting({ option, pluginSettings, definedSettings, id, onC
onChange(serialize(newValue));
}
if (option.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
if (setting.type === OptionType.NUMBER && BigInt(newValue) >= MAX_SAFE_NUMBER) {
setState(`${Number.MAX_SAFE_INTEGER}`);
} else {
setState(newValue);
@ -49,15 +50,15 @@ export function NumberSetting({ option, pluginSettings, definedSettings, id, onC
}
return (
<SettingsSection name={id} description={option.description} error={error}>
<SettingsSection name={id} description={setting.description} error={error}>
<TextInput
type="number"
pattern="-?[0-9]+"
placeholder={option.placeholder ?? "Enter a number"}
placeholder={setting.placeholder ?? "Enter a number"}
value={state}
onChange={handleChange}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
disabled={isSettingDisabled(definedSettings, setting)}
{...setting.componentProps}
/>
</SettingsSection>
);

View file

@ -16,19 +16,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionSelect } from "@utils/types";
import { isSettingDisabled } from "@api/PluginManager";
import { PluginSettingSelectDef } from "@utils/types";
import { React, Select, useState } from "@webpack/common";
import { resolveError, SettingProps, SettingsSection } from "./Common";
export function SelectSetting({ option, pluginSettings, definedSettings, onChange, id }: SettingProps<PluginOptionSelect>) {
const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value;
export function SelectSetting({ setting, pluginSettings, definedSettings, onChange, id }: SettingProps<PluginSettingSelectDef>) {
const def = pluginSettings[id] ?? setting.options?.find(o => o.default)?.value;
const [state, setState] = useState<any>(def ?? null);
const [error, setError] = useState<string | null>(null);
function handleChange(newValue: any) {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
setState(newValue);
setError(resolveError(isValid));
@ -39,17 +40,17 @@ export function SelectSetting({ option, pluginSettings, definedSettings, onChang
}
return (
<SettingsSection name={id} description={option.description} error={error}>
<SettingsSection name={id} description={setting.description} error={error}>
<Select
placeholder={option.placeholder ?? "Select an option"}
options={option.options}
placeholder={setting.placeholder ?? "Select an option"}
options={setting.options}
maxVisibleItems={5}
closeOnSelect={true}
select={handleChange}
isSelected={v => v === state}
serialize={v => String(v)}
isDisabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
isDisabled={isSettingDisabled(definedSettings, setting)}
{...setting.componentProps}
/>
</SettingsSection>
);

View file

@ -16,18 +16,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionSlider } from "@utils/types";
import { isSettingDisabled } from "@api/PluginManager";
import { PluginSettingSliderDef } from "@utils/types";
import { React, Slider, useState } from "@webpack/common";
import { resolveError, SettingProps, SettingsSection } from "./Common";
export function SliderSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionSlider>) {
const def = pluginSettings[id] ?? option.default;
export function SliderSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingSliderDef>) {
const def = pluginSettings[id] ?? setting.default;
const [error, setError] = useState<string | null>(null);
function handleChange(newValue: number): void {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
setError(resolveError(isValid));
@ -37,17 +38,17 @@ export function SliderSetting({ option, pluginSettings, definedSettings, id, onC
}
return (
<SettingsSection name={id} description={option.description} error={error}>
<SettingsSection name={id} description={setting.description} error={error}>
<Slider
markers={option.markers}
minValue={option.markers[0]}
maxValue={option.markers[option.markers.length - 1]}
markers={setting.markers}
minValue={setting.markers[0]}
maxValue={setting.markers[setting.markers.length - 1]}
initialValue={def}
onValueChange={handleChange}
onValueRender={(v: number) => String(v.toFixed(2))}
stickToMarkers={option.stickToMarkers ?? true}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
stickToMarkers={setting.stickToMarkers ?? true}
disabled={isSettingDisabled(definedSettings, setting)}
{...setting.componentProps}
/>
</SettingsSection>
);

View file

@ -16,17 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { PluginOptionString } from "@utils/types";
import { isSettingDisabled } from "@api/PluginManager";
import { PluginSettingStringDef } from "@utils/types";
import { React, TextArea, TextInput, useState } from "@webpack/common";
import { resolveError, SettingProps, SettingsSection } from "./Common";
export function TextSetting({ option, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginOptionString>) {
const [state, setState] = useState(pluginSettings[id] ?? option.default ?? null);
export function TextSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps<PluginSettingStringDef>) {
const [state, setState] = useState(pluginSettings[id] ?? setting.default ?? null);
const [error, setError] = useState<string | null>(null);
function handleChange(newValue: string) {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
const isValid = setting.isValid?.call(definedSettings, newValue) ?? true;
setState(newValue);
setError(resolveError(isValid));
@ -37,22 +38,22 @@ export function TextSetting({ option, pluginSettings, definedSettings, id, onCha
}
return (
<SettingsSection name={id} description={option.description} error={error}>
{option.multiline
<SettingsSection name={id} description={setting.description} error={error}>
{setting.multiline
? <TextArea
placeholder={option.placeholder ?? "Enter a value"}
placeholder={setting.placeholder ?? "Enter a value"}
value={state}
onChange={handleChange}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps} />
disabled={isSettingDisabled(definedSettings, setting)}
{...setting.componentProps} />
: <TextInput
type="text"
placeholder={option.placeholder ?? "Enter a value"}
placeholder={setting.placeholder ?? "Enter a value"}
value={state}
onChange={handleChange}
maxLength={null}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
disabled={isSettingDisabled(definedSettings, setting)}
{...setting.componentProps}
/>
}
</SettingsSection>

View file

@ -30,7 +30,6 @@ import { Link } from "@components/Link";
import { Notice } from "@components/Notice";
import { Paragraph } from "@components/Paragraph";
import { SettingsTab, wrapTab } from "@components/settings/tabs/BaseTab";
import { localStorage } from "@utils/localStorage";
import { Margins } from "@utils/margins";
import { useForceUpdater } from "@utils/react";
import { findComponentByCodeLazy } from "@webpack";

View file

@ -5,7 +5,7 @@
*/
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { isPluginEnabled, plugins } from "@api/PluginManager";
import { isPluginEnabled, isSettingDisabled, isSettingHidden, plugins } from "@api/PluginManager";
import { Settings, useSettings } from "@api/Settings";
import { openPluginModal, openSettingsTabModal, PluginsTab, ThemesTab } from "@components/settings";
import { useAwaiter } from "@utils/react";
@ -81,8 +81,8 @@ export function buildPluginMenuEntries(includeEmpty = false) {
let hasAnyOption = false;
if (p.options) for (const [key, option] of Object.entries(p.options)) {
if ("hidden" in option && option.hidden) continue;
if (p.settings) for (const [key, option] of Object.entries(p.settings.def)) {
if (isSettingHidden(p.settings, option)) continue;
hasAnyOption = true;
@ -92,7 +92,7 @@ export function buildPluginMenuEntries(includeEmpty = false) {
id: `${p.name}-${key}`,
key: key,
label: wordsToTitle(wordsFromCamel(key)),
disabled: "disabled" in option ? option.disabled?.call(p.settings) : false,
disabled: isSettingDisabled(p.settings, option)
};
switch (option.type) {

View file

@ -18,7 +18,6 @@
import "./spotifyStyles.css";
import { Settings } from "@api/Settings";
import { Flex } from "@components/Flex";
import { CopyIcon, ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { Paragraph } from "@components/Paragraph";
@ -29,6 +28,7 @@ import { copyWithToast, openImageModal } from "@utils/discord";
import { classes } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
import { settings } from "../settings";
import { SeekBar } from "./SeekBar";
import { SpotifyStore, Track } from "./SpotifyStore";
@ -132,7 +132,7 @@ function Controls() {
<Shuffle />
</Button>
<Button onClick={() => {
Settings.plugins.MusicControls.previousButtonRestartsTrack && SpotifyStore.position > 3000 ? SpotifyStore.seek(0) : SpotifyStore.prev();
settings.store.previousButtonRestartsTrack && SpotifyStore.position > 3000 ? SpotifyStore.seek(0) : SpotifyStore.prev();
}}>
<SkipPrev />
</Button>

View file

@ -17,11 +17,12 @@
*/
import { isPluginEnabled } from "@api/PluginManager";
import { Settings } from "@api/Settings";
import OpenInAppPlugin from "@plugins/openInApp";
import { findByProps, findByPropsLazy, proxyLazyWebpack } from "@webpack";
import { Flux, FluxDispatcher } from "@webpack/common";
import { settings } from ".";
export interface Track {
id: string;
name: string;
@ -91,7 +92,7 @@ export const SpotifyStore = proxyLazyWebpack(() => {
public isSettingPosition = false;
public openExternal(path: string) {
const url = Settings.plugins.SpotifyControls.useSpotifyUris || isPluginEnabled(OpenInAppPlugin.name)
const url = settings.store.useSpotifyUris || isPluginEnabled(OpenInAppPlugin.name)
? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":")
: "https://open.spotify.com" + path;

View file

@ -152,6 +152,7 @@ ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => {
title: "Equicord QuickCSS Editor",
autoHideMenuBar: true,
darkTheme: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#1e1e1e" : "white",
webPreferences: {
preload: join(__dirname, "preload.js"),
contextIsolation: true,

View file

@ -107,6 +107,11 @@ const settings = definePluginSettings({
{ label: "Below Activity Settings", value: "belowActivity" },
{ label: "At the very bottom", value: "bottom" },
] as { label: string; value: SettingsLocation; default?: boolean; }[]
},
includeVencordInfoWhenCopying: {
type: OptionType.BOOLEAN,
description: "Also copy Vencord info (Vencord, Electron, Chromium) when clicking the version info in the bottom left area of the Settings page",
default: true
}
});
@ -254,7 +259,7 @@ export default definePlugin({
belowNitro: "billing_section",
aboveActivity: "activity_section",
belowActivity: "activity_section",
bottom: "logout_section"
bottom: "utility_section"
};
const key = places[settingsLocation] ?? places.top;
@ -317,6 +322,7 @@ export default definePlugin({
},
getInfoString() {
if (!settings.store.includeVencordInfoWhenCopying) return "";
return "\n" + this.getInfoRows().join("\n");
},

View file

@ -60,15 +60,20 @@ const settings = definePluginSettings({
randomisedLength: {
description: "Random characters length",
type: OptionType.NUMBER,
default: 7,
disabled: () => settings.store.method !== Methods.Random,
default: 7
},
consistent: {
description: "Consistent filename",
type: OptionType.STRING,
default: "image",
disabled: () => settings.store.method !== Methods.Consistent,
default: "image"
},
}, {
randomisedLength: {
disabled() { return this.store.method !== Methods.Random; },
},
consistent: {
disabled() { return this.store.method !== Methods.Consistent; },
}
});
export default definePlugin({

View file

@ -1,78 +0,0 @@
/*
* 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 { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { canonicalizeMatch } from "@utils/patches";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
hide: {
type: OptionType.BOOLEAN,
description: "Hide notes",
default: false,
restartNeeded: true
},
noSpellCheck: {
type: OptionType.BOOLEAN,
description: "Disable spellcheck in notes",
disabled: () => Settings.plugins.BetterNotesBox.hide,
default: false
}
});
export default definePlugin({
name: "BetterNotesBox",
description: "Hide notes or disable spellcheck (Configure in settings!!)",
authors: [Devs.Ven],
tags: ["Customisation", "Utility"],
settings,
patches: [
{
find: "hideNote:",
all: true,
// Some modules match the find but the replacement is returned untouched
noWarn: true,
predicate: () => settings.store.hide,
replacement: {
match: /hideNote:.+?(?=([,}].*?\)))/g,
replace: (m, rest) => {
const destructuringMatch = rest.match(/}=.+/);
if (destructuringMatch) {
const defaultValueMatch = m.match(canonicalizeMatch(/hideNote:(\i)=!?\d/));
return defaultValueMatch ? `hideNote:${defaultValueMatch[1]}=!0` : m;
}
return "hideNote:!0";
}
}
},
{
find: "#{intl::NOTE_PLACEHOLDER}",
replacement: {
match: /#{intl::NOTE_PLACEHOLDER}\),/,
replace: "$&spellCheck:!$self.noSpellCheck,"
}
}
],
get noSpellCheck() {
return settings.store.noSpellCheck;
}
});

View file

@ -9,12 +9,18 @@ import { getUserSettingLazy } from "@api/UserSettings";
import { ImageIcon } from "@components/Icons";
import { copyToClipboard } from "@utils/clipboard";
import { Devs } from "@utils/constants";
import { getCurrentGuild, openImageModal } from "@utils/discord";
import { getCurrentChannel, getCurrentGuild, openImageModal } from "@utils/discord";
import { isTruthy } from "@utils/guards";
import { classes } from "@utils/misc";
import definePlugin, { OptionType } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { GuildRoleStore, Menu, PermissionStore } from "@webpack/common";
import { Guild, PopoutProps, Role } from "@vencord/discord-types";
import { findByCodeLazy, findByPropsLazy, findCssClassesLazy } from "@webpack";
import { GuildRoleStore, Menu, PermissionStore, Popout, useRef } from "@webpack/common";
import { ComponentType } from "react";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
const MenuItemClasses = findCssClassesLazy("item", "labelContainer", "colorDefault", "label", "iconContainer");
const loadRoleMembers = findByCodeLazy(".GUILD_ROLE_MEMBER_IDS(", "requestMembersById");
const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;
@ -65,6 +71,14 @@ function AppearanceIcon() {
);
}
function RoleMembersIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM11.53 11A9.53 9.53 0 0 0 2 20.53c0 .81.66 1.47 1.47 1.47h.22c.24 0 .44-.17.5-.4.29-1.12.84-2.17 1.32-2.91.14-.21.43-.1.4.15l-.26 2.61c-.02.3.2.55.5.55h11.7a.5.5 0 0 0 .5-.55l-.27-2.6c-.02-.26.27-.37.41-.16.48.74 1.03 1.8 1.32 2.9.06.24.26.41.5.41h.22c.81 0 1.47-.66 1.47-1.47A9.53 9.53 0 0 0 12.47 11h-.94Z" />
</svg>
);
}
const settings = definePluginSettings({
roleIconFileFormat: {
type: OptionType.SELECT,
@ -87,6 +101,101 @@ const settings = definePluginSettings({
}
});
interface RoleMemberPopoutProps {
popoutProps: PopoutProps;
guildId: string;
channelId: string;
roleId: string;
}
type RoleMemberPopout = ComponentType<RoleMemberPopoutProps>;
let RoleMemberPopout: RoleMemberPopout = () => null;
export function buildExtraRoleContextMenuItems(role: Role, guild: Guild, popoutRef?: React.RefObject<any>) {
if (!role) return { before: [], after: [] };
const before = [
PermissionStore.getGuildPermissionProps(guild).canManageRoles && (
<Menu.MenuItem
key="vc-edit-role"
id="vc-edit-role"
label="Edit Role"
action={async () => {
await GuildSettingsActions.open(guild.id, "ROLES");
GuildSettingsActions.selectRole(role.id);
}}
icon={PencilIcon}
/>
),
role.colorString && (
<Menu.MenuItem
key="vc-copy-role-color"
id="vc-copy-role-color"
label="Copy Role Color"
action={() => copyToClipboard(role.colorString!)}
icon={AppearanceIcon}
/>
)
].filter(isTruthy);
const after = [
role.icon && (
<Menu.MenuItem
key="vc-view-role-icon"
id="vc-view-role-icon"
label="View Role Icon"
action={() => {
openImageModal({
url: `${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${role.id}/${role.icon}.${settings.store.roleIconFileFormat}`,
height: 128,
width: 128
});
}}
icon={ImageIcon}
/>
),
popoutRef && (
<Menu.MenuItem
key="vc-view-role-members"
id="vc-view-role-members"
label="View Role Members"
render={() => (
<Popout
position="right"
align="center"
targetElementRef={popoutRef}
preload={() => loadRoleMembers(guild.id, role.id)}
renderPopout={popoutProps => (
<RoleMemberPopout
popoutProps={popoutProps}
guildId={guild.id}
channelId={getCurrentChannel()!.id}
roleId={role.id}
/>
)}
>
{popoutProps => (
<div
className={classes(MenuItemClasses.item, MenuItemClasses.labelContainer, MenuItemClasses.colorDefault)}
ref={popoutRef}
role="menuitem"
{...popoutProps}
>
<div className={MenuItemClasses.label}>View Role Members</div>
<div className={MenuItemClasses.iconContainer}>
<RoleMembersIcon />
</div>
</div>
)}
</Popout>
)}
/>
)
].filter(isTruthy);
return { before, after };
}
export default definePlugin({
name: "BetterRoleContext",
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
@ -96,13 +205,27 @@ export default definePlugin({
settings,
patches: [{
find: ".ROLE_MENTION)",
replacement: {
match: /function (\i)(?=.+?renderPopout:.{0,20}\1,\{guildId:\i,channelId:\i)/,
replace: "$self.RoleMembers=$1;$&"
}
}],
start() {
// DeveloperMode needs to be enabled for the context menu to be shown
DeveloperMode.updateSetting(true);
},
set RoleMembers(component: RoleMemberPopout) {
RoleMemberPopout = component;
},
contextMenus: {
"dev-context"(children, { id }: { id: string; }) {
const popoutRef = useRef(null);
const guild = getCurrentGuild();
if (!guild) return;

View file

@ -33,7 +33,7 @@ const settings = definePluginSettings({
description: "Allow click on role dot in profile popout to copy role color",
restartNeeded: true,
default: false
},
}
});
export default definePlugin({

View file

@ -12,6 +12,15 @@ import definePlugin, { OptionType } from "@utils/types";
let style: HTMLStyleElement;
const settings = definePluginSettings({
blurAmount: {
type: OptionType.NUMBER,
description: "Blur Amount (in pixels)",
default: 10,
onChange: setCss
}
});
function setCss() {
style.textContent = `
.vc-nsfw-img [class*=imageContainer],
@ -26,20 +35,6 @@ function setCss() {
`;
}
const settings = definePluginSettings({
blurAmount: {
type: OptionType.NUMBER,
description: "Blur Amount (in pixels)",
default: 10,
onChange: setCss
},
blurAllChannels: {
type: OptionType.BOOLEAN,
description: "Blur attachments in all channels (not just NSFW)",
default: false
},
});
export default definePlugin({
name: "BlurNSFW",
description: "Blur attachments in NSFW channels until hovered",

View file

@ -4,12 +4,13 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { HeadingSecondary } from "@components/Heading";
import { Paragraph } from "@components/Paragraph";
import CustomRpcPlugin from "@plugins/customRPC";
import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
@ -85,7 +86,7 @@ function ImportCustomRPCComponent() {
<div>
<Button
onClick={() => {
const id = Settings.plugins.CustomRPC?.appID as string | undefined;
const id = CustomRpcPlugin.settings.store.appID;
if (!id) {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
}

View file

@ -97,7 +97,7 @@ const settings = definePluginSettings({
default: false,
},
statusName: {
description: "Custom status text",
description: "Custom status text. You can use the following variables: {artist} | {album} | {title}",
type: OptionType.STRING,
default: "some music",
},
@ -176,11 +176,11 @@ const settings = definePluginSettings({
type: OptionType.BOOLEAN,
default: true,
},
alwaysHideArt: {
description: "Disable downloading album art",
showAlbumCover: {
description: "Show album cover. Disabling this will display a placeholder. Useful if your Music has inappropriate art",
type: OptionType.BOOLEAN,
default: false,
},
default: true,
}
});
export default definePlugin({
@ -258,7 +258,7 @@ export default definePlugin({
},
getLargeImage(track: TrackData): string | undefined {
if (!settings.store.alwaysHideArt && track.imageUrl && !track.imageUrl.includes(LASTFM_PLACEHOLDER_IMAGE_HASH))
if (settings.store.showAlbumCover && track.imageUrl && !track.imageUrl.includes(LASTFM_PLACEHOLDER_IMAGE_HASH))
return track.imageUrl;
if (settings.store.missingArt === "placeholder")
@ -315,9 +315,15 @@ export default definePlugin({
case NameFormat.SongOnly:
return trackData.name;
case NameFormat.AlbumName:
return trackData.album || settings.store.statusName;
return trackData.album || settings.store.statusName
.replaceAll("{artist}", trackData.artist || "")
.replaceAll("{album}", trackData.album || "")
.replaceAll("{title}", trackData.name || "");
default:
return settings.store.statusName;
return settings.store.statusName
.replaceAll("{artist}", trackData.artist || "")
.replaceAll("{album}", trackData.album || "")
.replaceAll("{title}", trackData.name || "");
}
})();

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs, IS_LINUX } from "@utils/constants";
import definePlugin from "@utils/types";
function preventMiddleClick(e: MouseEvent) {
if (e.button === 1) {
e.preventDefault();
}
}
export default definePlugin({
name: "NoMiddleClickPaste",
description: "Disable Linux middle-click paste - Linux only",
authors: [Devs.Darxoon],
hidden: !IS_LINUX,
start() {
window.addEventListener("mouseup", preventMiddleClick);
},
stop() {
window.removeEventListener("mouseup", preventMiddleClick);
},
});

View file

@ -27,6 +27,13 @@ export default definePlugin({
settings,
patches: [
{
find: '"PLAINTEXT_PREVIEW":"OTHER"',
replacement: {
match: /"IMAGE"===\i\|\|"VIDEO"===\i\|\|"CLIP"===\i/,
replace: "false"
}
},
{
find: "renderAttachments(",
predicate: () => settings.store.inlineVideo,

View file

@ -20,14 +20,15 @@ import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
import { cl, getGuildPermissionSpecMap } from "@plugins/permissionsViewer/utils";
import { buildExtraRoleContextMenuItems } from "@plugins/betterRoleContext";
import { cl, getGuildPermissionSpecMap, loadGetGuildPermissionSpecMap } from "@plugins/permissionsViewer/utils";
import { copyToClipboard } from "@utils/clipboard";
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModalLazy } from "@utils/modal";
import { Guild, Role, RoleOrUserPermission, UnicodeEmoji, User } from "@vencord/discord-types";
import { PermissionOverwriteType } from "@vencord/discord-types/enums";
import { findByCodeLazy } from "@webpack";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildRoleStore, i18n, Menu, PermissionsBits, ScrollerThin, Tooltip, useEffect, useMemo, useRef, UserStore, useState, useStateFromStores } from "@webpack/common";
import { settings } from "..";
import { PermissionAllowedIcon, PermissionDefaultIcon, PermissionDeniedIcon } from "./icons";
@ -205,25 +206,50 @@ function RolesAndUsersPermissionsComponent({ permissions, guild, modalProps, hea
);
}
function IDIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M15.3 14.48c-.46.45-1.08.67-1.86.67h-1.39V9.2h1.39c.78 0 1.4.22 1.86.67.46.45.68 1.22.68 2.31 0 1.1-.22 1.86-.68 2.31Z" />
<path fill="currentColor" fillRule="evenodd" clipRule="evenodd" d="M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm1 15h2.04V7.34H6V17Zm4-9.66V17h3.44c1.46 0 2.6-.42 3.38-1.25.8-.83 1.2-2.02 1.2-3.58s-.4-2.75-1.2-3.58c-.79-.83-1.92-1.25-3.38-1.25H10Z" />
</svg>
);
}
function ViewAsRoleIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24">
<path fill="currentColor" d="M20.7 12.7a1 1 0 0 0 0-1.4l-5-5a1 1 0 1 0-1.4 1.4l3.29 3.3H4a1 1 0 1 0 0 2h13.59l-3.3 3.3a1 1 0 0 0 1.42 1.4l5-5Z" />
</svg>
);
}
function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: string; onClose: () => void; }) {
const popoutRef = useRef(null);
const role = GuildRoleStore.getRole(guild.id, roleId);
const { before, after } = buildExtraRoleContextMenuItems(role, guild, popoutRef);
return (
<Menu.Menu
navId={cl("role-context-menu")}
onClose={ContextMenuApi.closeContextMenu}
aria-label="Role Options"
>
{before}
<Menu.MenuItem
id={cl("copy-role-id")}
label={getIntlMessage("COPY_ID_ROLE")}
action={() => {
copyToClipboard(roleId);
}}
icon={IDIcon}
action={() => copyToClipboard(roleId)}
/>
{after}
{(settings.store as any).unsafeViewAsRole && (
<Menu.MenuItem
id={cl("view-as-role")}
label={getIntlMessage("VIEW_AS_ROLE")}
icon={ViewAsRoleIcon}
action={() => {
const role = GuildRoleStore.getRole(guild.id, roleId);
if (!role) return;
@ -267,12 +293,16 @@ function UserContextMenu({ userId }: { userId: string; }) {
const RolesAndUsersPermissions = ErrorBoundary.wrap(RolesAndUsersPermissionsComponent);
export default function openRolesAndUsersPermissionsModal(permissions: Array<RoleOrUserPermission>, guild: Guild, header: string) {
return openModal(modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
));
return openModalLazy(async () => {
await loadGetGuildPermissionSpecMap();
return modalProps => (
<RolesAndUsersPermissions
modalProps={modalProps}
permissions={permissions}
guild={guild}
header={header}
/>
);
});
}

View file

@ -29,11 +29,11 @@ import definePlugin, { OptionType } from "@utils/types";
import type { Guild, RoleOrUserPermission } from "@vencord/discord-types";
import { PermissionOverwriteType } from "@vencord/discord-types/enums";
import { findCssClassesLazy } from "@webpack";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, useRef, UserStore } from "@webpack/common";
import { Button, ChannelStore, Dialog, GuildMemberStore, GuildRoleStore, GuildStore, match, Menu, PermissionsBits, Popout, useEffect, useRef, UserStore } from "@webpack/common";
import openRolesAndUsersPermissionsModal from "./components/RolesAndUsersPermissions";
import UserPermissions from "./components/UserPermissions";
import { getSortedRolesForMember, sortPermissionOverwrites } from "./utils";
import { getSortedRolesForMember, loadGetGuildPermissionSpecMap, sortPermissionOverwrites } from "./utils";
const PopoutClasses = findCssClassesLazy("container", "popoutRoleDot");
@ -175,11 +175,12 @@ export default definePlugin({
],
ViewPermissionsButton: ErrorBoundary.wrap(({ className, guild, userId }: { className: string; guild: Guild; userId: string; }) => {
const buttonRef = useRef(null);
useEffect(() => void loadGetGuildPermissionSpecMap(), []);
const guildMember = GuildMemberStore.getMember(guild.id, userId);
if (!guildMember) return null;
const buttonRef = useRef(null);
return (
<Popout
position="bottom"

View file

@ -19,10 +19,12 @@
import { classNameFactory } from "@utils/css";
import { Guild, GuildMember, Role } from "@vencord/discord-types";
import { PermissionOverwriteType } from "@vencord/discord-types/enums";
import { findByPropsLazy } from "@webpack";
import { extractAndLoadChunksLazy, findByPropsLazy } from "@webpack";
import { GuildRoleStore } from "@webpack/common";
import { PermissionsSortOrder, settings } from ".";
export const loadGetGuildPermissionSpecMap = extractAndLoadChunksLazy([".PRIMARY,badgeTooltipDelay:"]);
export const { getGuildPermissionSpecMap } = findByPropsLazy("getGuildPermissionSpecMap");
export const cl = classNameFactory("vc-permviewer-");

View file

@ -17,7 +17,6 @@
*/
import { isPluginEnabled } from "@api/PluginManager";
import { Settings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import PermissionsViewerPlugin from "@plugins/permissionsViewer";
@ -146,7 +145,7 @@ function HiddenChannelLockScreen({ channel }: { channel: Channel; }) {
});
}
if (Settings.plugins.PermissionsViewer.enabled) {
if (isPluginEnabled(PermissionsViewerPlugin.name)) {
setPermissions(sortPermissionOverwrites(Object.values(permissionOverwrites).map(overwrite => ({
type: overwrite.type,
id: overwrite.id,

View file

@ -18,13 +18,13 @@
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
import { Paragraph } from "@components/Paragraph";
import { TooltipContainer } from "@components/TooltipContainer";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { IconComponent } from "@utils/types";
import { Alerts, Tooltip, useEffect, useState } from "@webpack/common";
import { Alerts, useEffect, useState } from "@webpack/common";
import { settings } from "./settings";
import { TranslateModal } from "./TranslateModal";
import { openTranslateModal } from "./TranslateModal";
import { cl } from "./utils";
export const TranslateIcon: IconComponent = ({ height = 20, width = 20, className }) => {
@ -79,10 +79,7 @@ export const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
tooltip="Open Translate Modal"
onClick={e => {
if (e.shiftKey) return toggle();
openModal(props => (
<TranslateModal rootProps={props} />
));
else openTranslateModal();
}}
onContextMenu={toggle}
buttonProps={{
@ -95,9 +92,9 @@ export const TranslateChatBarIcon: ChatBarButtonFactory = ({ isMainChat }) => {
if (shouldShowTranslateEnabledTooltip && settings.store.showAutoTranslateTooltip)
return (
<Tooltip text="Auto Translate Enabled" forceOpen>
{() => button}
</Tooltip>
<TooltipContainer text="Auto Translate Enabled" forceOpen>
{button}
</TooltipContainer>
);
return button;

View file

@ -16,11 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { BaseText } from "@components/BaseText";
import { Divider } from "@components/Divider";
import { FormSwitch } from "@components/FormSwitch";
import { HeadingPrimary, HeadingSecondary } from "@components/Heading";
import { HeadingSecondary } from "@components/Heading";
import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal";
import { SearchableSelect, useMemo } from "@webpack/common";
import { settings } from "./settings";
@ -77,9 +78,9 @@ export function TranslateModal({ rootProps }: { rootProps: ModalProps; }) {
return (
<ModalRoot {...rootProps}>
<ModalHeader className={cl("modal-header")}>
<HeadingPrimary className={cl("modal-title")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("modal-title")}>
Translate
</HeadingPrimary>
</BaseText>
<ModalCloseButton onClick={rootProps.onClose} />
</ModalHeader>
@ -99,3 +100,7 @@ export function TranslateModal({ rootProps }: { rootProps: ModalProps; }) {
</ModalRoot>
);
}
export function openTranslateModal() {
openModal(props => <TranslateModal rootProps={props} />);
}

View file

@ -62,10 +62,10 @@ let tooltipTimeout: any;
export default definePlugin({
name: "Translate",
description: "Translate messages with Google Translate or DeepL",
description: "Translate messages with Google Translate, DeepL or Kagi.",
dependencies: ["ChatInputButtonAPI", "MessageAccessoriesAPI", "MessagePopoverAPI"],
tags: ["Chat", "Utility"],
authors: [Devs.Ven, Devs.AshtonMemer],
authors: [Devs.Ven, Devs.AshtonMemer, Devs.koish1],
settings,
contextMenus: {
"message": messageCtxPatch

View file

@ -33,6 +33,7 @@ copy(Object.fromEntries(
export type GoogleLanguage = keyof typeof GoogleLanguages;
export type DeeplLanguage = keyof typeof DeeplLanguages;
export type KagiLanguage = keyof typeof KagiLanguages;
export const GoogleLanguages = {
"auto": "Detect language",
@ -318,3 +319,392 @@ export function deeplLanguageToGoogleLanguage(language: string) {
return language;
}
}
// Generated using:
// curl https://translate.kagi.com/api/list-languages | jq 'map({(.language | ascii_downcase): .name}) | add | to_entries | sort_by(.value) | from_entries'
// Add "auto": "Detect language", as kagi has no "language" for Detect Language, however anything not
// in the list will work as auto. Using "auto" will have this work using Google Translate defaults.
// Retrieved 2025.06.16
export const KagiLanguages = {
"auto": "Detect language",
"om": "(Afan)/Oromoor/Oriya",
"ab": "Abkhazian",
"ace": "Acehnese",
"ach": "Acholi",
"aa": "Afar",
"af": "Afrikaans",
"ak": "Akan",
"sq": "Albanian",
"alz": "Alur",
"am": "Amharic",
"ar": "Arabic",
"ar_dz": "Arabic (Algeria)",
"ar_eg": "Arabic (Egypt)",
"ar_iq": "Arabic (Iraq)",
"ar_jo": "Arabic (Jordan)",
"ar_kw": "Arabic (Kuwait)",
"ar_lb": "Arabic (Lebanon)",
"ar_ly": "Arabic (Libya)",
"ar_ma": "Arabic (Morocco)",
"ar_qa": "Arabic (Qatar)",
"ar_sa": "Arabic (Saudi Arabia)",
"ar_sy": "Arabic (Syria)",
"ar_tn": "Arabic (Tunisia)",
"ar_ae": "Arabic (UAE)",
"an": "Aragonese",
"hy": "Armenian",
"as": "Assamese",
"av": "Avaric",
"ae": "Avestan",
"ava": "Awadhi",
"ay": "Aymara",
"az": "Azeri",
"ban": "Balinese",
"bal": "Baluchi",
"bm": "Bambara",
"bci": "Baoulé",
"ba": "Bashkir",
"eu": "Basque",
"btk": "Batak",
"be": "Belarusian",
"bem": "Bemba",
"bn": "Bengali",
"bn_bd": "Bengali (Bangladesh)",
"bn_in": "Bengali (India)",
"bew": "Betawi",
"bho": "Bhojpuri",
"dz": "Bhutani",
"bh": "Bihari",
"bcl": "Bikol",
"bi": "Bislama",
"bs": "Bosnian",
"br": "Breton",
"bg": "Bulgarian",
"my": "Burmese",
"bua": "Buryat",
"km": "Cambodian",
"yue": "Cantonese",
"ca": "Catalan",
"ceb": "Cebuano",
"ch": "Chamorro",
"ce": "Chechen",
"chr": "Cherokee",
"ny": "Chichewa",
"zh": "Chinese",
"zh_hk": "Chinese (Hong Kong)",
"zh_mo": "Chinese (Macau)",
"zh_cn": "Chinese (Simplified)",
"zh_sg": "Chinese (Singapore)",
"zh_tw": "Chinese (Traditional)",
"cu": "Church Slavonic",
"chk": "Chuukese",
"cv": "Chuvash",
"kw": "Cornish",
"co": "Corsican",
"cr": "Cree",
"crh": "Crimean Tatar",
"hr": "Croatian",
"hr_ba": "Croatian (Bosnia and Herzegovina)",
"hr_hr": "Croatian (Croatia)",
"cs": "Czech",
"da": "Danish",
"din": "Dinka",
"div": "Divehi",
"dv": "Divehi",
"doi": "Dogri",
"dov": "Dombe",
"nl": "Dutch",
"nl_aw": "Dutch (Aruba)",
"nl_be": "Dutch (Belgium)",
"nl_cw": "Dutch (Curaçao)",
"nl_nl": "Dutch (Netherlands)",
"nl_sr": "Dutch (Suriname)",
"dyu": "Dyula",
"elvish": "Elvish (Sindarin)",
"emoji": "Emoji Speak",
"en": "English",
"en_au": "English (Australia)",
"en_ca": "English (Canada)",
"en_in": "English (India)",
"en_ie": "English (Ireland)",
"en_nz": "English (New Zealand)",
"en_ph": "English (Philippines)",
"en_sg": "English (Singapore)",
"en_za": "English (South Africa)",
"en_gb": "English (UK)",
"en_us": "English (US)",
"eo": "Esperanto",
"et": "Estonian",
"ee": "Ewe",
"mk": "FYRO Macedonian",
"fo": "Faroese",
"fa": "Farsi",
"fj": "Fiji",
"fil": "Filipino",
"fi": "Finnish",
"fon": "Fon",
"fr": "French",
"fr_be": "French (Belgium)",
"fr_ca": "French (Canada)",
"fr_ci": "French (Côte d'Ivoire)",
"fr_fr": "French (France)",
"fr_sn": "French (Senegal)",
"fr_ch": "French (Switzerland)",
"fy": "Frisian",
"fur": "Friulian",
"ff": "Fulah",
"gaa": "Ga",
"gd": "Gaelic",
"gl": "Galician",
"lg": "Ganda",
"ka": "Georgian",
"de": "German",
"de_at": "German (Austria)",
"de_de": "German (Germany)",
"de_li": "German (Liechtenstein)",
"de_lu": "German (Luxembourg)",
"el": "Greek",
"kl": "Greenlandic",
"gn": "Guarani",
"gu": "Gujarati",
"ht": "Haitian",
"ha": "Hausa",
"haw": "Hawaiian",
"he": "Hebrew",
"hz": "Herero",
"hil": "Hiligaynon",
"hi": "Hindi",
"hi_fj": "Hindi (Fiji)",
"hi_in": "Hindi (India)",
"ho": "Hiri Motu",
"hu": "Hungarian",
"hrx": "Hunsrik",
"iba": "Iban",
"is": "Icelandic",
"io": "Ido",
"ig": "Igbo",
"ilo": "Ilocano",
"id": "Indonesian",
"ia": "Interlingua",
"ie": "Interlingue",
"iu": "Inuktitut",
"ik": "Inupiak",
"ga": "Irish",
"it": "Italian",
"it_it": "Italian (Italy)",
"it_sm": "Italian (San Marino)",
"it_ch": "Italian (Switzerland)",
"it_va": "Italian (Vatican City)",
"jam": "Jamaican Patois",
"ja": "Japanese",
"jv": "Javanese",
"cpe": "Jingpo",
"kn": "Kannada",
"kr": "Kanuri",
"ks": "Kashmiri",
"kk": "Kazakh",
"kha": "Khasi",
"ki": "Kikuyu",
"rw": "Kinyarwanda",
"ky": "Kirghiz",
"rn": "Kirundi",
"ktu": "Kituba",
"klingon": "Klingon",
"kv": "Komi",
"kg": "Kongo",
"kok": "Konkani",
"ko": "Korean",
"ko_kp": "Korean (North Korea)",
"ko_kr": "Korean (South Korea)",
"kj": "Kuanyama",
"ku": "Kurdish",
"ckb": "Kurdish (Sorani)",
"lo": "Laothian",
"ltg": "Latgalian",
"la": "Latin",
"lv": "Latvian",
"lij": "Ligurian",
"li": "Limburgan",
"lim": "Limburgish",
"ln": "Lingala",
"lt": "Lithuanian",
"lmo": "Lombard",
"lu": "Luba_Katanga",
"luo": "Luo",
"lb": "Luxembourgish",
"mad": "Madurese",
"mai": "Maithili",
"mak": "Makassar",
"mg": "Malagasy",
"ms": "Malay",
"ms_bn": "Malay (Brunei Darussalam)",
"ms_id": "Malay (Indonesia)",
"ms_arab": "Malay (Jawi)",
"ms_my": "Malay (Malaysia)",
"ms_sg": "Malay (Singapore)",
"ml": "Malayalam",
"mt": "Maltese",
"gv": "Manx",
"mi": "Maori",
"mr": "Marathi",
"mh": "Marshallese",
"mwr": "Marwadi",
"middle_english": "Middle English",
"mo": "Moldavian",
"mn": "Mongolian",
"nqo": "NKo",
"nhe": "Nahuatl",
"na": "Nauru",
"nv": "Navajo",
"ndc": "Ndau",
"ng": "Ndonga",
"new": "Nepalbhasa",
"ne": "Nepali",
"nd": "North Ndebele",
"ns": "Northern Sotho",
"nb": "Norwegian (Bokmål)",
"nn": "Norwegian (Nynorsk)",
"nus": "Nuer",
"oc": "Occitan",
"oj": "Ojibwa",
"or": "Oriya",
"os": "Ossetian",
"pi": "Pali",
"pag": "Pangasinan",
"pap": "Papiamento",
"ps": "Pashto/Pushto",
"per": "Persian",
"fa_af": "Persian (Afghanistan)",
"fa_ir": "Persian (Iran)",
"fa_tj": "Persian (Tajikistan)",
"pirate": "Pirate Speak",
"pl": "Polish",
"pt": "Portuguese",
"pt_ao": "Portuguese (Angola)",
"pt_br": "Portuguese (Brazil)",
"pt_cv": "Portuguese (Cape Verde)",
"pt_mz": "Portuguese (Mozambique)",
"pt_pt": "Portuguese (Portugal)",
"pa": "Punjabi",
"qu": "Quechua",
"qu_bo": "Quechua (Bolivia)",
"qu_ec": "Quechua (Ecuador)",
"qu_pe": "Quechua (Peru)",
"kek": "Qʼeqchiʼ",
"rm": "Rhaeto_Romanic",
"rom": "Romani",
"ro": "Romanian",
"ru": "Russian",
"ru_by": "Russian (Belarus)",
"ru_kz": "Russian (Kazakhstan)",
"ru_kg": "Russian (Kyrgyzstan)",
"ru_ru": "Russian (Russia)",
"se": "Sami",
"se_fi": "Sami (Finland)",
"se_no": "Sami (Norway)",
"se_se": "Sami (Sweden)",
"sm": "Samoan",
"sg": "Sangro",
"sa": "Sanskrit",
"sat": "Santali",
"sc": "Sardinian",
"sr": "Serbian",
"sr_ba": "Serbian (Bosnia and Herzegovina)",
"sr_sp": "Serbian (Serbia and Montenegro)",
"sh": "Serbo_Croatian",
"st": "Sesotho",
"shn": "Shan",
"sn": "Shona",
"ii": "Sichuan Yi",
"scn": "Sicilian",
"szl": "Silesian",
"sd": "Sindhi",
"si": "Singhalese",
"ss": "Siswati",
"sk": "Slovak",
"sl": "Slovenian",
"so": "Somali",
"sb": "Sorbian",
"nr": "South Ndebele",
"es": "Spanish",
"es_ar": "Spanish (Argentina)",
"es_bo": "Spanish (Bolivia)",
"es_cl": "Spanish (Chile)",
"es_co": "Spanish (Colombia)",
"es_cr": "Spanish (Costa Rica)",
"es_cu": "Spanish (Cuba)",
"es_do": "Spanish (Dominican Republic)",
"es_ec": "Spanish (Ecuador)",
"es_sv": "Spanish (El Salvador)",
"es_gq": "Spanish (Equatorial Guinea)",
"es_gt": "Spanish (Guatemala)",
"es_hn": "Spanish (Honduras)",
"es_mx": "Spanish (Mexico)",
"es_ni": "Spanish (Nicaragua)",
"es_pa": "Spanish (Panama)",
"es_py": "Spanish (Paraguay)",
"es_pe": "Spanish (Peru)",
"es_pr": "Spanish (Puerto Rico)",
"es_es": "Spanish (Spain)",
"es_uy": "Spanish (Uruguay)",
"es_ve": "Spanish (Venezuela)",
"su": "Sundanese",
"sus": "Susu",
"sx": "Sutu",
"sw": "Swahili",
"sv": "Swedish",
"sv_fi": "Swedish (Finland)",
"sv_se": "Swedish (Sweden)",
"gsw": "Swiss German",
"de_ch": "Swiss High German",
"syr": "Syriac",
"tl": "Tagalog",
"ty": "Tahitian",
"tg": "Tajik",
"ta": "Tamil",
"ta_in": "Tamil (India)",
"ta_my": "Tamil (Malaysia)",
"ta_sg": "Tamil (Singapore)",
"ta_lk": "Tamil (Sri Lanka)",
"tt": "Tatar",
"te": "Telugu",
"tet": "Tetum",
"th": "Thai",
"bo": "Tibetan",
"ti": "Tigrinya",
"tiv": "Tiv",
"tpi": "Tok Pisin",
"to": "Tonga",
"ts": "Tsonga",
"tn": "Tswana",
"tum": "Tumbuka",
"tr": "Turkish",
"tr_cy": "Turkish (Cyprus)",
"tr_tr": "Turkish (Turkey)",
"tk": "Turkmen",
"tyv": "Tuvan",
"tw": "Twi",
"udm": "Udmurt",
"ug": "Uighur",
"uk": "Ukrainian",
"ur": "Urdu",
"ur_in": "Urdu (India)",
"ur_pk": "Urdu (Pakistan)",
"uz": "Uzbek",
"ve": "Venda",
"vec": "Venetian",
"vi": "Vietnamese",
"vo": "Volapuk",
"wa": "Walloon",
"war": "Waray",
"cy": "Welsh",
"wo": "Wolof",
"xh": "Xhosa",
"sah": "Yakut",
"yi": "Yiddish",
"yo": "Yoruba",
"yua": "Yucatec Maya",
"zap": "Zapotec",
"za": "Zhuang",
"zu": "Zulu"
} as const;

View file

@ -27,3 +27,28 @@ export async function makeDeeplTranslateRequest(_: IpcMainInvokeEvent, pro: bool
return { status: -1, data: String(e) };
}
}
export async function makeKagiTranslateRequest(_: IpcMainInvokeEvent, token: string, text: string, sourceLang: string, targetLang: string) {
const url = "https://translate.kagi.com/api/translate";
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Cookie": `kagi_session=${token}`
},
body: JSON.stringify({
text,
from: sourceLang,
to: targetLang,
model: "standard"
}),
});
const data = await res.json();
return { status: res.status, data };
} catch (e) {
return { status: -1, data: String(e) };
}
}

View file

@ -17,68 +17,89 @@
*/
import { definePluginSettings } from "@api/Settings";
import { Button } from "@components/Button";
import { OptionType } from "@utils/types";
import { openTranslateModal } from "./TranslateModal";
export const settings = definePluginSettings({
receivedInput: {
type: OptionType.STRING,
description: "Language that received messages should be translated from",
description: "Language incoming messages are translated from",
default: "auto",
hidden: true
},
receivedOutput: {
type: OptionType.STRING,
description: "Language that received messages should be translated to",
description: "Language incoming messages are translated to",
default: "en",
hidden: true
},
sentInput: {
type: OptionType.STRING,
description: "Language that your own messages should be translated from",
description: "Language your messages are translated from",
default: "auto",
hidden: true
},
sentOutput: {
type: OptionType.STRING,
description: "Language that your own messages should be translated to",
description: "Language your messages are translated to",
default: "en",
hidden: true
},
service: {
type: OptionType.SELECT,
description: IS_WEB ? "Translation service (Not supported on Web!)" : "Translation service",
disabled: () => IS_WEB,
description: IS_WEB ? "Translation provider (not available on web)" : "Translation provider",
hidden: IS_WEB,
options: [
{ label: "Google Translate", value: "google", default: true },
{ label: "DeepL Free", value: "deepl" },
{ label: "DeepL Pro", value: "deepl-pro" }
{ label: "DeepL Free — API key required", value: "deepl" },
{ label: "DeepL Pro — API key required", value: "deepl-pro" },
{ label: "Kagi Translate — API key required", value: "kagi" }
] as const,
onChange: resetLanguageDefaults
},
deeplApiKey: {
type: OptionType.STRING,
description: "DeepL API key",
default: "",
placeholder: "Get your API key from https://deepl.com/your-account",
disabled: () => IS_WEB
description: "Your DeepL API key (from deepl.com/your-account)",
default: ""
},
kagiSession: {
type: OptionType.STRING,
description: "Your Kagi session token (from kagi.com/settings?p=user_details)",
default: ""
},
autoTranslate: {
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
description: "Automatically translate your messages before sending. You can also Shift+click or right-click the translate button to toggle this",
default: false
},
showAutoTranslateTooltip: {
type: OptionType.BOOLEAN,
description: "Show a tooltip on the ChatBar button whenever a message is automatically translated",
description: "Show a tooltip on the chat bar button when a message is auto-translated",
default: true
},
manageTranslateSettings: {
type: OptionType.COMPONENT,
component: () => (
<Button onClick={openTranslateModal}>
Customize translation languages & Auto-Translate
</Button>
)
}
}, {
deeplApiKey: {
hidden() { return this.store.service !== "deepl" && this.store.service !== "deepl-pro"; }
},
kagiSession: {
hidden() { return this.store.service !== "kagi"; }
}
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
}>();
export function resetLanguageDefaults() {
if (IS_WEB || settings.store.service === "google") {
if (IS_WEB || settings.store.service === "google" || settings.store.service === "kagi") {
settings.store.receivedInput = "auto";
settings.store.receivedOutput = "en";
settings.store.sentInput = "auto";

View file

@ -21,7 +21,7 @@ import { onlyOnce } from "@utils/onlyOnce";
import { PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from "./languages";
import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages, KagiLanguages } from "./languages";
import { resetLanguageDefaults, settings } from "./settings";
export const cl = classNameFactory("vc-trans-");
@ -40,19 +40,43 @@ interface DeeplData {
}[];
}
interface KagiData {
translation: string;
detected_language: {
label: string;
};
}
export interface TranslationValue {
sourceLanguage: string;
text: string;
}
export const getLanguages = () => IS_WEB || settings.store.service === "google"
? GoogleLanguages
: DeeplLanguages;
export const getLanguages = () => {
if (IS_WEB) {
return GoogleLanguages;
}
switch (settings.store.service) {
case "google":
return GoogleLanguages;
case "kagi":
return KagiLanguages;
default:
return DeeplLanguages;
}
};
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> {
const translate = IS_WEB || settings.store.service === "google"
? googleTranslate
: deeplTranslate;
const translate = IS_WEB ? googleTranslate : (() => {
switch (settings.store.service) {
case "google":
return googleTranslate;
case "kagi":
return kagiTranslate;
default:
return deeplTranslate;
}
})();
try {
return await translate(
@ -153,3 +177,25 @@ async function deeplTranslate(text: string, sourceLang: string, targetLang: stri
text: translations[0].text
};
}
async function kagiTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
const { status, data } = await Native.makeKagiTranslateRequest(
settings.store.kagiSession, text, sourceLang, targetLang
);
switch (status) {
case 200:
break;
case 401:
throw "Invalid or expired Kagi session token";
default:
throw new Error(`Failed to translate "${text}" (${sourceLang} -> ${targetLang})\n${status} ${data}`);
}
const { detected_language, translation }: KagiData = data;
return {
sourceLanguage: detected_language.label,
text: translation
};
}

View file

@ -18,9 +18,10 @@
import "./style.css";
import { definePluginSettings, Settings } from "@api/Settings";
import { isPluginEnabled } from "@api/PluginManager";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { buildSeveralUsers } from "@plugins/typingTweaks";
import TypingTweaksPlugin, { buildSeveralUsers } from "@plugins/typingTweaks";
import { Devs } from "@utils/constants";
import { getIntlMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
@ -85,7 +86,7 @@ function TypingIndicator({ channelId, guildId }: { channelId: string; guildId: s
break;
}
default: {
tooltipText = Settings.plugins.TypingTweaks.enabled
tooltipText = isPluginEnabled(TypingTweaksPlugin.name)
? buildSeveralUsers({ users: [a, b].map(UserStore.getUser), count: typingUsersArray.length - 2, guildId })
: getIntlMessage("SEVERAL_USERS_TYPING");
break;

View file

@ -620,6 +620,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "jamesbt365",
id: 158567567487795200n
},
Darxoon: {
name: "Darxoon",
id: 409745838898937866n
},
samsam: {
name: "samsam",
id: 400482410279469056n
@ -628,6 +632,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Cootshk",
id: 921605971577548820n
},
koish1: {
name: "koish1",
id: 291089948709486593n,
badge: false,
},
thororen: {
name: "thororen",
id: 848339671629299742n

View file

@ -173,12 +173,6 @@ export interface PluginDef {
* Which parts of the plugin can be tested by the reporter. Defaults to all parts
*/
reporterTestable?: number;
/**
* Optionally provide settings that the user can configure in the Plugins tab of settings.
* @deprecated Use `settings` instead
*/
// TODO: Remove when everything is migrated to `settings`
options?: Record<string, PluginOptionsItem>;
/**
* Optionally provide settings that the user can configure in the Plugins tab of settings.
*/
@ -280,22 +274,21 @@ export const enum OptionType {
export type SettingsDefinition = Record<string, PluginSettingDef>;
export type SettingsChecks<D extends SettingsDefinition> = {
[K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabled<DefinedSettings<D>> :
(IsDisabled<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
[K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabledOrHidden<DefinedSettings<D>> :
(IsDisabledOrHidden<DefinedSettings<D>> & IsValid<PluginSettingType<D[K]>, DefinedSettings<D>>);
};
export type PluginSettingDef =
(PluginSettingCommon & PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">) |
(PluginSettingComponentDef & Omit<PluginSettingCommon, "description" | "placeholder">) | ((
| PluginSettingStringDef
| PluginSettingNumberDef
| PluginSettingBooleanDef
| PluginSettingSelectDef
| PluginSettingSliderDef
| PluginSettingBigIntDef
) & PluginSettingCommon);
| PluginSettingCustomDef
| PluginSettingComponentDef
| PluginSettingStringDef
| PluginSettingNumberDef
| PluginSettingBooleanDef
| PluginSettingSelectDef
| PluginSettingSliderDef
| PluginSettingBigIntDef;
export interface PluginSettingCommon {
export interface PluginSettingDefCommon extends IsDisabledOrHidden, IsValid<unknown> {
description: string;
placeholder?: string;
onChange?(newValue: any): void;
@ -304,50 +297,50 @@ export interface PluginSettingCommon {
*/
restartNeeded?: boolean;
componentProps?: Record<string, any>;
/**
* Hide this setting from the settings UI
*/
hidden?: boolean;
/**
* Set this if the setting only works on Browser or Desktop, not both
*/
target?: "WEB" | "DESKTOP" | "BOTH";
}
interface IsDisabled<D = unknown> {
interface IsDisabledOrHidden<D extends DefinedSettings = DefinedSettings> {
/**
* Checks if this setting should be disabled
* Whether this setting should be disabled
*/
disabled?(this: D): boolean;
disabled?: boolean | ((this: D) => boolean);
/**
* Whether this setting should be hidden from the user
*/
hidden?: boolean | ((this: D) => boolean);
}
interface IsValid<T, D = unknown> {
interface IsValid<T, D extends DefinedSettings = DefinedSettings> {
/**
* Prevents the user from saving settings if this is false or a string
* Whether the value the user provided is valid. Either return a boolean for a generic error message or a string for a custom error message.
*/
isValid?(this: D, value: T): boolean | string;
}
export interface PluginSettingStringDef {
export interface PluginSettingStringDef extends PluginSettingDefCommon {
type: OptionType.STRING;
default?: string;
/** Whether to use a multiline text area */
multiline?: boolean;
}
export interface PluginSettingNumberDef {
export interface PluginSettingNumberDef extends PluginSettingDefCommon {
type: OptionType.NUMBER;
default?: number;
}
export interface PluginSettingBigIntDef {
export interface PluginSettingBigIntDef extends PluginSettingDefCommon {
type: OptionType.BIGINT;
default?: BigInt;
}
export interface PluginSettingBooleanDef {
export interface PluginSettingBooleanDef extends PluginSettingDefCommon {
type: OptionType.BOOLEAN;
default?: boolean;
}
export interface PluginSettingSelectDef {
export interface PluginSettingSelectDef extends PluginSettingDefCommon {
type: OptionType.SELECT;
options: readonly PluginSettingSelectOption[];
}
@ -358,12 +351,13 @@ export interface PluginSettingSelectOption {
default?: boolean;
}
export interface PluginSettingCustomDef {
export interface PluginSettingCustomDef extends PluginSettingDefCommon {
type: OptionType.CUSTOM;
default?: any;
onChange?: PluginSettingDefCommon["onChange"];
}
export interface PluginSettingSliderDef {
export interface PluginSettingSliderDef extends PluginSettingDefCommon {
type: OptionType.SLIDER;
/**
* All the possible values in the slider. Needs at least two values.
@ -379,11 +373,14 @@ export interface PluginSettingSliderDef {
stickToMarkers?: boolean;
}
export interface IPluginOptionComponentProps {
export interface PluginSettingComponentDef extends Omit<PluginSettingDefCommon, "description" | "placeholder"> {
type: OptionType.COMPONENT;
component: (props: PluginSettingComponentProps) => ReactNode | Promise<ReactNode>;
default?: any;
}
export interface PluginSettingComponentProps {
/**
* Run this when the value changes.
*
* NOTE: The user will still need to click save to apply these changes.
*/
setValue(newValue: any): void;
/**
@ -392,12 +389,6 @@ export interface IPluginOptionComponentProps {
option: PluginSettingComponentDef;
}
export interface PluginSettingComponentDef {
type: OptionType.COMPONENT;
component: (props: IPluginOptionComponentProps) => ReactNode | Promise<ReactNode>;
default?: any;
}
/** Maps a `PluginSettingDef` to its value type */
type PluginSettingType<O extends PluginSettingDef> = O extends PluginSettingStringDef ? string :
O extends PluginSettingNumberDef ? number :
@ -420,56 +411,32 @@ type SettingsStore<D extends SettingsDefinition> = {
/** An instance of defined plugin settings */
export interface DefinedSettings<
Def extends SettingsDefinition = SettingsDefinition,
Checks extends SettingsChecks<Def> = {},
PrivateSettings extends object = {}
> {
/** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */
/** Definitions of each setting */
def: Def;
/** Reactive Read/Write settings store - This is a Proxy, use .plain for a plain object */
store: SettingsStore<Def> & PrivateSettings;
/** Shorthand for `Vencord.PlainSettings.plugins.PluginName`, but with typings */
/** Pure Read-only settings object */
plain: SettingsStore<Def> & PrivateSettings;
/**
* React hook for getting the settings for this plugin
* @param filter optional filter to avoid rerenders for irrelevent settings
* @param filter optional filter to avoid rerenders for irrelevant settings
*/
use<F extends Extract<keyof Def | keyof PrivateSettings, string>>(filter?: F[]): Pick<SettingsStore<Def> & PrivateSettings, F>;
/** Definitions of each setting */
def: Def;
/** Setting methods with return values that could rely on other settings */
checks: Checks;
/**
* Name of the plugin these settings belong to,
* will be an empty string until plugin is initialized
*/
pluginName: string;
withPrivateSettings<T extends object>(): DefinedSettings<Def, Checks, T>;
/** Extend this Settings object type with more properties, useful for non-user-facing settings */
withPrivateSettings<T extends object>(): DefinedSettings<Def, T>;
}
export type PartialExcept<T, R extends keyof T> = Partial<T> & Required<Pick<T, R>>;
export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };
/* -------------------------------------------- */
/* Legacy Options Types */
/* -------------------------------------------- */
export type PluginOptionBase = PluginSettingCommon & IsDisabled;
export type PluginOptionsItem =
| PluginOptionString
| PluginOptionNumber
| PluginOptionBoolean
| PluginOptionSelect
| PluginOptionSlider
| PluginOptionComponent
| PluginOptionCustom;
export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid<string>;
export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid<number | BigInt>;
export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid<boolean>;
export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid<PluginSettingSelectOption>;
export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid<number>;
export type PluginOptionComponent = PluginSettingComponentDef & Omit<PluginSettingCommon, "description" | "placeholder">;
export type PluginOptionCustom = PluginSettingCustomDef & Pick<PluginSettingCommon, "onChange">;
export type PluginNative<PluginExports extends Record<string, (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any>> = {
[key in keyof PluginExports]:
PluginExports[key] extends (event: Electron.IpcMainInvokeEvent, ...args: infer Args) => infer Return