diff --git a/eslint.config.mjs b/eslint.config.mjs index c0602e84e..7fcd15014 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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", diff --git a/packages/discord-types/src/components.d.ts b/packages/discord-types/src/components.d.ts index 5f341fd92..c7d01e481 100644 --- a/packages/discord-types/src/components.d.ts +++ b/packages/discord-types/src/components.d.ts @@ -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; - renderPopout(args: { - closePopout(): void; - isPositioned: boolean; - nudge: number; - position: PopoutPosition; - setPopoutRef(ref: any): void; - updatePosition(): void; - }): ReactNode; + renderPopout(props: PopoutProps): ReactNode; + preload?(): Promise; onRequestOpen?(): void; onRequestClose?(): void; diff --git a/src/Vencord.ts b/src/Vencord.ts index 45c9326f1..f39f41a7c 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -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(); }); } diff --git a/src/api/Notifications/styles.css b/src/api/Notifications/styles.css index 400fb7d4a..e15e01a7d 100644 --- a/src/api/Notifications/styles.css +++ b/src/api/Notifications/styles.css @@ -15,7 +15,7 @@ position: absolute; z-index: 2147483647; right: 1rem; - width: 25vw; + width: min(25vw, 400px); min-height: 10vh; } diff --git a/src/api/PluginManager.ts b/src/api/PluginManager.ts index 3f968ca5b..95331ea29 100644 --- a/src/api/PluginManager.ts +++ b/src/api/PluginManager.ts @@ -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, 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); } } diff --git a/src/api/Settings.ts b/src/api/Settings.ts index e4d309609..ea9e23088 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -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, 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; @@ -368,11 +374,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/api/SettingsSync/cloudSync.ts b/src/api/SettingsSync/cloudSync.ts index 29b16bc5b..b1f0f5785 100644 --- a/src/api/SettingsSync/cloudSync.ts +++ b/src/api/SettingsSync/cloudSync.ts @@ -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> { return await DataStore.get>(API_VERSION_STORE_KEY) ?? {}; } diff --git a/src/components/settings/tabs/plugins/PluginCard.tsx b/src/components/settings/tabs/plugins/PluginCard.tsx index 96d1cb788..72507ab1e 100644 --- a/src/components/settings/tabs/plugins/PluginCard.tsx +++ b/src/components/settings/tabs/plugins/PluginCard.tsx @@ -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) ? : } diff --git a/src/components/settings/tabs/plugins/PluginModal.tsx b/src/components/settings/tabs/plugins/PluginModal.tsx index 177fce95d..186f2be1f 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 { 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 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; + 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 ); diff --git a/src/components/settings/tabs/plugins/components/BooleanSetting.tsx b/src/components/settings/tabs/plugins/components/BooleanSetting.tsx index 51cef49d3..22596c3ba 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 56bb24f10..d5f0ac69c 100644 --- a/src/components/settings/tabs/plugins/components/Common.tsx +++ b/src/components/settings/tabs/plugins/components/Common.tsx @@ -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 { - 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 ( - +