diff --git a/package.json b/package.json index 9fd84f9b8..0e5845987 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.8.2", + "version": "1.8.3", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { diff --git a/src/api/Commands/commandHelpers.ts b/src/api/Commands/commandHelpers.ts index 759bb0e3c..2693ad747 100644 --- a/src/api/Commands/commandHelpers.ts +++ b/src/api/Commands/commandHelpers.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { mergeDefaults } from "@utils/misc"; +import { mergeDefaults } from "@utils/mergeDefaults"; import { findByProps } from "@webpack"; import { MessageActions, SnowflakeUtils } from "@webpack/common"; import { Message } from "discord-types/general"; diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 696c12c28..490e6ef7f 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -20,7 +20,7 @@ import { debounce } from "@shared/debounce"; import { SettingsStore as SettingsStoreClass } from "@shared/SettingsStore"; import { localStorage } from "@utils/localStorage"; import { Logger } from "@utils/Logger"; -import { mergeDefaults } from "@utils/misc"; +import { mergeDefaults } from "@utils/mergeDefaults"; import { putCloudSettings } from "@utils/settingsSync"; import { DefinedSettings, OptionType, SettingsChecks, SettingsDefinition } from "@utils/types"; import { React } from "@webpack/common"; diff --git a/src/components/VencordSettings/AddonCard.tsx b/src/components/VencordSettings/AddonCard.tsx index c4c3aaca9..1161a6411 100644 --- a/src/components/VencordSettings/AddonCard.tsx +++ b/src/components/VencordSettings/AddonCard.tsx @@ -21,7 +21,7 @@ import "./addonCard.css"; import { classNameFactory } from "@api/Styles"; import { Badge } from "@components/Badge"; import { Switch } from "@components/Switch"; -import { Text } from "@webpack/common"; +import { Text, useRef } from "@webpack/common"; import type { MouseEventHandler, ReactNode } from "react"; const cl = classNameFactory("vc-addon-"); @@ -42,6 +42,8 @@ interface Props { } export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) { + const titleRef = useRef(null); + const titleContainerRef = useRef(null); return (
- {name}{isNew && } +
+
{ + const title = titleRef.current!; + const titleContainer = titleContainerRef.current!; + + title.style.setProperty("--offset", `${titleContainer.clientWidth - title.scrollWidth}px`); + title.style.setProperty("--duration", `${Math.max(0.5, (title.scrollWidth - titleContainer.clientWidth) / 7)}s`); + }} + > + {name} +
+
{isNew && }
{!!author && ( diff --git a/src/components/VencordSettings/addonCard.css b/src/components/VencordSettings/addonCard.css index f2dee11d9..e46e4c29c 100644 --- a/src/components/VencordSettings/addonCard.css +++ b/src/components/VencordSettings/addonCard.css @@ -62,3 +62,36 @@ .vc-addon-author::before { content: "by "; } + +.vc-addon-title-container { + width: 100%; + overflow: hidden; + height: 1.25em; + position: relative; +} + +.vc-addon-title { + position: absolute; + inset: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +@keyframes vc-addon-title { + 0% { + transform: translateX(0); + } + + 50% { + transform: translateX(var(--offset)); + } + + 100% { + transform: translateX(0); + } +} + +.vc-addon-title:hover { + overflow: visible; + animation: vc-addon-title var(--duration) linear infinite; +} diff --git a/src/main/settings.ts b/src/main/settings.ts index 96efdd672..3d367a945 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -7,6 +7,7 @@ import type { Settings } from "@api/Settings"; import { IpcEvents } from "@shared/IpcEvents"; import { SettingsStore } from "@shared/SettingsStore"; +import { mergeDefaults } from "@utils/mergeDefaults"; import { ipcMain } from "electron"; import { mkdirSync, readFileSync, writeFileSync } from "fs"; @@ -42,7 +43,22 @@ ipcMain.handle(IpcEvents.SET_SETTINGS, (_, data: Settings, pathToNotify?: string RendererSettings.setData(data, pathToNotify); }); -export const NativeSettings = new SettingsStore(readSettings("native", NATIVE_SETTINGS_FILE)); +export interface NativeSettings { + plugins: { + [plugin: string]: { + [setting: string]: any; + }; + }; +} + +const DefaultNativeSettings: NativeSettings = { + plugins: {} +}; + +const nativeSettings = readSettings("native", NATIVE_SETTINGS_FILE); +mergeDefaults(nativeSettings, DefaultNativeSettings); + +export const NativeSettings = new SettingsStore(nativeSettings); NativeSettings.addGlobalChangeListener(() => { try { diff --git a/src/plugins/dearrow/index.tsx b/src/plugins/dearrow/index.tsx index b02c80d3d..888e2bb45 100644 --- a/src/plugins/dearrow/index.tsx +++ b/src/plugins/dearrow/index.tsx @@ -6,10 +6,11 @@ import "./styles.css"; +import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { Tooltip } from "@webpack/common"; import type { Component } from "react"; @@ -34,11 +35,19 @@ interface Props { }; } +const enum ReplaceElements { + ReplaceAllElements, + ReplaceTitlesOnly, + ReplaceThumbnailsOnly +} + const embedUrlRe = /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/; async function embedDidMount(this: Component) { try { const { embed } = this.props; + const { replaceElements } = settings.store; + if (!embed || embed.dearrow || embed.provider?.name !== "YouTube" || !embed.video?.url) return; const videoId = embedUrlRe.exec(embed.video.url)?.[1]; @@ -58,12 +67,12 @@ async function embedDidMount(this: Component) { enabled: true }; - if (hasTitle) { + if (hasTitle && replaceElements !== ReplaceElements.ReplaceThumbnailsOnly) { embed.dearrow.oldTitle = embed.rawTitle; embed.rawTitle = titles[0].title.replace(/ >(\S)/g, " $1"); } - if (hasThumb) { + if (hasThumb && replaceElements !== ReplaceElements.ReplaceTitlesOnly) { embed.dearrow.oldThumb = embed.thumbnail.proxyURL; embed.thumbnail.proxyURL = `https://dearrow-thumb.ajay.app/api/v1/getThumbnail?videoID=${videoId}&time=${thumbnails[0].timestamp}`; } @@ -128,10 +137,30 @@ function DearrowButton({ component }: { component: Component; }) { ); } +const settings = definePluginSettings({ + hideButton: { + description: "Hides the Dearrow button from YouTube embeds", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + }, + replaceElements: { + description: "Choose which elements of the embed will be replaced", + type: OptionType.SELECT, + restartNeeded: true, + options: [ + { label: "Everything (Titles & Thumbnails)", value: ReplaceElements.ReplaceAllElements, default: true }, + { label: "Titles", value: ReplaceElements.ReplaceTitlesOnly }, + { label: "Thumbnails", value: ReplaceElements.ReplaceThumbnailsOnly }, + ], + } +}); + export default definePlugin({ name: "Dearrow", description: "Makes YouTube embed titles and thumbnails less sensationalist, powered by Dearrow", authors: [Devs.Ven], + settings, embedDidMount, renderButton(component: Component) { @@ -154,7 +183,8 @@ export default definePlugin({ // add dearrow button { match: /children:\[(?=null!=\i\?\i\.renderSuppressButton)/, - replace: "children:[$self.renderButton(this)," + replace: "children:[$self.renderButton(this),", + predicate: () => !settings.store.hideButton } ] }], diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index bdd679308..829b046fd 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -110,7 +110,7 @@ const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/; const settings = definePluginSettings({ enableEmojiBypass: { - description: "Allow sending fake emojis", + description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)", type: OptionType.BOOLEAN, default: true, restartNeeded: true @@ -128,7 +128,7 @@ const settings = definePluginSettings({ restartNeeded: true }, enableStickerBypass: { - description: "Allow sending fake stickers", + description: "Allows sending fake stickers (also bypasses missing permission to use stickers)", type: OptionType.BOOLEAN, default: true, restartNeeded: true diff --git a/src/plugins/pronoundb/index.ts b/src/plugins/pronoundb/index.ts index b14b26572..a5891d2e8 100644 --- a/src/plugins/pronoundb/index.ts +++ b/src/plugins/pronoundb/index.ts @@ -33,7 +33,7 @@ const PRONOUN_TOOLTIP_PATCH = { export default definePlugin({ name: "PronounDB", - authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven], + authors: [Devs.Tyman, Devs.TheKodeToad, Devs.Ven, Devs.Elvyra], description: "Adds pronouns to user messages using pronoundb", patches: [ { diff --git a/src/plugins/pronoundb/pronoundbUtils.ts b/src/plugins/pronoundb/pronoundbUtils.ts index 6373c56a0..d4fdb09d3 100644 --- a/src/plugins/pronoundb/pronoundbUtils.ts +++ b/src/plugins/pronoundb/pronoundbUtils.ts @@ -24,7 +24,7 @@ import { useAwaiter } from "@utils/react"; import { UserProfileStore, UserStore } from "@webpack/common"; import { settings } from "./settings"; -import { PronounCode, PronounMapping, PronounsResponse } from "./types"; +import { CachePronouns, PronounCode, PronounMapping, PronounsResponse } from "./types"; type PronounsWithSource = [string | null, string]; const EmptyPronouns: PronounsWithSource = [null, ""]; @@ -40,9 +40,9 @@ export const enum PronounSource { } // A map of cached pronouns so the same request isn't sent twice -const cache: Record = {}; +const cache: Record = {}; // A map of ids and callbacks that should be triggered on fetch -const requestQueue: Record void)[]> = {}; +const requestQueue: Record void)[]> = {}; // Executes all queued requests and calls their callbacks const bulkFetch = debounce(async () => { @@ -50,7 +50,7 @@ const bulkFetch = debounce(async () => { const pronouns = await bulkFetchPronouns(ids); for (const id of ids) { // Call all callbacks for the id - requestQueue[id]?.forEach(c => c(pronouns[id])); + requestQueue[id]?.forEach(c => c(pronouns[id] ? extractPronouns(pronouns[id].sets) : "")); delete requestQueue[id]; } }); @@ -78,8 +78,8 @@ export function useFormattedPronouns(id: string, useGlobalProfile: boolean = fal if (settings.store.pronounSource === PronounSource.PreferDiscord && discordPronouns) return [discordPronouns, "Discord"]; - if (result && result !== "unspecified") - return [formatPronouns(result), "PronounDB"]; + if (result && result !== PronounMapping.unspecified) + return [result, "PronounDB"]; return [discordPronouns, "Discord"]; } @@ -98,8 +98,9 @@ const NewLineRe = /\n+/g; // Gets the cached pronouns, if you're too impatient for a promise! export function getCachedPronouns(id: string): string | null { - const cached = cache[id]; - if (cached && cached !== "unspecified") return cached; + const cached = cache[id] ? extractPronouns(cache[id].sets) : undefined; + + if (cached && cached !== PronounMapping.unspecified) return cached; return cached || null; } @@ -125,7 +126,7 @@ async function bulkFetchPronouns(ids: string[]): Promise { params.append("ids", ids.join(",")); try { - const req = await fetch("https://pronoundb.org/api/v1/lookup-bulk?" + params.toString(), { + const req = await fetch("https://pronoundb.org/api/v2/lookup?" + params.toString(), { method: "GET", headers: { "Accept": "application/json", @@ -140,21 +141,24 @@ async function bulkFetchPronouns(ids: string[]): Promise { } catch (e) { // If the request errors, treat it as if no pronouns were found for all ids, and log it console.error("PronounDB fetching failed: ", e); - const dummyPronouns = Object.fromEntries(ids.map(id => [id, "unspecified"] as const)); + const dummyPronouns = Object.fromEntries(ids.map(id => [id, { sets: {} }] as const)); Object.assign(cache, dummyPronouns); return dummyPronouns; } } -export function formatPronouns(pronouns: string): string { +export function extractPronouns(pronounSet?: { [locale: string]: PronounCode[] }): string { + if (!pronounSet || !pronounSet.en) return PronounMapping.unspecified; + // PronounDB returns an empty set instead of {sets: {en: ["unspecified"]}}. + const pronouns = pronounSet.en; const { pronounsFormat } = Settings.plugins.PronounDB as { pronounsFormat: PronounsFormat, enabled: boolean; }; - // For capitalized pronouns, just return the mapping (it is by default capitalized) - if (pronounsFormat === PronounsFormat.Capitalized) return PronounMapping[pronouns]; - // If it is set to lowercase and a special code (any, ask, avoid), then just return the capitalized text - else if ( - pronounsFormat === PronounsFormat.Lowercase - && ["any", "ask", "avoid", "other"].includes(pronouns) - ) return PronounMapping[pronouns]; - // Otherwise (lowercase and not a special code), then convert the mapping to lowercase - else return PronounMapping[pronouns].toLowerCase(); + + if (pronouns.length === 1) { + // For capitalized pronouns or special codes (any, ask, avoid), we always return the normal (capitalized) string + if (pronounsFormat === PronounsFormat.Capitalized || ["any", "ask", "avoid", "other", "unspecified"].includes(pronouns[0])) + return PronounMapping[pronouns[0]]; + else return PronounMapping[pronouns[0]].toLowerCase(); + } + const pronounString = pronouns.map(p => p[0].toUpperCase() + p.slice(1)).join("/"); + return pronounsFormat === PronounsFormat.Capitalized ? pronounString : pronounString.toLowerCase(); } diff --git a/src/plugins/pronoundb/types.ts b/src/plugins/pronoundb/types.ts index 9cfd77c8a..d099a7de8 100644 --- a/src/plugins/pronoundb/types.ts +++ b/src/plugins/pronoundb/types.ts @@ -26,31 +26,29 @@ export interface UserProfilePronounsProps { } export interface PronounsResponse { - [id: string]: PronounCode; + [id: string]: { + sets?: { + [locale: string]: PronounCode[]; + } + } +} + +export interface CachePronouns { + sets?: { + [locale: string]: PronounCode[]; + } } export type PronounCode = keyof typeof PronounMapping; export const PronounMapping = { - hh: "He/Him", - hi: "He/It", - hs: "He/She", - ht: "He/They", - ih: "It/Him", - ii: "It/Its", - is: "It/She", - it: "It/They", - shh: "She/He", - sh: "She/Her", - si: "She/It", - st: "She/They", - th: "They/He", - ti: "They/It", - ts: "They/She", - tt: "They/Them", + he: "He/Him", + it: "It/Its", + she: "She/Her", + they: "They/Them", any: "Any pronouns", other: "Other pronouns", ask: "Ask me my pronouns", avoid: "Avoid pronouns, use my name", - unspecified: "Unspecified" + unspecified: "No pronouns specified.", } as const; diff --git a/src/plugins/translate/TranslateIcon.tsx b/src/plugins/translate/TranslateIcon.tsx index cc0ed5e93..b22c488eb 100644 --- a/src/plugins/translate/TranslateIcon.tsx +++ b/src/plugins/translate/TranslateIcon.tsx @@ -40,9 +40,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?: } export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => { - const { autoTranslate } = settings.use(["autoTranslate"]); + const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]); - if (!isMainChat) return null; + if (!isMainChat || !showChatBarButton) return null; const toggle = () => { const newState = !autoTranslate; diff --git a/src/plugins/translate/settings.ts b/src/plugins/translate/settings.ts index cef003a83..65d845353 100644 --- a/src/plugins/translate/settings.ts +++ b/src/plugins/translate/settings.ts @@ -48,6 +48,11 @@ export const settings = definePluginSettings({ type: OptionType.BOOLEAN, description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this", default: false + }, + showChatBarButton: { + type: OptionType.BOOLEAN, + description: "Show translate button in chat bar", + default: true } }).withPrivateSettings<{ showAutoTranslateAlert: boolean; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 09c27d15f..c1e2cea2c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -462,6 +462,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "Oleh Polisan", id: 242305263313485825n }, + HAHALOSAH: { + name: "HAHALOSAH", + id: 903418691268513883n + }, GabiRP: { name: "GabiRP", id: 507955112027750401n diff --git a/src/utils/mergeDefaults.ts b/src/utils/mergeDefaults.ts new file mode 100644 index 000000000..58ba136dd --- /dev/null +++ b/src/utils/mergeDefaults.ts @@ -0,0 +1,24 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * Recursively merges defaults into an object and returns the same object + * @param obj Object + * @param defaults Defaults + * @returns obj + */ +export function mergeDefaults(obj: T, defaults: T): T { + for (const key in defaults) { + const v = defaults[key]; + if (typeof v === "object" && !Array.isArray(v)) { + obj[key] ??= {} as any; + mergeDefaults(obj[key], v); + } else { + obj[key] ??= v; + } + } + return obj; +} diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 32010e59b..fb08c93f6 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -20,25 +20,6 @@ import { Clipboard, Toasts } from "@webpack/common"; import { DevsById } from "./constants"; -/** - * Recursively merges defaults into an object and returns the same object - * @param obj Object - * @param defaults Defaults - * @returns obj - */ -export function mergeDefaults(obj: T, defaults: T): T { - for (const key in defaults) { - const v = defaults[key]; - if (typeof v === "object" && !Array.isArray(v)) { - obj[key] ??= {} as any; - mergeDefaults(obj[key], v); - } else { - obj[key] ??= v; - } - } - return obj; -} - /** * Calls .join(" ") on the arguments * classes("one", "two") => "one two" diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 843922f2f..f19928ac4 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -18,7 +18,7 @@ import { showNotification } from "@api/Notifications"; import { PlainSettings, Settings } from "@api/Settings"; -import { Toasts } from "@webpack/common"; +import { moment, Toasts } from "@webpack/common"; import { deflateSync, inflateSync } from "fflate"; import { getCloudAuth, getCloudUrl } from "./cloud"; @@ -49,7 +49,7 @@ export async function exportSettings({ minify }: { minify?: boolean; } = {}) { } export async function downloadSettingsBackup() { - const filename = "vencord-settings-backup.json"; + const filename = `vencord-settings-backup-${moment().format("YYYY-MM-DD")}.json`; const backup = await exportSettings(); const data = new TextEncoder().encode(backup);