From 9794ab1cba188d85bb8b0dcba8af06aefedac42f Mon Sep 17 00:00:00 2001 From: Ruben Date: Sat, 2 May 2026 14:59:22 +0200 Subject: [PATCH 01/14] Translate: add support for Kagi Translate (#3416) Co-authored-by: Vending Machine --- scripts/build/build.mjs | 0 .../settings/tabs/plugins/PluginModal.tsx | 9 +- src/plugins/translate/index.tsx | 4 +- src/plugins/translate/languages.ts | 390 ++++++++++++++++++ src/plugins/translate/native.ts | 25 ++ src/plugins/translate/settings.ts | 28 +- src/plugins/translate/utils.ts | 60 ++- src/plugins/vencordToolbox/menu.tsx | 7 +- src/utils/constants.ts | 5 + src/utils/types.ts | 26 +- 10 files changed, 523 insertions(+), 31 deletions(-) mode change 100755 => 100644 scripts/build/build.mjs diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs old mode 100755 new mode 100644 diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index c67e39647..865a19891 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -107,7 +107,14 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti return There are no settings for this plugin.; const options = Object.entries(plugin.options).map(([key, setting]) => { - if (setting.type === OptionType.CUSTOM || setting.hidden) return null; + if (setting.type === OptionType.CUSTOM) return null; + + const shouldHide = "hidden" in setting && ( + typeof setting.hidden === "function" + ? setting.hidden.call(plugin.settings) + : setting.hidden + ); + if (shouldHide) return null; function onChange(newValue: any) { const option = plugin.options?.[key]; diff --git a/src/plugins/translate/index.tsx b/src/plugins/translate/index.tsx index c62a3860d..68a3a6d29 100644 --- a/src/plugins/translate/index.tsx +++ b/src/plugins/translate/index.tsx @@ -63,9 +63,9 @@ 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.", tags: ["Chat", "Utility"], - authors: [Devs.Ven, Devs.AshtonMemer], + authors: [Devs.Ven, Devs.AshtonMemer, Devs.koish1], settings, contextMenus: { "message": messageCtxPatch diff --git a/src/plugins/translate/languages.ts b/src/plugins/translate/languages.ts index 1023983f8..b8e12107e 100644 --- a/src/plugins/translate/languages.ts +++ b/src/plugins/translate/languages.ts @@ -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; diff --git a/src/plugins/translate/native.ts b/src/plugins/translate/native.ts index 3415e95e9..93c7a25f8 100644 --- a/src/plugins/translate/native.ts +++ b/src/plugins/translate/native.ts @@ -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) }; + } +} diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts index a29e7d84c..b80be974a 100644 --- a/src/plugins/translate/settings.ts +++ b/src/plugins/translate/settings.ts @@ -44,24 +44,27 @@ export const settings = definePluginSettings({ default: "en", hidden: true }, - service: { type: OptionType.SELECT, description: IS_WEB ? "Translation service (Not supported on Web!)" : "Translation service", - disabled: () => IS_WEB, + 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: "Get your API key from https://deepl.com/your-account", + default: "" + }, + kagiSession: { + type: OptionType.STRING, + description: "Get your session token from https://kagi.com/settings?p=user_details", + default: "" }, autoTranslate: { type: OptionType.BOOLEAN, @@ -73,12 +76,19 @@ export const settings = definePluginSettings({ description: "Show a tooltip on the ChatBar button whenever a message is automatically translated", default: true }, +}, { + 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"; diff --git a/src/plugins/translate/utils.ts b/src/plugins/translate/utils.ts index 4ee16c93e..2c77db511 100644 --- a/src/plugins/translate/utils.ts +++ b/src/plugins/translate/utils.ts @@ -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 { - 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 { + 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 + }; +} diff --git a/src/plugins/vencordToolbox/menu.tsx b/src/plugins/vencordToolbox/menu.tsx index f229b0227..b3d6e0b54 100644 --- a/src/plugins/vencordToolbox/menu.tsx +++ b/src/plugins/vencordToolbox/menu.tsx @@ -82,7 +82,12 @@ 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; + const shouldHide = "hidden" in option && ( + typeof option.hidden === "function" + ? option.hidden.call(p.settings) + : option.hidden + ); + if (shouldHide) continue; hasAnyOption = true; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c3b784716..ebe10a109 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -609,6 +609,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Cootshk", id: 921605971577548820n }, + koish1: { + name: "koish1", + id: 291089948709486593n, + badge: false, + }, thororen: { name: "thororen", id: 848339671629299742n diff --git a/src/utils/types.ts b/src/utils/types.ts index 7d38bf5a1..c2f4d875f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -259,8 +259,8 @@ export const enum OptionType { export type SettingsDefinition = Record; export type SettingsChecks = { - [K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabled> : - (IsDisabled> & IsValid, DefinedSettings>); + [K in keyof D]?: D[K] extends PluginSettingComponentDef ? IsDisabledOrHidden> : + (IsDisabledOrHidden> & IsValid, DefinedSettings>); }; export type PluginSettingDef = @@ -286,18 +286,22 @@ export interface PluginSettingCommon { /** * Hide this setting from the settings UI */ - hidden?: boolean; + hidden?: boolean | (() => boolean); /** * Set this if the setting only works on Browser or Desktop, not both */ target?: "WEB" | "DESKTOP" | "BOTH"; } -interface IsDisabled { +interface IsDisabledOrHidden { /** * Checks if this setting should be disabled */ disabled?(this: D): boolean; + /** + * Checks if this setting should be hidden + */ + hidden?(this: D): boolean; } interface IsValid { @@ -432,7 +436,7 @@ export type IpcRes = { ok: true; value: V; } | { ok: false, error: any; /* Legacy Options Types */ /* -------------------------------------------- */ -export type PluginOptionBase = PluginSettingCommon & IsDisabled; +export type PluginOptionBase = PluginSettingCommon & IsDisabledOrHidden; export type PluginOptionsItem = | PluginOptionString | PluginOptionNumber @@ -441,12 +445,12 @@ export type PluginOptionsItem = | PluginOptionSlider | PluginOptionComponent | PluginOptionCustom; -export type PluginOptionString = PluginSettingStringDef & PluginSettingCommon & IsDisabled & IsValid; -export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginSettingCommon & IsDisabled & IsValid; -export type PluginOptionBoolean = PluginSettingBooleanDef & PluginSettingCommon & IsDisabled & IsValid; -export type PluginOptionSelect = PluginSettingSelectDef & PluginSettingCommon & IsDisabled & IsValid; -export type PluginOptionSlider = PluginSettingSliderDef & PluginSettingCommon & IsDisabled & IsValid; -export type PluginOptionComponent = PluginSettingComponentDef & Omit; +export type PluginOptionString = PluginSettingStringDef & PluginOptionBase & IsValid; +export type PluginOptionNumber = (PluginSettingNumberDef | PluginSettingBigIntDef) & PluginOptionBase & IsValid; +export type PluginOptionBoolean = PluginSettingBooleanDef & PluginOptionBase & IsValid; +export type PluginOptionSelect = PluginSettingSelectDef & PluginOptionBase & IsValid; +export type PluginOptionSlider = PluginSettingSliderDef & PluginOptionBase & IsValid; +export type PluginOptionComponent = PluginSettingComponentDef & Omit; export type PluginOptionCustom = PluginSettingCustomDef & Pick; export type PluginNative any>> = { From dbca8ef263054fd1a11c6258e529fdc76c195128 Mon Sep 17 00:00:00 2001 From: V Date: Mon, 4 May 2026 11:40:05 +0200 Subject: [PATCH 02/14] remove deprecated plugin.options field (#4165) --- eslint.config.mjs | 2 + src/api/PluginManager.ts | 39 +++-- src/api/Settings.ts | 15 +- .../settings/tabs/plugins/PluginCard.tsx | 5 +- .../settings/tabs/plugins/PluginModal.tsx | 23 ++- .../plugins/components/BooleanSetting.tsx | 13 +- .../tabs/plugins/components/Common.tsx | 10 +- .../plugins/components/ComponentSetting.tsx | 6 +- .../tabs/plugins/components/NumberSetting.tsx | 21 +-- .../tabs/plugins/components/SelectSetting.tsx | 19 +-- .../tabs/plugins/components/SliderSetting.tsx | 23 +-- .../tabs/plugins/components/TextSetting.tsx | 25 ++-- .../settings/tabs/plugins/index.tsx | 2 +- .../settings/tabs/vencord/index.tsx | 3 +- src/plugins/anonymiseFileNames/index.tsx | 13 +- src/plugins/betterNotes/index.tsx | 78 ---------- src/plugins/betterRoleDot/index.ts | 39 ++--- src/plugins/blurNsfw/index.ts | 23 +-- src/plugins/callTimer/index.tsx | 41 +++--- src/plugins/ignoreActivities/index.tsx | 5 +- src/plugins/messageLogger/index.tsx | 138 +++++++++--------- src/plugins/platformIndicators/index.tsx | 128 ++++++++-------- src/plugins/serverListIndicators/index.tsx | 29 ++-- .../components/HiddenChannelLockScreen.tsx | 3 +- .../spotifyControls/PlayerComponent.tsx | 4 +- src/plugins/spotifyControls/SpotifyStore.ts | 5 +- src/plugins/spotifyControls/index.tsx | 42 +++--- src/plugins/typingIndicator/index.tsx | 7 +- src/plugins/vencordToolbox/menu.tsx | 13 +- src/utils/types.ts | 109 +++++--------- 30 files changed, 397 insertions(+), 486 deletions(-) delete mode 100644 src/plugins/betterNotes/index.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 99fb1fb2a..aa3ee641f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -136,6 +136,8 @@ export default tseslint.config( "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", diff --git a/src/api/PluginManager.ts b/src/api/PluginManager.ts index d6e7bcefb..b3cce69b8 100644 --- a/src/api/PluginManager.ts +++ b/src/api/PluginManager.ts @@ -31,7 +31,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"; @@ -54,6 +54,26 @@ export function isPluginEnabled(p: string) { ) ?? 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, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) { // TODO: this causes crashes if (pluginName === "Vesktop" && newPatch.find === ".STREAMING_AUTO_STREAMER_MODE,") return; @@ -359,22 +379,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); } } diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 8ab14f97d..88d5cc131 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -150,7 +150,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) @@ -180,7 +180,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} */ @@ -260,7 +260,13 @@ export function definePluginSettings< Checks extends SettingsChecks, PrivateSettings extends object = {} >(def: Def, checks?: Checks) { - const definedSettings: DefinedSettings = { + if (checks) { + for (const [name, check] of Object.entries(checks)) { + Object.assign(def[name], check); + } + } + + const definedSettings: DefinedSettings = { get store() { if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized"); return Settings.plugins[definedSettings.pluginName] as any; @@ -275,11 +281,10 @@ export function definePluginSettings< : [`plugins.${definedSettings.pluginName}.*`] ) as UseSettings[]).plugins[definedSettings.pluginName] as any, def, - checks: checks ?? {} as any, pluginName: "", withPrivateSettings() { - return this as DefinedSettings; + return this as DefinedSettings; } }; diff --git a/src/components/settings/tabs/plugins/PluginCard.tsx b/src/components/settings/tabs/plugins/PluginCard.tsx index b563ca0d3..ac8b9b839 100644 --- a/src/components/settings/tabs/plugins/PluginCard.tsx +++ b/src/components/settings/tabs/plugins/PluginCard.tsx @@ -5,11 +5,10 @@ */ 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 { isObjectEmpty } from "@utils/misc"; import { Plugin } from "@utils/types"; import { React, showToast, Toasts } from "@webpack/common"; @@ -94,7 +93,7 @@ export function PluginCard({ plugin, disabled, onRestartNeeded, onMouseEnter, on onClick={() => openPluginModal(plugin, onRestartNeeded)} className={cl("info-button")} > - {plugin.options && !isObjectEmpty(plugin.options) + {hasAnyVisibleSettings(plugin) ? : } diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 865a19891..bd5d8e98d 100644 --- a/src/components/settings/tabs/plugins/PluginModal.tsx +++ b/src/components/settings/tabs/plugins/PluginModal.tsx @@ -19,6 +19,7 @@ import "./PluginModal.css"; import { generateId } from "@api/Commands"; +import { hasAnyVisibleSettings, isSettingHidden } from "@api/PluginManager"; import { useSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { debounce } from "@shared/debounce"; @@ -26,7 +27,7 @@ import { gitRemote } from "@shared/vencordUserAgent"; import { classNameFactory } from "@utils/css"; import { proxyLazy } from "@utils/lazy"; import { Margins } from "@utils/margins"; -import { classes, isObjectEmpty } from "@utils/misc"; +import { classes } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { OptionType, Plugin, PluginTag } from "@utils/types"; import { User } from "@vencord/discord-types"; @@ -79,7 +80,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" })], []); @@ -103,21 +104,17 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti }, [plugin.authors]); function renderSettings() { - if (!hasSettings || !plugin.options) + const { settings } = plugin; + if (!hasSettings || !settings) return There are no settings for this plugin.; - const options = Object.entries(plugin.options).map(([key, setting]) => { + const options = Object.entries(settings.def).map(([key, setting]) => { if (setting.type === OptionType.CUSTOM) return null; - const shouldHide = "hidden" in setting && ( - typeof setting.hidden === "function" - ? setting.hidden.call(plugin.settings) - : setting.hidden - ); - if (shouldHide) 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; @@ -130,10 +127,10 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti ); diff --git a/src/components/settings/tabs/plugins/components/BooleanSetting.tsx b/src/components/settings/tabs/plugins/components/BooleanSetting.tsx index 96ff57625..76a887ea1 100644 --- a/src/components/settings/tabs/plugins/components/BooleanSetting.tsx +++ b/src/components/settings/tabs/plugins/components/BooleanSetting.tsx @@ -16,20 +16,21 @@ * along with this program. If not, see . */ +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) { - const def = pluginSettings[id] ?? option.default; +export function BooleanSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps) { + const def = pluginSettings[id] ?? setting.default; const [state, setState] = useState(def ?? false); const [error, setError] = useState(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 ( - + ); diff --git a/src/components/settings/tabs/plugins/components/Common.tsx b/src/components/settings/tabs/plugins/components/Common.tsx index 0280714f3..ea947b8cf 100644 --- a/src/components/settings/tabs/plugins/components/Common.tsx +++ b/src/components/settings/tabs/plugins/components/Common.tsx @@ -7,25 +7,25 @@ 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 { Text } from "@webpack/common"; import { PropsWithChildren } from "react"; export const cl = classNameFactory("vc-plugins-setting-"); interface SettingBaseProps { - option: T; + setting: T; onChange(newValue: any): void; pluginSettings: { [setting: string]: any; enabled: boolean; }; id: string; - definedSettings?: DefinedSettings; + definedSettings: DefinedSettings; } -export type SettingProps = SettingBaseProps; -export type ComponentSettingProps> = SettingBaseProps; +export type SettingProps = SettingBaseProps; +export type ComponentSettingProps> = SettingBaseProps; export function resolveError(isValidResult: boolean | string) { if (typeof isValidResult === "string") return isValidResult; diff --git a/src/components/settings/tabs/plugins/components/ComponentSetting.tsx b/src/components/settings/tabs/plugins/components/ComponentSetting.tsx index c43b72768..e1521f02a 100644 --- a/src/components/settings/tabs/plugins/components/ComponentSetting.tsx +++ b/src/components/settings/tabs/plugins/components/ComponentSetting.tsx @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -import { PluginOptionComponent } from "@utils/types"; +import { PluginSettingComponentDef } from "@utils/types"; import { ComponentSettingProps } from "./Common"; -export function ComponentSetting({ option, onChange }: ComponentSettingProps) { - return option.component({ setValue: onChange, option }); +export function ComponentSetting({ setting, onChange }: ComponentSettingProps) { + return setting.component({ setValue: onChange, option: setting }); } diff --git a/src/components/settings/tabs/plugins/components/NumberSetting.tsx b/src/components/settings/tabs/plugins/components/NumberSetting.tsx index 108ddc3e4..fa348a108 100644 --- a/src/components/settings/tabs/plugins/components/NumberSetting.tsx +++ b/src/components/settings/tabs/plugins/components/NumberSetting.tsx @@ -16,24 +16,25 @@ * along with this program. If not, see . */ -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) { +export function NumberSetting({ setting, pluginSettings, definedSettings, id, onChange }: SettingProps) { 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(`${pluginSettings[id] ?? option.default ?? 0}`); + const [state, setState] = useState(`${pluginSettings[id] ?? setting.default ?? 0}`); const [error, setError] = useState(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 ( - + ); diff --git a/src/components/settings/tabs/plugins/components/SelectSetting.tsx b/src/components/settings/tabs/plugins/components/SelectSetting.tsx index 4c912c515..89f652e7a 100644 --- a/src/components/settings/tabs/plugins/components/SelectSetting.tsx +++ b/src/components/settings/tabs/plugins/components/SelectSetting.tsx @@ -16,19 +16,20 @@ * along with this program. If not, see . */ -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) { - const def = pluginSettings[id] ?? option.options?.find(o => o.default)?.value; +export function SelectSetting({ setting, pluginSettings, definedSettings, onChange, id }: SettingProps) { + const def = pluginSettings[id] ?? setting.options?.find(o => o.default)?.value; const [state, setState] = useState(def ?? null); const [error, setError] = useState(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 ( - +