From eaa23d8b328a35484611291e74b4f8deeefa6543 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:20:07 -0400 Subject: [PATCH 001/881] Merge Controls & Lyrics --- .../hoverOnly.css | 13 +- src/equicordplugins/musicControls/index.tsx | 108 ++++++++ .../musicControls/settings.tsx | 187 ++++++++++++++ .../spotify}/PlayerComponent.tsx | 199 ++++++++------- .../musicControls/spotify/SeekBar.ts | 25 ++ .../musicControls/spotify/SpotifyStore.ts | 198 +++++++++++++++ .../spotify/lyrics}/api.tsx | 5 +- .../spotify/lyrics}/components/ctxMenu.tsx | 0 .../spotify/lyrics}/components/lyrics.tsx | 6 +- .../spotify/lyrics}/components/modal.tsx | 2 +- .../spotify/lyrics}/components/util.tsx | 5 +- .../lyrics}/providers/SpotifyAPI/index.ts | 0 .../lyrics}/providers/lrclibAPI/index.ts | 3 +- .../spotify/lyrics}/providers/store.ts | 4 +- .../lyrics}/providers/translator/index.ts | 2 +- .../lyrics}/providers/translator/languages.ts | 0 .../spotify/lyrics}/providers/types.ts | 0 .../musicControls/spotify/spotifyStyles.css | 229 +++++++++++++++++ .../{tidalLyrics => musicControls}/styles.css | 84 +++++- .../tidal}/TidalPlayer.tsx | 8 +- .../tidal}/TidalStore.ts | 0 .../tidal/lyrics}/api.tsx | 16 +- .../tidal/lyrics}/components/ctxMenu.tsx | 0 .../tidal/lyrics}/components/lyrics.tsx | 6 +- .../tidal/lyrics}/components/modal.tsx | 6 +- .../tidal/lyrics}/components/util.tsx | 4 +- .../tidal/lyrics}/providers/store.ts | 4 +- .../tidal/lyrics}/types.ts | 0 .../tidal}/tidalStyles.css | 0 src/equicordplugins/spotifyLyrics/index.tsx | 61 ----- .../spotifyLyrics/settings.tsx | 145 ----------- src/equicordplugins/spotifyLyrics/styles.css | 77 ------ src/equicordplugins/tidalControls/index.tsx | 136 ---------- src/equicordplugins/tidalLyrics/index.tsx | 69 ----- src/equicordplugins/tidalLyrics/settings.tsx | 91 ------- .../youtubeMusicControls/README.md | 31 --- .../youtubeMusicControls/YtmStore.ts | 240 ------------------ .../youtubeMusicControls/hoverOnly.css | 16 -- .../youtubeMusicControls/index.tsx | 170 ------------- .../youtubeMusicControls/ytmStyles.css | 208 --------------- 40 files changed, 983 insertions(+), 1375 deletions(-) rename src/equicordplugins/{tidalControls => musicControls}/hoverOnly.css (56%) create mode 100644 src/equicordplugins/musicControls/index.tsx create mode 100644 src/equicordplugins/musicControls/settings.tsx rename src/equicordplugins/{youtubeMusicControls => musicControls/spotify}/PlayerComponent.tsx (62%) create mode 100644 src/equicordplugins/musicControls/spotify/SeekBar.ts create mode 100644 src/equicordplugins/musicControls/spotify/SpotifyStore.ts rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/api.tsx (97%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/components/ctxMenu.tsx (100%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/components/lyrics.tsx (94%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/components/modal.tsx (97%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/components/util.tsx (96%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/providers/SpotifyAPI/index.ts (100%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/providers/lrclibAPI/index.ts (96%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/providers/store.ts (97%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/providers/translator/index.ts (98%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/providers/translator/languages.ts (100%) rename src/equicordplugins/{spotifyLyrics => musicControls/spotify/lyrics}/providers/types.ts (100%) create mode 100644 src/equicordplugins/musicControls/spotify/spotifyStyles.css rename src/equicordplugins/{tidalLyrics => musicControls}/styles.css (72%) rename src/equicordplugins/{tidalControls => musicControls/tidal}/TidalPlayer.tsx (97%) rename src/equicordplugins/{tidalControls => musicControls/tidal}/TidalStore.ts (100%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/api.tsx (69%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/components/ctxMenu.tsx (100%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/components/lyrics.tsx (97%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/components/modal.tsx (96%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/components/util.tsx (96%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/providers/store.ts (93%) rename src/equicordplugins/{tidalLyrics => musicControls/tidal/lyrics}/types.ts (100%) rename src/equicordplugins/{tidalControls => musicControls/tidal}/tidalStyles.css (100%) delete mode 100644 src/equicordplugins/spotifyLyrics/index.tsx delete mode 100644 src/equicordplugins/spotifyLyrics/settings.tsx delete mode 100644 src/equicordplugins/spotifyLyrics/styles.css delete mode 100644 src/equicordplugins/tidalControls/index.tsx delete mode 100644 src/equicordplugins/tidalLyrics/index.tsx delete mode 100644 src/equicordplugins/tidalLyrics/settings.tsx delete mode 100644 src/equicordplugins/youtubeMusicControls/README.md delete mode 100644 src/equicordplugins/youtubeMusicControls/YtmStore.ts delete mode 100644 src/equicordplugins/youtubeMusicControls/hoverOnly.css delete mode 100644 src/equicordplugins/youtubeMusicControls/index.tsx delete mode 100644 src/equicordplugins/youtubeMusicControls/ytmStyles.css diff --git a/src/equicordplugins/tidalControls/hoverOnly.css b/src/equicordplugins/musicControls/hoverOnly.css similarity index 56% rename from src/equicordplugins/tidalControls/hoverOnly.css rename to src/equicordplugins/musicControls/hoverOnly.css index b5bed59db..9df7594ae 100644 --- a/src/equicordplugins/tidalControls/hoverOnly.css +++ b/src/equicordplugins/musicControls/hoverOnly.css @@ -1,3 +1,11 @@ +.vc-spotify-button-row { + height: 0; + opacity: 0; + pointer-events: none; + transition: 0.2s; + transition-property: height; +} + .eq-tdl-button-row { height: 0; opacity: 0; @@ -6,11 +14,12 @@ transition-property: height; } -#eq-tdl-player:hover .eq-tdl-button-row { +#eq-tdl-player:hover .eq-tdl-button-row, +#vc-spotify-player:hover .vc-spotify-button-row { opacity: 1; height: 32px; pointer-events: auto; /* only transition opacity on show to prevent clipping */ transition-property: height, opacity; -} +} \ No newline at end of file diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx new file mode 100644 index 000000000..262ce4c62 --- /dev/null +++ b/src/equicordplugins/musicControls/index.tsx @@ -0,0 +1,108 @@ +/* + * 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 . +*/ + +import "./styles.css"; + +import { Settings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs, EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +import { settings, toggleHoverControls } from "./settings"; +import { migrateOldLyrics } from "./spotify/lyrics/api"; +import { SpotifyLyrics } from "./spotify/lyrics/components/lyrics"; +import { SpotifyPlayer } from "./spotify/PlayerComponent"; +import { TidalLyrics } from "./tidal/lyrics/components/lyrics"; +import { TidalPlayer } from "./tidal/TidalPlayer"; + +export default definePlugin({ + name: "MusicControls", + description: "Music Controls and Lyrics for multiple services ", + authors: [EquicordDevs.thororen, EquicordDevs.vmohammad, Devs.Joona], + settings, + patches: [ + { + find: "this.isCopiedStreakGodlike", + replacement: { + // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) + match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/, + // react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ... + replace: "$self.PanelWrapper,{VencordOriginal:$1," + }, + }, + { + find: ".PLAYER_DEVICES", + replacement: [{ + // Adds POST and a Marker to the SpotifyAPI (so we can easily find it) + match: /get:(\i)\.bind\(null,(\i\.\i)\.get\)/, + replace: "post:$1.bind(null,$2.post),vcSpotifyMarker:1,$&" + }, + { + // Spotify Connect API returns status 202 instead of 204 when skipping tracks. + // Discord rejects 202 which causes the request to send twice. This patch prevents this. + match: /202===\i\.status/, + replace: "false", + }] + }, + { + find: 'repeat:"off"!==', + replacement: [ + { + // Discord doesn't give you shuffle state and the repeat kind, only a boolean + match: /repeat:"off"!==(\i),/, + replace: "shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&" + }, + { + match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/, + replace: "" + } + ] + }, + ], + + + PanelWrapper({ VencordOriginal, ...props }) { + const { showTidalControls, showTidalLyrics, showSpotifyLyrics, showSpotifyControls, LyricsPosition } = settings.store; + return ( + <> + ( +
+

Failed to render Modal :(

+

Check the console for errors

+
+ )} + > + {showTidalLyrics && LyricsPosition === "above" && } + {showTidalControls && } + {showTidalLyrics && LyricsPosition === "below" && } + {showSpotifyLyrics && LyricsPosition === "above" && } + {showSpotifyControls && } + {showSpotifyLyrics && LyricsPosition === "below" && } +
+ + + + ); + }, + + async start() { + await migrateOldLyrics(); + toggleHoverControls(Settings.plugins.MusicControls.hoverControls); + }, +}); diff --git a/src/equicordplugins/musicControls/settings.tsx b/src/equicordplugins/musicControls/settings.tsx new file mode 100644 index 000000000..6b3c3cae5 --- /dev/null +++ b/src/equicordplugins/musicControls/settings.tsx @@ -0,0 +1,187 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { disableStyle, enableStyle } from "@api/Styles"; +import { useAwaiter } from "@utils/react"; +import { makeRange, OptionType } from "@utils/types"; +import { Button, Forms, MaskedLink, showToast, Text, Toasts, useMemo } from "@webpack/common"; + +import hoverOnlyStyle from "./hoverOnly.css?managed"; +import { clearLyricsCache, getLyricsCount } from "./spotify/lyrics/api"; +import { useLyrics } from "./spotify/lyrics/components/util"; +import { Provider } from "./spotify/lyrics/providers/types"; + +const sliderOptions = { + markers: makeRange(-2500, 2500, 250), + stickToMarkers: true, +}; + +export function toggleHoverControls(value: boolean) { + (value ? enableStyle : disableStyle)(hoverOnlyStyle); +} + +function Details() { + const { lyricsInfo } = useLyrics(); + + const [count, error, loading] = useAwaiter( + useMemo(() => getLyricsCount, []), + { + onError: () => console.error("Failed to get lyrics count"), + fallbackValue: null, + } + ); + + return ( + <> + Current lyrics provider: {lyricsInfo?.useLyric || "None"} + {loading ? Loading lyrics count... : error ? Failed to get lyrics count : Lyrics count: {count}} + + ); +} + + +function InstallInstructions() { + return ( + + How to install + + Install TidaLuna from here, then go to TidalLuna settings → Plugin stores → Install @vmohammad/api + + + ); +} + +export const settings = definePluginSettings({ + showSpotifyControls: { + description: "Show Spotify Controls", + type: OptionType.BOOLEAN, + default: false, + }, + showSpotifyLyrics: { + description: "Show Spotify Lyrics", + type: OptionType.BOOLEAN, + default: false, + }, + installTidalWithWS: { + type: OptionType.COMPONENT, + component: () => + }, + showTidalControls: { + description: "Show Tidal Player", + type: OptionType.BOOLEAN, + default: false, + }, + showTidalLyrics: { + description: "Show Tidal Controls", + type: OptionType.BOOLEAN, + default: false, + }, + hoverControls: { + description: "Show controls on hover", + type: OptionType.BOOLEAN, + default: false, + onChange: v => toggleHoverControls(v) + }, + useSpotifyUris: { + type: OptionType.BOOLEAN, + description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms", + default: false + }, + previousButtonRestartsTrack: { + type: OptionType.BOOLEAN, + description: "Restart currently playing track when pressing the previous button if playtime is >3s", + default: true + }, + ShowMusicNoteOnNoLyrics: { + description: "Show a music note icon when no lyrics are found", + type: OptionType.BOOLEAN, + default: true, + }, + LyricsPosition: { + description: "Position of the lyrics", + type: OptionType.SELECT, + options: [ + { value: "above", label: "Above Player(s)" }, + { value: "below", label: "Below Player(s)", default: true }, + ], + }, + LyricsProvider: { + description: "Where lyrics are fetched from", + type: OptionType.SELECT, + options: [ + { value: Provider.Spotify, label: "Spotify (Musixmatch)", default: true }, + { value: Provider.Lrclib, label: "LRCLIB" }, + ], + }, + LyricsConversion: { + description: "Automatically translate or romanize lyrics", + type: OptionType.SELECT, + options: [ + { value: Provider.None, label: "None", default: true }, + { value: Provider.Translated, label: "Translate" }, + { value: Provider.Romanized, label: "Romanize" }, + ] + }, + FallbackProvider: { + description: "When a lyrics provider fails, try other providers", + type: OptionType.BOOLEAN, + default: true, + }, + ShowFailedToasts: { + description: "Hide toasts when lyrics fail to fetch", + type: OptionType.BOOLEAN, + default: true, + }, + TidalLyricFetch: { + description: "Custom URL for fetching lyrics", + type: OptionType.STRING, + default: "https://api.vmohammad.dev/", + placeholder: "https://api.vmohammad.dev/", + onChange: (value: string) => { + if (!value.endsWith("/")) { + value += "/"; + } + if (URL.canParse(value)) { + settings.store.TidalLyricFetch = value; + } else { + showToast("Invalid URL format for CustomUrl: " + value, Toasts.Type.FAILURE); + settings.store.TidalLyricFetch = "https://api.vmohammad.dev/"; + } + } + }, + LyricDelay: { + description: "", + type: OptionType.SLIDER, + default: 0, + ...sliderOptions + }, + SyncMode: { + description: "Lyrics sync mode", + type: OptionType.SELECT, + options: [ + { value: "line", label: "Line" }, + { value: "word", label: "Word", default: true }, + { value: "character", label: "Character" }, + ], + default: "word", + }, + PurgeLyricsCache: { + description: "Purge the lyrics cache", + type: OptionType.COMPONENT, + component: () => ( + + ), + }, +}); diff --git a/src/equicordplugins/youtubeMusicControls/PlayerComponent.tsx b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx similarity index 62% rename from src/equicordplugins/youtubeMusicControls/PlayerComponent.tsx rename to src/equicordplugins/musicControls/spotify/PlayerComponent.tsx index 8ebc7fc77..1c4a2d0a9 100644 --- a/src/equicordplugins/youtubeMusicControls/PlayerComponent.tsx +++ b/src/equicordplugins/musicControls/spotify/PlayerComponent.tsx @@ -16,18 +16,21 @@ * along with this program. If not, see . */ -import "./ytmStyles.css"; +import "./spotifyStyles.css"; +import { Settings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; +import { Flex } from "@components/Flex"; import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; -import { SeekBar } from "@plugins/spotifyControls/SeekBar"; import { debounce } from "@shared/debounce"; import { openImageModal } from "@utils/discord"; import { classes, copyWithToast } from "@utils/misc"; -import { ContextMenuApi, Flex, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; +import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; -import { type PlayerState, type Repeat, YoutubeMusicStore } from "./YtmStore"; +import { SeekBar } from "./SeekBar"; +import { SpotifyStore, Track } from "./SpotifyStore"; -const cl = (className: string) => `vc-ytm-${className}`; +const cl = classNameFactory("vc-spotify-"); function msToHuman(ms: number) { const minutes = ms / 1000 / 60; @@ -39,8 +42,7 @@ function msToHuman(ms: number) { function Svg(path: string, label: string) { return () => ( - {label} ); @@ -76,27 +77,27 @@ function Button(props: React.ButtonHTMLAttributes) { } function CopyContextMenu({ name, path }: { name: string; path: string; }) { - const copyId = `ytm-copy-${name}`; - const openId = `ytm-open-${name}`; + const copyId = `spotify-copy-${name}`; + const openId = `spotify-open-${name}`; return ( FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label={`YouTube Music ${name} Menu`} + aria-label={`Spotify ${name} Menu`} > copyWithToast(`https://music.youtube.com${path}`)} + action={() => copyWithToast("https://open.spotify.com" + path)} icon={LinkIcon} /> YoutubeMusicStore.openExternal(path)} + label={`Open ${name} in Spotify`} + action={() => SpotifyStore.openExternal(path)} icon={OpenExternalIcon} /> @@ -110,15 +111,15 @@ function makeContextMenu(name: string, path: string) { function Controls() { const [isPlaying, shuffle, repeat] = useStateFromStores( - [YoutubeMusicStore], - () => [YoutubeMusicStore.isPlaying, YoutubeMusicStore.shuffle, YoutubeMusicStore.repeat] + [SpotifyStore], + () => [SpotifyStore.isPlaying, SpotifyStore.shuffle, SpotifyStore.repeat] ); const [nextRepeat, repeatClassName] = (() => { switch (repeat) { - case "NONE": return ["ALL", "repeat-off"] as const; - case "ALL": return ["ONE", "repeat-context"] as const; - case "ONE": return ["NONE", "repeat-track"] as const; + case "off": return ["context", "repeat-off"] as const; + case "context": return ["track", "repeat-context"] as const; + case "track": return ["off", "repeat-track"] as const; default: throw new Error(`Invalid repeat state ${repeat}`); } })(); @@ -128,24 +129,27 @@ function Controls() { - - - @@ -153,34 +157,32 @@ function Controls() { } const seek = debounce((v: number) => { - YoutubeMusicStore.seek(v); + SpotifyStore.seek(v); }); -function YtmSeekBar() { - const { songDuration, videoId } = YoutubeMusicStore.song ?? { songDuration: 0, videoId: 0 }; +function SpotifySeekBar() { + const { duration } = SpotifyStore.track!; - const [storePosition, isPlaying] = useStateFromStores( - [YoutubeMusicStore], - () => [YoutubeMusicStore.mPosition, YoutubeMusicStore.isPlaying] + const [storePosition, isSettingPosition, isPlaying] = useStateFromStores( + [SpotifyStore], + () => [SpotifyStore.mPosition, SpotifyStore.isSettingPosition, SpotifyStore.isPlaying] ); - const [position, setPosition] = useState(storePosition); + const [position, setPosition] = useState(storePosition); useEffect(() => { - setPosition(storePosition); - }, [videoId, songDuration, storePosition]); - - useEffect(() => { - if (isPlaying) { - setPosition(YoutubeMusicStore.position); + if (isPlaying && !isSettingPosition) { + setPosition(SpotifyStore.position); const interval = setInterval(() => { setPosition(p => p + 1000); }, 1000); + return () => clearInterval(interval); } - }, [storePosition, isPlaying]); + }, [storePosition, isSettingPosition, isPlaying]); const onChange = (v: number) => { + if (isSettingPosition) return; setPosition(v); seek(v); }; @@ -189,7 +191,7 @@ function YtmSeekBar() {
{msToHuman(position)} @@ -197,49 +199,50 @@ function YtmSeekBar() { - {msToHuman(songDuration * 1000)} + {msToHuman(duration)}
); } -function AlbumContextMenu({ track }: { track: PlayerState["song"]; }) { - const volume = useStateFromStores([YoutubeMusicStore], () => YoutubeMusicStore.volume); + +function AlbumContextMenu({ track }: { track: Track; }) { + const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume); return ( FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label="YouTube Music Album Menu" + aria-label="Spotify Album Menu" > - {/* YoutubeMusicStore.openExternal(`/album/${track?.album}`)} + action={() => SpotifyStore.openExternal(`/album/${track.album.id}`)} icon={OpenExternalIcon} - /> */} + /> track?.imageSrc && openImageModal({ url: track.imageSrc })} + action={() => openImageModal(track.album.image)} icon={ImageIcon} /> ( YoutubeMusicStore.setVolume(v))} + onChange={debounce((v: number) => SpotifyStore.setVolume(v))} /> )} /> @@ -261,13 +264,13 @@ function makeLinkProps(name: string, condition: unknown, path: string) { return { role: "link", - onClick: () => YoutubeMusicStore.openExternal(path), + onClick: () => SpotifyStore.openExternal(path), onContextMenu: makeContextMenu(name, path) } satisfies React.HTMLAttributes; } -function Info({ track }: { track: NonNullable; }) { - const img = track?.imageSrc; +function Info({ track }: { track: Track; }) { + const img = track?.album?.image; const [coverExpanded, setCoverExpanded] = useState(false); @@ -276,7 +279,7 @@ function Info({ track }: { track: NonNullable; }) { {img && ( Album Image setCoverExpanded(!coverExpanded)} onContextMenu={e => { @@ -287,11 +290,12 @@ function Info({ track }: { track: NonNullable; }) { ); - if (coverExpanded && img) return ( -
- {i} -
- ); + if (coverExpanded && img) + return ( +
+ {i} +
+ ); return (
@@ -301,29 +305,40 @@ function Info({ track }: { track: NonNullable; }) { variant="text-sm/semibold" id={cl("song-title")} className={cl("ellipoverflow")} - title={track?.title} - {...makeLinkProps("Song", track?.videoId, `/watch?v=${track?.videoId}`)} + title={track.name} + {...makeLinkProps("Song", track.id, `/track/${track.id}`)} > - {track?.title} + {track.name} - {track.artist && ( - - by  - - {track.artist} - + {track.artists.some(a => a.name) && ( + + by  + {track.artists.map((a, i) => ( + + + {a.name} + + {i !== track.artists.length - 1 && {", "}} + + ))} )} - {track.album && ( - - on  + {track.album.name && ( + + on  - {track.album} + {track.album.name} )} @@ -332,17 +347,25 @@ function Info({ track }: { track: NonNullable; }) { ); } -export function Player() { +export function SpotifyPlayer() { const track = useStateFromStores( - [YoutubeMusicStore], - () => YoutubeMusicStore.song, + [SpotifyStore], + () => SpotifyStore.track, null, - (prev, next) => prev?.videoId ? (prev.videoId === next?.videoId) : prev?.title === next?.title + (prev, next) => prev?.id ? (prev.id === next?.id) : prev?.name === next?.name ); - const isPlaying = useStateFromStores([YoutubeMusicStore], () => YoutubeMusicStore.isPlaying); + const device = useStateFromStores( + [SpotifyStore], + () => SpotifyStore.device, + null, + (prev, next) => prev?.id === next?.id + ); + + const isPlaying = useStateFromStores([SpotifyStore], () => SpotifyStore.isPlaying); const [shouldHide, setShouldHide] = useState(false); + // Hide player after 5 minutes of inactivity React.useEffect(() => { setShouldHide(false); @@ -352,18 +375,18 @@ export function Player() { } }, [isPlaying]); - if (!track || shouldHide) return; + if (!track || !device?.is_active || shouldHide) + return null; const exportTrackImageStyle = { - "--vc-ytm-track-image": `url(${track?.imageSrc || ""})`, + "--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`, } as React.CSSProperties; return (
- +
); } - diff --git a/src/equicordplugins/musicControls/spotify/SeekBar.ts b/src/equicordplugins/musicControls/spotify/SeekBar.ts new file mode 100644 index 000000000..8d6c8a30c --- /dev/null +++ b/src/equicordplugins/musicControls/spotify/SeekBar.ts @@ -0,0 +1,25 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { LazyComponent } from "@utils/lazyReact"; +import { Slider } from "@webpack/common"; + +export const SeekBar = LazyComponent(() => { + const SliderClass = Slider.$$vencordGetWrappedComponent(); + + // Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish. + // We extend their class and override their `getDerivedStateFromProps` to update the value + return class SeekBar extends SliderClass { + static getDerivedStateFromProps(props: any, state: any) { + const newState = super.getDerivedStateFromProps!(props, state); + if (newState) { + newState.value = props.initialValue; + } + + return newState; + } + }; +}); diff --git a/src/equicordplugins/musicControls/spotify/SpotifyStore.ts b/src/equicordplugins/musicControls/spotify/SpotifyStore.ts new file mode 100644 index 000000000..efdfa176f --- /dev/null +++ b/src/equicordplugins/musicControls/spotify/SpotifyStore.ts @@ -0,0 +1,198 @@ +/* + * 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 . +*/ + +import { Settings } from "@api/Settings"; +import { findByProps, findByPropsLazy, proxyLazyWebpack } from "@webpack"; +import { Flux, FluxDispatcher } from "@webpack/common"; + +export interface Track { + id: string; + name: string; + duration: number; + isLocal: boolean; + album: { + id: string; + name: string; + image: { + height: number; + width: number; + url: string; + }; + }; + artists: { + id: string; + href: string; + name: string; + type: string; + uri: string; + }[]; +} + +interface PlayerState { + accountId: string; + track: Track | null; + volumePercent: number, + isPlaying: boolean, + repeat: boolean, + position: number, + context?: any; + device?: Device; + + // added by patch + actual_repeat: Repeat; + shuffle: boolean; +} + +interface Device { + id: string; + is_active: boolean; +} + +type Repeat = "off" | "track" | "context"; + +// Don't wanna run before Flux and Dispatcher are ready! +export const SpotifyStore = proxyLazyWebpack(() => { + // For some reason ts hates extends Flux.Store + const { Store } = Flux; + + const SpotifySocket = findByProps("getActiveSocketAndDevice"); + const SpotifyAPI = findByPropsLazy("vcSpotifyMarker"); + + const API_BASE = "https://api.spotify.com/v1/me/player"; + + class SpotifyStore extends Store { + public mPosition = 0; + public _start = 0; + + public track: Track | null = null; + public device: Device | null = null; + public isPlaying = false; + public repeat: Repeat = "off"; + public shuffle = false; + public volume = 0; + + public isSettingPosition = false; + + public openExternal(path: string) { + const url = Settings.plugins.MusicControls.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp") + ? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":") + : "https://open.spotify.com" + path; + + VencordNative.native.openExternal(url); + } + + // Need to keep track of this manually + public get position(): number { + let pos = this.mPosition; + if (this.isPlaying) { + pos += Date.now() - this._start; + } + return pos; + } + + public set position(p: number) { + this.mPosition = p; + this._start = Date.now(); + } + + prev() { + this._req("post", "/previous"); + } + + next() { + this._req("post", "/next"); + } + + setVolume(percent: number) { + this._req("put", "/volume", { + query: { + volume_percent: Math.round(percent) + } + + }).then(() => { + this.volume = percent; + this.emitChange(); + }); + } + + setPlaying(playing: boolean) { + this._req("put", playing ? "/play" : "/pause"); + } + + setRepeat(state: Repeat) { + this._req("put", "/repeat", { + query: { state } + }); + } + + setShuffle(state: boolean) { + this._req("put", "/shuffle", { + query: { state } + }).then(() => { + this.shuffle = state; + this.emitChange(); + }); + } + + seek(ms: number) { + if (this.isSettingPosition) return Promise.resolve(); + + this.isSettingPosition = true; + + return this._req("put", "/seek", { + query: { + position_ms: Math.round(ms) + } + }).catch((e: any) => { + console.error("[VencordSpotifyControls] Failed to seek", e); + this.isSettingPosition = false; + }); + } + + _req(method: "post" | "get" | "put", route: string, data: any = {}) { + if (this.device?.is_active) + (data.query ??= {}).device_id = this.device.id; + + const { socket } = SpotifySocket.getActiveSocketAndDevice(); + return SpotifyAPI[method](socket.accountId, socket.accessToken, { + url: API_BASE + route, + ...data + }); + } + } + + const store = new SpotifyStore(FluxDispatcher, { + SPOTIFY_PLAYER_STATE(e: PlayerState) { + store.track = e.track; + store.device = e.device ?? null; + store.isPlaying = e.isPlaying ?? false; + store.volume = e.volumePercent ?? 0; + store.repeat = e.actual_repeat || "off"; + store.shuffle = e.shuffle ?? false; + store.position = e.position ?? 0; + store.isSettingPosition = false; + store.emitChange(); + }, + SPOTIFY_SET_DEVICES({ devices }: { devices: Device[]; }) { + store.device = devices.find(d => d.is_active) ?? devices[0] ?? null; + store.emitChange(); + } + }); + + return store; +}); diff --git a/src/equicordplugins/spotifyLyrics/api.tsx b/src/equicordplugins/musicControls/spotify/lyrics/api.tsx similarity index 97% rename from src/equicordplugins/spotifyLyrics/api.tsx rename to src/equicordplugins/musicControls/spotify/lyrics/api.tsx index 379a564d1..a92381d77 100644 --- a/src/equicordplugins/spotifyLyrics/api.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/api.tsx @@ -5,13 +5,12 @@ */ import { DataStore } from "@api/index"; -import { Track } from "plugins/spotifyControls/SpotifyStore"; +import { settings } from "../../settings"; +import { Track } from "../SpotifyStore"; import { getLyricsLrclib } from "./providers/lrclibAPI"; import { getLyricsSpotify } from "./providers/SpotifyAPI"; import { LyricsData, Provider, SyncedLyric } from "./providers/types"; -import settings from "./settings"; - const LyricsCacheKey = "SpotifyLyricsCacheNew"; diff --git a/src/equicordplugins/spotifyLyrics/components/ctxMenu.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/ctxMenu.tsx similarity index 100% rename from src/equicordplugins/spotifyLyrics/components/ctxMenu.tsx rename to src/equicordplugins/musicControls/spotify/lyrics/components/ctxMenu.tsx diff --git a/src/equicordplugins/spotifyLyrics/components/lyrics.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx similarity index 94% rename from src/equicordplugins/spotifyLyrics/components/lyrics.tsx rename to src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx index f323ab2ef..7a3770093 100644 --- a/src/equicordplugins/spotifyLyrics/components/lyrics.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx @@ -6,10 +6,10 @@ import { openModal } from "@utils/modal"; import { ContextMenuApi, React, Text, TooltipContainer, useEffect, useState, useStateFromStores } from "@webpack/common"; -import { SpotifyStore } from "plugins/spotifyControls/SpotifyStore"; +import { settings } from "../../../settings"; +import { SpotifyStore } from "../../SpotifyStore"; import { SpotifyLrcStore } from "../providers/store"; -import settings from "../settings"; import { LyricsContextMenu } from "./ctxMenu"; import { LyricsModal } from "./modal"; import { cl, NoteSvg, useLyrics } from "./util"; @@ -63,7 +63,7 @@ function LyricsDisplay({ scroll = true }: { scroll?: boolean; }) { ); } -export function Lyrics({ scroll = true }: { scroll?: boolean; } = {}) { +export function SpotifyLyrics({ scroll = true }: { scroll?: boolean; } = {}) { SpotifyLrcStore.init(); const track = useStateFromStores( [SpotifyStore], diff --git a/src/equicordplugins/spotifyLyrics/components/modal.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx similarity index 97% rename from src/equicordplugins/spotifyLyrics/components/modal.tsx rename to src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx index 855bcf967..6a3423fec 100644 --- a/src/equicordplugins/spotifyLyrics/components/modal.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx @@ -7,8 +7,8 @@ import { openImageModal } from "@utils/discord"; import { ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { React, Text } from "@webpack/common"; -import { SpotifyStore, Track } from "plugins/spotifyControls/SpotifyStore"; +import { SpotifyStore, Track } from "../../SpotifyStore"; import { cl, NoteSvg, scrollClasses, useLyrics } from "./util"; const formatTime = (time: number) => { diff --git a/src/equicordplugins/spotifyLyrics/components/util.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx similarity index 96% rename from src/equicordplugins/spotifyLyrics/components/util.tsx rename to src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx index a3ed7884b..176df03a7 100644 --- a/src/equicordplugins/spotifyLyrics/components/util.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx @@ -7,12 +7,11 @@ import { classNameFactory } from "@api/Styles"; import { findByPropsLazy } from "@webpack"; import { React, useEffect, useState, useStateFromStores } from "@webpack/common"; -import { SpotifyStore } from "plugins/spotifyControls/SpotifyStore"; +import { settings } from "../../../settings"; +import { SpotifyStore } from "../../SpotifyStore"; import { SpotifyLrcStore } from "../providers/store"; import { SyncedLyric } from "../providers/types"; -import settings from "../settings"; - export const scrollClasses = findByPropsLazy("auto", "customTheme"); export const cl = classNameFactory("vc-spotify-lyrics-"); diff --git a/src/equicordplugins/spotifyLyrics/providers/SpotifyAPI/index.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/SpotifyAPI/index.ts similarity index 100% rename from src/equicordplugins/spotifyLyrics/providers/SpotifyAPI/index.ts rename to src/equicordplugins/musicControls/spotify/lyrics/providers/SpotifyAPI/index.ts diff --git a/src/equicordplugins/spotifyLyrics/providers/lrclibAPI/index.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/lrclibAPI/index.ts similarity index 96% rename from src/equicordplugins/spotifyLyrics/providers/lrclibAPI/index.ts rename to src/equicordplugins/musicControls/spotify/lyrics/providers/lrclibAPI/index.ts index e38be95b4..5ee570476 100644 --- a/src/equicordplugins/spotifyLyrics/providers/lrclibAPI/index.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/lrclibAPI/index.ts @@ -4,8 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { Track } from "plugins/spotifyControls/SpotifyStore"; - +import { Track } from "../../../SpotifyStore"; import { LyricsData, Provider } from "../types"; const baseUrlLrclib = "https://lrclib.net/api/get"; diff --git a/src/equicordplugins/spotifyLyrics/providers/store.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts similarity index 97% rename from src/equicordplugins/spotifyLyrics/providers/store.ts rename to src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts index 34c8a321c..5f9049891 100644 --- a/src/equicordplugins/spotifyLyrics/providers/store.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts @@ -7,10 +7,10 @@ import { showNotification } from "@api/Notifications"; import { proxyLazyWebpack } from "@webpack"; import { Flux, FluxDispatcher } from "@webpack/common"; -import { SpotifyStore, type Track } from "plugins/spotifyControls/SpotifyStore"; +import { settings } from "../../../settings"; +import { SpotifyStore, type Track } from "../../SpotifyStore"; import { getLyrics, lyricFetchers, providers, updateLyrics } from "../api"; -import settings from "../settings"; import { lyricsAlternativeFetchers } from "./translator"; import { LyricsData, Provider } from "./types"; diff --git a/src/equicordplugins/spotifyLyrics/providers/translator/index.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts similarity index 98% rename from src/equicordplugins/spotifyLyrics/providers/translator/index.ts rename to src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts index 37555103a..02d7308ec 100644 --- a/src/equicordplugins/spotifyLyrics/providers/translator/index.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import settings from "../../settings"; +import settings from "../../../.."; import { LyricsData, Provider, SyncedLyric } from "../types"; // stolen from src\plugins\translate\utils.ts diff --git a/src/equicordplugins/spotifyLyrics/providers/translator/languages.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/languages.ts similarity index 100% rename from src/equicordplugins/spotifyLyrics/providers/translator/languages.ts rename to src/equicordplugins/musicControls/spotify/lyrics/providers/translator/languages.ts diff --git a/src/equicordplugins/spotifyLyrics/providers/types.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/types.ts similarity index 100% rename from src/equicordplugins/spotifyLyrics/providers/types.ts rename to src/equicordplugins/musicControls/spotify/lyrics/providers/types.ts diff --git a/src/equicordplugins/musicControls/spotify/spotifyStyles.css b/src/equicordplugins/musicControls/spotify/spotifyStyles.css new file mode 100644 index 000000000..e9dfeffde --- /dev/null +++ b/src/equicordplugins/musicControls/spotify/spotifyStyles.css @@ -0,0 +1,229 @@ +#vc-spotify-player { + padding: 12px; + background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt))); + margin: 0; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + border-bottom: 1px solid var(--border-subtle); + + /* so custom themes can easily change it */ + --vc-spotify-green: var(--spotify, #1db954); + --vc-spotify-green-90: color-mix(in hsl, var(--vc-spotify-green), transparent 90%); + --vc-spotify-green-80: color-mix(in hsl, var(--vc-spotify-green), transparent 80%); +} + +.theme-light #vc-spotify-player { + background: var(--bg-overlay-3, var(--background-base-lower-alt)); +} + +.theme-dark #vc-spotify-player { + background: var(--bg-overlay-1, var(--background-base-lower-alt)); +} + +.vc-spotify-button { + margin: 0 2px; + border-radius: var(--radius-sm); + background: none; + color: var(--interactive-normal); + padding: 0; + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; +} + +.vc-spotify-button:hover { + color: var(--interactive-hover); + background-color: var(--background-modifier-selected); +} + +.vc-spotify-button-icon { + height: 24px; + width: 24px; +} + +.vc-spotify-shuffle .vc-spotify-button-icon, +.vc-spotify-repeat .vc-spotify-button-icon { + width: 22px; + height: 22px; +} + +/* .vc-spotify-button:hover { + filter: brightness(1.3); +} */ + +.vc-spotify-repeat-context, +.vc-spotify-repeat-track, +.vc-spotify-shuffle-on { + background-color: var(--vc-spotify-green-90); +} + +.vc-spotify-repeat-context:hover, +.vc-spotify-repeat-track:hover, +.vc-spotify-shuffle-on:hover { + background-color: var(--vc-spotify-green-80); +} + +.vc-spotify-tooltip-text { + overflow: hidden; + white-space: nowrap; + padding-right: 0.2em; + max-width: 100%; + margin: unset; +} + +.vc-spotify-repeat-1 { + font-size: 70%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.vc-spotify-button-row { + justify-content: center; + margin-top: 14px; +} + +.vc-spotify-secondary-song-info { + font-size: 12px; +} + +.vc-spotify-song-info-prefix { + display: none; +} + +#vc-spotify-info-wrapper { + display: flex; + flex-direction: row; + align-items: center; + height: 3em; + gap: 0.5em; +} + +#vc-spotify-album-image { + height: 90%; + object-fit: contain; + border-radius: 3px; + transition: filter 0.2s; +} + +#vc-spotify-album-image:hover { + filter: brightness(1.2); + cursor: pointer; +} + +#vc-spotify-album-expanded-wrapper #vc-spotify-album-image { + width: 100%; + object-fit: contain; +} + +#vc-spotify-titles { + display: flex; + flex-direction: column; + padding: 0.2rem; + align-items: flex-start; + place-content: flex-start center; + overflow: hidden; +} + +#vc-spotify-song-title { + color: var(--header-primary); + font-size: 14px; + font-weight: 600; +} + +.vc-spotify-ellipoverflow { + white-space: nowrap; + overflow: hidden; + width: 100%; + text-overflow: ellipsis; +} + +.vc-spotify-artist, +.vc-spotify-album { + font-size: 12px; + text-decoration: none; + color: var(--header-primary); +} + +.vc-spotify-comma { + color: var(--header-secondary); +} + +.vc-spotify-artist[role="link"]:hover, +#vc-spotify-album-title[role="link"]:hover, +#vc-spotify-song-title[role="link"]:hover { + text-decoration: underline; + cursor: pointer; +} + +#vc-spotify-progress-bar { + position: relative; + color: var(--text-default); + width: 100%; + margin: 0.5em 0; + margin-bottom: 5px; +} + +#vc-spotify-progress-bar > [class^="slider"] { + flex-grow: 1; + width: 100%; + padding: 0 !important; +} + +#vc-spotify-progress-bar > [class^="slider"] [class^="bar"] { + height: 3px !important; + top: calc(12px - 4px / 2 + var(--bar-offset)); +} + +#vc-spotify-progress-bar > [class^="slider"] [class^="barFill"] { + background-color: var(--interactive-active); +} + +#vc-spotify-progress-bar > [class^="slider"]:hover [class^="barFill"] { + background-color: var(--vc-spotify-green); +} + +#vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] { + /* these importants are necessary, it applies a width and height through inline styles */ + height: 16px !important; + width: 16px !important; + margin-top: calc(17px/-2 + var(--bar-offset)/2); + margin-left: -0.5px; + background-color: var(--interactive-active); + border-color: var(--interactive-normal); + color: var(--interactive-normal); + opacity: 0; + transition: opacity 0.1s; +} + +#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] { + opacity: 1; +} + +#vc-spotify-progress-text { + margin: 0; +} + +.vc-spotify-progress-time { + font-size: 12px; + top: 10px; + position: absolute; + margin-top: 8px; + font-family: var(--font-code); +} + +.vc-spotify-time-left { + left: 0; +} + +.vc-spotify-time-right { + right: 0; +} + +.vc-spotify-fallback { + padding: 0.5em; + color: var(--text-default); +} \ No newline at end of file diff --git a/src/equicordplugins/tidalLyrics/styles.css b/src/equicordplugins/musicControls/styles.css similarity index 72% rename from src/equicordplugins/tidalLyrics/styles.css rename to src/equicordplugins/musicControls/styles.css index d22c2718a..88beabc8b 100644 --- a/src/equicordplugins/tidalLyrics/styles.css +++ b/src/equicordplugins/musicControls/styles.css @@ -1,4 +1,81 @@ -/* Rework: polish container and sync mode visuals; modernize color notation; fix selector order; merge reduced-motion rules */ +.vc-spotify-lyrics { + padding: 0.375rem 0.5rem; + text-align: center; + overflow: hidden; + max-height: 60px; + cursor: pointer; +} + +.vc-spotify-lyrics-music-note { + font-size: 16px; + text-align: center; + animation: side-to-side 2s ease-in-out infinite; + color: var(--text-muted); + height: 18px; + margin: 3px; +} + +.vc-spotify-lyrics-next, +.vc-spotify-lyrics-prev { + color: var(--text-muted); + margin-bottom: 1%; +} + +@keyframes side-to-side { + + 0%, + 100% { + transform: translateX(0); + } + + 50% { + transform: translateX(-10px); + } +} + +.vc-spotify-lyrics-header-content { + display: flex; + align-items: center; + width: 100%; +} + +.vc-spotify-lyrics-album-image { + width: 100px; + height: 100px; + margin-right: 5%; + border-radius: 5px; + cursor: pointer; +} + +.vc-spotify-lyrics-modal-line-current, +.vc-spotify-lyrics-modal-line { + margin: 4px 0; + padding: 4px 8px; + position: relative; +} + +.vc-spotify-lyrics-modal-line-current { + font-weight: bold; +} + +.vc-spotify-lyrics-modal-note { + height: 1lh; +} + +.vc-spotify-lyrics-modal-timestamp { + color: var(--text-muted); + margin-right: 0.5em; + cursor: pointer; +} + +.vc-spotify-lyrics-modal-timestamp:hover { + text-decoration: underline; +} + +.vc-spotify-lyrics-modal-no-lyrics { + text-align: center; + padding: 1rem; +} .eq-tidal-lyrics { padding: 0.375rem 0.5rem; @@ -95,7 +172,6 @@ color: var(--brand-experiment); font-weight: 600; text-shadow: 0 0 .25em currentcolor; - transform: scale(1.04); } .eq-tidal-lyrics-word { @@ -118,7 +194,6 @@ background: none; box-shadow: none; text-shadow: 0 0 .25em currentcolor; - transform: scale(1.04); } .eq-tidal-lyrics-current, @@ -154,6 +229,7 @@ @media (prefers-reduced-motion: reduce) { + .eq-tidal-lyrics-current, .eq-tidal-lyrics-next, .eq-tidal-lyrics-prev, @@ -167,4 +243,4 @@ .eq-tidal-lyrics-word-active { transform: none; } -} +} \ No newline at end of file diff --git a/src/equicordplugins/tidalControls/TidalPlayer.tsx b/src/equicordplugins/musicControls/tidal/TidalPlayer.tsx similarity index 97% rename from src/equicordplugins/tidalControls/TidalPlayer.tsx rename to src/equicordplugins/musicControls/tidal/TidalPlayer.tsx index ca4326c2f..e4651707e 100644 --- a/src/equicordplugins/tidalControls/TidalPlayer.tsx +++ b/src/equicordplugins/musicControls/tidal/TidalPlayer.tsx @@ -6,13 +6,14 @@ import "./tidalStyles.css"; +import { Settings } from "@api/Settings"; import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; -import { SeekBar } from "@plugins/spotifyControls/SeekBar"; import { debounce } from "@shared/debounce"; import { openImageModal } from "@utils/discord"; import { classes, copyWithToast } from "@utils/misc"; import { ContextMenuApi, Flex, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; +import { SeekBar } from "../spotify/SeekBar"; import { type PlayerState, type Repeat, TidalStore } from "./TidalStore"; const cl = (className: string) => `eq-tdl-${className}`; @@ -120,7 +121,10 @@ function Controls() { > - - ), - }, - TestingCache: { - description: "Save songs to a testing cache instead", - type: OptionType.BOOLEAN, - default: false, - hidden: true, - } -}); - -export default settings; diff --git a/src/equicordplugins/spotifyLyrics/styles.css b/src/equicordplugins/spotifyLyrics/styles.css deleted file mode 100644 index 64518c826..000000000 --- a/src/equicordplugins/spotifyLyrics/styles.css +++ /dev/null @@ -1,77 +0,0 @@ -.vc-spotify-lyrics { - padding: 0.375rem 0.5rem; - text-align: center; - overflow: hidden; - max-height: 60px; - cursor: pointer; -} - -.vc-spotify-lyrics-music-note { - font-size: 16px; - text-align: center; - animation: side-to-side 2s ease-in-out infinite; - color: var(--text-muted); - height: 18px; - margin: 3px; -} - -.vc-spotify-lyrics-next, -.vc-spotify-lyrics-prev { - color: var(--text-muted); - margin-bottom: 1%; -} - -@keyframes side-to-side { - 0%, - 100% { - transform: translateX(0); - } - - 50% { - transform: translateX(-10px); - } -} - -.vc-spotify-lyrics-header-content { - display: flex; - align-items: center; - width: 100%; -} - -.vc-spotify-lyrics-album-image { - width: 100px; - height: 100px; - margin-right: 5%; - border-radius: 5px; - cursor: pointer; -} - -.vc-spotify-lyrics-modal-line-current, -.vc-spotify-lyrics-modal-line { - margin: 4px 0; - padding: 4px 8px; - position: relative; -} - -.vc-spotify-lyrics-modal-line-current { - font-weight: bold; -} - -.vc-spotify-lyrics-modal-note { - height: 1lh; -} - -.vc-spotify-lyrics-modal-timestamp { - color: var(--text-muted); - margin-right: 0.5em; - cursor: pointer; -} - -.vc-spotify-lyrics-modal-timestamp:hover { - text-decoration: underline; -} - -.vc-spotify-lyrics-modal-no-lyrics { - text-align: center; - padding: 1rem; -} diff --git a/src/equicordplugins/tidalControls/index.tsx b/src/equicordplugins/tidalControls/index.tsx deleted file mode 100644 index 4c5a502ec..000000000 --- a/src/equicordplugins/tidalControls/index.tsx +++ /dev/null @@ -1,136 +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 . -*/ - -import { definePluginSettings, Settings } from "@api/Settings"; -import { disableStyle, enableStyle } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Lyrics as SpotifyLyrics } from "@equicordplugins/spotifyLyrics/components/lyrics"; -import { Player as SpotifyPlayer } from "@plugins/spotifyControls/PlayerComponent"; -import { Player as YTMPlayer } from "@equicordplugins/youtubeMusicControls/PlayerComponent"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Forms, MaskedLink } from "@webpack/common"; - -import hoverOnlyStyle from "./hoverOnly.css?managed"; -import { TidalPlayer } from "./TidalPlayer"; - -function toggleHoverControls(value: boolean) { - (value ? enableStyle : disableStyle)(hoverOnlyStyle); -} - - -function InstallInstructions() { - return ( - - How to install - - Install TidaLuna from here, then go to TidalLuna settings → Plugin stores → Install @vmohammad/api - - - ); -} - -const settings = definePluginSettings({ - installYtmWithWebsocket: { - type: OptionType.COMPONENT, - component: () => - }, - hoverControls: { - description: "Show controls on hover", - type: OptionType.BOOLEAN, - default: false, - onChange: v => toggleHoverControls(v) - }, - // SOON™️™️™️ - // lyricsProvider: { - // description: "Which provider should be used for lyrics", - // type: OptionType.SELECT, - // options: [ - // { label: "Tidal", value: "tidal", default: true }, - // { label: "vMohammad's Synced API", value: "synced" } - // ] - // } -}); - -export default definePlugin({ - name: "TidalControls", - description: "Adds a TIDAL player above the account panel", - authors: [EquicordDevs.vmohammad], - settings, - patches: [ - { - find: "this.isCopiedStreakGodlike", - replacement: [ - { - match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/, - replace: "$self.PanelWrapper,{VencordOriginal:$1,", - predicate: () => !Settings.plugins.SpotifyControls.enabled, - noWarn: true, - }, - { - match: /Vencord\.Plugins\.plugins\["SpotifyLyrics"\]\.FakePanelWrapper/, - replace: "$self.PanelWrapper", - predicate: () => Settings.plugins.SpotifyLyrics.enabled, - noWarn: true, - }, - { - match: /Vencord\.Plugins\.plugins\["SpotifyControls"\]\.PanelWrapper/, - replace: "$self.PanelWrapper", - predicate: () => Settings.plugins.SpotifyControls.enabled && !Settings.plugins.SpotifyLyrics.enabled, - noWarn: true, - }, - { - match: /Vencord\.Plugins\.plugins\["YouTubeMusicControls"\]\.FakePanelWrapper/, - replace: "$self.PanelWrapper", - predicate: () => Settings.plugins.YouTubeMusicControls.enabled, - noWarn: true, - }, - ], - }, - ], - - start: () => toggleHoverControls(Settings.plugins.YouTubeMusicControls.hoverControls), - - PanelWrapper({ VencordOriginal, ...props }) { - const showYTMPlayer = Settings.plugins.YouTubeMusicControls.enabled; - const showSpotifyControls = Settings.plugins.SpotifyControls.enabled; - const showSpotifyLyrics = Settings.plugins.SpotifyLyrics.enabled; - const LyricsPosition = showSpotifyLyrics ? Settings.plugins.SpotifyLyrics.LyricsPosition : null; - return ( - <> - ( -
-

Failed to render YouTube Music Modal :(

-

Check the console for errors

-
- )} - > - - - {showYTMPlayer && } - {showSpotifyLyrics && LyricsPosition === "above" && } - {showSpotifyControls && } - {showSpotifyLyrics && LyricsPosition === "below" && } -
- - - - ); - } -}); diff --git a/src/equicordplugins/tidalLyrics/index.tsx b/src/equicordplugins/tidalLyrics/index.tsx deleted file mode 100644 index 8a89896e2..000000000 --- a/src/equicordplugins/tidalLyrics/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./styles.css"; - -import { Settings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Lyrics as SpotifyLyrics } from "@equicordplugins/spotifyLyrics/components/lyrics"; -import { TidalPlayer } from "@equicordplugins/tidalControls/TidalPlayer"; -import { Player as SpotifyPlayer } from "@plugins/spotifyControls/PlayerComponent"; -import { Player as YTMPlayer } from "@equicordplugins/youtubeMusicControls/PlayerComponent"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -import { Lyrics } from "./components/lyrics"; -import settings from "./settings"; - - -export default definePlugin({ - name: "TidalLyrics", - authors: [EquicordDevs.vmohammad], - description: "Adds lyrics to TidalControls", - dependencies: ["TidalControls"], - patches: [ - { - find: "this.isCopiedStreakGodlike", - replacement: { - match: /Vencord\.Plugins\.plugins\["TidalControls"\]\.PanelWrapper/, - replace: "$self.FakePanelWrapper", - }, - predicate: () => Settings.plugins.TidalControls.enabled, - noWarn: true, - }, - ], - FakePanelWrapper({ VencordOriginal, ...props }) { - const { LyricsPosition } = settings.use(["LyricsPosition"]); - const showYTMPlayer = Settings.plugins.YouTubeMusicControls.enabled; - const showSpotifyControls = Settings.plugins.SpotifyControls.enabled; - const showSpotifyLyrics = Settings.plugins.SpotifyLyrics.enabled; - const SpotifyLyricsPosition = showSpotifyLyrics ? Settings.plugins.SpotifyLyrics.LyricsPosition : null; - return ( - <> - ( -
-

Failed to render Tidal Lyrics Modal :(

-

Check the console for errors

-
- )} - > - {LyricsPosition === "above" && } - - {LyricsPosition === "below" && } - - {showYTMPlayer && } - {showSpotifyLyrics && SpotifyLyricsPosition === "above" && } - {showSpotifyControls && } - {showSpotifyLyrics && SpotifyLyricsPosition === "below" && } -
- - - - ); - }, - settings, -}); diff --git a/src/equicordplugins/tidalLyrics/settings.tsx b/src/equicordplugins/tidalLyrics/settings.tsx deleted file mode 100644 index ca52a1334..000000000 --- a/src/equicordplugins/tidalLyrics/settings.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { SliderSetting } from "@components/settings/tabs/plugins/components/SliderSetting"; -import { makeRange, OptionType } from "@utils/types"; -import { showToast, Toasts } from "@webpack/common"; - -import { Lyrics } from "./components/lyrics"; - -const sliderOptions = { - markers: makeRange(-2500, 2500, 250), - stickToMarkers: true, -}; - -const settings = definePluginSettings({ - ShowMusicNoteOnNoLyrics: { - description: "Show a music note icon when no lyrics are found", - type: OptionType.BOOLEAN, - default: true, - }, - LyricsPosition: { - description: "Position of the lyrics", - type: OptionType.SELECT, - options: [ - { value: "above", label: "Above TidalControls" }, - { value: "below", label: "Below TidalControls", default: true }, - ], - }, - ShowFailedToasts: { - description: "Hide toasts when lyrics fail to fetch", - type: OptionType.BOOLEAN, - default: true, - }, - CustomUrl: { - description: "Custom URL for fetching lyrics", - type: OptionType.STRING, - default: "https://api.vmohammad.dev/", - placeholder: "https://api.vmohammad.dev/", - onChange: (value: string) => { - if (!value.endsWith("/")) { - value += "/"; - } - if (URL.canParse(value)) { - settings.store.CustomUrl = value; - } else { - showToast("Invalid URL format for CustomUrl: " + value, Toasts.Type.FAILURE); - settings.store.CustomUrl = "https://api.vmohammad.dev/"; - } - } - }, - LyricDelay: { - description: "", - type: OptionType.SLIDER, - default: 0, - hidden: true, - ...sliderOptions - }, - SyncMode: { - description: "Lyrics sync mode", - type: OptionType.SELECT, - options: [ - { value: "line", label: "Line" }, - { value: "word", label: "Word", default: true }, - { value: "character", label: "Character" }, - ], - default: "word", - }, - Display: { - description: "", - type: OptionType.COMPONENT, - component: () => ( - <> - { - settings.store.LyricDelay = v; - }} - pluginSettings={Vencord.Settings.plugins.TidalLyrics} - id={"LyricDelay"} - /> - - - ) - }, -}); - -export default settings; diff --git a/src/equicordplugins/youtubeMusicControls/README.md b/src/equicordplugins/youtubeMusicControls/README.md deleted file mode 100644 index 8ad40cd85..000000000 --- a/src/equicordplugins/youtubeMusicControls/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# YouTubeMusicControls - -Adds a YouTube Music player above the account panel. -Shows the current playing playing track and adds controls same, -basicly the same as SpotifyControls - -# Prerequirements - -This only works with [th-ch's YouTube Music](https://github.com/th-ch/youtube-music) -and my plugin [ApiWebsocket Plugin](https://github.com/Johannes7k75/youtube-music/tree/feat/api-websocket) installed (Currently not included in [th-ch's YouTube Music](https://github.com/th-ch/youtube-music)) -Plugins that have to be enabled -- API Server -- API Websocket - -# Installation -- [git](https://git-scm.com/downloads) -- [node.js](https://nodejs.org/en/download) >= 22.12.0 -- [pnpm](https://pnpm.io/installation) - -```bash -git clone https://github.com/th-ch/youtube-music -cd .\youtube-music\ -git remote add johannes https://github.com/Johannes7k75/youtube-music.git -git fetch johannes -git merge johannes/feat/api-websocket -pnpm install --frozen-lockfile -pnpm dist:win -``` - - -See the [forum thread](https://discord.com/channels/1015060230222131221/1257038407503446176/1257038407503446176) / [Vencord docs](https://docs.vencord.dev/installing/custom-plugins/) \ No newline at end of file diff --git a/src/equicordplugins/youtubeMusicControls/YtmStore.ts b/src/equicordplugins/youtubeMusicControls/YtmStore.ts deleted file mode 100644 index ee6ff1388..000000000 --- a/src/equicordplugins/youtubeMusicControls/YtmStore.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Logger } from "@utils/Logger"; -import { proxyLazyWebpack } from "@webpack"; -import { Flux, FluxDispatcher } from "@webpack/common"; -import { Settings } from "Vencord"; - -enum MediaType { - /** - * Audio uploaded by the original artist - */ - Audio = "AUDIO", - /** - * Official music video uploaded by the original artist - */ - OriginalMusicVideo = "ORIGINAL_MUSIC_VIDEO", - /** - * Normal YouTube video uploaded by a user - */ - UserGeneratedContent = "USER_GENERATED_CONTENT", - /** - * Podcast episode - */ - PodcastEpisode = "PODCAST_EPISODE", - OtherVideo = "OTHER_VIDEO", -} - -interface Song { - title: string; - artist: string; - views: number; - uploadDate?: string; - imageSrc?: string | null; - isPaused?: boolean; - songDuration: number; - elapsedSeconds?: number; - url?: string; - album?: string | null; - videoId: string; - playlistId?: string; - mediaType: MediaType; -} - -export interface PlayerState { - song: Song | null; - isPlaying: boolean, - position: number, - repeat: Repeat, - volume: number, -} - -export type Repeat = "NONE" | "ONE" | "ALL"; - -const logger = new Logger("YoutubeMusicControls"); - -type Message = { type: "PLAYER_STATE"; } & PlayerState; - -class YoutubeMusicSocket { - public onChange: (e: Message) => void; - public ready = false; - - public socket: WebSocket | undefined; - - constructor(onChange: typeof this.onChange) { - this.reconnect(); - this.onChange = onChange; - } - - public reconnect() { - if (this.ready) return; - try { - this.initWs(); - } catch (e) { - logger.error("Failed to connect to YouTube Music WebSocket", e); - return; - } - this.ready = true; - } - - get routes() { - return { - "play": () => this.socket?.send(JSON.stringify({ type: "ACTION", action: "play" })), - "pause": () => this.socket?.send(JSON.stringify({ type: "ACTION", action: "pause" })), - "previous": () => this.socket?.send(JSON.stringify({ type: "ACTION", action: "previous" })), - "next": () => this.socket?.send(JSON.stringify({ type: "ACTION", action: "next" })), - "seek": (seconds: number) => this.socket?.send(JSON.stringify({ type: "ACTION", action: "seek", data: seconds })), - "shuffle": () => this.socket?.send(JSON.stringify({ type: "ACTION", action: "shuffle" })), - "setVolume": (percent: number) => this.socket?.send(JSON.stringify({ type: "ACTION", action: "setVolume", data: percent })), - "getVolume": () => this.socket?.send(JSON.stringify({ type: "ACTION", action: "getVolume" })), - "repeat": (repeat: Repeat) => this.socket?.send(JSON.stringify({ type: "ACTION", action: "repeat", data: repeat })), - }; - } - - private initWs() { - const url = Settings.plugins.YouTubeMusicControls.websocketUrl; - if (!url) { - return; - } - this.socket = new WebSocket(url); - - this.socket.addEventListener("open", () => { - this.ready = true; - this.routes.pause(); - this.routes.play(); - }); - - this.socket.addEventListener("error", e => { - // logger.error("YouTube Music Socket Error:", e); - if (!this.ready) setTimeout(() => this.reconnect(), 5_000); - this.onChange({ type: "PLAYER_STATE", song: null, isPlaying: false, position: 0, repeat: "NONE", volume: 0 }); - - }); - - this.socket.addEventListener("close", e => { - this.ready = false; - if (!this.ready) setTimeout(() => this.reconnect(), 10_000); - - this.onChange({ type: "PLAYER_STATE", song: null, isPlaying: false, position: 0, repeat: "NONE", volume: 0 }); - }); - - - this.socket.addEventListener("message", e => { - let message: Message; - try { - message = JSON.parse(e.data) as Message; - - switch (message.type) { - case "PLAYER_STATE": - this.onChange(message); - break; - } - } catch (err) { - logger.error("Invalid JSON:", err, `\n${e.data}`); - return; - } - }); - } -} - -export const YoutubeMusicStore = proxyLazyWebpack(() => { - const { Store } = Flux; - - class YoutubeMusicStore extends Store { - public mPosition = 0; - public start = 0; - - public song: Song | null = null; - public isPlaying = false; - public repeat: Repeat = "NONE"; - public shuffle = false; - public volume = 0; - - public socket = new YoutubeMusicSocket((message: Message) => { - if (message.song) { - store.song = message.song; - store.position = message.song.elapsedSeconds ?? 0; - } - if (message.isPlaying != null) store.isPlaying = message.isPlaying; - if (message.position && message.position !== store.position) store.position = message.position ?? 0; - if (message.volume) store.volume = message.volume ?? 0; - if (message.repeat) store.repeat = message.repeat; - - store.emitChange(); - }); - - public openExternal(path: string) { - const videoId = path.match(/watch\?v=([\w-]+)/); - - const url = (Settings.plugins.YouTubeMusicControls.useYoutubeMusicUri || Vencord.Plugins.isPluginEnabled("OpenInApp")) && videoId - ? encodeURI("youtubemusic://openVideo " + videoId[1]) - : "https://music.youtube.com" + path; - - VencordNative.native.openExternal(url); - } - - set position(p: number) { - this.mPosition = p * 1000; - this.start = Date.now(); - } - - get position(): number { - let pos = this.mPosition; - if (this.isPlaying) { - pos += Date.now() - this.start; - } - return pos; - } - - previous() { - if (!this.ensureSocketReady()) return; - this.socket.routes.previous(); - } - next() { - if (!this.ensureSocketReady()) return; - this.socket.routes.next(); - } - setVolume(percent: number) { - if (!this.ensureSocketReady()) return; - this.socket.routes.setVolume(Math.round(percent)); - this.volume = percent; - this.emitChange(); - } - setPlaying(playing: boolean) { - if (!this.ensureSocketReady()) return; - this.socket.routes[playing ? "play" : "pause"](); - this.isPlaying = playing; - } - setRepeat(state: Repeat) { - if (!this.ensureSocketReady()) return; - this.socket.routes.repeat(state); - this.repeat = state; - this.emitChange(); - } - setShuffle(state: boolean) { - if (!this.ensureSocketReady()) return; - this.socket.routes.shuffle(); - this.shuffle = state; - this.emitChange(); - } - seek(ms: number) { - if (!this.ensureSocketReady()) return; - this.socket.routes.seek(Math.round(ms / 1000)); - } - - public ensureSocketReady(): boolean { - if (!this.socket || !this.socket.ready) { - return false; - } - return true; - } - } - - const store = new YoutubeMusicStore(FluxDispatcher); - - return store; -}); diff --git a/src/equicordplugins/youtubeMusicControls/hoverOnly.css b/src/equicordplugins/youtubeMusicControls/hoverOnly.css deleted file mode 100644 index 29b14da0b..000000000 --- a/src/equicordplugins/youtubeMusicControls/hoverOnly.css +++ /dev/null @@ -1,16 +0,0 @@ -.vc-ytm-button-row { - height: 0; - opacity: 0; - pointer-events: none; - transition: 0.2s; - transition-property: height; -} - -#vc-ytm-player:hover .vc-ytm-button-row { - opacity: 1; - height: 32px; - pointer-events: auto; - - /* only transition opacity on show to prevent clipping */ - transition-property: height, opacity; -} diff --git a/src/equicordplugins/youtubeMusicControls/index.tsx b/src/equicordplugins/youtubeMusicControls/index.tsx deleted file mode 100644 index 4e746e5b3..000000000 --- a/src/equicordplugins/youtubeMusicControls/index.tsx +++ /dev/null @@ -1,170 +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 . -*/ - -import { definePluginSettings, Settings } from "@api/Settings"; -import { disableStyle, enableStyle } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Lyrics } from "@equicordplugins/spotifyLyrics/components/lyrics"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Forms, MaskedLink } from "@webpack/common"; -import { Player as SpotifyPlayer } from "plugins/spotifyControls/PlayerComponent"; - -import hoverOnlyStyle from "./hoverOnly.css?managed"; -import { Player } from "./PlayerComponent"; - -function toggleHoverControls(value: boolean) { - (value ? enableStyle : disableStyle)(hoverOnlyStyle); -} - -function isUrlValid(value: string) { - try { - const url = new URL(value); - return url.protocol !== "" && url.host !== ""; - } catch (e) { - return false; - } -} - -/* -function InstallInstructions() { - return ( - - How to install - Instructions on how to install YouTube music with websocket plugin - - - - Software needed: git,node.js >= 22.12.0,pnpm - - - git clone https://github.com/th-ch/youtube-music
- cd .\youtube-music\
- git remote add johannes https://github.com/Johannes7k75/youtube-music.git
- git fetch johannes
- git merge johannes/feat/api-websocket
- pnpm install --frozen-lockfile
- pnpm dist:win
-
-
-
- ); -} -*/ - -function InstallInstructions() { - return ( - - How to install - Instructions on how to install YouTube music with websocket plugin - - - - Software needed: git,node.js >= 22.12.0,pnpm - - - git clone https://github.com/thororen1234/youtube-music.git
- cd .\youtube-music\
- pnpm install --frozen-lockfile
- pnpm dist:win
-
-
-
- ); -} - -const settings = definePluginSettings({ - installYtmWithWebsocket: { - type: OptionType.COMPONENT, - component: () => - }, - hoverControls: { - description: "Show controls on hover", - type: OptionType.BOOLEAN, - default: false, - onChange: v => toggleHoverControls(v) - }, - websocketUrl: { - description: "The websocket url", - type: OptionType.STRING, - placeholder: "ws://localhost:26539", - default: "ws://localhost:26539", - isValid: isUrlValid, - } -}); - -export default definePlugin({ - name: "YouTubeMusicControls", - description: "Adds a YouTube Music player above the account panel, requires `https://github.com/th-ch/youtube-music` with an custom websocket plugin. See Readme of ths Plugin for instructions", - authors: [EquicordDevs.Johannes7k75], - settings, - patches: [ - { - find: "this.isCopiedStreakGodlike", - replacement: [ - { - match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/, - replace: "$self.PanelWrapper,{VencordOriginal:$1,", - predicate: () => !Settings.plugins.SpotifyControls.enabled, - noWarn: true, - }, - { - match: /Vencord\.Plugins\.plugins\["SpotifyLyrics"\]\.FakePanelWrapper/, - replace: "$self.PanelWrapper", - predicate: () => Settings.plugins.SpotifyLyrics.enabled, - noWarn: true, - }, - { - match: /Vencord\.Plugins\.plugins\["SpotifyControls"\]\.PanelWrapper/, - replace: "$self.PanelWrapper", - predicate: () => Settings.plugins.SpotifyControls.enabled && !Settings.plugins.SpotifyLyrics.enabled, - noWarn: true, - } - ], - }, - ], - - start: () => toggleHoverControls(Settings.plugins.YouTubeMusicControls.hoverControls), - - PanelWrapper({ VencordOriginal, ...props }) { - const showSpotifyControls = Settings.plugins.SpotifyControls.enabled; - const showSpotifyLyrics = Settings.plugins.SpotifyLyrics.enabled; - const LyricsPosition = showSpotifyLyrics ? Settings.plugins.SpotifyLyrics.LyricsPosition : null; - - return ( - <> - ( -
-

Failed to render YouTube Music Modal :(

-

Check the console for errors

-
- )} - > - - - {showSpotifyLyrics && LyricsPosition === "above" && } - {showSpotifyControls && } - {showSpotifyLyrics && LyricsPosition === "below" && } -
- - - - ); - } -}); diff --git a/src/equicordplugins/youtubeMusicControls/ytmStyles.css b/src/equicordplugins/youtubeMusicControls/ytmStyles.css deleted file mode 100644 index 652f90bf3..000000000 --- a/src/equicordplugins/youtubeMusicControls/ytmStyles.css +++ /dev/null @@ -1,208 +0,0 @@ -#vc-ytm-player { - padding: 12px; - background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt))); - margin: 0; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - - --vc-ytm-slider-gradient: linear-gradient(to right, #f03 80%, #ff2791 100%); - --vc-ytm-slider-grabber: #f03; - - /* so custom themes can easily change it */ - --vc-ytm-red-90: color-mix(in hsl, var(--vc-ytm-slider-grabber), transparent 90%); - --vc-ytm-red-80: color-mix(in hsl, var(--vc-ytm-slider-grabber), transparent 80%); -} - -.vc-ytm-song-info-prefix { - display: none; -} - -.vc-ytm-artist, -.vc-ytm-album { - color: var(--header-primary); -} - -.vc-ytm-secondary-song-info { - font-size: 12px; -} - -#vc-ytm-progress-bar { - position: relative; - color: var(--text-default); - width: 100%; - margin: 0.5em 0; - margin-bottom: 5px; -} - -#vc-ytm-progress-bar > [class^="slider"] { - flex-grow: 1; - width: 100%; - padding: 0 !important; -} - -#vc-ytm-progress-bar > [class^="slider"] [class^="bar"] { - height: 3px !important; - top: calc(12px - 4px / 2 + var(--bar-offset)); -} - -#vc-ytm-progress-bar > [class^="slider"] [class^="barFill"] { - background-image: var(--vc-ytm-slider-gradient); -} - -#vc-ytm-progress-bar > [class^="slider"]:hover [class^="barFill"] { - background-image: var(--vc-ytm-slider-gradient); -} - -#vc-ytm-progress-bar > [class^="slider"] [class^="grabber"] { - background-color: var(--vc-ytm-slider-grabber); - width: 12px !important; - height: 12px !important; - margin-top: calc(13px/-2 + var(--bar-offset)/2); - margin-left: -0.5px; - border: unset; - color: var(--interactive-normal); - opacity: 0; - transition: opacity 0.1s; -} - -#vc-ytm-progress-bar:hover > [class^="slider"] [class^="grabber"] { - opacity: 1; -} - -.vc-ytm-progress-time { - margin-top: 8px; - font-family: var(--font-code); - font-size: 12px; - top: 10px; - position: absolute; -} - -.vc-ytm-button-row { - justify-content: center; - margin-top: 14px; -} - -.vc-ytm-button { - margin: 0 2px; - border-radius: var(--radius-sm); - background: none; - color: var(--interactive-normal); - padding: 0; - width: 32px; - height: 32px; - display: flex; - justify-content: center; - align-items: center; - position: relative; -} - -.vc-ytm-button:hover { - color: var(--interactive-hover); - background-color: var(--background-modifier-selected); -} - -.vc-ytm-button svg { - height: 24px; - width: 24px; -} - -[class*="shuffle"] > svg, -[class*="repeat"] > svg { - width: 22px; - height: 22px; -} - -.vc-ytm-button svg path { - width: 100%; - height: 100%; -} - - -.vc-ytm-repeat-context, -.vc-ytm-repeat-track, -.vc-ytm-shuffle-on { - background-color: var(--vc-ytm-red-90); -} - -.vc-ytm-repeat-context:hover, -.vc-ytm-repeat-track:hover, -.vc-ytm-shuffle-on:hover { - background-color: var(--vc-ytm-red-80); -} - -.vc-ytm-repeat-1 { - font-size: 70%; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -#vc-ytm-info-wrapper { - display: flex; - flex-direction: row; - align-items: center; - height: 3em; - gap: 0.5em; -} - -#vc-ytm-info-wrapper img { - height: 90%; - object-fit: contain; -} - -#vc-ytm-album-expanded-wrapper img { - width: 100%; - object-fit: contain; -} - -#vc-ytm-titles { - display: flex; - flex-direction: column; - padding: 0.2rem; - align-items: flex-start; - place-content: flex-start center; - overflow: hidden; -} - -#vc-ytm-song-title { - color: var(--header-primary); - font-size: 14px; - font-weight: 600; -} - -.vc-ytm-ellipoverflow { - white-space: nowrap; - overflow: hidden; - width: 100%; - text-overflow: ellipsis; -} - -.vc-ytm-artist[role="link"]:hover, -#vc-ytm-album-title[role="link"]:hover, -#vc-ytm-song-title[role="link"]:hover { - text-decoration: underline; - cursor: pointer; -} - -#vc-ytm-album-image { - border-radius: 3px; - transition: filter 0.2s; -} - -#vc-ytm-album-image:hover { - filter: brightness(1.2); - cursor: pointer; -} - -#vc-ytm-progress-text { - margin: 0; -} - -.vc-ytm-time-left { - left: 0; -} - -.vc-ytm-time-right { - right: 0; -} \ No newline at end of file From 8cc9957bf71086e45d9977c3b2a3f4812fbcf2a1 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:22:21 -0400 Subject: [PATCH 002/881] MusicControls: Styles --- src/equicordplugins/musicControls/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/equicordplugins/musicControls/styles.css b/src/equicordplugins/musicControls/styles.css index 88beabc8b..b0a42927f 100644 --- a/src/equicordplugins/musicControls/styles.css +++ b/src/equicordplugins/musicControls/styles.css @@ -1,3 +1,4 @@ +/* stylelint-disable rule-empty-line-before */ .vc-spotify-lyrics { padding: 0.375rem 0.5rem; text-align: center; From 5409b11a413c830a451ae5a3937767f903e93a9f Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 9 Aug 2025 14:24:19 -0400 Subject: [PATCH 003/881] =?UTF-8?q?SpotifyControls:=20=F0=9F=91=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spotifyControls/PlayerComponent.tsx | 392 ------------------ src/plugins/spotifyControls/SeekBar.ts | 25 -- src/plugins/spotifyControls/SpotifyStore.ts | 198 --------- src/plugins/spotifyControls/hoverOnly.css | 16 - src/plugins/spotifyControls/index.tsx | 115 ----- src/plugins/spotifyControls/spotifyStyles.css | 229 ---------- 6 files changed, 975 deletions(-) delete mode 100644 src/plugins/spotifyControls/PlayerComponent.tsx delete mode 100644 src/plugins/spotifyControls/SeekBar.ts delete mode 100644 src/plugins/spotifyControls/SpotifyStore.ts delete mode 100644 src/plugins/spotifyControls/hoverOnly.css delete mode 100644 src/plugins/spotifyControls/index.tsx delete mode 100644 src/plugins/spotifyControls/spotifyStyles.css diff --git a/src/plugins/spotifyControls/PlayerComponent.tsx b/src/plugins/spotifyControls/PlayerComponent.tsx deleted file mode 100644 index 32c78c364..000000000 --- a/src/plugins/spotifyControls/PlayerComponent.tsx +++ /dev/null @@ -1,392 +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 . -*/ - -import "./spotifyStyles.css"; - -import { Settings } from "@api/Settings"; -import { classNameFactory } from "@api/Styles"; -import { Flex } from "@components/Flex"; -import { ImageIcon, LinkIcon, OpenExternalIcon } from "@components/Icons"; -import { debounce } from "@shared/debounce"; -import { openImageModal } from "@utils/discord"; -import { classes, copyWithToast } from "@utils/misc"; -import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common"; - -import { SeekBar } from "./SeekBar"; -import { SpotifyStore, Track } from "./SpotifyStore"; - -const cl = classNameFactory("vc-spotify-"); - -function msToHuman(ms: number) { - const minutes = ms / 1000 / 60; - const m = Math.floor(minutes); - const s = Math.floor((minutes - m) * 60); - return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`; -} - -function Svg(path: string, label: string) { - return () => ( - - - - ); -} - -// KraXen's icons :yesyes: -// from https://fonts.google.com/icons?icon.style=Rounded&icon.set=Material+Icons -// older material icon style, but still really good -const PlayButton = Svg("M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z", "play"); -const PauseButton = Svg("M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2zm6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2z", "pause"); -const SkipPrev = Svg("M7 6c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1s-1-.45-1-1V7c0-.55.45-1 1-1zm3.66 6.82l5.77 4.07c.66.47 1.58-.01 1.58-.82V7.93c0-.81-.91-1.28-1.58-.82l-5.77 4.07c-.57.4-.57 1.24 0 1.64z", "previous"); -const SkipNext = Svg("M7.58 16.89l5.77-4.07c.56-.4.56-1.24 0-1.63L7.58 7.11C6.91 6.65 6 7.12 6 7.93v8.14c0 .81.91 1.28 1.58.82zM16 7v10c0 .55.45 1 1 1s1-.45 1-1V7c0-.55-.45-1-1-1s-1 .45-1 1z", "next"); -const Repeat = Svg("M7 7h10v1.79c0 .45.54.67.85.35l2.79-2.79c.2-.2.2-.51 0-.71l-2.79-2.79c-.31-.31-.85-.09-.85.36V5H6c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1s1-.45 1-1V7zm10 10H7v-1.79c0-.45-.54-.67-.85-.35l-2.79 2.79c-.2.2-.2.51 0 .71l2.79 2.79c.31.31.85.09.85-.36V19h11c.55 0 1-.45 1-1v-4c0-.55-.45-1-1-1s-1 .45-1 1v3z", "repeat"); -const Shuffle = Svg("M10.59 9.17L6.12 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41l4.46 4.46 1.42-1.4zm4.76-4.32l1.19 1.19L4.7 17.88c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L17.96 7.46l1.19 1.19c.31.31.85.09.85-.36V4.5c0-.28-.22-.5-.5-.5h-3.79c-.45 0-.67.54-.36.85zm-.52 8.56l-1.41 1.41 3.13 3.13-1.2 1.2c-.31.31-.09.85.36.85h3.79c.28 0 .5-.22.5-.5v-3.79c0-.45-.54-.67-.85-.35l-1.19 1.19-3.13-3.14z", "shuffle"); - -function Button(props: React.ButtonHTMLAttributes) { - return ( - - ); -} - -function CopyContextMenu({ name, path }: { name: string; path: string; }) { - const copyId = `spotify-copy-${name}`; - const openId = `spotify-open-${name}`; - - return ( - FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label={`Spotify ${name} Menu`} - > - copyWithToast("https://open.spotify.com" + path)} - icon={LinkIcon} - /> - SpotifyStore.openExternal(path)} - icon={OpenExternalIcon} - /> - - ); -} - -function makeContextMenu(name: string, path: string) { - return (e: React.MouseEvent) => - ContextMenuApi.openContextMenu(e, () => ); -} - -function Controls() { - const [isPlaying, shuffle, repeat] = useStateFromStores( - [SpotifyStore], - () => [SpotifyStore.isPlaying, SpotifyStore.shuffle, SpotifyStore.repeat] - ); - - const [nextRepeat, repeatClassName] = (() => { - switch (repeat) { - case "off": return ["context", "repeat-off"] as const; - case "context": return ["track", "repeat-context"] as const; - case "track": return ["off", "repeat-track"] as const; - default: throw new Error(`Invalid repeat state ${repeat}`); - } - })(); - - // the 1 is using position absolute so it does not make the button jump around - return ( - - - - - - - - ); -} - -const seek = debounce((v: number) => { - SpotifyStore.seek(v); -}); - -function SpotifySeekBar() { - const { duration } = SpotifyStore.track!; - - const [storePosition, isSettingPosition, isPlaying] = useStateFromStores( - [SpotifyStore], - () => [SpotifyStore.mPosition, SpotifyStore.isSettingPosition, SpotifyStore.isPlaying] - ); - - const [position, setPosition] = useState(storePosition); - - useEffect(() => { - if (isPlaying && !isSettingPosition) { - setPosition(SpotifyStore.position); - const interval = setInterval(() => { - setPosition(p => p + 1000); - }, 1000); - - return () => clearInterval(interval); - } - }, [storePosition, isSettingPosition, isPlaying]); - - const onChange = (v: number) => { - if (isSettingPosition) return; - setPosition(v); - seek(v); - }; - - return ( -
- - {msToHuman(position)} - - - - {msToHuman(duration)} - -
- ); -} - - -function AlbumContextMenu({ track }: { track: Track; }) { - const volume = useStateFromStores([SpotifyStore], () => SpotifyStore.volume); - - return ( - FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label="Spotify Album Menu" - > - SpotifyStore.openExternal(`/album/${track.album.id}`)} - icon={OpenExternalIcon} - /> - openImageModal(track.album.image)} - icon={ImageIcon} - /> - ( - SpotifyStore.setVolume(v))} - /> - )} - /> - - ); -} - -function makeLinkProps(name: string, condition: unknown, path: string) { - if (!condition) return {}; - - return { - role: "link", - onClick: () => SpotifyStore.openExternal(path), - onContextMenu: makeContextMenu(name, path) - } satisfies React.HTMLAttributes; -} - -function Info({ track }: { track: Track; }) { - const img = track?.album?.image; - - const [coverExpanded, setCoverExpanded] = useState(false); - - const i = ( - <> - {img && ( - Album Image setCoverExpanded(!coverExpanded)} - onContextMenu={e => { - ContextMenuApi.openContextMenu(e, () => ); - }} - /> - )} - - ); - - if (coverExpanded && img) - return ( -
- {i} -
- ); - - return ( -
- {i} -
- - {track.name} - - {track.artists.some(a => a.name) && ( - - by  - {track.artists.map((a, i) => ( - - - {a.name} - - {i !== track.artists.length - 1 && {", "}} - - ))} - - )} - {track.album.name && ( - - on  - - {track.album.name} - - - )} -
-
- ); -} - -export function Player() { - const track = useStateFromStores( - [SpotifyStore], - () => SpotifyStore.track, - null, - (prev, next) => prev?.id ? (prev.id === next?.id) : prev?.name === next?.name - ); - - const device = useStateFromStores( - [SpotifyStore], - () => SpotifyStore.device, - null, - (prev, next) => prev?.id === next?.id - ); - - const isPlaying = useStateFromStores([SpotifyStore], () => SpotifyStore.isPlaying); - const [shouldHide, setShouldHide] = useState(false); - - // Hide player after 5 minutes of inactivity - - React.useEffect(() => { - setShouldHide(false); - if (!isPlaying) { - const timeout = setTimeout(() => setShouldHide(true), 1000 * 60 * 5); - return () => clearTimeout(timeout); - } - }, [isPlaying]); - - if (!track || !device?.is_active || shouldHide) - return null; - - const exportTrackImageStyle = { - "--vc-spotify-track-image": `url(${track?.album?.image?.url || ""})`, - } as React.CSSProperties; - - return ( -
- - - -
- ); -} diff --git a/src/plugins/spotifyControls/SeekBar.ts b/src/plugins/spotifyControls/SeekBar.ts deleted file mode 100644 index 8d6c8a30c..000000000 --- a/src/plugins/spotifyControls/SeekBar.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { LazyComponent } from "@utils/lazyReact"; -import { Slider } from "@webpack/common"; - -export const SeekBar = LazyComponent(() => { - const SliderClass = Slider.$$vencordGetWrappedComponent(); - - // Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish. - // We extend their class and override their `getDerivedStateFromProps` to update the value - return class SeekBar extends SliderClass { - static getDerivedStateFromProps(props: any, state: any) { - const newState = super.getDerivedStateFromProps!(props, state); - if (newState) { - newState.value = props.initialValue; - } - - return newState; - } - }; -}); diff --git a/src/plugins/spotifyControls/SpotifyStore.ts b/src/plugins/spotifyControls/SpotifyStore.ts deleted file mode 100644 index 65ee2c154..000000000 --- a/src/plugins/spotifyControls/SpotifyStore.ts +++ /dev/null @@ -1,198 +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 . -*/ - -import { Settings } from "@api/Settings"; -import { findByProps, findByPropsLazy, proxyLazyWebpack } from "@webpack"; -import { Flux, FluxDispatcher } from "@webpack/common"; - -export interface Track { - id: string; - name: string; - duration: number; - isLocal: boolean; - album: { - id: string; - name: string; - image: { - height: number; - width: number; - url: string; - }; - }; - artists: { - id: string; - href: string; - name: string; - type: string; - uri: string; - }[]; -} - -interface PlayerState { - accountId: string; - track: Track | null; - volumePercent: number, - isPlaying: boolean, - repeat: boolean, - position: number, - context?: any; - device?: Device; - - // added by patch - actual_repeat: Repeat; - shuffle: boolean; -} - -interface Device { - id: string; - is_active: boolean; -} - -type Repeat = "off" | "track" | "context"; - -// Don't wanna run before Flux and Dispatcher are ready! -export const SpotifyStore = proxyLazyWebpack(() => { - // For some reason ts hates extends Flux.Store - const { Store } = Flux; - - const SpotifySocket = findByProps("getActiveSocketAndDevice"); - const SpotifyAPI = findByPropsLazy("vcSpotifyMarker"); - - const API_BASE = "https://api.spotify.com/v1/me/player"; - - class SpotifyStore extends Store { - public mPosition = 0; - public _start = 0; - - public track: Track | null = null; - public device: Device | null = null; - public isPlaying = false; - public repeat: Repeat = "off"; - public shuffle = false; - public volume = 0; - - public isSettingPosition = false; - - public openExternal(path: string) { - const url = Settings.plugins.SpotifyControls.useSpotifyUris || Vencord.Plugins.isPluginEnabled("OpenInApp") - ? "spotify:" + path.replaceAll("/", (_, idx) => idx === 0 ? "" : ":") - : "https://open.spotify.com" + path; - - VencordNative.native.openExternal(url); - } - - // Need to keep track of this manually - public get position(): number { - let pos = this.mPosition; - if (this.isPlaying) { - pos += Date.now() - this._start; - } - return pos; - } - - public set position(p: number) { - this.mPosition = p; - this._start = Date.now(); - } - - prev() { - this._req("post", "/previous"); - } - - next() { - this._req("post", "/next"); - } - - setVolume(percent: number) { - this._req("put", "/volume", { - query: { - volume_percent: Math.round(percent) - } - - }).then(() => { - this.volume = percent; - this.emitChange(); - }); - } - - setPlaying(playing: boolean) { - this._req("put", playing ? "/play" : "/pause"); - } - - setRepeat(state: Repeat) { - this._req("put", "/repeat", { - query: { state } - }); - } - - setShuffle(state: boolean) { - this._req("put", "/shuffle", { - query: { state } - }).then(() => { - this.shuffle = state; - this.emitChange(); - }); - } - - seek(ms: number) { - if (this.isSettingPosition) return Promise.resolve(); - - this.isSettingPosition = true; - - return this._req("put", "/seek", { - query: { - position_ms: Math.round(ms) - } - }).catch((e: any) => { - console.error("[VencordSpotifyControls] Failed to seek", e); - this.isSettingPosition = false; - }); - } - - _req(method: "post" | "get" | "put", route: string, data: any = {}) { - if (this.device?.is_active) - (data.query ??= {}).device_id = this.device.id; - - const { socket } = SpotifySocket.getActiveSocketAndDevice(); - return SpotifyAPI[method](socket.accountId, socket.accessToken, { - url: API_BASE + route, - ...data - }); - } - } - - const store = new SpotifyStore(FluxDispatcher, { - SPOTIFY_PLAYER_STATE(e: PlayerState) { - store.track = e.track; - store.device = e.device ?? null; - store.isPlaying = e.isPlaying ?? false; - store.volume = e.volumePercent ?? 0; - store.repeat = e.actual_repeat || "off"; - store.shuffle = e.shuffle ?? false; - store.position = e.position ?? 0; - store.isSettingPosition = false; - store.emitChange(); - }, - SPOTIFY_SET_DEVICES({ devices }: { devices: Device[]; }) { - store.device = devices.find(d => d.is_active) ?? devices[0] ?? null; - store.emitChange(); - } - }); - - return store; -}); diff --git a/src/plugins/spotifyControls/hoverOnly.css b/src/plugins/spotifyControls/hoverOnly.css deleted file mode 100644 index 8051cc42c..000000000 --- a/src/plugins/spotifyControls/hoverOnly.css +++ /dev/null @@ -1,16 +0,0 @@ -.vc-spotify-button-row { - height: 0; - opacity: 0; - pointer-events: none; - transition: 0.2s; - transition-property: height; -} - -#vc-spotify-player:hover .vc-spotify-button-row { - opacity: 1; - height: 32px; - pointer-events: auto; - - /* only transition opacity on show to prevent clipping */ - transition-property: height, opacity; -} diff --git a/src/plugins/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx deleted file mode 100644 index add54025e..000000000 --- a/src/plugins/spotifyControls/index.tsx +++ /dev/null @@ -1,115 +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 . -*/ - -import { Settings } from "@api/Settings"; -import { disableStyle, enableStyle } from "@api/Styles"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -import hoverOnlyStyle from "./hoverOnly.css?managed"; -import { Player } from "./PlayerComponent"; - -function toggleHoverControls(value: boolean) { - (value ? enableStyle : disableStyle)(hoverOnlyStyle); -} - -export default definePlugin({ - name: "SpotifyControls", - description: "Adds a Spotify player above the account panel", - authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev], - options: { - hoverControls: { - description: "Show controls on hover", - type: OptionType.BOOLEAN, - default: false, - onChange: v => toggleHoverControls(v) - }, - useSpotifyUris: { - type: OptionType.BOOLEAN, - description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms", - default: false - }, - previousButtonRestartsTrack: { - type: OptionType.BOOLEAN, - description: "Restart currently playing track when pressing the previous button if playtime is >3s", - default: true - } - }, - patches: [ - { - // HEY THOR UPDATE YOUTUBE MUSIC CONTROLS IF THIS SHIT CHANGES TY :) - find: "this.isCopiedStreakGodlike", - replacement: { - // react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah }) - match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/, - // react.jsx(WrapperComponent, { VencordOriginal: AccountPanel, ... - replace: "$self.PanelWrapper,{VencordOriginal:$1," - } - }, - { - find: ".PLAYER_DEVICES", - replacement: [{ - // Adds POST and a Marker to the SpotifyAPI (so we can easily find it) - match: /get:(\i)\.bind\(null,(\i\.\i)\.get\)/, - replace: "post:$1.bind(null,$2.post),vcSpotifyMarker:1,$&" - }, - { - // Spotify Connect API returns status 202 instead of 204 when skipping tracks. - // Discord rejects 202 which causes the request to send twice. This patch prevents this. - match: /202===\i\.status/, - replace: "false", - }] - }, - { - find: 'repeat:"off"!==', - replacement: [ - { - // Discord doesn't give you shuffle state and the repeat kind, only a boolean - match: /repeat:"off"!==(\i),/, - replace: "shuffle:arguments[2]?.shuffle_state??false,actual_repeat:$1,$&" - }, - { - match: /(?<=artists.filter\(\i=>).{0,10}\i\.id\)&&/, - replace: "" - } - ] - }, - ], - - start: () => toggleHoverControls(Settings.plugins.SpotifyControls.hoverControls), - - PanelWrapper({ VencordOriginal, ...props }) { - return ( - <> - ( -
-

Failed to render Spotify Modal :(

-

Check the console for errors

-
- )} - > - -
- - - - ); - } -}); diff --git a/src/plugins/spotifyControls/spotifyStyles.css b/src/plugins/spotifyControls/spotifyStyles.css deleted file mode 100644 index e9dfeffde..000000000 --- a/src/plugins/spotifyControls/spotifyStyles.css +++ /dev/null @@ -1,229 +0,0 @@ -#vc-spotify-player { - padding: 12px; - background: var(--bg-overlay-floating, var(--background-base-low, var(--background-base-lower-alt))); - margin: 0; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - border-bottom: 1px solid var(--border-subtle); - - /* so custom themes can easily change it */ - --vc-spotify-green: var(--spotify, #1db954); - --vc-spotify-green-90: color-mix(in hsl, var(--vc-spotify-green), transparent 90%); - --vc-spotify-green-80: color-mix(in hsl, var(--vc-spotify-green), transparent 80%); -} - -.theme-light #vc-spotify-player { - background: var(--bg-overlay-3, var(--background-base-lower-alt)); -} - -.theme-dark #vc-spotify-player { - background: var(--bg-overlay-1, var(--background-base-lower-alt)); -} - -.vc-spotify-button { - margin: 0 2px; - border-radius: var(--radius-sm); - background: none; - color: var(--interactive-normal); - padding: 0; - width: 32px; - height: 32px; - display: flex; - justify-content: center; - align-items: center; -} - -.vc-spotify-button:hover { - color: var(--interactive-hover); - background-color: var(--background-modifier-selected); -} - -.vc-spotify-button-icon { - height: 24px; - width: 24px; -} - -.vc-spotify-shuffle .vc-spotify-button-icon, -.vc-spotify-repeat .vc-spotify-button-icon { - width: 22px; - height: 22px; -} - -/* .vc-spotify-button:hover { - filter: brightness(1.3); -} */ - -.vc-spotify-repeat-context, -.vc-spotify-repeat-track, -.vc-spotify-shuffle-on { - background-color: var(--vc-spotify-green-90); -} - -.vc-spotify-repeat-context:hover, -.vc-spotify-repeat-track:hover, -.vc-spotify-shuffle-on:hover { - background-color: var(--vc-spotify-green-80); -} - -.vc-spotify-tooltip-text { - overflow: hidden; - white-space: nowrap; - padding-right: 0.2em; - max-width: 100%; - margin: unset; -} - -.vc-spotify-repeat-1 { - font-size: 70%; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.vc-spotify-button-row { - justify-content: center; - margin-top: 14px; -} - -.vc-spotify-secondary-song-info { - font-size: 12px; -} - -.vc-spotify-song-info-prefix { - display: none; -} - -#vc-spotify-info-wrapper { - display: flex; - flex-direction: row; - align-items: center; - height: 3em; - gap: 0.5em; -} - -#vc-spotify-album-image { - height: 90%; - object-fit: contain; - border-radius: 3px; - transition: filter 0.2s; -} - -#vc-spotify-album-image:hover { - filter: brightness(1.2); - cursor: pointer; -} - -#vc-spotify-album-expanded-wrapper #vc-spotify-album-image { - width: 100%; - object-fit: contain; -} - -#vc-spotify-titles { - display: flex; - flex-direction: column; - padding: 0.2rem; - align-items: flex-start; - place-content: flex-start center; - overflow: hidden; -} - -#vc-spotify-song-title { - color: var(--header-primary); - font-size: 14px; - font-weight: 600; -} - -.vc-spotify-ellipoverflow { - white-space: nowrap; - overflow: hidden; - width: 100%; - text-overflow: ellipsis; -} - -.vc-spotify-artist, -.vc-spotify-album { - font-size: 12px; - text-decoration: none; - color: var(--header-primary); -} - -.vc-spotify-comma { - color: var(--header-secondary); -} - -.vc-spotify-artist[role="link"]:hover, -#vc-spotify-album-title[role="link"]:hover, -#vc-spotify-song-title[role="link"]:hover { - text-decoration: underline; - cursor: pointer; -} - -#vc-spotify-progress-bar { - position: relative; - color: var(--text-default); - width: 100%; - margin: 0.5em 0; - margin-bottom: 5px; -} - -#vc-spotify-progress-bar > [class^="slider"] { - flex-grow: 1; - width: 100%; - padding: 0 !important; -} - -#vc-spotify-progress-bar > [class^="slider"] [class^="bar"] { - height: 3px !important; - top: calc(12px - 4px / 2 + var(--bar-offset)); -} - -#vc-spotify-progress-bar > [class^="slider"] [class^="barFill"] { - background-color: var(--interactive-active); -} - -#vc-spotify-progress-bar > [class^="slider"]:hover [class^="barFill"] { - background-color: var(--vc-spotify-green); -} - -#vc-spotify-progress-bar > [class^="slider"] [class^="grabber"] { - /* these importants are necessary, it applies a width and height through inline styles */ - height: 16px !important; - width: 16px !important; - margin-top: calc(17px/-2 + var(--bar-offset)/2); - margin-left: -0.5px; - background-color: var(--interactive-active); - border-color: var(--interactive-normal); - color: var(--interactive-normal); - opacity: 0; - transition: opacity 0.1s; -} - -#vc-spotify-progress-bar:hover > [class^="slider"] [class^="grabber"] { - opacity: 1; -} - -#vc-spotify-progress-text { - margin: 0; -} - -.vc-spotify-progress-time { - font-size: 12px; - top: 10px; - position: absolute; - margin-top: 8px; - font-family: var(--font-code); -} - -.vc-spotify-time-left { - left: 0; -} - -.vc-spotify-time-right { - right: 0; -} - -.vc-spotify-fallback { - padding: 0.5em; - color: var(--text-default); -} \ No newline at end of file From fce729829b5cbc29684c129c9b0d5a91498fd6fd Mon Sep 17 00:00:00 2001 From: VR <101288516+VillainsRule@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:31:22 -0400 Subject: [PATCH 004/881] add tiktokTTS plugin (#342) * add tiktokTTS plugin * folder --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/tiktokTTS/index.ts | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/equicordplugins/tiktokTTS/index.ts diff --git a/src/equicordplugins/tiktokTTS/index.ts b/src/equicordplugins/tiktokTTS/index.ts new file mode 100644 index 000000000..41f988f30 --- /dev/null +++ b/src/equicordplugins/tiktokTTS/index.ts @@ -0,0 +1,45 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { EquicordDevs } from "@utils/constants"; +import { getCurrentChannel } from "@utils/discord"; +import definePlugin from "@utils/types"; + +async function readOutText(text: string): Promise { + const mp3Response = await fetch("https://tiktok-tts.weilnet.workers.dev/api/generation", { + body: JSON.stringify({ text: text, voice: "en_us_001" }), + headers: { "Content-Type": "application/json" }, + method: "POST" + }); + + const mp3JSON = await mp3Response.json(); + + const audioElement = new Audio(); + + audioElement.src = `data:audio/mpeg;base64,${mp3JSON.data}`; + audioElement.volume = 0.5; + audioElement.onended = () => audioElement.remove(); + + document.body.appendChild(audioElement); + + audioElement.play(); +} + +export default definePlugin({ + name: "TiktokTTS", + description: "Reads out chat messages with the good ol' Tiktok TTS voice :sob:", + authors: [EquicordDevs.VillainsRule], + flux: { + async MESSAGE_CREATE({ optimistic, type, message }) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content || message.content.length < 1 || message.content.length > 300) return; + if (message.channel_id !== getCurrentChannel()?.id) return; + + readOutText(message.content); + } + } +}); From e717a6b1628f2b5bdc07ac539040b7777fffafa2 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:31:48 -0400 Subject: [PATCH 005/881] BetterActivites: Patch Optimizations --- src/equicordplugins/betterActivities/index.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/equicordplugins/betterActivities/index.tsx b/src/equicordplugins/betterActivities/index.tsx index 1f9fca2b9..86fc8c7ed 100644 --- a/src/equicordplugins/betterActivities/index.tsx +++ b/src/equicordplugins/betterActivities/index.tsx @@ -55,17 +55,8 @@ export default definePlugin({ // Show all activities in the user popout/sidebar find: '"UserProfilePopoutBody"', replacement: { - match: /(?<=(\i)\.id\)\}\)\),(\i).*?,)\i\?.{0,250}onClose:\i\}\)/, - replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })" - }, - predicate: () => settings.store.userPopout - }, - // DM Sidebar - { - find: ".SIDEBAR}),nicknameIcons", - replacement: { - match: /(?<=(\i)\.id\)\}\)\),(\i).*?,)\i\?.{0,250}\i\.card\}\)/, - replace: "$self.showAllActivitiesComponent({ activity: $2, user: $1 })" + match: /((\i)=.{0,10}(\i)\.id\).*?,)\i\?.{0,250}onClose:\i\}\)/, + replace: "$1$self.showAllActivitiesComponent({ activity: $2, user: $3 })" }, predicate: () => settings.store.userPopout }, @@ -73,8 +64,8 @@ export default definePlugin({ { find: "#{intl::STATUS_MENU_LABEL}", replacement: { - match: /(?<=,(\i)=.{0,10}\i\.id.{0,150}userId:(\i).*?,)\i\?.{0,250}onClose:\i\}\)/, - replace: "$self.showAllActivitiesComponent({ activity: $1, user: $2 })" + match: /((\i)=.{0,10}(\i)\.id\).*?,)\i\?.{0,250}onClose:\i\}\)/, + replace: "$1$self.showAllActivitiesComponent({ activity: $2, user: $3 })" }, predicate: () => settings.store.userPopout } From 553c4adc5e9c53b85126c59849e7a3c8fc28c8cd Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:36:32 -0400 Subject: [PATCH 006/881] Remove Reporter JSON --- .github/workflows/reportBrokenPlugins.yml | 24 ----------------------- scripts/generateReport.ts | 3 --- 2 files changed, 27 deletions(-) diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 12cb1a5a5..b989f896f 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -20,9 +20,6 @@ on: env: FORCE_COLOR: true - GITHUB_TOKEN: ${{ secrets.ETOKEN }} - REPO: Equicord/Equibored - USERNAME: GitHub-Actions jobs: TestPlugins: @@ -95,27 +92,6 @@ jobs: done cat "$stable_output_file" "$canary_output_file" >> $GITHUB_STEP_SUMMARY - - git config --global user.name "GitHub-Actions" - git config --global user.email actions@github.com - git clone https://$USERNAME:$GITHUB_TOKEN@github.com/$REPO.git report - cd report - - cp ../dist/reporter*.json . - git add -A - - if git diff --cached --quiet; then - echo "Nothing to commit" - if [[ $exit_code -eq 0 ]]; then - exit 0 - else - exit 1 - fi - else - git commit -m "Reporter Results" - git push --force https://$USERNAME:$GITHUB_TOKEN@github.com/$REPO.git - fi - exit $exit_code env: WEBHOOK_URL: ${{ inputs.webhook_url || secrets.WEBHOOK_URL }} diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 1e54aa161..5a1c42585 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -92,9 +92,6 @@ function toCodeBlock(s: string, indentation = 0, isDiscord = false) { } async function printReport() { - const filePath = join("dist", (CANARY ? "reporter-canary.json" : "reporter-stable.json")); - writeFileSync(filePath, JSON.stringify(report, null, 2)); - console.log(); console.log("# Equicord Report" + (CANARY ? " (Canary)" : "")); From 348261b1e858a6badd8632999d77b77dad159849 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:02:25 -0400 Subject: [PATCH 007/881] MusicControls: Migrate Settings --- src/equicordplugins/musicControls/index.tsx | 10 ++++++- .../musicControls/settings.tsx | 26 ++----------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index 09c299c44..0031aeb0d 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -102,7 +102,15 @@ export default definePlugin({ }, async start() { + if (Settings.plugins?.SpotifyControls?.enabled) { + settings.store.showSpotifyControls = true; + Settings.plugins.SpotifyControls.enabled = false; + } + if (Settings.plugins?.SpotifyLyrics?.enabled) { + settings.store.showSpotifyLyrics = true; + Settings.plugins.SpotifyLyrics.enabled = false; + } await migrateOldLyrics(); - toggleHoverControls(Settings.plugins.MusicControls.hoverControls); + toggleHoverControls(settings.store.hoverControls); }, }); diff --git a/src/equicordplugins/musicControls/settings.tsx b/src/equicordplugins/musicControls/settings.tsx index 5d62d84f3..8e9c4eac0 100644 --- a/src/equicordplugins/musicControls/settings.tsx +++ b/src/equicordplugins/musicControls/settings.tsx @@ -6,13 +6,11 @@ import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; -import { useAwaiter } from "@utils/react"; import { makeRange, OptionType } from "@utils/types"; -import { Button, Forms, MaskedLink, showToast, Text, Toasts, useMemo } from "@webpack/common"; +import { Button, Forms, MaskedLink, showToast, Toasts } from "@webpack/common"; import hoverOnlyStyle from "./hoverOnly.css?managed"; -import { clearLyricsCache, getLyricsCount } from "./spotify/lyrics/api"; -import { useLyrics } from "./spotify/lyrics/components/util"; +import { clearLyricsCache } from "./spotify/lyrics/api"; import { Provider } from "./spotify/lyrics/providers/types"; const sliderOptions = { @@ -24,26 +22,6 @@ export function toggleHoverControls(value: boolean) { (value ? enableStyle : disableStyle)(hoverOnlyStyle); } -function Details() { - const { lyricsInfo } = useLyrics(); - - const [count, error, loading] = useAwaiter( - useMemo(() => getLyricsCount, []), - { - onError: () => console.error("Failed to get lyrics count"), - fallbackValue: null, - } - ); - - return ( - <> - Current lyrics provider: {lyricsInfo?.useLyric || "None"} - {loading ? Loading lyrics count... : error ? Failed to get lyrics count : Lyrics count: {count}} - - ); -} - - function InstallInstructions() { return ( From 57a1140a0b53f5929a51c7d08908d7f933bf454a Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:17:55 -0400 Subject: [PATCH 008/881] MusicControls: Tags --- src/equicordplugins/musicControls/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index 0031aeb0d..4d206c529 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -33,6 +33,7 @@ import { TidalPlayer } from "./tidal/TidalPlayer"; export default definePlugin({ name: "MusicControls", description: "Music Controls and Lyrics for multiple services ", + tags: ["Spotify", "SpotifyControls", "SpotifyLyrics"], authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev, EquicordDevs.thororen, EquicordDevs.vmohammad, Devs.Joona], settings, patches: [ From e6a89e146e0c0a20c828042d933c4b7081cd649e Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:36:05 -0400 Subject: [PATCH 009/881] MusicControls: Updated Tags --- src/equicordplugins/musicControls/index.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index 4d206c529..319abe21d 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -30,12 +30,29 @@ import { SpotifyPlayer } from "./spotify/PlayerComponent"; import { TidalLyrics } from "./tidal/lyrics/components/lyrics"; import { TidalPlayer } from "./tidal/TidalPlayer"; +const tags = [ + // Spotify + "Spotify", + "SpotifyControls", + "SpotifyLyrics", + // Tidal + "Tidal", + "TidalControls", + "TidalLyrics", + // Youtube + /* Deprecated RN + "Youtube", + "YoutubeMusic", + "YoutubeMusicControls" + */ +]; + export default definePlugin({ name: "MusicControls", description: "Music Controls and Lyrics for multiple services ", - tags: ["Spotify", "SpotifyControls", "SpotifyLyrics"], authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev, EquicordDevs.thororen, EquicordDevs.vmohammad, Devs.Joona], settings, + tags, patches: [ { find: "this.isCopiedStreakGodlike", From 9a27e47a8ab3ac16c7a9f9083f712d6e4c41e92b Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:32:17 -0400 Subject: [PATCH 010/881] Fix Lint --- src/equicordplugins/musicControls/index.tsx | 35 ++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/equicordplugins/musicControls/index.tsx b/src/equicordplugins/musicControls/index.tsx index 319abe21d..db1801eb2 100644 --- a/src/equicordplugins/musicControls/index.tsx +++ b/src/equicordplugins/musicControls/index.tsx @@ -30,29 +30,28 @@ import { SpotifyPlayer } from "./spotify/PlayerComponent"; import { TidalLyrics } from "./tidal/lyrics/components/lyrics"; import { TidalPlayer } from "./tidal/TidalPlayer"; -const tags = [ - // Spotify - "Spotify", - "SpotifyControls", - "SpotifyLyrics", - // Tidal - "Tidal", - "TidalControls", - "TidalLyrics", - // Youtube - /* Deprecated RN - "Youtube", - "YoutubeMusic", - "YoutubeMusicControls" - */ -]; - export default definePlugin({ name: "MusicControls", description: "Music Controls and Lyrics for multiple services ", authors: [Devs.Ven, Devs.afn, Devs.KraXen72, Devs.Av32000, Devs.nin0dev, EquicordDevs.thororen, EquicordDevs.vmohammad, Devs.Joona], settings, - tags, + tags: [ + // Spotify + "Spotify", + "SpotifyControls", + "SpotifyLyrics", + // Tidal + "Tidal", + "TidalControls", + "TidalLyrics", + // Youtube + /* Deprecated RN + "Youtube", + "YoutubeMusic", + "YoutubeMusicControls" + */ + ], + patches: [ { find: "this.isCopiedStreakGodlike", From aad88fe9cd5cafd34691e487f39acd4d215d74ed Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 13 Aug 2025 12:55:24 +0200 Subject: [PATCH 011/881] fix plugins sending messages --- src/api/MessageEvents.ts | 10 +++++----- src/utils/discord.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/api/MessageEvents.ts b/src/api/MessageEvents.ts index 8b1d9e782..6e752c90c 100644 --- a/src/api/MessageEvents.ts +++ b/src/api/MessageEvents.ts @@ -62,7 +62,7 @@ export interface MessageReplyOptions { }; } -export interface MessageExtra { +export interface MessageOptions { stickers?: string[]; uploads?: Upload[]; replyOptions: MessageReplyOptions; @@ -72,17 +72,17 @@ export interface MessageExtra { openWarningPopout: (props: any) => any; } -export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable; +export type MessageSendListener = (channelId: string, messageObj: MessageObject, options: MessageOptions) => Promisable; export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable; const sendListeners = new Set(); const editListeners = new Set(); -export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) { - extra.replyOptions = replyOptions; +export async function _handlePreSend(channelId: string, messageObj: MessageObject, options: MessageOptions, replyOptions: MessageReplyOptions) { + options.replyOptions = replyOptions; for (const listener of sendListeners) { try { - const result = await listener(channelId, messageObj, extra); + const result = await listener(channelId, messageObj, options); if (result?.cancel) { return true; } diff --git a/src/utils/discord.tsx b/src/utils/discord.tsx index d7a38ebb0..f236943b1 100644 --- a/src/utils/discord.tsx +++ b/src/utils/discord.tsx @@ -110,7 +110,7 @@ export function insertTextIntoChatInputBox(text: string) { }); } -interface MessageExtra { +interface MessageOptions { messageReference: Message["messageReference"]; allowedMentions: { parse: string[]; @@ -122,8 +122,8 @@ interface MessageExtra { export function sendMessage( channelId: string, data: Partial, - waitForChannelReady?: boolean, - extra?: Partial + waitForChannelReady = true, + options: Partial = {} ) { const messageData = { content: "", @@ -133,7 +133,7 @@ export function sendMessage( ...data }; - return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, extra); + return MessageActions.sendMessage(channelId, messageData, waitForChannelReady, options); } /** From f356f647ff90d1abed69dc47c05e641a94dde7a5 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 13 Aug 2025 12:58:29 +0200 Subject: [PATCH 012/881] bump to v1.12.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5438d37f7..498b8919f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.12.9", + "version": "1.12.10", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 56743c6734428927d153b40fcf6a278630cf96d2 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:35:00 -0400 Subject: [PATCH 013/881] Move PKG Ver Check --- .github/workflows/build.yml | 7 ++++++- .github/workflows/test.yml | 6 ------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9aff5e65..2fa4a68ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ on: push: branches: - main + env: FORCE_COLOR: true GITHUB_TOKEN: ${{ secrets.ETOKEN }} @@ -65,10 +66,14 @@ jobs: rm release/*.map - name: Upload Equicord Stable - if: ${{ github.ref_name == 'main' }} run: | gh release upload latest --clobber dist/release/* + - name: Create Tag + uses: butlerlogic/action-autotag@1.1.2 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + - name: Upload Plugin JSONs to Equibored repo run: | git config --global user.name "GitHub-Actions" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3df68ee0..0c56e36c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,9 +37,3 @@ jobs: - name: Test if plugin structure is valid run: pnpm generatePluginJson - - - name: Create Tag - if: ${{ github.ref_name == 'main' }} - uses: butlerlogic/action-autotag@1.1.2 - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From bfb765097c9b9604f29ed8ce107e11a9a0d1071f Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:38:29 -0400 Subject: [PATCH 014/881] =?UTF-8?q?Prefix=20AutoTag=20=F0=9F=98=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2fa4a68ff..a0da3af2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,7 @@ jobs: uses: butlerlogic/action-autotag@1.1.2 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + tag_prefix: "v" - name: Upload Plugin JSONs to Equibored repo run: | From fc36d08465028b260e539424e62afcefabb555cb Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:40:26 -0400 Subject: [PATCH 015/881] MusicControls: Update SpotifyLyrics --- .../musicControls/settings.tsx | 12 +++- .../musicControls/spotify/lyrics/api.tsx | 5 +- .../spotify/lyrics/components/lyrics.tsx | 36 ++++------ .../spotify/lyrics/components/modal.tsx | 26 ++++--- .../spotify/lyrics/components/util.tsx | 69 +++++++++++++------ .../lyrics/providers/translator/index.ts | 15 ++-- src/equicordplugins/musicControls/styles.css | 2 +- 7 files changed, 103 insertions(+), 62 deletions(-) diff --git a/src/equicordplugins/musicControls/settings.tsx b/src/equicordplugins/musicControls/settings.tsx index 8e9c4eac0..a3b94de87 100644 --- a/src/equicordplugins/musicControls/settings.tsx +++ b/src/equicordplugins/musicControls/settings.tsx @@ -10,7 +10,8 @@ import { makeRange, OptionType } from "@utils/types"; import { Button, Forms, MaskedLink, showToast, Toasts } from "@webpack/common"; import hoverOnlyStyle from "./hoverOnly.css?managed"; -import { clearLyricsCache } from "./spotify/lyrics/api"; +import { clearLyricsCache, removeTranslations } from "./spotify/lyrics/api"; +import languages from "./spotify/lyrics/providers/translator/languages"; import { Provider } from "./spotify/lyrics/providers/types"; const sliderOptions = { @@ -61,6 +62,15 @@ export const settings = definePluginSettings({ { value: Provider.Lrclib, label: "LRCLIB" }, ], }, + TranslateTo: { + description: "Translate lyrics to - Changing this will clear existing translations", + type: OptionType.SELECT, + options: languages, + onChange: async () => { + await removeTranslations(); + showToast("Translations cleared", Toasts.Type.SUCCESS); + } + }, LyricsConversion: { description: "Automatically translate or romanize lyrics", type: OptionType.SELECT, diff --git a/src/equicordplugins/musicControls/spotify/lyrics/api.tsx b/src/equicordplugins/musicControls/spotify/lyrics/api.tsx index a92381d77..d9a95d040 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/api.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/api.tsx @@ -29,8 +29,7 @@ export const lyricFetchers = { export const providers = Object.keys(lyricFetchers) as Provider[]; export async function getLyrics(track: Track | null): Promise { - if (!track) return null; - if (!track.id) return null; + if (!track || !track.id) return null; const cacheKey = track.id; const cached = await DataStore.get(LyricsCacheKey) as Record; @@ -76,7 +75,7 @@ export async function clearLyricsCache() { export async function getLyricsCount(): Promise { const cache = await DataStore.get(LyricsCacheKey) as Record; - return Object.keys(cache).length; + return Object.keys(cache ?? {}).length; } export async function updateLyrics(trackId: string, newLyrics: SyncedLyric[], provider: Provider) { diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx index 7a3770093..9e89a1c84 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/lyrics.tsx @@ -14,51 +14,45 @@ import { LyricsContextMenu } from "./ctxMenu"; import { LyricsModal } from "./modal"; import { cl, NoteSvg, useLyrics } from "./util"; +const prevCl = cl("prev"); +const nextCl = cl("next"); +const currentCl = cl("current"); + function LyricsDisplay({ scroll = true }: { scroll?: boolean; }) { const { ShowMusicNoteOnNoLyrics } = settings.use(["ShowMusicNoteOnNoLyrics"]); const { lyricsInfo, lyricRefs, currLrcIndex } = useLyrics({ scroll }); const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null; - const NoteElement = NoteSvg(cl("music-note")); - const makeClassName = (index: number): string => { - if (currLrcIndex === null) return ""; + if (currLrcIndex == null) return prevCl; const diff = index - currLrcIndex; - if (diff === 0) return cl("current"); - return cl(diff > 0 ? "next" : "prev"); + if (diff === 0) return currentCl; + return diff > 0 ? nextCl : prevCl; }; - if (!lyricsInfo) { - return ShowMusicNoteOnNoLyrics ? ( -
ContextMenuApi.openContextMenu(e, () => )} - > - - {NoteElement} - -
- ) : null; - } - return (
openModal(props => )} + onClick={() => openModal(props => )} onContextMenu={e => ContextMenuApi.openContextMenu(e, () => )} > - {currentLyrics?.map((line, i) => ( + {currentLyrics ? currentLyrics.map((line, i) => (
- {line.text || NoteElement} + {line.text || NoteSvg()}
- ))} + )) : ShowMusicNoteOnNoLyrics ? ( + + + + ) : null}
); } diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx index 6a3423fec..59831cbc7 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/modal.tsx @@ -17,7 +17,14 @@ const formatTime = (time: number) => { return `${minutes}:${seconds.toString().padStart(2, "0")}`; }; -function ModalHeaderContent({ track }: { track: Track; }) { +function ModalHeaderContent({ track }: { track: Track | null; }) { + if (!track) { + return ( + + No track playing + + ); + } return (
@@ -43,27 +50,30 @@ function ModalHeaderContent({ track }: { track: Track; }) { ); } -export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) { +const modalCurrentLine = cl("modal-line-current"); +const modalLine = cl("modal-line"); + +export function LyricsModal({ props }: { props: ModalProps; }) { const { track, lyricsInfo, currLrcIndex } = useLyrics({ scroll: false }); - const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null; + const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric]; return ( - - + + -
+
{currentLyrics ? ( currentLyrics.map((line, i) => ( SpotifyStore.seek(line.time * 1000)}> {formatTime(line.time)} - {line.text || NoteSvg(cl("modal-note"))} + {line.text || NoteSvg()} )) ) : ( diff --git a/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx index 176df03a7..5c63eb967 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx +++ b/src/equicordplugins/musicControls/spotify/lyrics/components/util.tsx @@ -12,23 +12,51 @@ import { settings } from "../../../settings"; import { SpotifyStore } from "../../SpotifyStore"; import { SpotifyLrcStore } from "../providers/store"; import { SyncedLyric } from "../providers/types"; + export const scrollClasses = findByPropsLazy("auto", "customTheme"); export const cl = classNameFactory("vc-spotify-lyrics-"); -export function NoteSvg(className: string) { +export function NoteSvg() { return ( - + ); } -const calculateIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => { - const posInSec = position / 1000; - const currentIndex = lyrics.findIndex(l => l.time - (delay / 1000) > posInSec && l.time < posInSec + 8) - 1; - const nextLyric = lyrics.findIndex(l => l.time >= posInSec); - return [currentIndex, nextLyric]; + +const getIndexes = (lyrics: SyncedLyric[], position: number, delay: number) => { + const posInSec = (position + delay) / 1000; + + let left = 0, right = lyrics.length - 1; + let currentIndex: number | null = null; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const curr = lyrics[mid]; + const next = lyrics[mid + 1]; + + if (curr.time <= posInSec && (!next || next.time > posInSec)) { + currentIndex = mid; + break; + } + + if (curr.time > posInSec) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + const nextIdx = currentIndex !== null ? currentIndex + 1 : left; + const nextLyricIdx = nextIdx < lyrics.length ? nextIdx : null; + + if (currentIndex !== null && posInSec - lyrics[currentIndex].time > 8) { + return [null, nextLyricIdx]; + } + + return [currentIndex, nextLyricIdx]; }; export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { @@ -47,7 +75,7 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { const [position, setPosition] = useState(storePosition); const [lyricRefs, setLyricRefs] = useState[]>([]); - const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric] || null; + const currentLyrics = lyricsInfo?.lyricsVersions[lyricsInfo.useLyric]; useEffect(() => { if (currentLyrics) { @@ -57,12 +85,15 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { useEffect(() => { - if (currentLyrics && position) { - const [currentIndex, nextLyric] = calculateIndexes(currentLyrics, position, LyricDelay); + if (currentLyrics && position != null) { + const [currentIndex, nextLyricIndex] = getIndexes(currentLyrics, position, LyricDelay); setCurrLrcIndex(currentIndex); - setNextLyric(nextLyric); + setNextLyric(nextLyricIndex); + } else { + setCurrLrcIndex(null); + setNextLyric(null); } - }, [currentLyrics, position]); + }, [currentLyrics, position, LyricDelay]); useEffect(() => { if (scroll && currLrcIndex !== null) { @@ -73,17 +104,15 @@ export function useLyrics({ scroll = true }: { scroll?: boolean; } = {}) { lyricRefs[nextLyric]?.current?.scrollIntoView({ behavior: "smooth", block: "center" }); } } - }, [currLrcIndex, nextLyric, scroll]); + }, [currLrcIndex, nextLyric, scroll, lyricRefs]); useEffect(() => { - if (isPlaying) { - setPosition(SpotifyStore.position); - const interval = setInterval(() => { - setPosition(p => p + 1000); - }, 1000); + if (!isPlaying) return; - return () => clearInterval(interval); - } + setPosition(SpotifyStore.position); + const interval = setInterval(() => setPosition(p => p + 1000), 1000); + + return () => clearInterval(interval); }, [storePosition, isPlaying]); return { track, lyricsInfo, lyricRefs, currLrcIndex, nextLyric }; diff --git a/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts index 02d7308ec..898072d13 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/translator/index.ts @@ -5,7 +5,7 @@ */ import settings from "../../../.."; -import { LyricsData, Provider, SyncedLyric } from "../types"; +import { Provider, SyncedLyric } from "../types"; // stolen from src\plugins\translate\utils.ts @@ -45,7 +45,7 @@ async function googleTranslate(text: string, targetLang: string, romanize: boole } async function processLyrics( - lyrics: LyricsData["lyricsVersions"][Provider], + lyrics: SyncedLyric[], targetLang: string, romanize: boolean ): Promise { @@ -67,7 +67,7 @@ async function processLyrics( }) ); - if (processedLyricsResp[0][1] === null) return null; + if (processedLyricsResp.every(mapping => mapping[1] === null)) return null; return lyrics.map(lyric => ({ ...lyric, @@ -75,13 +75,12 @@ async function processLyrics( })); } -async function translateLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise { - const language = settings.store.TranslateTo; - return processLyrics(lyrics, language, false); +async function translateLyrics(lyrics: SyncedLyric[]) { + return await processLyrics(lyrics, settings.store.TranslateTo, false); } -async function romanizeLyrics(lyrics: LyricsData["lyricsVersions"][Provider]): Promise { - return processLyrics(lyrics, "", true); +async function romanizeLyrics(lyrics: SyncedLyric[]) { + return await processLyrics(lyrics, "", true); } export const lyricsAlternativeFetchers = { diff --git a/src/equicordplugins/musicControls/styles.css b/src/equicordplugins/musicControls/styles.css index 3252938e7..878b13208 100644 --- a/src/equicordplugins/musicControls/styles.css +++ b/src/equicordplugins/musicControls/styles.css @@ -243,4 +243,4 @@ .eq-tidal-lyrics-word-active { transform: none; } -} +} \ No newline at end of file From 9e71344002ce257eecb435693c802674e75ce776 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:36:59 -0400 Subject: [PATCH 016/881] Fix Patches --- src/equicordplugins/customUserColors/index.tsx | 2 +- src/equicordplugins/keywordNotify/index.tsx | 4 ++-- src/equicordplugins/whosWatching/index.tsx | 2 +- src/plugins/pinDms/index.tsx | 13 +++++++++++++ src/webpack/common/components.ts | 2 +- src/webpack/common/utils.ts | 4 ++-- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/equicordplugins/customUserColors/index.tsx b/src/equicordplugins/customUserColors/index.tsx index 69a108db0..9efc00684 100644 --- a/src/equicordplugins/customUserColors/index.tsx +++ b/src/equicordplugins/customUserColors/index.tsx @@ -105,7 +105,7 @@ export default definePlugin({ { find: "PrivateChannel.renderAvatar", replacement: { - match: /(subText:\i\(\),)/, + match: /(withDisplayNameStyles\]:\i\}\),children:\i\}\),)/, replace: "$1style:{color:`${$self.colorDMList(arguments[0])}`}," }, predicate: () => settings.store.dmList, diff --git a/src/equicordplugins/keywordNotify/index.tsx b/src/equicordplugins/keywordNotify/index.tsx index 33ba20a4a..d89bd4020 100644 --- a/src/equicordplugins/keywordNotify/index.tsx +++ b/src/equicordplugins/keywordNotify/index.tsx @@ -322,8 +322,8 @@ export default definePlugin({ { find: "location:\"RecentsPopout\"});", replacement: { - match: /:(\i)===\i\.\i\.MENTIONS\?\(0,.+?setTab:(\i),onJump:(\i),badgeState:\i,closePopout:(\i)/, - replace: ": $1 === 8 ? $self.tryKeywordMenu($2, $3, $4) $&" + match: /(?<=setTab:(\i),badgeState:\i,closePopout:(\i).{0,50}):(\i)===\i\.\i\.MENTIONS\?\(0,.+?onJump:(\i)/, + replace: ": $3 === 8 ? $self.tryKeywordMenu($1, $2, $4) $&" } }, { diff --git a/src/equicordplugins/whosWatching/index.tsx b/src/equicordplugins/whosWatching/index.tsx index 8bee85b52..c7e77211a 100644 --- a/src/equicordplugins/whosWatching/index.tsx +++ b/src/equicordplugins/whosWatching/index.tsx @@ -84,7 +84,7 @@ export default definePlugin({ { find: "this.renderEmbeddedActivity()", replacement: { - match: /(?<=render\(\).{0,500}children.{0,50})"div"(?=.{0,500}this\.renderEmbeddedActivity\(\))/, + match: /"div"(?=.{0,100}this.renderActions)/, replace: "$self.WrapperComponent" } } diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 6426c8038..bf4f5252c 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -61,6 +61,11 @@ export const settings = definePluginSettings({ type: OptionType.CUSTOM, default: {} as Record, description: "", + }, + disableCreateDMButton: { + type: OptionType.BOOLEAN, + description: "Disabled the create dm button", + default: true } }); @@ -154,6 +159,14 @@ export default definePlugin({ replace: "$self.getAllUncollapsedChannels().concat($&.filter(c=>!$self.isPinned(c)))" } }, + { + find: ".createDMButtonContainer", + replacement: { + match: /"full-width"===\i/, + replace: "$&&&false" + }, + predicate: () => settings.store.disableCreateDMButton + }, ], sections: null as number[] | null, diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index bf7c9dd1b..847579587 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -24,7 +24,7 @@ import { waitForComponent } from "./internal"; const FormTitle = waitForComponent("FormTitle", filters.componentByCode('["defaultMargin".concat', '="h5"')); -const FormText = waitForComponent("FormText", filters.componentByCode(".SELECTABLE),", ".DISABLED:")); +const FormText = waitForComponent("FormText", filters.componentByCode(".Text,", /\i=\i:\i&&\(\i=\i\)/)); const FormSection = waitForComponent("FormSection", filters.componentByCode(".titleId)")); const FormDivider = waitForComponent("FormDivider", filters.componentByCode(".divider,", ",style:", '"div"', /\.divider,\i\),style:/)); diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index c106a107c..e001bae8b 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -211,7 +211,7 @@ export const DisplayProfileUtils: t.DisplayProfileUtils = mapMangledModuleLazy(/ export const DateUtils: t.DateUtils = mapMangledModuleLazy("millisecondsInUnit:", { calendarFormat: filters.byCode("sameElse"), - dateFormat: filters.byCode('":'), - isSameDay: filters.byCode("Math.abs(+"), + dateFormat: filters.byCode('":"'), + isSameDay: filters.byCode(/Math.abs\(\i-/), diffAsUnits: filters.byCode("days:0", "millisecondsInUnit") }); From 204f916b2a1a29198331c1d862998710b7681412 Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:42:58 -0400 Subject: [PATCH 017/881] fix Settings UI and various plugins (#3608) Co-authored-by: Vendicated --- packages/discord-types/src/components.d.ts | 7 +----- src/plugins/betterSettings/index.tsx | 4 +-- src/plugins/consoleJanitor/index.tsx | 2 +- src/plugins/decor/settings.tsx | 2 +- .../decor/ui/modals/ChangeDecorationModal.tsx | 2 +- .../decor/ui/modals/CreateDecorationModal.tsx | 4 +-- src/plugins/fakeNitro/index.tsx | 2 +- src/plugins/ignoreActivities/index.tsx | 4 +-- src/plugins/showHiddenChannels/index.tsx | 2 +- src/webpack/common/FormText.tsx | 25 +++++++++++++++++++ src/webpack/common/components.ts | 2 +- src/webpack/common/utils.ts | 2 +- 12 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 src/webpack/common/FormText.tsx diff --git a/packages/discord-types/src/components.d.ts b/packages/discord-types/src/components.d.ts index 1df037359..f9e778d8d 100644 --- a/packages/discord-types/src/components.d.ts +++ b/packages/discord-types/src/components.d.ts @@ -43,12 +43,7 @@ export type FormDivider = ComponentType<{ }>; -export type FormText = ComponentType & TextProps> & { Types: FormTextTypes; }; +export type FormText = ComponentType; export type Tooltip = ComponentType<{ text: ReactNode | ComponentType; diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index cbf94c2bc..8fecb2623 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -123,8 +123,8 @@ export default definePlugin({ find: "#{intl::USER_SETTINGS_ACTIONS_MENU_LABEL}", replacement: [ { - match: /(EXPERIMENTS:.+?)(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/, - replace: "$1$self.wrapMenu($2)" + match: /(\(0,\i.\i\)\(\))(?=\.filter\(\i=>\{let\{section:\i\}=)/, + replace: "$self.wrapMenu($1)" }, { match: /case \i\.\i\.DEVELOPER_OPTIONS:return \i;/, diff --git a/src/plugins/consoleJanitor/index.tsx b/src/plugins/consoleJanitor/index.tsx index 8a11cf01f..4981ac2ca 100644 --- a/src/plugins/consoleJanitor/index.tsx +++ b/src/plugins/consoleJanitor/index.tsx @@ -60,7 +60,7 @@ const AllowLevelSettings = ErrorBoundary.wrap(() => { return ( Filter List - Always allow loggers of these types + Always allow loggers of these types {Object.keys(settings.store.allowLevel).map(key => ( diff --git a/src/plugins/decor/settings.tsx b/src/plugins/decor/settings.tsx index 8e82bbae2..361ecf898 100644 --- a/src/plugins/decor/settings.tsx +++ b/src/plugins/decor/settings.tsx @@ -24,7 +24,7 @@ export const settings = definePluginSettings({ return
- + You can also access Decor decorations from the { diff --git a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx index d9cfd50c2..ed81eaf06 100644 --- a/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/ChangeDecorationModal.tsx @@ -83,7 +83,7 @@ function SectionHeader({ section }: SectionHeaderProps) { />} {hasSubtitle && - + {section.subtitle} } diff --git a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx index 4afb74643..d772ac4a4 100644 --- a/src/plugins/decor/ui/modals/CreateDecorationModal.tsx +++ b/src/plugins/decor/ui/modals/CreateDecorationModal.tsx @@ -97,7 +97,7 @@ function CreateDecorationModal(props: ModalProps) { filters={[{ name: "Decoration file", extensions: ["png", "apng"] }]} onFileSelect={setFile} /> - + File should be APNG or PNG. @@ -107,7 +107,7 @@ function CreateDecorationModal(props: ModalProps) { value={name} onChange={setName} /> - + This name will be used when referring to this decoration. diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 981a7da89..8dcb2300e 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -800,7 +800,7 @@ export default definePlugin({ however you do not have permissions to embed links in the current channel. Are you sure you want to send this message? Your FakeNitro items will appear as a link only. - + You can disable this notice in the plugin settings.
, diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 94334f5af..88906c385 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -82,7 +82,7 @@ function recalculateActivities() { function ImportCustomRPCComponent() { return ( - Import the application id of the CustomRPC plugin to the filter list + Import the application id of the CustomRPC plugin to the filter list
, diff --git a/src/equicordplugins/identity/index.tsx b/src/equicordplugins/identity/index.tsx index 2e6ed7861..e8b228581 100644 --- a/src/equicordplugins/identity/index.tsx +++ b/src/equicordplugins/identity/index.tsx @@ -86,7 +86,7 @@ function ResetCard() { Saving your base profile will allow you to have a backup of your actual profile - + If you save, it will overwrite your previous data.
, @@ -107,7 +107,7 @@ function ResetCard() { Loading your base profile will restore your actual profile settings - + If you load, it will overwrite your current profile configuration.
, diff --git a/src/equicordplugins/instantScreenshare/utils.tsx b/src/equicordplugins/instantScreenshare/utils.tsx index 7f85ab4a6..4c4d7d4b5 100644 --- a/src/equicordplugins/instantScreenshare/utils.tsx +++ b/src/equicordplugins/instantScreenshare/utils.tsx @@ -95,7 +95,7 @@ function SettingSection() { return ( Media source to stream - Resets to main screen if not found + Resets to main screen if not found ); diff --git a/src/plugins/webContextMenus.web/index.ts b/src/plugins/webContextMenus.web/index.ts index d1d6a59af..b19a9265f 100644 --- a/src/plugins/webContextMenus.web/index.ts +++ b/src/plugins/webContextMenus.web/index.ts @@ -153,7 +153,7 @@ export default definePlugin({ replace: "false" }, { - match: /return\s*?\[.{0,50}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/, + match: /return\s*?\[.{0,100}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/, replace: "return [true" }, { From b4b7dfddba5d2f11f51f48242d6254e1097ea3f4 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:52:27 -0400 Subject: [PATCH 020/881] Update Building --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0da3af2d..17b520fbd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,7 @@ jobs: uses: butlerlogic/action-autotag@1.1.2 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + with: tag_prefix: "v" - name: Upload Plugin JSONs to Equibored repo From e56c93a9249a22985eaa4bd800ece332bd78d5f7 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:09:40 -0400 Subject: [PATCH 021/881] Use Full GitHash --- scripts/build/common.mjs | 2 +- src/components/settings/tabs/updater/index.tsx | 8 ++++---- src/equicordplugins/commandPalette/commands.tsx | 5 ++--- .../discordDevBanner/components/makeDevBanner.tsx | 5 ++--- src/plugins/_core/settings.tsx | 4 ++-- src/plugins/_core/supportHelper.tsx | 4 ++-- src/utils/updater.ts | 5 +++++ 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 87540a6e8..b9b603396 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -48,7 +48,7 @@ if (!IS_COMPANION_TEST && process.argv.includes("--companion-test")) console.error("--companion-test must be run with --reporter for any effect"); export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater"); -export const gitHash = process.env.EQUICORD_HASH || execSync("git rev-parse --short=9 HEAD", { encoding: "utf-8" }).trim(); +export const gitHash = process.env.EQUICORD_HASH || execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim(); export const banner = { js: ` diff --git a/src/components/settings/tabs/updater/index.tsx b/src/components/settings/tabs/updater/index.tsx index e243020ef..44599c807 100644 --- a/src/components/settings/tabs/updater/index.tsx +++ b/src/components/settings/tabs/updater/index.tsx @@ -26,7 +26,7 @@ import { classes } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { relaunch } from "@utils/native"; import { useAwaiter } from "@utils/react"; -import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater"; +import { changes, checkForUpdates, getRepo, isNewer, shortGitHash, update, updateError, UpdateLogger } from "@utils/updater"; import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common"; import gitHash from "~git-hash"; @@ -76,8 +76,8 @@ interface CommonProps { repoPending: boolean; } -function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) { - return +function HashLink({ repo, hash, disabled = false, longHash = hash }: { repo: string, hash: string, disabled?: boolean; longHash?: string; }) { + return {hash} ; } @@ -233,7 +233,7 @@ function Updater() { ) } - {" "}() + {" "}() diff --git a/src/equicordplugins/commandPalette/commands.tsx b/src/equicordplugins/commandPalette/commands.tsx index c324c90b0..81f17e91c 100644 --- a/src/equicordplugins/commandPalette/commands.tsx +++ b/src/equicordplugins/commandPalette/commands.tsx @@ -8,10 +8,9 @@ import { showNotification } from "@api/Notifications"; import { Settings } from "@api/Settings"; import { copyToClipboard } from "@utils/clipboard"; import { relaunch, showItemInFolder } from "@utils/native"; -import { checkForUpdates, getRepo } from "@utils/updater"; +import { checkForUpdates, getRepo, shortGitHash } from "@utils/updater"; import { GuildStore, NavigationRouter, SettingsRouter, Toasts } from "@webpack/common"; -import gitHash from "~git-hash"; import gitRemote from "~git-remote"; import Plugins from "~plugins"; @@ -116,7 +115,7 @@ export const actions: ButtonAction[] = [ { id: "copyGitInfo", label: "Copy Git Info", callback: async () => { - copyToClipboard(`gitHash: ${gitHash}\ngitRemote: ${gitRemote}`); + copyToClipboard(`gitHash: ${shortGitHash()}\ngitRemote: ${gitRemote}`); Toasts.show({ message: "Copied git info to clipboard!", diff --git a/src/equicordplugins/discordDevBanner/components/makeDevBanner.tsx b/src/equicordplugins/discordDevBanner/components/makeDevBanner.tsx index e4e79c685..796368918 100644 --- a/src/equicordplugins/discordDevBanner/components/makeDevBanner.tsx +++ b/src/equicordplugins/discordDevBanner/components/makeDevBanner.tsx @@ -4,12 +4,11 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { shortGitHash } from "@utils/updater"; import { React } from "@webpack/common"; import SettingsPlugin from "plugins/_core/settings"; import { JSX } from "react"; -import gitHash from "~git-hash"; - import { ChromiumIcon, DevBannerIcon, DiscordIcon, ElectronIcon, EquicordIcon } from "../icons"; import { names, settings } from "."; @@ -27,7 +26,7 @@ export function makeDevBanner(state?: string): string | JSX.Element { .replace(/{buildHash}/g, VERSION_HASH.slice(0, 9)) .replace(/{equicordName}/g, "Equicord") .replace(/{equicordVersion}/g, VERSION) - .replace(/{equicordHash}/g, gitHash) + .replace(/{equicordHash}/g, shortGitHash()) .replace(/{equicordPlatform}/g, getVersionInfo(false)) .replace(/{electronName}/g, "Electron") .replace(/{electronVersion}/g, electronVersion) diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index 2afe1c1ef..9633624d3 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -22,9 +22,9 @@ import ThemesTab from "@components/ThemeSettings/ThemesTab"; import { Devs } from "@utils/constants"; import { getIntlMessage } from "@utils/discord"; import definePlugin, { OptionType } from "@utils/types"; +import { shortGitHash } from "@utils/updater"; import { React } from "@webpack/common"; -import gitHash from "~git-hash"; type SectionType = "HEADER" | "DIVIDER" | "CUSTOM"; type SectionTypes = Record; @@ -237,7 +237,7 @@ export default definePlugin({ getInfoRows() { const { electronVersion, chromiumVersion, getVersionInfo } = this; - const rows = [`Equicord ${gitHash}${getVersionInfo()}`]; + const rows = [`Equicord ${shortGitHash()}${getVersionInfo()}`]; if (electronVersion) rows.push(`Electron ${electronVersion}`); if (chromiumVersion) rows.push(`Chromium ${chromiumVersion}`); diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index 5359de222..6a8e01ba1 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -32,7 +32,7 @@ import { relaunch } from "@utils/native"; import { onlyOnce } from "@utils/onlyOnce"; import { makeCodeblock } from "@utils/text"; import definePlugin from "@utils/types"; -import { checkForUpdates, isOutdated, update } from "@utils/updater"; +import { checkForUpdates, isOutdated, shortGitHash, update } from "@utils/updater"; import { Alerts, Button, Card, ChannelStore, Forms, GuildMemberStore, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common"; import { JSX } from "react"; @@ -112,7 +112,7 @@ async function generateDebugInfoMessage() { const info = { Equicord: - `v${VERSION} • [${gitHash}]()` + + `v${VERSION} • [${shortGitHash()}]()` + `${SettingsPlugin.getVersionInfo()} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`, Client: `${RELEASE_CHANNEL} ~ ${client}`, Platform: platformName() diff --git a/src/utils/updater.ts b/src/utils/updater.ts index 25fe6ee8c..f8c800012 100644 --- a/src/utils/updater.ts +++ b/src/utils/updater.ts @@ -28,6 +28,11 @@ export let isNewer = false; export let updateError: any; export let changes: Record<"hash" | "author" | "message", string>[]; + +export function shortGitHash(length = 7) { + return gitHash.slice(0, length); +} + async function Unwrap(p: Promise>) { const res = await p; From a3f4108e100c83a0c452e6800b9fc5d1c32e5eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=90=80=F0=9D=90=A5=F0=9D=90=A5=F0=9D=90=9A?= =?UTF-8?q?=F0=9D=90=B2?= <71098898+0x36D76289@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:03:22 +0200 Subject: [PATCH 022/881] feat(channelTabs): hide tabs during fullscreen screenshare (#344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Merge Controls & Lyrics * MusicControls: Styles * SpotifyControls: 👋 * feat(channelTabs): hide tabs during fullscreen screenshare - Add fullscreen detection using ChannelRTCStore - Hide ChannelTabs container when in fullscreen context - Improves user experience during screen sharing - Follows same pattern as BetterFolders plugin --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- .../channelTabs/components/ChannelTabsContainer.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx b/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx index fdce2833d..f296ebb8b 100644 --- a/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx +++ b/src/equicordplugins/channelTabs/components/ChannelTabsContainer.tsx @@ -7,8 +7,8 @@ import { classNameFactory } from "@api/Styles"; import { classes } from "@utils/misc"; import { useForceUpdater } from "@utils/react"; -import { findComponentByCodeLazy } from "@webpack"; -import { Button, ContextMenuApi, Flex, FluxDispatcher, Forms, useCallback, useEffect, useRef, UserStore, useState } from "@webpack/common"; +import { findComponentByCodeLazy, findStoreLazy } from "@webpack"; +import { Button, ContextMenuApi, Flex, FluxDispatcher, Forms, useCallback, useEffect, useRef, UserStore, useState, useStateFromStores } from "@webpack/common"; import { BasicChannelTabsProps, ChannelTabsProps, createTab, handleChannelSwitch, moveToTab, openedTabs, openStartupTabs, saveTabs, settings, setUpdaterFunction, useGhostTabs } from "../util"; import BookmarkContainer from "./BookmarkContainer"; @@ -18,6 +18,7 @@ import { BasicContextMenu } from "./ContextMenus"; type TabSet = Record; const PlusSmallIcon = findComponentByCodeLazy("0v-5h5a1"); +const ChannelRTCStore = findStoreLazy("ChannelRTCStore"); const cl = classNameFactory("vc-channeltabs-"); @@ -25,6 +26,7 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) { const [userId, setUserId] = useState(""); const { showBookmarkBar, widerTabsAndBookmarks, enableHotkeys, hotkeyCount, tabBarPosition } = settings.use(["showBookmarkBar", "widerTabsAndBookmarks", "enableHotkeys", "hotkeyCount", "tabBarPosition"]); const GhostTabs = useGhostTabs(); + const isFullscreen = useStateFromStores([ChannelRTCStore], () => ChannelRTCStore.isFullscreenInContext() ?? false); const _update = useForceUpdater(); const update = useCallback((save = true) => { @@ -102,6 +104,8 @@ export default function ChannelsTabsContainer(props: BasicChannelTabsProps) { if (!userId) return null; + if (isFullscreen) return null; + return (
Date: Fri, 15 Aug 2025 13:12:03 -0400 Subject: [PATCH 023/881] CustomSounds: Stuff --- .../alwaysExpandProfiles/index.ts | 2 +- .../customSounds/SoundOverrideComponent.tsx | 295 +++++++++++++ .../customSounds/audioStore.ts | 112 +++++ .../components/SoundOverrideComponent.tsx | 126 ------ src/equicordplugins/customSounds/index.tsx | 412 ++++++++++++++++-- src/equicordplugins/customSounds/styles.css | 61 ++- src/equicordplugins/customSounds/types.ts | 121 +++-- src/equicordplugins/questify/index.tsx | 4 +- 8 files changed, 930 insertions(+), 203 deletions(-) create mode 100644 src/equicordplugins/customSounds/SoundOverrideComponent.tsx create mode 100644 src/equicordplugins/customSounds/audioStore.ts delete mode 100644 src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx diff --git a/src/equicordplugins/alwaysExpandProfiles/index.ts b/src/equicordplugins/alwaysExpandProfiles/index.ts index 9c9a1f911..b52b3296d 100644 --- a/src/equicordplugins/alwaysExpandProfiles/index.ts +++ b/src/equicordplugins/alwaysExpandProfiles/index.ts @@ -13,7 +13,7 @@ import { User } from "@vencord/discord-types"; migratePluginSettings("AlwaysExpandProfiles", "AlwaysExpandProfile"); export default definePlugin({ name: "AlwaysExpandProfiles", - description: "Always display a user's full popout", + description: "Always expands profile popouts to the full modal", authors: [EquicordDevs.thororen], patches: [ { diff --git a/src/equicordplugins/customSounds/SoundOverrideComponent.tsx b/src/equicordplugins/customSounds/SoundOverrideComponent.tsx new file mode 100644 index 000000000..246cf691d --- /dev/null +++ b/src/equicordplugins/customSounds/SoundOverrideComponent.tsx @@ -0,0 +1,295 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { Margins } from "@utils/margins"; +import { useForceUpdater } from "@utils/react"; +import { makeRange } from "@utils/types"; +import { findByCodeLazy, findLazy } from "@webpack"; +import { Button, Card, Forms, React, Select, showToast, Slider, Switch } from "@webpack/common"; +import { ComponentType, Ref, SyntheticEvent } from "react"; + +import { deleteAudio, getAllAudio, saveAudio, StoredAudioFile } from "./audioStore"; +import { ensureDataURICached } from "./index"; +import { SoundOverride, SoundPlayer, SoundType } from "./types"; + +type FileInput = ComponentType<{ + ref: Ref; + onChange: (e: SyntheticEvent) => void; + multiple?: boolean; + filters?: { name?: string; extensions: string[]; }[]; +}>; + +const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "webm", "wma", "mp4"]; +const cl = classNameFactory("vc-custom-sounds-"); +const playSound: (id: string) => SoundPlayer = findByCodeLazy(".playWithListener().then"); +const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); + +const capitalizeWords = (str: string) => + str.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + +export function SoundOverrideComponent({ type, override, onChange }: { + type: SoundType; + override: SoundOverride; + onChange: () => Promise; +}) { + const fileInputRef = React.useRef(null); + const update = useForceUpdater(); + const sound = React.useRef(null); + const [files, setFiles] = React.useState>({}); + + React.useEffect(() => { + getAllAudio().then(setFiles); + }, []); + + const saveAndNotify = async () => { + await onChange(); + update(); + }; + + const previewSound = async () => { + sound.current?.stop(); + + if (!override.enabled) { + sound.current = playSound(type.id); + return; + } + + const { selectedSound } = override; + + if (selectedSound === "custom" && override.selectedFileId) { + try { + const dataUri = await ensureDataURICached(override.selectedFileId); + + if (!dataUri || !dataUri.startsWith("data:audio/")) { + showToast("No custom sound file available for preview"); + return; + } + + const audio = new Audio(dataUri); + audio.volume = override.volume / 100; + + audio.onerror = e => { + console.error("[CustomSounds] Error playing custom audio:", e); + showToast("Error playing custom sound. File may be corrupted."); + }; + + await audio.play(); + sound.current = { + play: () => audio.play(), + pause: () => audio.pause(), + stop: () => { + audio.pause(); + audio.currentTime = 0; + }, + loop: () => { audio.loop = true; } + }; + } catch (error) { + console.error("[CustomSounds] Error in previewSound:", error); + showToast("Error playing sound."); + } + } else if (selectedSound === "default") { + sound.current = playSound(type.id); + } else { + sound.current = playSound(selectedSound); + } + }; + + const uploadFile = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const fileExtension = file.name.split(".").pop()?.toLowerCase(); + if (!fileExtension || !AUDIO_EXTENSIONS.includes(fileExtension)) { + showToast("Invalid file type. Please upload an audio file."); + event.target.value = ""; + return; + } + + try { + showToast("Uploading file..."); + const id = await saveAudio(file); + + const savedFiles = await getAllAudio(); + setFiles(savedFiles); + + override.selectedFileId = id; + override.selectedSound = "custom"; + + await ensureDataURICached(id); + await saveAndNotify(); + + showToast(`File uploaded successfully: ${file.name}`); + } catch (error) { + console.error("[CustomSounds] Error uploading file:", error); + showToast(`Error uploading file: ${error}`); + } + + event.target.value = ""; + }; + + const deleteFile = async (id: string) => { + try { + await deleteAudio(id); + const updated = await getAllAudio(); + setFiles(updated); + + if (override.selectedFileId === id) { + override.selectedFileId = undefined; + override.selectedSound = "default"; + await saveAndNotify(); + } else { + update(); + } + showToast("File deleted successfully"); + } catch (error) { + console.error("[CustomSounds] Error deleting file:", error); + showToast("Error deleting file."); + } + }; + + const customFileOptions = Object.entries(files) + .filter(([id, file]) => !!id && !!file?.name) + .map(([id, file]) => ({ + value: id, + label: file.name + })); + + return ( + + { + console.log(`[CustomSounds] Setting ${type.id} enabled to:`, val); + + override.enabled = val; + + if (val && override.selectedSound === "custom" && override.selectedFileId) { + try { + await ensureDataURICached(override.selectedFileId); + } catch (error) { + console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error); + showToast("Error loading custom sound file"); + } + } + + await saveAndNotify(); + console.log("[CustomSounds] After setting enabled, override.enabled =", override.enabled); + }} + className={Margins.bottom16} + hideBorder + > + {type.name} + + + {override.enabled && ( + <> +
+ + +
+ + Volume + { + override.volume = val; + saveAndNotify(); + }} + className={Margins.bottom16} + disabled={!override.enabled} + /> + + Sound Source + v === (override.selectedFileId || "")} + select={async id => { + if (!id) { + override.selectedFileId = undefined; + } else { + override.selectedFileId = id; + await ensureDataURICached(id); + } + + await saveAndNotify(); + }} + serialize={opt => opt.value} + className={Margins.bottom8} + /> + +
+ + + {override.selectedFileId && files[override.selectedFileId] && ( + + )} +
+ + )} + + )} +
+ ); +} diff --git a/src/equicordplugins/customSounds/audioStore.ts b/src/equicordplugins/customSounds/audioStore.ts new file mode 100644 index 000000000..614942ff0 --- /dev/null +++ b/src/equicordplugins/customSounds/audioStore.ts @@ -0,0 +1,112 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { get, set } from "@api/DataStore"; + +const STORAGE_KEY = "ScattrdCustomSounds"; + +export interface StoredAudioFile { + id: string; + name: string; + buffer: ArrayBuffer; + type: string; + dataUri?: string; +} + +export async function saveAudio(file: File): Promise { + const id = crypto.randomUUID(); + const buffer = await file.arrayBuffer(); + + const dataUri = await generateDataURI(buffer, file.type, file.name); + + const current = (await get(STORAGE_KEY)) ?? {}; + current[id] = { + id, + name: file.name, + buffer, + type: file.type, + dataUri + }; + await set(STORAGE_KEY, current); + return id; +} + +export async function getAllAudio(): Promise> { + return (await get(STORAGE_KEY)) ?? {}; +} + +async function generateDataURI(buffer: ArrayBuffer, type: string, name: string): Promise { + try { + let mimeType = type || "audio/mpeg"; + + if (!mimeType || mimeType === "application/octet-stream") { + if (name) { + const extension = name.split(".").pop()?.toLowerCase(); + switch (extension) { + case "ogg": mimeType = "audio/ogg"; break; + case "mp3": mimeType = "audio/mpeg"; break; + case "wav": mimeType = "audio/wav"; break; + case "m4a": + case "mp4": mimeType = "audio/mp4"; break; + case "flac": mimeType = "audio/flac"; break; + case "aac": mimeType = "audio/aac"; break; + case "webm": mimeType = "audio/webm"; break; + case "wma": mimeType = "audio/x-ms-wma"; break; + default: mimeType = "audio/mpeg"; + } + } + } + + const uint8Array = new Uint8Array(buffer); + const blob = new Blob([uint8Array], { type: mimeType }); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (error) { + console.error("[CustomSounds] Error generating data URI:", error); + + const uint8Array = new Uint8Array(buffer); + let binary = ""; + const chunkSize = 8192; + + for (let i = 0; i < uint8Array.length; i += chunkSize) { + const chunk = uint8Array.slice(i, i + chunkSize); + binary += String.fromCharCode(...chunk); + } + + const base64 = btoa(binary); + return `data:${type || "audio/mpeg"};base64,${base64}`; + } +} + +export async function getAudioDataURI(id: string): Promise { + const all = await getAllAudio(); + const entry = all[id]; + if (!entry) return undefined; + + if (entry.dataUri) { + return entry.dataUri; + } + + console.log(`[CustomSounds] No cached data URI for ${id}, generating...`); + const dataUri = await generateDataURI(entry.buffer, entry.type, entry.name); + + const current = await getAllAudio(); + current[id].dataUri = dataUri; + await set(STORAGE_KEY, current); + + return dataUri; +} + +export async function deleteAudio(id: string): Promise { + const all = await getAllAudio(); + delete all[id]; + await set(STORAGE_KEY, all); +} diff --git a/src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx b/src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx deleted file mode 100644 index 4519955ca..000000000 --- a/src/equicordplugins/customSounds/components/SoundOverrideComponent.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { classNameFactory } from "@api/Styles"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; -import { useForceUpdater } from "@utils/react"; -import { findByCodeLazy, findLazy } from "@webpack"; -import { Button, Card, Forms, Slider, Switch, Tooltip, useRef } from "@webpack/common"; -import { ComponentType, Ref, SyntheticEvent } from "react"; - -import { SoundOverride, SoundPlayer, SoundType } from "../types"; - -type FileInput = ComponentType<{ - ref: Ref; - onChange: (event: SyntheticEvent) => void; - multiple?: boolean; - filters?: { name?: string; extensions: string[]; }[]; - disabled?: boolean; -}>; - -const playSound: (id: string) => SoundPlayer = findByCodeLazy(".playWithListener().then"); -const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef); -const cl = classNameFactory("vc-custom-sounds-"); - -export function SoundOverrideComponent({ type, override, onChange }: { type: SoundType; override: SoundOverride; onChange: () => Promise; }) { - const fileInputRef = useRef(null); - const sound = useRef(null); - const update = useForceUpdater(); - - return ( - - { - override.enabled = value; - onChange(); - update(); - }} - className={Margins.bottom16} - hideBorder={true} - > - - {tooltipProps => {type.name}} - - - Sound File -
- {override.url.length === 0 ? "Discord Default" : (override.fileName || "Custom Sound")} - - -
- Volume - { - override.volume = value; - onChange(); - update(); - }} - className={Margins.bottom16} - disabled={!override.enabled} - /> - -
- ); -} diff --git a/src/equicordplugins/customSounds/index.tsx b/src/equicordplugins/customSounds/index.tsx index c910cd10f..e810577c4 100644 --- a/src/equicordplugins/customSounds/index.tsx +++ b/src/equicordplugins/customSounds/index.tsx @@ -1,75 +1,398 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors + * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import "./styles.css"; -import { DataStore } from "@api/index"; +import { get as getFromDataStore } from "@api/DataStore"; import { definePluginSettings } from "@api/Settings"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; +import { classNameFactory } from "@api/Styles"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { Button, Forms, React, showToast, TextInput } from "@webpack/common"; -import { SoundOverrideComponent } from "./components/SoundOverrideComponent"; -import { makeEmptyOverride, SoundOverride, soundTypes } from "./types"; +import { getAllAudio, getAudioDataURI } from "./audioStore"; +import { SoundOverrideComponent } from "./SoundOverrideComponent"; +import { makeEmptyOverride, seasonalSounds, SoundOverride, soundTypes } from "./types"; -const OVERRIDES_KEY = "CustomSounds_overrides"; -let overrides: Record = soundTypes.reduce((result, sound) => ({ ...result, [sound.id]: makeEmptyOverride() }), {}); +const cl = classNameFactory("vc-custom-sounds-"); + +const allSoundTypes = soundTypes || []; + +const AUDIO_STORE_KEY = "ScattrdCustomSounds"; + +const dataUriCache = new Map(); + +function getOverride(id: string): SoundOverride { + const stored = settings.store[id]; + if (!stored) return makeEmptyOverride(); + + if (typeof stored === "object") return stored; + + try { + return JSON.parse(stored); + } catch { + return makeEmptyOverride(); + } +} + +function setOverride(id: string, override: SoundOverride) { + settings.store[id] = JSON.stringify(override); +} + +export function getCustomSoundURL(id: string): string | null { + const override = getOverride(id); + + if (!override?.enabled) { + return null; + } + + if (override.selectedSound === "custom" && override.selectedFileId) { + const dataUri = dataUriCache.get(override.selectedFileId); + if (dataUri) { + console.log(`[CustomSounds] Returning cached data URI for ${id}`); + return dataUri; + } else { + console.warn(`[CustomSounds] No cached data URI for ${id} with file ID ${override.selectedFileId}`); + return null; + } + } + + if (override.selectedSound !== "default" && override.selectedSound !== "custom") { + if (override.selectedSound in seasonalSounds) { + return seasonalSounds[override.selectedSound]; + } + + const soundType = allSoundTypes.find(t => t.id === id); + if (soundType?.seasonal) { + const seasonalId = soundType.seasonal.find(seasonalId => + seasonalId.startsWith(`${override.selectedSound}_`) + ); + if (seasonalId && seasonalId in seasonalSounds) { + return seasonalSounds[seasonalId]; + } + } + } + + return null; +} + +export async function ensureDataURICached(fileId: string): Promise { + if (dataUriCache.has(fileId)) { + return dataUriCache.get(fileId)!; + } + + try { + const dataUri = await getAudioDataURI(fileId); + if (dataUri) { + dataUriCache.set(fileId, dataUri); + console.log(`[CustomSounds] Cached data URI for file ${fileId}`); + return dataUri; + } + } catch (error) { + console.error(`[CustomSounds] Error generating data URI for ${fileId}:`, error); + } + + return null; +} + +export async function refreshDataURI(id: string): Promise { + const override = getOverride(id); + if (!override?.selectedFileId) { + console.log(`[CustomSounds] refreshDataURI called for ${id} but no selectedFileId`); + return; + } + + console.log(`[CustomSounds] Refreshing data URI for ${id} with file ID ${override.selectedFileId}`); + + const dataUri = await ensureDataURICached(override.selectedFileId); + if (dataUri) { + console.log(`[CustomSounds] Successfully cached data URI for ${id} (length: ${dataUri.length})`); + } else { + console.error(`[CustomSounds] Failed to cache data URI for ${id}`); + } +} + +async function preloadDataURIs() { + console.log("[CustomSounds] Preloading data URIs into memory cache..."); + + for (const soundType of allSoundTypes) { + const override = getOverride(soundType.id); + if (override?.enabled && override.selectedSound === "custom" && override.selectedFileId) { + try { + await ensureDataURICached(override.selectedFileId); + console.log(`[CustomSounds] Preloaded data URI for ${soundType.id}`); + } catch (error) { + console.error(`[CustomSounds] Failed to preload data URI for ${soundType.id}:`, error); + } + } + } + + console.log(`[CustomSounds] Memory cache contains ${dataUriCache.size} data URIs`); +} + +export async function debugCustomSounds() { + console.log("[CustomSounds] === DEBUG INFO ==="); + + const rawDataStore = await getFromDataStore(AUDIO_STORE_KEY); + console.log("[CustomSounds] Raw DataStore content:", rawDataStore); + + const allFiles = await getAllAudio(); + console.log(`[CustomSounds] Stored files: ${Object.keys(allFiles).length}`); + + let totalBufferSize = 0; + let totalDataUriSize = 0; + + for (const [id, file] of Object.entries(allFiles)) { + const bufferSize = file.buffer?.byteLength || 0; + const dataUriSize = file.dataUri?.length || 0; + totalBufferSize += bufferSize; + totalDataUriSize += dataUriSize; + + console.log(`[CustomSounds] File ${id}:`, { + name: file.name, + type: file.type, + bufferSize: `${(bufferSize / 1024).toFixed(1)}KB`, + hasValidBuffer: file.buffer instanceof ArrayBuffer, + hasDataUri: !!file.dataUri, + dataUriSize: `${(dataUriSize / 1024).toFixed(1)}KB` + }); + } + + console.log(`[CustomSounds] Total storage - Buffers: ${(totalBufferSize / 1024).toFixed(1)}KB, DataURIs: ${(totalDataUriSize / 1024).toFixed(1)}KB`); + + console.log(`[CustomSounds] Memory cache contains ${dataUriCache.size} data URIs`); + + console.log("[CustomSounds] Settings store structure:", Object.keys(settings.store)); + + console.log("[CustomSounds] Sound override status:"); + let enabledCount = 0; + let totalSettingsSize = 0; + + for (const [soundId, storedValue] of Object.entries(settings.store)) { + if (soundId === "overrides") continue; + + const override = getOverride(soundId); + const settingsSize = JSON.stringify(override).length; + totalSettingsSize += settingsSize; + + console.log(`[CustomSounds] ${soundId}:`, { + enabled: override.enabled, + selectedSound: override.selectedSound, + selectedFileId: override.selectedFileId, + volume: override.volume, + settingsSize: `${settingsSize}B` + }); + + if (override.enabled) enabledCount++; + } + + console.log(`[CustomSounds] Total enabled overrides: ${enabledCount}`); + console.log(`[CustomSounds] Estimated settings size: ${(totalSettingsSize / 1024).toFixed(1)}KB`); + console.log("[CustomSounds] === END DEBUG ==="); +} + +const soundSettings = Object.fromEntries( + allSoundTypes.map(type => [ + type.id, + { + type: OptionType.STRING, + description: `Override for ${type.name}`, + default: JSON.stringify(makeEmptyOverride()), + hidden: true + } + ]) +); const settings = definePluginSettings({ + ...soundSettings, overrides: { type: OptionType.COMPONENT, description: "", - component: () => - <> - {soundTypes.map(type => - DataStore.set(OVERRIDES_KEY, overrides)} - /> - )} - + component: () => { + const [resetTrigger, setResetTrigger] = React.useState(0); + const [searchQuery, setSearchQuery] = React.useState(""); + const fileInputRef = React.useRef(null); + + React.useEffect(() => { + allSoundTypes.forEach(type => { + if (!settings.store[type.id]) { + setOverride(type.id, makeEmptyOverride()); + } + }); + }, []); + + const resetOverrides = () => { + allSoundTypes.forEach(type => { + setOverride(type.id, makeEmptyOverride()); + }); + dataUriCache.clear(); + setResetTrigger(prev => prev + 1); + showToast("All overrides reset successfully!"); + }; + + const triggerFileUpload = () => { + fileInputRef.current?.click(); + }; + + const handleSettingsUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = async (e: ProgressEvent) => { + try { + resetOverrides(); + const imported = JSON.parse(e.target?.result as string); + + if (imported.overrides && Array.isArray(imported.overrides)) { + imported.overrides.forEach((setting: any) => { + if (setting.id) { + const override: SoundOverride = { + enabled: setting.enabled ?? false, + selectedSound: setting.selectedSound ?? "default", + selectedFileId: setting.selectedFileId ?? undefined, + volume: setting.volume ?? 100, + useFile: false + }; + setOverride(setting.id, override); + } + }); + } + + setResetTrigger(prev => prev + 1); + showToast("Settings imported successfully!"); + } catch (error) { + console.error("Error importing settings:", error); + showToast("Error importing settings. Check console for details."); + } + }; + + reader.readAsText(file); + event.target.value = ""; + } + }; + + const downloadSettings = async () => { + const overrides = allSoundTypes.map(type => { + const override = getOverride(type.id); + return { + id: type.id, + enabled: override.enabled, + selectedSound: override.selectedSound, + selectedFileId: override.selectedFileId ?? undefined, + volume: override.volume + }; + }).filter(o => o.enabled || o.selectedSound !== "default"); + + const exportPayload = { + overrides, + __note: "Audio files are not included in exports and will need to be re-uploaded after import" + }; + + const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "customSounds-settings.json"; + a.click(); + URL.revokeObjectURL(url); + + showToast(`Exported ${overrides.length} settings (audio files not included)`); + }; + + const filteredSoundTypes = allSoundTypes.filter(type => + type.name.toLowerCase().includes(searchQuery.toLowerCase()) || + type.id.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( +
+
+ + + + + +
+ +
+ Search Sounds + setSearchQuery(e)} + placeholder="Search by name or ID" + /> +
+ +
+ {filteredSoundTypes.map(type => { + const currentOverride = getOverride(type.id); + + return ( + { + + setOverride(type.id, currentOverride); + + if (currentOverride.enabled && currentOverride.selectedSound === "custom" && currentOverride.selectedFileId) { + try { + await ensureDataURICached(currentOverride.selectedFileId); + } catch (error) { + console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error); + showToast("Error loading custom sound file"); + } + } + + console.log(`[CustomSounds] Settings saved for ${type.id}:`, currentOverride); + }} + /> + ); + })} +
+
+ ); + } } }); export function isOverriden(id: string): boolean { - return overrides[id]?.enabled ?? false; + return !!getOverride(id)?.enabled; } export function findOverride(id: string): SoundOverride | null { - const result = overrides[id]; - if (!result?.enabled) - return null; - - return result; + const override = getOverride(id); + return override?.enabled ? override : null; } export default definePlugin({ name: "CustomSounds", - description: "Replace Discord's sounds with your own.", - authors: [Devs.TheKodeToad, EquicordDevs.SpikeHD], + description: "Customize Discord's sounds.", + authors: [Devs.ScattrdBlade, Devs.TheKodeToad], patches: [ - // sound class { find: 'Error("could not play audio")', replacement: [ - // override URL { - match: /(?<=new Audio;\i\.src=)\i\([0-9]+\)\(.{0,20}\(this\.name,"\.mp3"\)/, - replace: "$self.findOverride(this.name)?.url || $&" + match: /(?<=new Audio;\i\.src=).{0,50}.concat\(this\.name,"\.mp3"\)\)/, + replace: "$self.getSoundUrl(this.name,$&)" }, - // override volume { match: /Math.min\(\i\.\i\.getOutputVolume\(\).{0,20}volume/, replace: "$& * ($self.findOverride(this.name)?.volume ?? 100) / 100" } ] }, - // force classic soundpack for overriden sounds { find: ".playWithListener().then", replacement: { @@ -78,12 +401,27 @@ export default definePlugin({ } } ], + getSoundUrl(name, extra) { + const customUrl = this.getCustomSoundURL(name); + return customUrl || extra; + }, settings, findOverride, isOverriden, + getCustomSoundURL, + refreshDataURI, + ensureDataURICached, + debugCustomSounds, + startAt: StartAt.Init, + async start() { - overrides = await DataStore.get(OVERRIDES_KEY) ?? {}; - for (const type of soundTypes) - overrides[type.id] ??= makeEmptyOverride(); + console.log("[CustomSounds] Plugin starting..."); + + try { + await preloadDataURIs(); + console.log("[CustomSounds] Startup complete"); + } catch (error) { + console.error("[CustomSounds] Startup failed:", error); + } } }); diff --git a/src/equicordplugins/customSounds/styles.css b/src/equicordplugins/customSounds/styles.css index f5ece128a..2e19c7a4f 100644 --- a/src/equicordplugins/customSounds/styles.css +++ b/src/equicordplugins/customSounds/styles.css @@ -2,17 +2,58 @@ padding: 1em 1em 0; } -.vc-custom-sounds-file { - display: flex; - align-items: center; - gap: 0.5em; +.vc-custom-sounds-id { + color: var(--text-muted); } -.vc-custom-sounds-file-name { - flex-grow: 1; -} - -.vc-custom-sounds-file-replace, -.vc-custom-sounds-file-reset { +.vc-custom-sounds-upload { display: inline; } + +.vc-custom-sounds-seasonal-title { + margin-top: 16px; + padding-top: 8px; + border-top: 1px solid var(--background-modifier-accent); +} + +.vc-custom-sounds-sound-section { + margin-bottom: 16px; +} + +.vc-custom-sounds-url-input { + width: 100%; + padding: 8px; + background: var(--input-background); + border: none; + border-radius: 3px; + color: var(--text-normal); +} + +.vc-custom-sounds-buttons { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.vc-custom-sounds-search { + margin-bottom: 16px; +} + +.vc-custom-sounds-search input { + width: 100%; + padding: 8px; + background: var(--input-background); + border: none; + border-radius: 3px; + color: var(--text-normal); +} + +.vc-custom-sounds-sounds-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.vc-custom-sounds-preview-controls { + display: flex; +} \ No newline at end of file diff --git a/src/equicordplugins/customSounds/types.ts b/src/equicordplugins/customSounds/types.ts index 9bb76e0d2..570a8b838 100644 --- a/src/equicordplugins/customSounds/types.ts +++ b/src/equicordplugins/customSounds/types.ts @@ -1,19 +1,21 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors + * Copyright (c) 2025 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ export interface SoundType { name: string; id: string; + seasonal?: string[]; } export interface SoundOverride { enabled: boolean; - fileName: string; - url: string; + selectedSound: string; volume: number; + useFile: boolean; + selectedFileId?: string; } export interface SoundPlayer { @@ -23,37 +25,102 @@ export interface SoundPlayer { stop(): void; } +export const seasonalSounds = { + "halloween_call_calling": "https://canary.discord.com/assets/0950a7ea4f1dd037870b.mp3", + "winter_call_calling": "https://canary.discord.com/assets/7b945e7be3f86c5b7c82.mp3", + "halloween_call_ringing": "https://canary.discord.com/assets/1b883b366ae11a303b82.mp3", + "winter_call_ringing": "https://canary.discord.com/assets/e087eb83aaa4c43a44bc.mp3", + "call_ringing_beat": "https://canary.discord.com/assets/3b3a2f5f29b9cb656efb.mp3", + "call_ringing_snow_halation": "https://canary.discord.com/assets/99b1d8a6fe0b95e99827.mp3", + "call_ringing_snowsgiving": "https://canary.discord.com/assets/54527e70cf0ddaeff76f.mp3", + "halloween_deafen": "https://canary.discord.com/assets/c4aedda3b528df50221c.mp3", + "winter_deafen": "https://canary.discord.com/assets/9bb77985afdb60704817.mp3", + "halloween_disconnect": "https://canary.discord.com/assets/ca7d2e46cb5a16819aff.mp3", + "winter_disconnect": "https://canary.discord.com/assets/ec5d85405877c27caeda.mp3", + "halloween_message1": "https://canary.discord.com/assets/e386c839fb98675c6a79.mp3", + "halloween_mute": "https://canary.discord.com/assets/ee7fdadf4c714eed6254.mp3", + "winter_mute": "https://canary.discord.com/assets/6d7616e08466ab9f1c6d.mp3", + "halloween_undeafen": "https://canary.discord.com/assets/045e5b9608df1607e0cf.mp3", + "winter_undeafen": "https://canary.discord.com/assets/fa8da1499894ecac36c7.mp3", + "halloween_unmute": "https://canary.discord.com/assets/260c581568eacca03f7e.mp3", + "winter_unmute": "https://canary.discord.com/assets/9dbfb1c211e3815cd7b1.mp3", + "halloween_user_join": "https://canary.discord.com/assets/80cf806f45467a5898cd.mp3", + "winter_user_join": "https://canary.discord.com/assets/badc42c2a9063b4a962c.mp3", + "halloween_user_leave": "https://canary.discord.com/assets/f407ad88a1dc40541769.mp3", + "winter_user_leave": "https://canary.discord.com/assets/ec3d9eaea30b33e16da6.mp3" +} as const; + export const soundTypes: readonly SoundType[] = [ - { name: "Message", id: "message1" }, - { name: "Message (Focused Channel)", id: "message3" }, - { name: "Defean", id: "deafen" }, - { name: "Undefean", id: "undeafen" }, - { name: "Mute", id: "mute" }, - { name: "Unmute", id: "unmute" }, - { name: "Voice Disconnected", id: "disconnect" }, - { name: "PTT Activate", id: "ptt_start" }, - { name: "PTT Deactive", id: "ptt_stop" }, - { name: "User Join", id: "user_join" }, - { name: "User Leave", id: "user_leave" }, - { name: "User Moved", id: "user_moved" }, - { name: "Outgoing Ring", id: "call_calling" }, - { name: "Incoming Ring", id: "call_ringing" }, - { name: "Stream Started", id: "stream_started" }, - { name: "Stream Ended", id: "stream_ended" }, - { name: "Viewer Join", id: "stream_user_joined" }, - { name: "Viewer Leave", id: "stream_user_left" }, - { name: "Activity Start", id: "activity_launch" }, { name: "Activity End", id: "activity_end" }, + { name: "Activity Launch", id: "activity_launch" }, { name: "Activity User Join", id: "activity_user_join" }, - { name: "Activity User Leave", id: "activity_user_left" }, - { name: "Invited to Speak", id: "reconnect" } + { name: "Activity User Left", id: "activity_user_left" }, + { name: "ASMR Message", id: "asmr_message1" }, + { name: "Bit Message", id: "bit_message1" }, + { name: "Bop Message", id: "bop_message1" }, + { name: "Call Calling", id: "call_calling", seasonal: ["halloween_call_calling", "winter_call_calling"] }, + { + name: "Call Ringing", + id: "call_ringing", + seasonal: [ + "halloween_call_ringing", + "winter_call_ringing", + "call_ringing_beat", + "call_ringing_snow_halation", + "call_ringing_snowsgiving" + ] + }, + { name: "Clip Error", id: "clip_error" }, + { name: "Clip Save", id: "clip_save" }, + { name: "DDR Down", id: "ddr-down" }, + { name: "DDR Left", id: "ddr-left" }, + { name: "DDR Right", id: "ddr-right" }, + { name: "DDR Up", id: "ddr-up" }, + { name: "Deafen", id: "deafen", seasonal: ["halloween_deafen", "winter_deafen"] }, + { name: "Discodo", id: "discodo" }, + { name: "Disconnect", id: "disconnect", seasonal: ["halloween_disconnect", "winter_disconnect"] }, + { name: "Ducky Message", id: "ducky_message1" }, + { name: "Hang Status Select", id: "hang_status_select" }, + { name: "Highfive Clap", id: "highfive_clap" }, + { name: "Highfive Whistle", id: "highfive_whistle" }, + { name: "Human Man", id: "human_man" }, + { name: "LoFi Message", id: "lofi_message1" }, + { name: "Mention 1", id: "mention1" }, + { name: "Mention 2", id: "mention2" }, + { name: "Mention 3", id: "mention3" }, + { name: "Message 1", id: "message1", seasonal: ["halloween_message1"] }, + { name: "Message 2", id: "message2" }, + { name: "Message 3", id: "message3" }, + { name: "Mute", id: "mute", seasonal: ["halloween_mute", "winter_mute"] }, + { name: "Overlay Unlock", id: "overlayunlock" }, + { name: "Poggermode Achievement", id: "poggermode_achievement_unlock" }, + { name: "Poggermode Applause", id: "poggermode_applause" }, + { name: "Poggermode Enabled", id: "poggermode_enabled" }, + { name: "Poggermode Message", id: "poggermode_message_send" }, + { name: "PTT Start", id: "ptt_start" }, + { name: "PTT Stop", id: "ptt_stop" }, + { name: "Reconnect", id: "reconnect" }, + { name: "Robot Man", id: "robot_man" }, + { name: "Stage Waiting", id: "stage_waiting" }, + { name: "Stream Ended", id: "stream_ended" }, + { name: "Stream Started", id: "stream_started" }, + { name: "Stream User Joined", id: "stream_user_joined" }, + { name: "Stream User Left", id: "stream_user_left" }, + { name: "Success", id: "success" }, + { name: "Undeafen", id: "undeafen", seasonal: ["halloween_undeafen", "winter_undeafen"] }, + { name: "Unmute", id: "unmute", seasonal: ["halloween_unmute", "winter_unmute"] }, + { name: "User Join", id: "user_join", seasonal: ["halloween_user_join", "winter_user_join"] }, + { name: "User Leave", id: "user_leave", seasonal: ["halloween_user_leave", "winter_user_leave"] }, + { name: "User Moved", id: "user_moved" }, + { name: "Vibing Wumpus", id: "vibing_wumpus" } ] as const; export function makeEmptyOverride(): SoundOverride { return { enabled: false, - fileName: "", - url: "", - volume: 100 + selectedSound: "default", + volume: 100, + useFile: false, + selectedFileId: undefined }; } diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index b61181a9a..133c0272a 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -801,8 +801,8 @@ export default definePlugin({ replacement: [ { // Enables external audio sources for playing audio. - match: /(\i\(\d+\)\(".\/".concat\(this.name,".mp3"\)\))/, - replace: "this.name.startsWith('https')?this.name:$1" + match: /(?<=new Audio;\i\.src=)/, + replace: "this.name.startsWith('https')?this.name:" }, { // Adds an optional callback to the audio player. This is needed to detect From c075216af3103809fc1ac95caf3a016fd8c5396e Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:57:05 -0700 Subject: [PATCH 024/881] Check for ENHANCED_ROLE_COLORS in guild features. (#346) --- src/plugins/showMeYourName/index.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index b469fb636..05bd9bd74 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -11,7 +11,7 @@ import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { GuildMember, Message, User } from "@vencord/discord-types"; import { findByCodeLazy, findStoreLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, MessageStore, RelationshipStore, StreamerModeStore, UserStore } from "@webpack/common"; +import { ChannelStore, GuildMemberStore, GuildStore, MessageStore, RelationshipStore, StreamerModeStore, UserStore } from "@webpack/common"; import { JSX } from "react"; const wrapEmojis = findByCodeLazy("lastIndex;return"); @@ -67,7 +67,7 @@ function validColor(color: string) { return isValid; } -function resolveColor(user: User | GuildMember, savedColor: string, fallbackColor: string): any { +function resolveColor(user: User | GuildMember, savedColor: string, fallbackColor: string, canUseGradient: boolean): any { const fallbackReturn = { normal: { original: { @@ -119,7 +119,7 @@ function resolveColor(user: User | GuildMember, savedColor: string, fallbackColo } } - gradient = !secondaryColor + gradient = !canUseGradient || !secondaryColor ? null : tertiaryColor ? "linear-gradient(to right,var(--custom-gradient-color-1),var(--custom-gradient-color-2),var(--custom-gradient-color-3),var(--custom-gradient-color-1))" @@ -356,12 +356,13 @@ function renderUsername( const config = hookless ? settings.store : settings.use(); const { messages, replies, mentions, memberList, profilePopout, reactions, discriminators, hideDefaultAtSign, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, animateGradients, includedNames, friendNameColor, nicknameColor, displayNameColor, usernameColor, triggerNameRerender } = config; + const canUseGradient = ((author as GuildMember)?.guildId ? (GuildStore.getGuild((author as GuildMember).guildId) ?? {}).premiumFeatures?.features.includes("ENHANCED_ROLE_COLORS") : false); const textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d"; const options = splitTemplate(includedNames); - const resolvedUsernameColor = author ? resolveColor(author, usernameColor.trim(), "") : null; - const resolvedDisplayNameColor = author ? resolveColor(author, displayNameColor.trim(), "") : null; - const resolvedNicknameColor = author ? resolveColor(author, nicknameColor.trim(), "") : null; - const resolvedFriendNameColor = author ? resolveColor(author, friendNameColor.trim(), "") : null; + const resolvedUsernameColor = author ? resolveColor(author, usernameColor.trim(), "", canUseGradient) : null; + const resolvedDisplayNameColor = author ? resolveColor(author, displayNameColor.trim(), "", canUseGradient) : null; + const resolvedNicknameColor = author ? resolveColor(author, nicknameColor.trim(), "", canUseGradient) : null; + const resolvedFriendNameColor = author ? resolveColor(author, friendNameColor.trim(), "", canUseGradient) : null; const affixColor = { color: textMutedValue, "-webkit-text-fill-color": textMutedValue, isolation: "isolate" }; const [username, display, nick, friend] = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators); @@ -481,7 +482,7 @@ function renderUsername( third = remainingNames.shift(); fourth = remainingNames.shift(); - const topRoleStyle = isMention || isReactionsPopout ? resolveColor(author, "Role", "") : null; + const topRoleStyle = isMention || isReactionsPopout ? resolveColor(author, "Role", "", canUseGradient) : null; const hasGradient = !!topRoleStyle?.gradient && Object.keys(topRoleStyle.gradient).length > 0; const message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null; const groupId = (message as any)?.showMeYourNameGroupId || null; From 563d6f3f07dde14f98776eb1976112a2e3430799 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 15 Aug 2025 14:02:29 -0400 Subject: [PATCH 025/881] WhosWatching: Fix Patch --- src/equicordplugins/whosWatching/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/whosWatching/index.tsx b/src/equicordplugins/whosWatching/index.tsx index c7e77211a..b7b805fb0 100644 --- a/src/equicordplugins/whosWatching/index.tsx +++ b/src/equicordplugins/whosWatching/index.tsx @@ -84,7 +84,7 @@ export default definePlugin({ { find: "this.renderEmbeddedActivity()", replacement: { - match: /"div"(?=.{0,100}this.renderActions)/, + match: /"div"(?=.{0,50}this.renderActions)/, replace: "$self.WrapperComponent" } } From 7332f1b59be7910e815eb2ce9820e56b957c8ff3 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 15 Aug 2025 15:45:33 -0400 Subject: [PATCH 026/881] CustomSounds: Update Patch Char Count --- src/equicordplugins/customSounds/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/customSounds/index.tsx b/src/equicordplugins/customSounds/index.tsx index e810577c4..3458a0554 100644 --- a/src/equicordplugins/customSounds/index.tsx +++ b/src/equicordplugins/customSounds/index.tsx @@ -384,7 +384,7 @@ export default definePlugin({ find: 'Error("could not play audio")', replacement: [ { - match: /(?<=new Audio;\i\.src=).{0,50}.concat\(this\.name,"\.mp3"\)\)/, + match: /(?<=new Audio;\i\.src=).{0,75}.concat\(this\.name,"\.mp3"\)\)/, replace: "$self.getSoundUrl(this.name,$&)" }, { From 48c79d612d4c8f0dfa2c1e8d1e6427a70ee31638 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:59:02 -0400 Subject: [PATCH 027/881] Fix Patches: settings.store.showWithoutHover, replacement: [ { - match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, + match: /function\(\)\{.+:""(?=.*?userId:(\i))/, replace: "$&,$self.renderTimer($1.id)," } ] diff --git a/src/equicordplugins/noModalAnimation/index.ts b/src/equicordplugins/noModalAnimation/index.ts index f27dc029f..17433d8dd 100644 --- a/src/equicordplugins/noModalAnimation/index.ts +++ b/src/equicordplugins/noModalAnimation/index.ts @@ -36,8 +36,8 @@ export default definePlugin({ { find: "renderLurkerModeUpsellPopout,position:", replacement: { - match: /(\i)=(200|300)/g, - replace: "$1=0", + match: /200:300/g, + replace: "0:0", }, } ] diff --git a/src/equicordplugins/searchFix/index.tsx b/src/equicordplugins/searchFix/index.tsx index c1ae81917..e45e4cd16 100644 --- a/src/equicordplugins/searchFix/index.tsx +++ b/src/equicordplugins/searchFix/index.tsx @@ -43,7 +43,7 @@ export default definePlugin({ authors: [EquicordDevs.Jaxx], patches: [ { - find: '"SearchQueryStore");', + find: '"SearchQueryStore";', replacement: { match: /\i\.searchResultsQuery=(\i)/, replace: "$&,$self.main($1)" From d35b8927eda8ab0ce30e73684faf90a00cefa484 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:59:48 -0400 Subject: [PATCH 028/881] Fix Timezones Patch --- src/equicordplugins/timezones/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/timezones/index.tsx b/src/equicordplugins/timezones/index.tsx index 4390c7fba..abc60abb2 100644 --- a/src/equicordplugins/timezones/index.tsx +++ b/src/equicordplugins/timezones/index.tsx @@ -233,8 +233,8 @@ export default definePlugin({ { find: 'backgroundColor:"COMPLETE"', replacement: { - match: /(?<=backgroundImage.+?)children:\[/, - replace: "$&$self.renderProfileTimezone(arguments[0])," + match: /(?<=backgroundImage.+?children:)!\i.{0,100}gifTag\}\)/, + replace: "[$self.renderProfileTimezone(arguments[0]),$&]" } }, { From b50e785a3ba7b63db73e48347a67d86541bbeccf Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Sat, 16 Aug 2025 04:52:18 -0700 Subject: [PATCH 029/881] Match icon size to other icons. (#347) --- src/plugins/viewRaw/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/viewRaw/index.tsx b/src/plugins/viewRaw/index.tsx index e6781784d..1dcef31b2 100644 --- a/src/plugins/viewRaw/index.tsx +++ b/src/plugins/viewRaw/index.tsx @@ -32,7 +32,7 @@ import { Button, ChannelStore, Forms, GuildRoleStore, Menu, Text } from "@webpac const CopyIcon = () => { - return
+ + {defaultDirectory || "No Directory Set"} + +
+ + +
+
+ + + ); +} + +export const settings = definePluginSettings({ + displayStatus: { + description: "Display a status notification when downloads start, finish, or error.", + type: OptionType.BOOLEAN, + default: true, + }, + statusDuration: { + type: OptionType.SLIDER, + description: "The number of seconds to display status notifications.", + markers: [1, 3, 5], + default: 2.5, + stickToMarkers: false, + }, + voiceMessages: { + description: "Add a download button to voice messages.", + type: OptionType.BOOLEAN, + default: true, + }, + allowUnicode: { + description: "Allow non-ASCII characters in file names.", + type: OptionType.BOOLEAN, + default: true, + }, + overwriteFiles: { + description: "If a default directory is set and a download file name matches an existing file, overwrite the file. If disabled, a number will be appended to the file name.", + type: OptionType.BOOLEAN, + default: false, + }, + defaultDirectory: { + component: DefaultDirectorySetting, + type: OptionType.COMPONENT, + default: "", + }, +}); diff --git a/src/equicordplugins/downloadify/style.css b/src/equicordplugins/downloadify/style.css new file mode 100644 index 000000000..2b7f3df5e --- /dev/null +++ b/src/equicordplugins/downloadify/style.css @@ -0,0 +1,77 @@ +.downloadify-voice-message-container { + flex-shrink: 0; + align-items: center; + display: flex; + flex-direction: column; + justify-content: flex-end; + position: relative; + margin: 0 -2px; +} + +.downloadify-voice-message-button { + cursor: pointer; + line-height: 0; + border: none; + width: auto; + background: transparent; + color: var(--interactive-normal); + margin: 0; + padding: 0; + align-items: center; + border-radius: 8px; + box-sizing: border-box; + display: flex; + font-size: 14px; + font-weight: var(--font-weight-medium); + justify-content: center; + position: relative; + transition-duration: .2s; + user-select: none; +} + +.downloadify-voice-message-button:hover { + color: var(--interactive-hover); +} + +.downloadify-directory-container { + margin-top: 10px; +} + +.downloadify-directory-buttons { + margin-top: 10px; + display: flex; + gap: 10px; +} + +.downloadify-directory-button { + border-radius: 4px; + min-height: 34px; + color: white; + height: 34px; + flex: 1; +} + +.downloadify-directory-button:not(.downloadify-disabled):hover { + filter: brightness(0.75); + transition: filter 0.15s ease-in-out; +} + +.downloadify-clear-button { + background-color: rgb(180 71 78); +} + +.downloadify-browse-button { + background-color: rgb(87 100 240); +} + +.downloadify-directory-display { + background-color: var(--background-base-lower); + overflow-wrap: break-word; + font-family: monospace; + word-wrap: break-word; + white-space: pre-wrap; + border-radius: 4px; + user-select: text; + font-size: 14px; + padding: 8px; +} \ No newline at end of file diff --git a/src/equicordplugins/downloadify/utils/definitions.tsx b/src/equicordplugins/downloadify/utils/definitions.tsx new file mode 100644 index 000000000..8f65a964a --- /dev/null +++ b/src/equicordplugins/downloadify/utils/definitions.tsx @@ -0,0 +1,371 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { classNameFactory } from "@api/Styles"; +import { Logger } from "@utils/Logger"; +import { PluginNative } from "@utils/types"; +import { Channel, Guild, GuildMember, Message, User } from "@vencord/discord-types"; +import { findByPropsLazy } from "@webpack"; +import { JSX } from "react"; + +export const d = classNameFactory("downloadify-"); +export const CDN_BASE = "https://cdn.discordapp.com"; +export const MEDIA_PROXY_BASE = "https://media.discordapp.net"; +export const PRIMARY_DOMAIN_BASE = "https://discord.com"; +export const IMAGE_EXT_1_DOMAIN_BASE = "https://images-ext-1.discordapp.net"; +export const IMAGE_EXT_2_DOMAIN_BASE = "https://images-ext-2.discordapp.net"; +export const TENOR_BASE = "https://c.tenor.com"; +export const TENOR_GIF_ID = "Ad"; +export const TENOR_MP4_ID = "Po"; +export const ASSET_TYPE_EXTRACTOR = new RegExp(/^https:\/\/(?:cdn\.discordapp\.com|media.discordapp.net)\/([a-z-]+(?:\/[a-z-]+){0,2})(?:(?:\/\d+\/[a-z-]+\/\d+\/([a-z-]+))|(?:\/([a-z-]+(?:\/[a-z-_]+)?)))?/i); +export const DownloadifyLogger = new Logger("Downloadify"); +export const DownloadifyNative = VencordNative.pluginHelpers.Downloadify as PluginNative; +export const defaultAssets = findByPropsLazy("DEFAULT_GROUP_DM_AVATARS") as DefaultAssets; + +interface DefaultAssets { + BOT_AVATARS: Record; + DEFAULT_AVATARS: string[]; + DEFAULT_CHANNEL_ICON: string; + DEFAULT_GROUP_DM_AVATARS: string[]; + DEFAULT_PROVISIONAL_AVATARS: string[]; +} + +export interface AssetInfo { + source: AssetSource; + static: string[]; + animated: string[]; + type?: string; + forceAnimated?: boolean; +} + +export enum AssetSource { + CDN = "cdn", + EXTERNAL_IMAGE_PROXY = "external_image_proxy", + ATTACHMENT_MEDIA_PROXY = "attachment_media_proxy", + ASSET_MEDIA_PROXY = "asset_media_proxy", + PRIMARY_DOMAIN = "primary_domain", + EXTERNAL = "external", + TENOR = "tenor" +} + +export const unknownExternal = { + source: AssetSource.EXTERNAL, + static: [], + animated: [] +}; + +export const unknownPrimaryDomain = { + source: AssetSource.PRIMARY_DOMAIN, + static: [], + animated: [] +}; + +export const unknownCDN = { + source: AssetSource.CDN, + static: [], + animated: [] +}; + +export const unknownAssetMediaProxy = { + source: AssetSource.ASSET_MEDIA_PROXY, + static: [], + animated: [] +}; + +export const unknownAttachmentMediaProxy = { + source: AssetSource.ATTACHMENT_MEDIA_PROXY, + static: [], + animated: [] +}; + +export const unknownExternalImageProxy = { + source: AssetSource.EXTERNAL_IMAGE_PROXY, + static: [], + animated: [] +}; + +export const defaultResourceAvailability = { + source: AssetSource.PRIMARY_DOMAIN, + static: ["png"], + animated: [] +}; + +export const APNGAvailability = { + source: AssetSource.ASSET_MEDIA_PROXY, + static: ["png", "webp", "jpg"], + animated: ["apng", "png", "webp", "jpg"] +}; + +export const hashableAvailability = { + source: AssetSource.ASSET_MEDIA_PROXY, + static: ["png", "webp", "jpg"], + animated: ["gif", "awebp", "png", "webp", "jpg"] +}; + +// Defines known asset type availability. Unknown types which may be +// encountered are AssetSource.EXTERNAL which are present in some embeds, +// as well as OTHER attachment types. +export const assetAvailability = { + "user-avatar": hashableAvailability, + "user-banner": hashableAvailability, + "avatar-decoration": APNGAvailability, + "clan-badge": { + source: AssetSource.ASSET_MEDIA_PROXY, + static: ["png"], + animated: [] + }, + "nameplate": { + source: AssetSource.CDN, + static: ["png"], + animated: ["webm", "apng", "png"], + type: "nameplate" + }, + "default-user-avatar": defaultResourceAvailability, + "default-group-icon": defaultResourceAvailability, + "role-icon-custom": { + source: AssetSource.ASSET_MEDIA_PROXY, + static: ["png", "webp", "jpg"], + animated: [], + }, + "role-icon-unicode-emoji": { + source: AssetSource.PRIMARY_DOMAIN, + static: ["svg"], + animated: [], + }, + "guild-icon": hashableAvailability, + "guild-banner": hashableAvailability, + "guild-invite-splash": hashableAvailability, + "guild-discovery-splash": hashableAvailability, + "custom-emoji": hashableAvailability, + "unicode-emoji": { + source: AssetSource.PRIMARY_DOMAIN, + static: ["svg"], + animated: [] + }, + "sticker": { + "APNG": APNGAvailability, + "PNG": { + source: AssetSource.ASSET_MEDIA_PROXY, + static: ["png", "webp", "jpg"], + animated: [] + }, + "GIF": { + source: AssetSource.ASSET_MEDIA_PROXY, + static: [], + animated: ["gif", "awebp", "png", "webp", "jpg"], + forceAnimated: true + }, + "LOTTIE": { + source: AssetSource.CDN, + static: ["json"], + animated: ["json"] + }, + }, + "tenor": { + source: AssetSource.TENOR, + static: [], + animated: ["gif", "mp4"], + forceAnimated: true + }, + "external": { + "image/png": { + source: AssetSource.EXTERNAL_IMAGE_PROXY, + static: ["png", "webp", "jpg"], + animated: [] + }, + "image/webp": { + source: AssetSource.EXTERNAL_IMAGE_PROXY, + static: ["png", "webp", "jpg"], + animated: [] + }, + "image/jpeg": { + source: AssetSource.EXTERNAL_IMAGE_PROXY, + static: ["png", "webp", "jpg"], + animated: [] + }, + "video/mp4": { + source: AssetSource.EXTERNAL, + static: [], + animated: ["mp4"], + forceAnimated: true + }, + }, + "attachment": { + // Image attachments over a certain size and/or resolution are treated as + // non-image files and are therefore not available in alternative formats. + // This is designated by an attachment type of OTHER instead of IMAGE. + "image/png": { + source: AssetSource.ATTACHMENT_MEDIA_PROXY, + static: ["png", "webp", "jpg"], + // APNG attachments are technically supported, but appear to get + // broken by Discord squashing them into static PNGs. APNG is kept + // here in case it is fixed some day. + animated: ["apng", "png", "webp", "jpg"] + }, + "image/webp": { + source: AssetSource.ATTACHMENT_MEDIA_PROXY, + static: ["webp", "png", "jpg"], + animated: ["awebp", "webp", "png", "jpg"] + }, + "image/avif": { + source: AssetSource.ATTACHMENT_MEDIA_PROXY, + static: ["avif", "png", "webp"], + animated: ["avif", "awebp", "png", "webp"] + }, + "image/jpeg": { + source: AssetSource.ATTACHMENT_MEDIA_PROXY, + static: ["jpg", "png", "webp"], + animated: [] + }, + "image/gif": { + source: AssetSource.ATTACHMENT_MEDIA_PROXY, + static: [], + animated: ["gif", "awebp", "png", "webp", "jpg"], + forceAnimated: true + }, + } +}; + +export function DownloadIcon({ width, height }: { width: number; height: number; }): JSX.Element { + return ( + + + + ); +} + +export interface ParsedFile { + path: string; + baseName: string; + extension: string | null; + twitterExtra: string | null; +} + +export interface ParsedURL { + url: string; + host: string; + path: string; + baseName: string; + extension: string | null; + params: { expiry: Record, other: Record; }; + twitterExtra: string | null; +} + +export interface InsertGroup { + id: { group?: string, child?: string; }; + type: "WITH_GROUP" | "BEFORE_GROUP" | "AFTER_GROUP"; + position?: "START" | "END"; +} + +export interface VoiceMessageDownloadButtonProps { + message: Message; + item: { + type: "AUDIO"; + originalItem: { + filename: string; + proxy_url: string; + }; + }; +} + +export interface HoverDownloadProps { + item: { + type: "AUDIO" | "IMAGE" | "VIDEO" | "OTHER"; + downloadUrl: string; + contentType: string; + srcIsAnimated: boolean; + originalItem: { + title?: string; + filename: string; + proxy_url: string; + url: string; + }; + }; +} + +export interface ExpandedModalDownloadProps { + item: { + type: "AUDIO" | "IMAGE" | "VIDEO" | "OTHER"; + url: string; + original: string; + proxyUrl?: string; + contentType?: string; + srcIsAnimated: boolean; + sourceMetadata?: { + identifier: { + type: "attachment" | "embed"; + title?: string; + }, + }; + }; +} + +export interface MessageContextMenuProps { + message: Message; + channel: Channel; + itemSrc: string; + itemSafeSrc?: string; + favoriteableType: "sticker" | "emoji" | null; + favoriteableName: string | null; + favoriteableId: string | null; + contextMenuAPIArguments: any[]; + mediaItem?: { + contentType: string; + proxyUrl: string; + url: string; + }; +} + +export interface GDMContextMenuProps { + channel: Channel & { + icon: string | null; + }; +} + +export interface GuildContextMenuProps { + guild: Guild; +} + +export interface UserContextMenuProps { + user: User; + guildId?: string; +} + +export interface CustomizedUser extends User { + avatarDecorationData: null | { + asset: string; + }; + collectibles: null | Record<"nameplate", { + asset: string; + label: string; + palette: string; + }>; +} + +export interface CustomizedMember extends Omit { + avatarDecoration: null | { + asset: string; + }; +} + +export const AttachmentFlags = { + IS_CLIP: 1 << 0, + IS_THUMBNAIL: 1 << 1, + IS_REMIX: 1 << 2, + IS_SPOILER: 1 << 3, + CONTAINS_EXPLICIT_MEDIA: 1 << 4, + IS_ANIMATED: 1 << 5, + CONTAINS_GORE_CONTENT: 1 << 6 +}; diff --git a/src/equicordplugins/downloadify/utils/handlers.tsx b/src/equicordplugins/downloadify/utils/handlers.tsx new file mode 100644 index 000000000..42dd32fc7 --- /dev/null +++ b/src/equicordplugins/downloadify/utils/handlers.tsx @@ -0,0 +1,1006 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { ImageIcon } from "@components/Icons"; +import { StickerFormatType } from "@vencord/discord-types/enums"; +import { GuildMemberStore, GuildRoleStore, GuildStore, Menu, showToast, Toasts, UserProfileStore, UserStore } from "@webpack/common"; +import { JSX } from "react"; + +import { settings } from "../settings"; +import { ASSET_TYPE_EXTRACTOR, assetAvailability, AssetInfo, AssetSource, AttachmentFlags, CDN_BASE, CustomizedMember, CustomizedUser, d, defaultAssets, DownloadIcon, DownloadifyLogger, DownloadifyNative, ExpandedModalDownloadProps, GDMContextMenuProps, GuildContextMenuProps, HoverDownloadProps, IMAGE_EXT_1_DOMAIN_BASE, IMAGE_EXT_2_DOMAIN_BASE, MEDIA_PROXY_BASE, MessageContextMenuProps, ParsedURL, PRIMARY_DOMAIN_BASE, TENOR_BASE, TENOR_GIF_ID, TENOR_MP4_ID, unknownAttachmentMediaProxy, unknownCDN, unknownExternal, unknownExternalImageProxy, unknownPrimaryDomain, UserContextMenuProps, VoiceMessageDownloadButtonProps } from "./definitions"; +import { getFormattedNow, joinOrCreateContextMenuGroup, parseFile, parseURL, sanitizeFilename } from "./misc"; + +export function MessageContextMenu(children: Array, props: MessageContextMenuProps): void { + if (!children?.length || !props?.message?.id) { + return; + } + + const { allowUnicode } = settings.store; + let { favoriteableId, favoriteableName, favoriteableType } = props; + const { itemSrc, itemSafeSrc, message, channel, mediaItem, contextMenuAPIArguments } = props; + const target = contextMenuAPIArguments?.[0]?.target; + let targetURL = itemSafeSrc ?? itemSrc; + + const emojiTarget = target.dataset?.type === "emoji" + ? target + : target.children?.[1]?.firstChild?.dataset?.type === "emoji" + ? target.children[1].firstChild + : target.parentElement?.children?.[1]?.firstChild?.dataset?.type === "emoji" + ? target.parentElement.children[1].firstChild + : null; + + if (!!emojiTarget?.dataset?.id) { + favoriteableType = "emoji"; + favoriteableId = emojiTarget.dataset.id; + targetURL = emojiTarget.src; + } else if (!!emojiTarget?.dataset?.name) { + favoriteableType = "emoji"; + favoriteableName = emojiTarget.alt ?? emojiTarget.dataset.name; + favoriteableId = null; + targetURL = emojiTarget.src; + } + + const roleIconTarget = target.classList && (Array.from(target.classList) as string[]).some(cls => cls.includes("roleIcon")) + ? target + : target.firstChild?.classList && (Array.from(target.firstChild.classList) as string[]).some(cls => cls.includes("roleIcon")) + ? target.firstChild + : null; + + const clanBadgeTarget = target.classList && (Array.from(target.classList) as string[]).some(cls => cls.includes("badge")) + ? target + : target.parentElement?.firstChild?.classList && (Array.from(target.parentElement.firstChild.classList) as string[]).some(cls => cls.includes("badge")) + ? target.parentElement.firstChild + : target.firstChild?.firstChild?.classList && (Array.from(target.firstChild.firstChild.classList) as string[]).some(cls => cls.includes("badge")) + ? target.firstChild.firstChild + : null; + + if (target.tagName === "VIDEO" && !!target.src) { + targetURL = contextMenuAPIArguments[0].target.src; + } else if (target.parentElement?.parentElement?.firstChild?.tagName === "VIDEO" && !!target.parentElement?.parentElement?.firstChild?.src) { + targetURL = contextMenuAPIArguments[0].target.parentElement.parentElement.firstChild.src; + } else if (target.parentElement?.parentElement?.parentElement?.firstChild?.tagName === "VIDEO" && !!target.parentElement?.parentElement?.parentElement?.firstChild?.src) { + targetURL = contextMenuAPIArguments[0].target.parentElement.parentElement.parentElement.firstChild.src; + } else if (target.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.firstChild?.tagName === "VIDEO" && !!target.parentElement?.parentElement?.parentElement?.parentElement?.parentElement?.firstChild?.src) { + targetURL = contextMenuAPIArguments[0].target.parentElement.parentElement.parentElement.parentElement.parentElement.firstChild.src; + } + + const isRoleIcon = !!roleIconTarget; + const isCustomEmoji = favoriteableType === "emoji" && !!favoriteableId; + const isUnicodeEmoji = favoriteableType === "emoji" && !favoriteableId; + const isSticker = favoriteableType === "sticker"; + const isAttachment = !!mediaItem; + const emojiRegex = favoriteableId ? new RegExp(`<(a)?:(\\w+):(${favoriteableId})>`) : null; + let emojiName = isUnicodeEmoji ? (favoriteableName?.replaceAll(":", "") || null) : (favoriteableName ?? (emojiRegex?.exec(message.content || "")?.[2] || null)); + let emojiAnimated = isCustomEmoji ? emojiRegex?.exec(message.content || "")?.[1] === "a" : false; + const emojiURL = (isCustomEmoji || isUnicodeEmoji) ? parseURL(targetURL.replace(CDN_BASE, MEDIA_PROXY_BASE)) : null; + const sticker = isSticker ? message.stickerItems?.find(sticker => sticker.id === favoriteableId) : null; + const downloadifyItems: any[] = []; + + function findNestedComponent(components: any[], checkFor: string): any { + for (const innerComponent of components) { + if (innerComponent.emoji?.id === checkFor || innerComponent.emoji?.name === checkFor) { + return innerComponent; + } + + if (innerComponent.components?.length) { + const found = findNestedComponent(innerComponent.components, checkFor); + if (found) { + return found; + } + } + } + return null; + } + + if (isRoleIcon) { + const parsedURL = parseURL(roleIconTarget.src.replace(CDN_BASE, MEDIA_PROXY_BASE)); + const isUnicodeRoleIcon = itemSrc.startsWith(PRIMARY_DOMAIN_BASE); + const isCustomRoleIcon = !isUnicodeRoleIcon; + const roleID = GuildMemberStore.getMember(channel.guild_id, message.author.id)?.iconRoleId; + const role = roleID ? GuildRoleStore.getRole(channel.guild_id, roleID) : null; + const sanitizedRoleName = role ? sanitizeFilename(role.name, allowUnicode) : null; + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + parsedURL, + sanitizedRoleName ? `${sanitizedRoleName}-icon` : null, + isCustomRoleIcon ? assetAvailability["role-icon-custom"] : assetAvailability["role-icon-unicode-emoji"], + false + ); + }} + /> + ); + } else if (clanBadgeTarget && !!(message.author as any).primaryGuild) { + const { primaryGuild } = (message.author as any); + const sanitizedGuildTag = sanitizeFilename(primaryGuild.tag, allowUnicode); + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + parseURL(clanBadgeTarget.src.replace(CDN_BASE, MEDIA_PROXY_BASE)), + sanitizedGuildTag ? `${sanitizedGuildTag}-clan-badge` : null, + assetAvailability["clan-badge"], + false + ); + }} + /> + ); + } else if (isCustomEmoji) { + const reaction = message.reactions?.find(reaction => reaction.emoji.id === favoriteableId)?.emoji; + const component = message.components?.length ? findNestedComponent(message.components, favoriteableId as string) : null; + emojiAnimated = component?.emoji?.animated || reaction?.animated || emojiAnimated; + emojiName = component?.emoji?.name || reaction?.name || emojiName; + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + emojiURL as ParsedURL, + emojiName, + assetAvailability["custom-emoji"], + emojiAnimated + ); + }} + /> + ); + } else if (isUnicodeEmoji) { + const reaction = message.reactions?.find(reaction => reaction.emoji.name === favoriteableName)?.emoji; + const component = message.components?.length ? findNestedComponent(message.components, favoriteableName as string) : null; + emojiName = component?.emoji?.name || reaction?.name || emojiName; + emojiAnimated = false; + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + emojiURL as ParsedURL, + emojiName, + assetAvailability["unicode-emoji"], + emojiAnimated + ); + }} + /> + ); + } else if (isSticker) { + let stickerURL; + let stickerSource; + let stickerAnimated; + + if (sticker?.format_type === StickerFormatType.PNG) { + stickerURL = parseURL(`${MEDIA_PROXY_BASE}/stickers/${sticker.id}.png`); + stickerSource = assetAvailability.sticker.PNG; + stickerAnimated = false; + } else if (sticker?.format_type === StickerFormatType.GIF) { + stickerURL = parseURL(`${MEDIA_PROXY_BASE}/stickers/${sticker.id}.gif`); + stickerSource = assetAvailability.sticker.GIF; + stickerAnimated = true; + } else if (sticker?.format_type === StickerFormatType.APNG) { + stickerURL = parseURL(`${MEDIA_PROXY_BASE}/stickers/${sticker.id}.png`); + stickerSource = assetAvailability.sticker.APNG; + stickerAnimated = true; + } else if (sticker?.format_type === StickerFormatType.LOTTIE) { + stickerURL = parseURL(`${CDN_BASE}/stickers/${sticker.id}.json`); + stickerSource = assetAvailability.sticker.LOTTIE; + stickerAnimated = true; + } else { + return; + } + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + stickerURL, + sticker.name, + stickerSource, + stickerAnimated + ); + }} + /> + ); + } else if (isAttachment) { + const assetInfo = assetAvailability.attachment[mediaItem.contentType] ?? unknownCDN; + const attachment = message.attachments?.find(attachment => attachment.proxy_url === mediaItem.proxyUrl || attachment.url === mediaItem.url); + + if (!attachment) { + return; + } + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + parseURL(assetInfo.source === AssetSource.CDN ? mediaItem.url : mediaItem.proxyUrl), + (attachment as any)?.title || null, + assetInfo, + !!((attachment as any).flags & AttachmentFlags.IS_ANIMATED) + ); + }} + /> + ); + } else if (targetURL) { + let isTenor = false; + + const embedImage = message.embeds?.find(embed => { + const images = (embed as any).images ? (embed as any).images : embed.image ? [embed.image] : []; + return images.some(image => targetURL.startsWith(image.url) || targetURL.startsWith(image.proxyURL)); + }); + + const embedVideo = embedImage ? undefined : message.embeds?.find(embed => { + isTenor = embed.provider?.name === "Tenor"; + const videos = (embed as any).videos ? (embed as any).videos : embed.video ? [embed.video] : []; + return videos.some(video => targetURL.startsWith(video.url) || targetURL.startsWith(video.proxyURL)); + }); + + const embedThumbnail = message.embeds?.find(embed => { + return (embed.thumbnail?.url && targetURL.startsWith(embed.thumbnail.url)) || (embed.thumbnail?.proxyURL && targetURL.startsWith(embed.thumbnail.proxyURL)); + }); + + const embedAuthor = message.embeds?.find(embed => { + return (embed.author?.iconURL && targetURL.startsWith(embed.author.iconURL)) || (embed.author?.iconProxyURL && targetURL.startsWith(embed.author.iconProxyURL)); + }); + + const embedFooter = (message.embeds?.find(embed => { + return ((embed as any).footer?.iconURL && targetURL.startsWith((embed as any).footer.iconURL)) || ((embed as any).footer?.iconProxyURL && targetURL.startsWith((embed as any).footer.iconProxyURL)); + }) as any); + + const targetEmbed = (embedImage || embedVideo || embedThumbnail || embedAuthor || embedFooter); + const targetEmbedItem = (embedImage?.image || embedVideo?.video || embedThumbnail?.thumbnail || embedAuthor?.author || embedFooter?.footer); + const labelEmbedMedia = embedFooter?.footer + ? "Footer Icon" + : embedAuthor?.author + ? "Author Icon" + : embedThumbnail?.thumbnail + ? "Thumbnail Image" + : targetEmbed?.type === "rich" || !!targetEmbed?.provider + ? "Embed Media" + : "Media"; + + downloadifyItems.push( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + let srcIsAnimated = !!(targetEmbedItem as any)?.srcIsAnimated; + let aliasBasename: string | null = null; + let assetInfo: any; + + const contentType = ((targetEmbedItem as any)?.contentType || await DownloadifyNative.queryURL(itemSrc)); + const isMediaProxy = targetURL?.startsWith(MEDIA_PROXY_BASE); + const isPrimaryDomain = targetURL?.startsWith(PRIMARY_DOMAIN_BASE); + const isCDN = targetURL?.startsWith(CDN_BASE); + const isImageExt1 = targetURL?.startsWith(IMAGE_EXT_1_DOMAIN_BASE); + const isImageExt2 = targetURL?.startsWith(IMAGE_EXT_2_DOMAIN_BASE); + const isImageExt = isImageExt1 || isImageExt2; + + if (isTenor && embedVideo) { + targetURL = embedVideo.url ?? targetURL; + } + + const parsedURL = parseURL(targetURL); + + if (isTenor) { + assetInfo = assetAvailability.tenor; + srcIsAnimated = true; + } else if (isImageExt) { + assetInfo = assetAvailability.attachment[contentType] + ?? assetAvailability.external[contentType] + ?? unknownExternalImageProxy; + } else if (isMediaProxy || isCDN) { + const guestimate = guesstimateAsset(parsedURL, contentType); + assetInfo = guestimate.assetInfo ?? (isCDN ? unknownCDN : (isImageExt ? unknownExternalImageProxy : unknownExternal)); + aliasBasename = guestimate.aliasBasename ?? aliasBasename; + srcIsAnimated = guestimate.srcIsAnimated ?? srcIsAnimated; + } else { + assetInfo = isPrimaryDomain ? unknownPrimaryDomain : unknownExternal; + } + + await handleDownload( + parsedURL, + aliasBasename, + assetInfo, + srcIsAnimated + ); + }} + /> + ); + } + + if (!downloadifyItems.length) { + return; + } + + joinOrCreateContextMenuGroup( + children, + downloadifyItems, + "message-content-group", + "downloadify-submenu", + "Download", + [{ + id: { child: "devmode-copy-id" }, + type: "WITH_GROUP", + position: "START" + }] + ); +} + +export function GDMContextMenu(children: Array, props: GDMContextMenuProps): void { + if (!children?.length || !props?.channel?.id) { + return; + } + + const { channel } = props; + const channelIconHash = channel.icon || null; + const channelIconURL = channelIconHash ? parseURL(`${MEDIA_PROXY_BASE}/channel-icons/${channel.id}/${channelIconHash}.png`) : null; + const channelTimestamp = Number(BigInt(channel.id) >> 22n); + const defaultChannelIconURL = channelTimestamp ? parseURL(`${PRIMARY_DOMAIN_BASE}${defaultAssets.DEFAULT_GROUP_DM_AVATARS[channelTimestamp % 8]}`) : null; + + const downloadifyItems = [ + channelIconURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + channelIconURL, + channel.name ? `${channel.name}-group-icon` : null, + assetAvailability["guild-icon"], + false + ); + }} + /> + ) : null, + defaultChannelIconURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => { + await handleDownload( + defaultChannelIconURL, + channel.name ? `${channel.name}-default-group-icon` : null, + assetAvailability["default-group-icon"], + false + ); + }} + /> + ) : null + ].filter(Boolean); + + joinOrCreateContextMenuGroup( + children, + downloadifyItems, + "channel-content-group", + "downloadify-submenu", + "Download", + [{ + id: { child: "devmode-copy-id" }, + type: "WITH_GROUP", + position: "START" + }] + ); +} + +export function GuildContextMenu(children: Array, props: GuildContextMenuProps): void { + if (!children?.length || !props?.guild?.id) { + return; + } + + const { guild } = props; + const guildIconHash = guild.icon || null; + const guildBannerHash = guild.banner || null; + const guildInviteHash = guild.splash !== guildBannerHash ? guild.splash || null : null; + const guildDiscoveryHash = guild.discoverySplash !== guildInviteHash && guild.discoverySplash !== guildBannerHash ? guild.discoverySplash || null : null; + const guildIconHashAnimated = guildIconHash?.startsWith("a_"); + const guildBannerHashAnimated = guildBannerHash?.startsWith("a_"); + const guildIconURL = guildIconHash ? parseURL(`${MEDIA_PROXY_BASE}/icons/${guild.id}/${guildIconHash}.${guildIconHashAnimated ? "gif" : "png"}`) : null; + const guildBannerURL = guildBannerHash ? parseURL(`${MEDIA_PROXY_BASE}/banners/${guild.id}/${guildBannerHash}.${guildBannerHashAnimated ? "gif" : "png"}`) : null; + const guildInviteURL = guildInviteHash ? parseURL(`${MEDIA_PROXY_BASE}/splashes/${guild.id}/${guildInviteHash}.png`) : null; + const guildDiscoveryURL = guildDiscoveryHash ? parseURL(`${MEDIA_PROXY_BASE}/discovery-splashes/${guild.id}/${guildDiscoveryHash}.png`) : null; + + if (!guildIconURL && !guildBannerURL && !guildInviteURL && !guildDiscoveryURL) { + return; + } + + const downloadifyItems = [guildIconURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + guildIconURL, + guild.name ? `${guild.name}-icon` : null, + assetAvailability["guild-icon"], + !!guildIconHash?.startsWith("a_") + )} + /> + ) : null, + guildBannerURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + guildBannerURL, + guild.name ? `${guild.name}-banner` : null, + assetAvailability["guild-banner"], + !!guildBannerHash?.startsWith("a_") + )} + /> + ) : null, + guildInviteURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + guildInviteURL, + guild.name ? `${guild.name}-invite-splash` : null, + assetAvailability["guild-invite-splash"], + !!guildInviteHash?.startsWith("a_") + )} + /> + ) : null, + guildDiscoveryURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + guildDiscoveryURL, + guild.name ? `${guild.name}-discovery-splash` : null, + assetAvailability["guild-discovery-splash"], + !!guildDiscoveryHash?.startsWith("a_") + )} + /> + ) : null].filter(Boolean); + + joinOrCreateContextMenuGroup( + children, + downloadifyItems, + "guild-content-group", + "downloadify-submenu", + "Download", + [{ + id: { child: "devmode-copy-id" }, + type: "WITH_GROUP", + position: "START" + }, + { + id: { child: "privacy" }, + type: "WITH_GROUP", + position: "END" + }] + ); +} + +export function UserContextMenu(children: Array, props: UserContextMenuProps): void { + if (!children?.length || !props.user?.id) { + return; + } + + const user = UserStore.getUser(props.user.id) as CustomizedUser; + const member = (props.guildId ? GuildMemberStore.getMember(props.guildId, props.user.id) : null) as CustomizedMember | null; + const guild = props.guildId ? GuildStore.getGuild(props.guildId) : null; + const userAvatarHash = user.avatar || null; + const memberAvatarHash = member?.avatar || null; + const userBannerHash = UserProfileStore.getUserProfile(user.id)?.banner || null; + const memberBannerHash = UserProfileStore.getGuildMemberProfile(user.id, props.guildId)?.banner || null; + const userAvatarDecorationHash = user.avatarDecorationData?.asset || null; + const memberAvatarDecorationHash = member?.avatarDecoration?.asset || null; + const userNameplatePath = user.collectibles?.nameplate?.asset || null; + const userAvatarURL = userAvatarHash ? parseURL(`${MEDIA_PROXY_BASE}/avatars/${user.id}/${userAvatarHash}.png`) : null; + const memberAvatarURL = memberAvatarHash ? parseURL(`${MEDIA_PROXY_BASE}/guilds/${props.guildId}/users/${user.id}/avatars/${memberAvatarHash}.png`) : null; + const userBannerURL = userBannerHash ? parseURL(`${MEDIA_PROXY_BASE}/banners/${user.id}/${userBannerHash}.png`) : null; + const memberBannerURL = memberBannerHash ? parseURL(`${MEDIA_PROXY_BASE}/guilds/${props.guildId}/users/${user.id}/banners/${memberBannerHash}.png`) : null; + const userAvatarDecorationURL = userAvatarDecorationHash ? parseURL(`${MEDIA_PROXY_BASE}/avatar-decoration-presets/${userAvatarDecorationHash}.png`) : null; + const memberAvatarDecorationURL = memberAvatarDecorationHash ? parseURL(`${MEDIA_PROXY_BASE}/avatar-decoration-presets/${memberAvatarDecorationHash}.png`) : null; + const userNameplateURL = userNameplatePath ? parseURL(`${CDN_BASE}/assets/collectibles/${userNameplatePath}static.png`) : null; + const defaultUserAvatarURL = parseURL(`${PRIMARY_DOMAIN_BASE}${defaultAssets.DEFAULT_AVATARS[user.discriminator === "0" ? (Math.floor(Number(BigInt(user.id) >> 22n)) % 6) : (Number(user.discriminator) % 5)]}`); + + const downloadifyItems = [userAvatarURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + userAvatarURL, + `${user.username}-avatar`, + assetAvailability["user-avatar"], + !!userAvatarHash?.startsWith("a_") + )} + /> + ) : null, + userBannerURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + userBannerURL, + `${user.username}-banner`, + assetAvailability["user-banner"], + !!userBannerHash?.startsWith("a_") + )} + /> + ) : null, + userAvatarDecorationURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + userAvatarDecorationURL, + `${user.username}-avatar-decoration`, + assetAvailability["avatar-decoration"], + true + )} + /> + ) : null, + memberAvatarURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + memberAvatarURL, + guild ? `${guild.name}-${user.username}-avatar` : `${user.username}-avatar`, + assetAvailability["user-avatar"], + !!memberAvatarHash?.startsWith("a_") + )} + /> + ) : null, + memberBannerURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + memberBannerURL, + guild ? `${guild.name}-${user.username}-banner` : `${user.username}-banner`, + assetAvailability["user-banner"], + !!memberBannerHash?.startsWith("a_") + )} + /> + ) : null, + memberAvatarDecorationURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + memberAvatarDecorationURL, + guild ? `${guild.name}-${user.username}-avatar-decoration` : `${user.username}-avatar-decoration`, + assetAvailability["avatar-decoration"], + true + )} + /> + ) : null, + defaultUserAvatarURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + defaultUserAvatarURL, + `${user.username}-default-avatar`, + assetAvailability["default-user-avatar"], + false + )} + /> + ) : null, + userNameplateURL ? ( + ImageIcon({ width: 20, height: 20 })} + action={async () => await handleDownload( + userNameplateURL, + `${user.username}-nameplate`, + assetAvailability.nameplate, + true + )} + /> + ) : null].filter(Boolean); + + joinOrCreateContextMenuGroup( + children, + downloadifyItems, + "user-content-group", + "downloadify-submenu", + "Download", + [{ + id: { child: "devmode-copy-id" }, + type: "WITH_GROUP", + position: "START" + }, + { + id: { child: "user-profile" }, + type: "WITH_GROUP", + position: "END" + }] + ); +} + +export async function handleHoverDownloadButtonClicked(props: HoverDownloadProps): Promise { + let { contentType } = props.item; + const { type, downloadUrl, srcIsAnimated, originalItem } = props.item; + const { displayStatus, statusDuration } = settings.store; + + if (!originalItem) { + // Fallback to default behavior. + DownloadifyLogger.info(`[${getFormattedNow()}] [UNRECOGNIZED HOVER DOWNLOAD ITEM]`, props.item); + displayStatus && showToast("File Opened in Browser", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); + window.open(downloadUrl, "_blank"); + } else { + contentType ??= (await DownloadifyNative.queryURL(downloadUrl) || ""); + const assetInfo = (type === "IMAGE" ? (assetAvailability.attachment[contentType]) : null) ?? unknownCDN; + + await handleDownload( + parseURL(assetInfo.source === AssetSource.CDN ? downloadUrl : originalItem.proxy_url), + originalItem.title || null, + assetInfo, + !!srcIsAnimated + ); + } +} + +export async function handleExpandedModalDownloadButtonClicked(props: ExpandedModalDownloadProps, fallback: () => Promise): Promise { + let { contentType, srcIsAnimated } = props.item; + const { url, original, proxyUrl, sourceMetadata } = props.item; + const { displayStatus, statusDuration } = settings.store; + + if (!url && !original) { + // Fallback to default behavior. + DownloadifyLogger.info(`[${getFormattedNow()}] [UNRECOGNIZED EXPANDED MODAL DOWNLOAD ITEM]`, props.item); + displayStatus && showToast("Download Started", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); + + try { + await fallback(); + displayStatus && showToast("Download Success", Toasts.Type.SUCCESS, { duration: statusDuration * 1000 }); + } catch (error) { + DownloadifyLogger.info(`[${getFormattedNow()}] [FALLBACK DOWNLOAD FAILED]`, error); + displayStatus && showToast("Download Failed", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); + } + } else { + contentType ??= (await DownloadifyNative.queryURL(url) || ""); + let aliasBasename = sourceMetadata?.identifier.title ?? null; + const isAttachment = sourceMetadata?.identifier.type === "attachment"; + const urlIsMediaProxy = url?.startsWith(MEDIA_PROXY_BASE); + const originalIsMediaProxy = original?.startsWith(MEDIA_PROXY_BASE); + const isMediaProxy = urlIsMediaProxy || originalIsMediaProxy; + const isPrimaryDomain = original?.startsWith(PRIMARY_DOMAIN_BASE); + const isCDN = original?.startsWith(CDN_BASE); + const isImageExt1 = url?.startsWith(IMAGE_EXT_1_DOMAIN_BASE); + const isImageExt2 = url?.startsWith(IMAGE_EXT_2_DOMAIN_BASE); + const isImageExt = isImageExt1 || isImageExt2; + let parsedURL: ParsedURL; + let assetInfo: any; + + if (isAttachment) { + parsedURL = parseURL(proxyUrl || url); + assetInfo = assetAvailability.attachment[contentType] ?? unknownAttachmentMediaProxy; + } else if (isImageExt) { + parsedURL = parseURL(url); + assetInfo = assetAvailability.attachment[contentType] ?? assetAvailability.external[contentType] ?? unknownExternalImageProxy; + } else if (isMediaProxy || isCDN) { + parsedURL = parseURL(urlIsMediaProxy ? url : original); + const guestimate = guesstimateAsset(parsedURL, contentType); + assetInfo = guestimate.assetInfo ?? (isCDN ? unknownCDN : unknownExternal); + aliasBasename = guestimate.aliasBasename ?? aliasBasename; + srcIsAnimated = guestimate.srcIsAnimated ?? srcIsAnimated; + } else { + parsedURL = parseURL(original ?? url); + assetInfo = isPrimaryDomain ? unknownPrimaryDomain : unknownExternal; + } + + await handleDownload( + parsedURL, + aliasBasename, + assetInfo, + !!srcIsAnimated, + ); + } +} + +function guesstimateAsset(parsedURL: ParsedURL, contentType: string): { + assetInfo?: AssetInfo; + aliasBasename?: string; + srcIsAnimated?: boolean; +} { + // Check if the URL is a known asset type. The only defined member + // of assetAvailability that we can't completely check for here are stickers + // due to their format information not being present in the URL. We can make + // accurate guesses based on certain extensions, but not for every format. + let assetInfo: AssetInfo | undefined; + let aliasBasename: string | undefined; + let srcIsAnimated: boolean | undefined; + const detected = parsedURL.url.match(ASSET_TYPE_EXTRACTOR); + const detectedPrimary = detected?.[1] ?? ""; + const detectedSecondary = detected?.[2] ?? ""; + const detectedTertiary = detected?.[3] ?? ""; + + if (detectedPrimary === "attachments") { + assetInfo = assetAvailability.attachment[contentType]; + } else if (detectedPrimary === "emojis") { + assetInfo = assetAvailability["custom-emoji"]; + } else if (detectedPrimary === "clan-badges") { + assetInfo = assetAvailability["clan-badge"]; + } else if (detectedPrimary === "role-icons") { + assetInfo = assetAvailability["role-icon-custom"]; + } else if (detectedPrimary === "discovery-splashes") { + assetInfo = assetAvailability["guild-discovery-splash"]; + } else if (detectedPrimary === "splashes") { + assetInfo = assetAvailability["guild-invite-splash"]; + } else if (detectedPrimary === "banners") { + assetInfo = assetAvailability["guild-banner"]; + } else if (detectedPrimary === "icons") { + assetInfo = assetAvailability["guild-icon"]; + } else if (detectedPrimary === "avatar-decoration-presets") { + assetInfo = assetAvailability["avatar-decoration"]; + srcIsAnimated = true; // Avatar decorations always have animated variants. + } else if (detectedPrimary === "avatars" || (detectedPrimary === "guilds" && detectedSecondary === "avatars")) { + assetInfo = assetAvailability["user-avatar"]; + } else if (detectedPrimary === "banners" || (detectedPrimary === "guilds" && detectedSecondary === "banners")) { + assetInfo = assetAvailability["user-banner"]; + } else if (detectedPrimary === "assets/collectibles/nameplates") { + assetInfo = assetAvailability.nameplate; + aliasBasename = detectedTertiary ?? "nameplate"; + srcIsAnimated = true; // Nameplates always have animated variants. + } else if (detectedPrimary === "stickers") { + if (parsedURL.extension === "gif") { + assetInfo = assetAvailability.sticker.GIF; + } else if (parsedURL.extension === "webp") { + if (srcIsAnimated) { + assetInfo = assetAvailability.sticker.GIF; + } else { + // There is no way to know if a static WEBP sticker + // link is sourced from an APNG, PNG, or GIF sticker. + } + } else if (parsedURL.extension === "png") { + if (srcIsAnimated) { + assetInfo = assetAvailability.sticker.PNG; + } else { + // There is no way to know if a static PNG sticker + // link is sourced from an APNG, PNG, or GIF sticker. + } + } else if (parsedURL.extension === "jpg") { + // There is no way to know if a JPG sticker link + // is sourced from an APNG, PNG, or GIF sticker. + } + } + + return { + assetInfo, + aliasBasename, + srcIsAnimated + }; +} + +export function VoiceMessageDownloadButton(props: VoiceMessageDownloadButtonProps): JSX.Element { + async function voiceMessageDownloadClicked(event: React.MouseEvent) { + event.preventDefault(); + const { item, message } = props; + const { proxy_url } = item.originalItem; + const baseName = `voice-message-${message.id}`; + const parsedURL = parseURL(proxy_url); + + await handleDownload( + parsedURL, + baseName, + { source: AssetSource.ATTACHMENT_MEDIA_PROXY, static: [], animated: [] }, + false, + ); + } + + const { voiceMessages } = settings.use(["voiceMessages"]); + + return ( + <> + {voiceMessages &&
+ +
} + + ); +} + +/** + * Common download handler for all other handlers. + */ +async function handleDownload( + url: ParsedURL, + alias: string | null, + asset: AssetInfo, + animated: boolean, +) { + const extensions = animated || !!asset.forceAnimated ? asset.animated : asset.static; + extensions.length < 1 && url.extension && extensions.push(url.extension); + const { defaultDirectory, overwriteFiles, displayStatus, allowUnicode, statusDuration } = settings.store; + const baseName = alias + ? sanitizeFilename(alias, allowUnicode, "discord-download") + : url.baseName + ? sanitizeFilename(url.baseName, allowUnicode, "discord-download") + : "discord-download"; + + let chosenExtension = extensions[0] ?? ""; + let resolvedExtension = (chosenExtension).replace("apng", "png").replace("awebp", "webp"); + let resolvedPath: string | null = null; + let resolvedURL = url.url; + + if (defaultDirectory) { + const resolvedDirectory = defaultDirectory.trim().replace(/^["']|["']$/g, "").replace(/[/\\]+$/, "").replace(/\\/g, "/"); + resolvedPath = `${resolvedDirectory}/${baseName}${resolvedExtension ? `.${resolvedExtension}` : ""}`; + displayStatus && showToast("Download Started", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); + + if (!overwriteFiles) { + for (let num = 1; await DownloadifyNative.fileExists(resolvedPath); num++) { + resolvedPath = `${resolvedDirectory}/${baseName}-${num}${resolvedExtension ? `.${resolvedExtension}` : ""}`; + } + } + } else { + try { + resolvedPath = await DownloadifyNative.getFilePath(baseName, extensions); + + if (!resolvedPath) { + DownloadifyLogger.info(`[${getFormattedNow()}] [SAVE DIALOGUE CLOSED / DOWNLOAD CANCELED]`); + displayStatus && showToast("Download Canceled", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); + return; + } + + const chosenFile = parseFile(resolvedPath); + chosenExtension = chosenFile.extension || ""; + resolvedExtension = (chosenExtension || "").replace("apng", "png").replace("awebp", "webp"); + + if (url.extension) { + const extReplacer = new RegExp(`\\.${chosenExtension}(?!.*\\.${chosenExtension})`, "i"); + resolvedPath = resolvedPath.replace(extReplacer, `.${resolvedExtension}`); + } + } catch (error) { + DownloadifyLogger.error(`[${getFormattedNow()}] [ERROR OPENING SAVE DIALOGUE]:`, error); + displayStatus && showToast("Error Opening Save Dialogue", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); + return; + } + } + + try { + let success = false; + + if ([AssetSource.ASSET_MEDIA_PROXY, AssetSource.ATTACHMENT_MEDIA_PROXY, AssetSource.EXTERNAL_IMAGE_PROXY].includes(asset.source)) { + let baseURL; + + if (asset.source === AssetSource.ASSET_MEDIA_PROXY) { + baseURL = url.host + url.path + url.baseName + (resolvedExtension ? `.${resolvedExtension}` : ""); + } else { + // Attachments on the media proxy use the `format` query parameter (seen below) to retrieve alternate + // formats instead of changing the file extension directly like assets on the media proxy do. + baseURL = url.host + url.path + url.baseName + + (url.extension ? `.${url.extension}` : "") + + (url.twitterExtra ? encodeURIComponent(url.twitterExtra) : ""); + } + + const authParams = url.params.expiry; + const newURL = new URL(baseURL); + + Object.entries(authParams).forEach(([key, value]) => { + newURL.searchParams.append(key, value); + }); + + if ([AssetSource.ATTACHMENT_MEDIA_PROXY, AssetSource.EXTERNAL_IMAGE_PROXY].includes(asset.source)) { + if (url.extension !== resolvedExtension) { + newURL.searchParams.append("format", resolvedExtension.replace("jpg", "jpeg")); + } + } else { + // Attachments on the media proxy do not support + // resizing but assets on the media proxy do. + newURL.searchParams.append("size", "4096"); + } + + if (chosenExtension === "awebp" && animated) { + newURL.searchParams.append("animated", "true"); + } else if (chosenExtension === "png" && animated) { + newURL.searchParams.append("passthrough", "false"); + } + + if (resolvedExtension === "webp") { + newURL.searchParams.append("lossless", "true"); + } + + resolvedURL = newURL.href; + } else if (asset.source === AssetSource.CDN) { + if (asset.type === "nameplate") { + resolvedURL = url.host + url.path + ( + chosenExtension === "apng" + ? "img.png" + : chosenExtension === "webm" + ? "asset.webm" + : "static.png" + ); + } else { + // Currently only the LOTTIE sticker. + resolvedURL = url.url; + } + } else if (asset.source === AssetSource.TENOR) { + const tenorID = url.path.replaceAll("/", "").slice(0, -2); + + if (chosenExtension === "gif") { + resolvedURL = `${TENOR_BASE}/${tenorID}${TENOR_GIF_ID}/tenor.gif`; + } else if (chosenExtension === "mp4") { + resolvedURL = `${TENOR_BASE}/${tenorID}${TENOR_MP4_ID}/tenor.mp4`; + } else { + DownloadifyLogger.warn(`[${getFormattedNow()}] [UNSUPPORTED TENOR EXTENSION]\n${chosenExtension}\n${resolvedPath}\n${resolvedURL}`); + displayStatus && showToast("Unsupported Tenor Extension", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); + return; + } + } else if ([AssetSource.EXTERNAL, AssetSource.PRIMARY_DOMAIN].includes(asset.source)) { + resolvedURL = url.url; + } + + DownloadifyLogger.info(`[${getFormattedNow()}] [STARTING DOWNLOAD]\n${resolvedPath}\n${resolvedURL}`); + success = await DownloadifyNative.downloadURL(resolvedURL, resolvedPath); + + if (success) { + DownloadifyLogger.info(`[${getFormattedNow()}] [DOWNLOAD SUCCESS]\n${resolvedPath}\n${resolvedURL}`); + displayStatus && showToast("File Downloaded", Toasts.Type.SUCCESS, { duration: statusDuration * 1000 }); + } else { + DownloadifyLogger.error(`[${getFormattedNow()}] [DOWNLOAD FAILED]\n${resolvedPath}\n${resolvedURL}`); + displayStatus && showToast("File Download Failed", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); + } + } catch (error) { + DownloadifyLogger.error(`[${getFormattedNow()}] [DOWNLOAD ERRORED]\n${resolvedPath}\n${resolvedURL}`, error); + displayStatus && showToast("Error Downloading File", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); + } +} diff --git a/src/equicordplugins/downloadify/utils/misc.tsx b/src/equicordplugins/downloadify/utils/misc.tsx new file mode 100644 index 000000000..ab1119fb4 --- /dev/null +++ b/src/equicordplugins/downloadify/utils/misc.tsx @@ -0,0 +1,253 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findGroupChildrenByChildId } from "@api/ContextMenu"; +import { Menu } from "@webpack/common"; + +import { InsertGroup, ParsedFile, ParsedURL } from "./definitions"; + +/** + * Retrieve information about a file path by parsing its parts. + */ +export function parseFile(filePath: string): ParsedFile { + const parts = filePath.split(/[\\/]/); + let fileNameWithExtra = parts.pop() as string; + + while (fileNameWithExtra.includes("%25")) { + fileNameWithExtra = decodeURIComponent(fileNameWithExtra); + } + + fileNameWithExtra = decodeURIComponent(fileNameWithExtra); + + const posColon = fileNameWithExtra.indexOf(":"); + const fileNameWithoutExtra = posColon === -1 ? fileNameWithExtra : fileNameWithExtra.slice(0, posColon); + const twitterExtra = posColon === -1 ? null : fileNameWithExtra.slice(posColon); + + const path = parts.join("/") + "/"; + const posDot = fileNameWithoutExtra.lastIndexOf("."); + + if (fileNameWithoutExtra === "" || posDot < 1) { + return { + path, + baseName: fileNameWithoutExtra, + extension: null, + twitterExtra: null + }; + } + + return { + path, + baseName: fileNameWithoutExtra.slice(0, posDot), + extension: fileNameWithoutExtra.slice(posDot + 1).toLowerCase(), + twitterExtra: twitterExtra ?? null + }; +} + +/** + * Retrieve information about a URL by parsing its parts. + */ +export function parseURL(url: string): ParsedURL { + const parsed = new URL(url); + const { path, baseName, extension, twitterExtra } = parseFile(parsed.pathname); + const params = Array.from(parsed.searchParams.entries()).reduce( + (acc, [key, value]) => { + if (["ex", "is", "hm"].includes(key)) { + acc.expiry[key] = value; + } else { + acc.other[key] = value; + } + + return acc; + }, { expiry: {}, other: {} } as { expiry: Record, other: Record; } + ); + + return { + url: url, + host: parsed.origin.toLowerCase(), + path, + baseName, + extension, + params, + twitterExtra + }; +} + +/** + * Sanitize a file name by replacing invalid characters with underscores. + */ +export function sanitizeFilename(filename: string, allowUnicode: boolean, fallback?: string): string { + let sanitized = filename.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_") // windows-reserved + .replace(/^\./, "_") // dot files + .replace(/(\.\.)+/g, "_") // relative paths + .replace(/[ \t\n\r]+/g, "_"); // spaces + + if (!allowUnicode) { + sanitized = sanitized.replace(/[^\x00-\x7F]/g, "_"); // non-ASCII characters + } else { + sanitized = sanitized.normalize("NFD"); // normalize unicode + } + + sanitized = sanitized.replace(/^_+|_+$/g, ""); // leading and trailing underscores + return sanitized || fallback || ""; +} + +/** + * Get the current date and time formatted as a string. + */ +export function getFormattedNow(): string { + return new Date().toLocaleString(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true + }).replace(",", ""); +} + +/** + * Join or create a group in the guild context menu. + */ +export function joinOrCreateContextMenuGroup( + children: Array, + items: Array, + groupId: string, + submenuId?: string, + submenuLabel?: string, + insertOrder?: InsertGroup[] +) { + function joinOrCreateSubmenu() { + const existingItemShouldJoinSubmenu = existingGroup.props.children.find(child => child?.props?.submenuId === submenuId); + let existingSubmenu = existingGroup.props.children.find(child => child?.props?.id === submenuId); + let numitems = items.length; + + if (existingItemShouldJoinSubmenu) { + existingSubmenu = null; + items.unshift(existingItemShouldJoinSubmenu); + numitems++; + const indexOfExistingItemShouldJoinSubmenu = existingGroup.props.children.findIndex(child => child === existingItemShouldJoinSubmenu); + existingGroup.props.children.splice(indexOfExistingItemShouldJoinSubmenu, 1); + } + + if (existingSubmenu) { + if (!Array.isArray(existingSubmenu.props.children)) { + existingSubmenu.props.children = [existingSubmenu.props.children]; + } + + items.forEach(member => member.props.label = member.props.submenuItemLabel ?? member.props.label); + existingSubmenu.props.children.push(...items); + } else if (numitems === 1) { + if (!chosenInsertOption?.position || chosenInsertOption.position === "END") { + existingGroup.props.children.push(items[0]); + } else { + existingGroup.props.children.unshift(items[0]); + } + } else { + existingSubmenu = ( + + {[...items]} + + ); + + items.forEach(member => member.props.label = member.props.submenuItemLabel ?? member.props.label); + + if (!chosenInsertOption?.position || chosenInsertOption.position === "END") { + existingGroup.props.children.push(existingSubmenu); + } else { + existingGroup.props.children.unshift(existingSubmenu); + } + } + } + + items.forEach(member => { + member.props.groupId = groupId; + member.props.submenuId = submenuId; + member.props.submenuLabel = submenuLabel; + }); + + const insertWithOptions = insertOrder?.filter(item => item.type === "WITH_GROUP"); + let existingGroup = children.find(child => child?.props?.id === groupId); + let chosenInsertOption; + + if (!existingGroup && insertWithOptions) { + let targetItem; + + for (const item of insertWithOptions) { + if (item.id.group) { + targetItem = children.find(child => child?.props?.id === item.id.group); + } + + if (!targetItem && item.id.child) { + targetItem = findGroupChildrenByChildId(item.id.child, children, true); + } + + if (targetItem) { + existingGroup = children.find(child => child?.props?.children === targetItem); + chosenInsertOption = item; + break; + } + } + } + + if (existingGroup) { + if (!Array.isArray(existingGroup.props.children)) { + existingGroup.props.children = [existingGroup.props.children]; + } + + if (!submenuId) { + if (!chosenInsertOption?.position || chosenInsertOption.position === "END") { + existingGroup.props.children.push(...items); + } else { + existingGroup.props.children.unshift(...items); + } + + return; + } else { + joinOrCreateSubmenu(); + } + } else { + if (!submenuId) { + existingGroup = ( + + {[...items]} + + ); + } else { + existingGroup = ({[]}); + joinOrCreateSubmenu(); + } + + const insertAfterBeforeOptions = insertOrder?.filter(item => item.type !== "WITH_GROUP") || []; + let targetIndex = children.length; + let targetItem; + + for (const item of insertAfterBeforeOptions) { + if (item.id.group) { + targetItem = children.find(child => child?.props?.id === item.id.group); + } + + if (!targetItem && item.id.child) { + targetItem = findGroupChildrenByChildId(item.id.child, children, true); + } + + if (targetItem) { + targetIndex = children.findIndex(child => child?.props?.children === targetItem); + + if (item.type === "AFTER_GROUP") { + targetIndex++; + } + + break; + } + } + + children.splice(targetIndex, 0, existingGroup); + } +} From f689aa6d89ac4e36877e9c8f695f9b61fe7e2ca7 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:32:06 -0400 Subject: [PATCH 044/881] HideMedia is back for now --- src/plugins/hideAttachments/index.tsx | 105 +++++++++++++++++++++++++ src/plugins/hideAttachments/styles.css | 10 +++ 2 files changed, 115 insertions(+) create mode 100644 src/plugins/hideAttachments/index.tsx create mode 100644 src/plugins/hideAttachments/styles.css diff --git a/src/plugins/hideAttachments/index.tsx b/src/plugins/hideAttachments/index.tsx new file mode 100644 index 000000000..94f3a86dd --- /dev/null +++ b/src/plugins/hideAttachments/index.tsx @@ -0,0 +1,105 @@ +/* + * 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 . +*/ + +import "./styles.css"; + +import { get, set } from "@api/DataStore"; +import { updateMessage } from "@api/MessageUpdater"; +import { migratePluginSettings } from "@api/Settings"; +import { ImageInvisible, ImageVisible } from "@components/Icons"; +import { Devs } from "@utils/constants"; +import { classes } from "@utils/misc"; +import definePlugin from "@utils/types"; +import { Message } from "@vencord/discord-types"; +import { ChannelStore } from "@webpack/common"; + +const KEY = "HideMedia_HiddenIds"; + +let hiddenMessages = new Set(); + +async function getHiddenMessages() { + const stored = await get(KEY); + hiddenMessages = new Set(stored ?? []); + return hiddenMessages; +} + +const saveHiddenMessages = (ids: Set) => set(KEY, [...ids]); + +migratePluginSettings("HideMedia", "HideAttachments"); + +const hasMedia = (msg: Message) => msg.attachments.length > 0 || msg.embeds.length > 0 || msg.stickerItems.length > 0; + +export default definePlugin({ + name: "HideMedia", + description: "Hide attachments and embeds for individual messages via hover button", + authors: [Devs.Ven], + dependencies: ["MessageUpdaterAPI"], + + patches: [{ + find: "this.renderAttachments(", + replacement: { + match: /(?<=\i=)this\.render(?:Attachments|Embeds|StickersAccessories)\((\i)\)/g, + replace: "$self.shouldHide($1?.id)?null:$&" + } + }], + + renderMessagePopoverButton(msg) { + if (!hasMedia(msg) && !msg.messageSnapshots.some(s => hasMedia(s.message))) return null; + + const isHidden = hiddenMessages.has(msg.id); + + return { + label: isHidden ? "Show Media" : "Hide Media", + icon: isHidden ? ImageVisible : ImageInvisible, + message: msg, + channel: ChannelStore.getChannel(msg.channel_id), + onClick: () => this.toggleHide(msg.channel_id, msg.id) + }; + }, + + renderMessageAccessory({ message }) { + if (!this.shouldHide(message.id)) return null; + + return ( + + Media Hidden + + ); + }, + + async start() { + await getHiddenMessages(); + }, + + stop() { + hiddenMessages.clear(); + }, + + shouldHide(messageId: string) { + return hiddenMessages.has(messageId); + }, + + async toggleHide(channelId: string, messageId: string) { + const ids = await getHiddenMessages(); + if (!ids.delete(messageId)) + ids.add(messageId); + + await saveHiddenMessages(ids); + updateMessage(channelId, messageId); + } +}); diff --git a/src/plugins/hideAttachments/styles.css b/src/plugins/hideAttachments/styles.css new file mode 100644 index 000000000..0c7498216 --- /dev/null +++ b/src/plugins/hideAttachments/styles.css @@ -0,0 +1,10 @@ +.vc-hideAttachments-accessory { + color: var(--text-muted); + margin-top: 0.5em; + font-style: italic; + font-weight: 400; +} + +.vc-hideAttachments-no-content { + margin-top: 0; +} \ No newline at end of file From cf04b80ebac76b2f42da6b9e21bd454b182ef0a3 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:51:12 -0400 Subject: [PATCH 045/881] Fix Patches --- src/equicordplugins/allCallTimers/index.tsx | 2 +- src/equicordplugins/betterBlockedUsers/index.tsx | 2 +- src/equicordplugins/noModalAnimation/index.ts | 4 ++-- src/equicordplugins/searchFix/index.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/equicordplugins/allCallTimers/index.tsx b/src/equicordplugins/allCallTimers/index.tsx index 3ce77fc04..b4fb0e852 100644 --- a/src/equicordplugins/allCallTimers/index.tsx +++ b/src/equicordplugins/allCallTimers/index.tsx @@ -119,7 +119,7 @@ export default definePlugin({ predicate: () => settings.store.showWithoutHover, replacement: [ { - match: /function\(\)\{.+:""(?=.*?userId:(\i))/, + match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, replace: "$&,$self.renderTimer($1.id)," } ] diff --git a/src/equicordplugins/betterBlockedUsers/index.tsx b/src/equicordplugins/betterBlockedUsers/index.tsx index c0747248c..d24086763 100644 --- a/src/equicordplugins/betterBlockedUsers/index.tsx +++ b/src/equicordplugins/betterBlockedUsers/index.tsx @@ -31,7 +31,7 @@ export default definePlugin({ replace: "$self.renderUser($1,$2)", }, { - match: /(?<=\}=(\i).{0,10}(\i).useState\(.{0,1}\);)/, + match: /(?<=\}=(\i).{0,10}(\i).useState\(\i\).{0,25}\};)/, replace: "let [searchResults,setSearchResults]=$2.useState([]);$self.setUpdateFunc($1,setSearchResults);" }, { diff --git a/src/equicordplugins/noModalAnimation/index.ts b/src/equicordplugins/noModalAnimation/index.ts index 17433d8dd..f27dc029f 100644 --- a/src/equicordplugins/noModalAnimation/index.ts +++ b/src/equicordplugins/noModalAnimation/index.ts @@ -36,8 +36,8 @@ export default definePlugin({ { find: "renderLurkerModeUpsellPopout,position:", replacement: { - match: /200:300/g, - replace: "0:0", + match: /(\i)=(200|300)/g, + replace: "$1=0", }, } ] diff --git a/src/equicordplugins/searchFix/index.tsx b/src/equicordplugins/searchFix/index.tsx index e45e4cd16..c1ae81917 100644 --- a/src/equicordplugins/searchFix/index.tsx +++ b/src/equicordplugins/searchFix/index.tsx @@ -43,7 +43,7 @@ export default definePlugin({ authors: [EquicordDevs.Jaxx], patches: [ { - find: '"SearchQueryStore";', + find: '"SearchQueryStore");', replacement: { match: /\i\.searchResultsQuery=(\i)/, replace: "$&,$self.main($1)" From 0a6bdc4edb52f9c0e088280a1af1bf1a51fd4090 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:57:57 -0400 Subject: [PATCH 046/881] Fix VC Patches --- src/equicordplugins/equicordToolbox/index.tsx | 2 +- src/plugins/_api/notices.ts | 4 ++-- src/plugins/ignoreActivities/index.tsx | 2 +- src/plugins/superReactionTweaks/index.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/equicordplugins/equicordToolbox/index.tsx b/src/equicordplugins/equicordToolbox/index.tsx index 51ee62c26..b20c123e7 100644 --- a/src/equicordplugins/equicordToolbox/index.tsx +++ b/src/equicordplugins/equicordToolbox/index.tsx @@ -165,7 +165,7 @@ export default definePlugin({ { find: ".controlButtonWrapper,", replacement: { - match: /(?<=toolbar:function\(.{0,100}\()\i.Fragment,/, + match: /(?<=function (\i).{0,100}\()\i.Fragment,(?=.+?toolbar:\1\(\))/, replace: "$self.ToolboxFragmentWrapper," } } diff --git a/src/plugins/_api/notices.ts b/src/plugins/_api/notices.ts index ace58e705..89aa1a465 100644 --- a/src/plugins/_api/notices.ts +++ b/src/plugins/_api/notices.ts @@ -33,8 +33,8 @@ export default definePlugin({ replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" }, { - match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/, - replace: (m, notice) => `if(${notice}?.id=="EquicordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}` + match: /(?<=function (\i)\(\i\){)return null!=(\i)(?=.+?NOTICE_DISMISS:\1)/, + replace: (m, _, notice) => `if(${notice}?.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}` } ] } diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index fa5573aa3..d541ffbff 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -259,7 +259,7 @@ export default definePlugin({ replacement: { // let { ... nowPlaying: a = !1 ... // let { overlay: b ... } = Props - match: /#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}.+?}\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)/, + match: /null:.{0,10}\i\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)/, replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` } }, diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 046ae4f55..8b0e55f97 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -42,8 +42,8 @@ export default definePlugin({ { find: ",BURST_REACTION_EFFECT_PLAY", replacement: { - match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=\d+?(?=\))/, - replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})` + match: /(?<=(\i)=\i=>{.+?)(\i\(\i,\i\))>=\i(?=\).+BURST_REACTION_EFFECT_PLAY:\1)/, + replace: "!$self.shouldPlayBurstReaction($2)" } }, { From 9b5a5f456c9855a249d03aefe80b667811bca8bc Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:41:59 -0400 Subject: [PATCH 047/881] Test --- src/equicordplugins/allCallTimers/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/allCallTimers/index.tsx b/src/equicordplugins/allCallTimers/index.tsx index b4fb0e852..3ce77fc04 100644 --- a/src/equicordplugins/allCallTimers/index.tsx +++ b/src/equicordplugins/allCallTimers/index.tsx @@ -119,7 +119,7 @@ export default definePlugin({ predicate: () => settings.store.showWithoutHover, replacement: [ { - match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, + match: /function\(\)\{.+:""(?=.*?userId:(\i))/, replace: "$&,$self.renderTimer($1.id)," } ] From 5542ac620279ba5629f020e0357060e967c28b87 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:42:33 -0700 Subject: [PATCH 048/881] Fix patch. (#352) --- src/plugins/showMeYourName/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 05bd9bd74..74863de73 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -788,7 +788,7 @@ export default definePlugin({ replacement: [ { // Replace names in messages and replies. - match: /(onContextMenu:\i,children:)(.{0,100}?),"data-text":(\i\+\i)/, + match: /(onContextMenu:\i,children:)(.{0,150}?),"data-text":(\i\+\i)/, replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" }, { From c4bd6b06b23ae4c566c556a29a4568e7e07873d1 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:42:41 -0700 Subject: [PATCH 049/881] Remove no longer used patch. (#351) --- src/equicordplugins/questify/index.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 260aaf8b4..13b95d4ff 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -973,14 +973,6 @@ export default definePlugin({ } ] }, - { - // Prevents default pinning of specific Quests to the top of the list. - find: "questTile,{[", - replacement: { - match: /\i.unshift\(\i\):(\i.push\(\i\))/, - replace: "$1:$1" - } - }, { // Sorts the "All Quests" tab quest tiles. // Also sets mobile-only Quests as desktop compatible if the setting is enabled. From 2a123dcefad7801f4719a49f9b43b542ba0a5f8a Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:43:23 -0400 Subject: [PATCH 050/881] Tests --- src/equicordplugins/allCallTimers/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/allCallTimers/index.tsx b/src/equicordplugins/allCallTimers/index.tsx index 3ce77fc04..b4fb0e852 100644 --- a/src/equicordplugins/allCallTimers/index.tsx +++ b/src/equicordplugins/allCallTimers/index.tsx @@ -119,7 +119,7 @@ export default definePlugin({ predicate: () => settings.store.showWithoutHover, replacement: [ { - match: /function\(\)\{.+:""(?=.*?userId:(\i))/, + match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, replace: "$&,$self.renderTimer($1.id)," } ] From 1392a3f6aeede92d812f46697dba1ba05124adf1 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:57:50 -0400 Subject: [PATCH 051/881] Fix Patches --- src/equicordplugins/questCompleter.discordDesktop/index.tsx | 6 +++--- src/plugins/showMeYourName/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/equicordplugins/questCompleter.discordDesktop/index.tsx b/src/equicordplugins/questCompleter.discordDesktop/index.tsx index 330aa7f64..614e827cc 100644 --- a/src/equicordplugins/questCompleter.discordDesktop/index.tsx +++ b/src/equicordplugins/questCompleter.discordDesktop/index.tsx @@ -262,14 +262,14 @@ export default definePlugin({ settings, patches: [ { - find: "BkZhUF)}", + find: "BkZhUF),", replacement: { - match: /(?<=questId:(\i\.id).*?\.PRIMARY,)disabled:!0/, + match: /(?<=questId:(\i\.id).*?"secondary",)disabled:!0/, replace: "onClick: () => $self.mobileQuestPatch($1)" }, }, { - find: "AppTitleBar", + find: '"BACK_FORWARD_NAVIGATION"', replacement: { match: /(?<=trailing:.{0,70}\(\i\.Fragment,{children:\[)/, replace: "$self.renderQuestButton()," diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 74863de73..52cc38a94 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -788,7 +788,7 @@ export default definePlugin({ replacement: [ { // Replace names in messages and replies. - match: /(onContextMenu:\i,children:)(.{0,150}?),"data-text":(\i\+\i)/, + match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/, replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" }, { From bb85e52dd9c78da7e77b4ad7ed49e67e5319ec9c Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:58:16 -0700 Subject: [PATCH 052/881] Fix logical flaw. (#353) --- src/plugins/showMeYourName/index.tsx | 44 +++++++++------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 52cc38a94..5eca3a0b3 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -487,9 +487,9 @@ function renderUsername( const message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null; const groupId = (message as any)?.showMeYourNameGroupId || null; const isHovering = (isMessage || isReply || isMention) - ? (hoveringMessageSet.has(Number(messageId)) || hoveringMessageSet.has(Number(groupId))) + ? ((messageId && hoveringMessageSet.has(messageId)) || (groupId && hoveringMessageSet.has(groupId))) : isReactionsPopout - ? hoveringReactionPopoutSet.has(Number((author as User).id)) + ? hoveringReactionPopoutSet.has((author as User).id) : false; const shouldGradientGlow = isHovering && hasGradient; @@ -632,43 +632,27 @@ function renderUsername( return [allDataText, nameElement, first.name]; } -const hoveringMessageSet = new Set(); -const hoveringReactionPopoutSet = new Set(); +const hoveringMessageSet = new Set(); +const hoveringReactionPopoutSet = new Set(); function addHoveringMessage(id: string) { - const prevSize = hoveringMessageSet.size; - hoveringMessageSet.add(Number(id)); - - if (hoveringMessageSet.size !== prevSize) { - settings.store.triggerNameRerender = !settings.store.triggerNameRerender; - } + hoveringMessageSet.add(id); + settings.store.triggerNameRerender = !settings.store.triggerNameRerender; } function removeHoveringMessage(id: string) { - const prevSize = hoveringMessageSet.size; - hoveringMessageSet.delete(Number(id)); - - if (hoveringMessageSet.size !== prevSize) { - settings.store.triggerNameRerender = !settings.store.triggerNameRerender; - } + hoveringMessageSet.delete(id); + settings.store.triggerNameRerender = !settings.store.triggerNameRerender; } function addHoveringReactionPopout(id: string) { - const prevSize = hoveringReactionPopoutSet.size; - hoveringReactionPopoutSet.add(Number(id)); - - if (hoveringReactionPopoutSet.size !== prevSize) { - settings.store.triggerNameRerender = !settings.store.triggerNameRerender; - } + hoveringReactionPopoutSet.add(id); + settings.store.triggerNameRerender = !settings.store.triggerNameRerender; } function removeHoveringReactionPopout(id: string) { - const prevSize = hoveringReactionPopoutSet.size; - hoveringReactionPopoutSet.delete(Number(id)); - - if (hoveringReactionPopoutSet.size !== prevSize) { - settings.store.triggerNameRerender = !settings.store.triggerNameRerender; - } + hoveringReactionPopoutSet.delete(id); + settings.store.triggerNameRerender = !settings.store.triggerNameRerender; } const settings = definePluginSettings({ @@ -792,7 +776,7 @@ export default definePlugin({ replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" }, { - // Animate gradients in first-level messages. + // Animate gradients for message authors. match: /(let{setAnimate:\i}=(\i);)/, replace: "$1if($2.animate){$self.addHoveringMessage(arguments[0].message.id)}else{$self.removeHoveringMessage(arguments[0].message.id)};" } @@ -832,7 +816,7 @@ export default definePlugin({ find: "CUSTOM_GIFT?\"\":", replacement: { match: /(\(\i,\i,\i\);)(let \i=\i.id===\i(?:.{0,500}?)hovering:(\i))/, - replace: "$1arguments[0].message.showMeYourNameGroupId=arguments[0].groupId;if($3){$self.addHoveringMessage(arguments[0].groupId);}else{$self.removeHoveringMessage(arguments[0].groupId)};$2" + replace: "$1arguments[0].message.showMeYourNameGroupId=arguments[0].groupId;if($3){$self.addHoveringMessage(arguments[0].groupId)}else{$self.removeHoveringMessage(arguments[0].groupId)};$2" }, }, { From 0165d01ed088bf97268871fcc7c0696d281e47e7 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:19:35 -0700 Subject: [PATCH 053/881] Fix patch. (#354) --- src/equicordplugins/questify/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 13b95d4ff..30de075fc 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -1046,7 +1046,7 @@ export default definePlugin({ { // The Quest Accepted button is disabled by default. If the user reloads the client, they need a way // to resume the automatic completion, so patch in optionally enabling it if the feature is enabled. - match: /(START_QUEST_CTA.{0,400}?)(!0)/, + match: /(START_QUEST_CTA.{0,400}?disabled:)(!0)/, replace: "$1$self.shouldDisableQuestAcceptedButton(arguments[0].quest)??$2" }, { From 382153f9ed1a57318b79dd394ec3374f8ac611af Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Thu, 21 Aug 2025 11:02:43 -0700 Subject: [PATCH 054/881] Improve some things. (#355) --- src/equicordplugins/questify/index.tsx | 37 ++++++++--- src/equicordplugins/questify/settings.tsx | 70 ++++++++++----------- src/equicordplugins/questify/utils/misc.tsx | 4 +- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 30de075fc..70baf9d43 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -430,7 +430,7 @@ function preprocessQuests(quests: Quest[]): Quest[] { const unclaimedSortFunction = createSortFunction(unclaimedSubsort || "Recent DESC"); - // Divide unclaimed quests by completion status before applying subsort. + // Divide unclaimed Quests by completion status before applying subsort. questGroups.unclaimed.sort((a: Quest, b: Quest) => { const aCompleted = !!a.userStatus?.completedAt; const bCompleted = !!b.userStatus?.completedAt; @@ -688,12 +688,12 @@ function processQuestForAutoComplete(quest: Quest): boolean { if (quest.userStatus?.completedAt || existingInterval) { return true; } else if (!playType && !watchType) { - QuestifyLogger.warn(`[${getFormattedNow()}] Could not recognize the quest type for ${questName}.`); + QuestifyLogger.warn(`[${getFormattedNow()}] Could not recognize the Quest type for ${questName}.`); return true; } else if ((watchType && !completeVideoQuestsInBackground) || (playType && (!completeGameQuestsInBackground || !IS_DISCORD_DESKTOP))) { return true; } else if (!questDuration) { - QuestifyLogger.warn(`[${getFormattedNow()}] Could not find duration for quest ${questName}.`); + QuestifyLogger.warn(`[${getFormattedNow()}] Could not find duration for Quest ${questName}.`); return true; } else if (watchType) { startVideoProgressTracking(quest, questDuration); @@ -747,7 +747,7 @@ function getQuestAcceptedButtonText(quest: Quest): string | null { return null; } -function getActiveQuestClosestToCompletion(): Quest | null { +function getQuestPanelOverride(): Quest | null { let closestQuest: Quest | null = null; let closestTimeRemaining = Infinity; @@ -766,6 +766,23 @@ function getActiveQuestClosestToCompletion(): Quest | null { } }); + if (!closestQuest) { + const completedQuests = Array.from(QuestsStore.quests.values() as Quest[]).filter(q => q.userStatus?.completedAt).sort((a, b) => { + const aTime = new Date(a.userStatus?.completedAt as string); + const bTime = new Date(b.userStatus?.completedAt as string); + return bTime.getTime() - aTime.getTime(); + }); + + completedQuests.forEach(quest => { + const completedQuest = quest.userStatus?.completedAt; + const questStatus = getQuestStatus(quest); + + if (completedQuest && questStatus === QuestStatus.Unclaimed) { + closestQuest = quest; + } + }); + } + return closestQuest; } @@ -789,7 +806,7 @@ export default definePlugin({ shouldHideGiftInventoryRelocationNotice, shouldHideFriendsListActiveNowPromotion, shouldDisableQuestAcceptedButton, - getActiveQuestClosestToCompletion, + getQuestPanelOverride, getQuestAcceptedButtonText, processQuestForAutoComplete, activeQuestIntervals, @@ -858,7 +875,7 @@ export default definePlugin({ }, { // Hides the new Quest popup above the account panel. - // Allows in-progress quests to still show. + // Allows in-progress Quests to still show. find: "QUESTS_BAR,questId", replacement: { match: /return null==(\i)\?null:\(/, @@ -871,7 +888,7 @@ export default definePlugin({ find: "questDeliveryOverride)?", replacement: { match: /(\i=)(\i.\i.questDeliveryOverride)/, - replace: "$1$self.getActiveQuestClosestToCompletion()??$2" + replace: "$1$self.getQuestPanelOverride()??$2" } }, { @@ -957,7 +974,7 @@ export default definePlugin({ group: true, replacement: [ { - // Restyles quest tiles with colors. + // Restyles Quest tiles with colors. match: /className:(\i\(\)\(\i.container,\i\)),/, replace: "className:$self.getQuestTileClasses($1,arguments[0].quest),style:$self.getQuestTileStyle(arguments[0].quest)," }, @@ -974,7 +991,7 @@ export default definePlugin({ ] }, { - // Sorts the "All Quests" tab quest tiles. + // Sorts the "All Quests" tab Quest tiles. // Also sets mobile-only Quests as desktop compatible if the setting is enabled. find: ".ALL);return(", replacement: { @@ -983,7 +1000,7 @@ export default definePlugin({ } }, { - // Sorts the "Claimed Quests" tab quest tiles. + // Sorts the "Claimed Quests" tab Quest tiles. find: ".ALL)}):(", replacement: { match: /(claimedQuests:(\i).{0,50}?;)/, diff --git a/src/equicordplugins/questify/settings.tsx b/src/equicordplugins/questify/settings.tsx index 65a133056..51c82ebb7 100644 --- a/src/equicordplugins/questify/settings.tsx +++ b/src/equicordplugins/questify/settings.tsx @@ -50,10 +50,10 @@ export function fetchAndAlertQuests(source: string, logger: Logger): void { const shouldAlert = settings.store.fetchingQuestsAlert; if (shouldAlert) { - logger.info(`[${getFormattedNow()}] New quests detected. Playing alert sound.`); + logger.info(`[${getFormattedNow()}] New Quests detected. Playing alert sound.`); AudioPlayer(shouldAlert, 1).play(); } else { - logger.info(`[${getFormattedNow()}] New quests detected.`); + logger.info(`[${getFormattedNow()}] New Quests detected.`); } } } @@ -75,13 +75,13 @@ export function startAutoFetchingQuests(seconds?: number): void { } const interval = seconds ? seconds * 1000 : settings.store.fetchingQuestsInterval * 1000; - QuestifyLogger.info(`[${getFormattedNow()}] Starting AutoFetch of quests every ${(interval / 60000).toFixed(2)} minutes.`); + QuestifyLogger.info(`[${getFormattedNow()}] Starting AutoFetch of Quests every ${(interval / 60000).toFixed(2)} minutes.`); autoFetchInterval = setInterval(() => { fetchAndAlertQuests("Questify-AutoFetch", QuestifyLogger); }, interval); } export function stopAutoFetchingQuests(): void { if (autoFetchInterval) { - QuestifyLogger.info(`[${getFormattedNow()}] Stopping AutoFetch of quests.`); + QuestifyLogger.info(`[${getFormattedNow()}] Stopping AutoFetch of Quests.`); clearInterval(autoFetchInterval); autoFetchInterval = null; } @@ -712,8 +712,7 @@ function RestyleQuestsSetting() { restyleQuestsIgnored, restyleQuestsExpired, restyleQuestsGradient, - restyleQuestsPreload, - ignoredQuestProfile + restyleQuestsPreload } = settings.use([ "restyleQuestsUnclaimed", "restyleQuestsClaimed", @@ -721,7 +720,6 @@ function RestyleQuestsSetting() { "restyleQuestsExpired", "restyleQuestsGradient", "restyleQuestsPreload", - "ignoredQuestProfile" ]); const [unclaimedColor, setUnclaimedColor] = useState(restyleQuestsUnclaimed); @@ -871,26 +869,6 @@ function RestyleQuestsSetting() { />
-
-
- - Ignored Quest Profile - - value === ignoredQuestProfile} + select={(value: string) => { + settings.store.ignoredQuestProfile = value; + }} + /> +
+
@@ -1314,7 +1314,7 @@ function FetchingQuestsSetting(): JSX.Element { Configure how often to fetch Quests from Discord and set up alerts for new Quests.

- By default, Discord only fetches quests on load and when visiting the Quests page. + By default, Discord only fetches Quests on load and when visiting the Quests page. This means that without a fetch interval defined below, this plugin will become unaware of new Quests added throughout the day.

@@ -1609,25 +1609,25 @@ export const settings = definePluginSettings({ }, unclaimedSubsort: { type: OptionType.STRING, - description: "Subsort method for unclaimed quests.", + description: "Subsort method for unclaimed Quests.", default: "Expiring ASC", // "Recent ASC", "Recent DESC", "Expiring ASC", "Expiring DESC" hidden: true }, claimedSubsort: { type: OptionType.STRING, - description: "Subsort method for claimed quests.", + description: "Subsort method for claimed Quests.", default: "Claimed DESC", // "Recent ASC", "Recent DESC", "Claimed ASC", "Claimed DESC" hidden: true }, ignoredSubsort: { type: OptionType.STRING, - description: "Subsort method for ignored quests.", + description: "Subsort method for ignored Quests.", default: "Recent DESC", // "Recent ASC", "Recent DESC", "Expiring ASC", "Expiring DESC" hidden: true }, expiredSubsort: { type: OptionType.STRING, - description: "Subsort method for expired quests.", + description: "Subsort method for expired Quests.", default: "Expiring DESC", // "Recent ASC", "Recent DESC", "Expiring ASC", "Expiring DESC" hidden: true }, @@ -1639,7 +1639,7 @@ export const settings = definePluginSettings({ }, onQuestsPage: { type: OptionType.BOOLEAN, - description: "Whether the user is currently on the quests page.", + description: "Whether the user is currently on the Quests page.", default: false, hidden: true }, diff --git a/src/equicordplugins/questify/utils/misc.tsx b/src/equicordplugins/questify/utils/misc.tsx index 1aeff166d..5f62452b5 100644 --- a/src/equicordplugins/questify/utils/misc.tsx +++ b/src/equicordplugins/questify/utils/misc.tsx @@ -18,7 +18,7 @@ export const QuestifyLogger = new Logger("Questify"); export const PlayAudio = findByCodeLazy("Unable to find sound for pack name:"); // Takes an audio name or URL, volume, and a callback function. Plays when play() is called. const AudioPlayerConstructor = findByCodeLazy("sound has no duration"); -export function AudioPlayer(name: string, volume: number = 1, callback?: () => void): any { return new AudioPlayerConstructor(name, null, volume, "default", callback || (() => { })); } +export function AudioPlayer(name: string, volume: number = 1, callback?: () => void): any { return new AudioPlayerConstructor(name, null, volume, "default", callback ?? (() => { })); } export const QuestsStore = findStoreLazy("QuestsStore"); export const questPath = "/discovery/quests"; export const leftClick = 0; @@ -267,7 +267,7 @@ export async function reportPlayGameQuestProgress(quest: Quest, terminal: boolea const progress = progressPlayType?.value || 1; if (!questPlayType) { - logger?.warn(`[${getFormattedNow()}] Could not recognize the quest type for Quest ${questName}.`); + logger?.warn(`[${getFormattedNow()}] Could not recognize the Quest type for Quest ${questName}.`); return { progress: null }; } From aa6915072caf1ae0594484c97f65ed8a0ddaf22c Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:17:52 -0400 Subject: [PATCH 055/881] Add Some Readmes --- .../{ABSRPC => absRPC}/index.tsx | 0 src/equicordplugins/allCallTimers/README.md | 12 +++++ src/equicordplugins/altKrispSwitch/README.md | 3 ++ .../alwaysExpandProfiles/README.md | 3 ++ src/equicordplugins/amITyping/README.md | 3 ++ src/equicordplugins/anammox/README.md | 9 ++++ src/equicordplugins/atSomeone/README.md | 10 ++++ src/equicordplugins/autoJump/README.md | 3 ++ .../bannersEverywhere/README.md | 5 ++ .../betterActivities/README.md | 7 +++ .../betterAudioPlayer/README.md | 3 ++ .../betterBanReasons/README.md | 7 +++ .../betterBlockedUsers/README.md | 3 ++ src/equicordplugins/betterCommands/README.md | 3 ++ src/equicordplugins/betterInvites/README.md | 11 +++++ .../betterPlusReacts/README.md | 3 ++ .../index.ts | 1 - src/equicordplugins/blockKrisp/README.md | 5 ++ src/equicordplugins/bypassPinPrompt/README.md | 3 ++ src/equicordplugins/bypassStatus/README.md | 3 ++ src/equicordplugins/channelBadges/README.md | 3 ++ src/equicordplugins/channelTabs/README.md | 7 +++ .../characterCounter/README.md | 3 ++ .../cleanChannelName/README.md | 3 ++ src/equicordplugins/clientSideBlock/README.md | 3 ++ src/equicordplugins/clientSideBlock/index.tsx | 8 --- .../README.md | 3 ++ src/equicordplugins/commandPalette/README.md | 3 ++ src/equicordplugins/commandPalette/doc.md | 49 +++++++++++++++++++ src/equicordplugins/contentWarning/README.md | 3 ++ src/equicordplugins/contentWarning/index.tsx | 2 +- .../copyProfileColors/README.md | 3 ++ src/equicordplugins/copyStatusUrls/README.md | 3 ++ src/equicordplugins/copyUserMention/README.md | 3 ++ .../customFolderIcons/README.md | 11 +++++ src/equicordplugins/customSounds/README.md | 3 ++ .../customTimestamps/README.md | 3 ++ .../customUserColors/README.md | 3 ++ src/equicordplugins/cuteAnimeBoys/README.md | 3 ++ src/equicordplugins/cuteNekos/README.md | 3 ++ src/equicordplugins/cutePats/README.md | 3 ++ src/equicordplugins/demonstration/README.md | 3 ++ src/equicordplugins/disableCameras/README.md | 3 ++ .../discordDevBanner/README.md | 3 ++ src/equicordplugins/dontFilterMe/README.md | 3 ++ src/equicordplugins/downloadify/README.md | 3 ++ src/equicordplugins/equicordHelper/README.md | 3 ++ src/equicordplugins/equicordToolbox/README.md | 3 ++ src/equicordplugins/equissant/README.md | 3 ++ src/equicordplugins/exportContacts/README.md | 3 ++ src/equicordplugins/exportMessages/README.md | 3 ++ .../fakeProfileThemesAndEffects/README.md | 3 ++ .../fastDeleteChannels/README.md | 3 ++ src/equicordplugins/findReply/README.md | 5 ++ .../fixFileExtensions/README.MD | 3 ++ src/equicordplugins/followVoiceUser/README.md | 3 ++ src/equicordplugins/fontLoader/README.md | 3 ++ src/equicordplugins/forwardAnywhere/README.md | 3 ++ src/equicordplugins/freaky/README.md | 3 ++ .../frequentQuickSwitcher/README.md | 3 ++ src/equicordplugins/friendCloud/README.md | 7 +++ src/equicordplugins/friendCodes/README.md | 3 ++ src/equicordplugins/friendTags/README.md | 3 ++ src/equicordplugins/friendshipRanks/README.md | 3 ++ src/equicordplugins/fullVcPfp/README.md | 3 ++ src/equicordplugins/ignoreTerms/README.md | 5 ++ .../messageLinkTooltip/README.md | 3 ++ .../README.md | 3 ++ .../index.tsx | 0 src/equicordplugins/sidebarChat/README.md | 5 ++ .../statusWhilePlaying.desktop/README.md | 5 ++ src/equicordplugins/teX/README.md | 8 +++ .../timelessClips.desktop/README.md | 3 ++ src/equicordplugins/title/README.md | 3 ++ .../unreadBadgeCount/README.md | 7 +++ src/equicordplugins/viewRawVariant/README.md | 3 ++ src/equicordplugins/voiceChatUtils/README.md | 5 ++ src/equicordplugins/wallpaperFree/README.md | 7 +++ src/equicordplugins/webpackTarball/README.md | 5 ++ .../accountPanelServerProfile/README.md | 7 +++ src/plugins/alwaysExpandRoles/README.md | 3 ++ src/plugins/appleMusic.desktop/README.md | 9 ++++ src/plugins/betterFolders/README.md | 11 +++++ src/plugins/betterRoleContext/README.md | 6 +++ src/plugins/betterSessions/README.md | 5 ++ src/plugins/betterSettings/README.md | 9 ++++ src/plugins/clientTheme/README.md | 7 +++ src/plugins/consoleJanitor/README.md | 5 ++ src/plugins/copyEmojiMarkdown/README.md | 5 ++ src/plugins/copyFileContents/README.md | 5 ++ src/plugins/copyStickerLinks/README.md | 5 ++ src/plugins/customIdle/README.md | 5 ++ src/plugins/dearrow/README.md | 5 ++ src/plugins/decor/README.md | 17 +++++++ src/plugins/favEmojiFirst/README.md | 6 +++ src/plugins/favGifSearch/README.md | 5 ++ src/plugins/fixImagesQuality/README.md | 3 ++ .../fixYoutubeEmbeds.desktop/README.md | 5 ++ src/plugins/friendsSince/README.md | 5 ++ src/plugins/fullSearchContext/README.md | 5 ++ src/plugins/ignoreActivities/README.md | 13 +++++ src/plugins/imageLink/README.md | 3 ++ src/plugins/imageZoom/README.md | 6 +++ src/plugins/implicitRelationships/README.md | 7 +++ src/plugins/mentionAvatars/README.md | 6 +++ src/plugins/messageClickActions/README.md | 5 ++ src/plugins/messageLatency/README.md | 31 ++++++++++++ src/plugins/noMaskedUrlPaste/README.md | 3 ++ src/plugins/noUnblockToJump/README.md | 5 ++ src/plugins/notificationVolume/README.md | 3 ++ .../{index.tsx => index.ts} | 0 src/plugins/onePingPerDM/README.md | 7 +++ src/plugins/openInApp/README.md | 11 +++++ src/plugins/pauseInvitesForever/README.md | 5 ++ src/plugins/permissionFreeWill/README.md | 9 ++++ src/plugins/previewMessage/README.md | 6 +++ src/plugins/quickMention/README.md | 5 ++ src/plugins/quickReply/README.md | 6 +++ src/plugins/replaceGoogleSearch/README.md | 5 ++ src/plugins/replyTimestamp/README.md | 5 ++ src/plugins/seeSummaries/README.md | 9 ++++ src/plugins/serverInfo/README.md | 7 +++ src/plugins/showHiddenThings/README.md | 19 +++++++ src/plugins/showTimeoutDuration/README.md | 8 +++ src/plugins/stickerPaste/README.md | 3 ++ src/plugins/superReactionTweaks/README.md | 11 +++++ src/plugins/themeAttributes/README.md | 29 +++++++++++ src/plugins/unlockedAvatarZoom/README.md | 5 ++ src/plugins/userMessagesPronouns/README.md | 5 ++ src/plugins/userVoiceShow/README.md | 7 +++ src/plugins/validReply/README.md | 7 +++ src/plugins/volumeBooster/README.md | 9 ++++ src/plugins/whoReacted/README.md | 5 ++ src/plugins/xsOverlay/README.md | 14 ++++++ src/plugins/youtubeAdblock.desktop/README.md | 7 +++ 135 files changed, 771 insertions(+), 10 deletions(-) rename src/equicordplugins/{ABSRPC => absRPC}/index.tsx (100%) create mode 100644 src/equicordplugins/allCallTimers/README.md create mode 100644 src/equicordplugins/altKrispSwitch/README.md create mode 100644 src/equicordplugins/alwaysExpandProfiles/README.md create mode 100644 src/equicordplugins/amITyping/README.md create mode 100644 src/equicordplugins/anammox/README.md create mode 100644 src/equicordplugins/atSomeone/README.md create mode 100644 src/equicordplugins/autoJump/README.md create mode 100644 src/equicordplugins/bannersEverywhere/README.md create mode 100644 src/equicordplugins/betterActivities/README.md create mode 100644 src/equicordplugins/betterAudioPlayer/README.md create mode 100644 src/equicordplugins/betterBanReasons/README.md create mode 100644 src/equicordplugins/betterBlockedUsers/README.md create mode 100644 src/equicordplugins/betterCommands/README.md create mode 100644 src/equicordplugins/betterInvites/README.md create mode 100644 src/equicordplugins/betterPlusReacts/README.md rename src/equicordplugins/{betterplusreacts => betterPlusReacts}/index.ts (96%) create mode 100644 src/equicordplugins/blockKrisp/README.md create mode 100644 src/equicordplugins/bypassPinPrompt/README.md create mode 100644 src/equicordplugins/bypassStatus/README.md create mode 100644 src/equicordplugins/channelBadges/README.md create mode 100644 src/equicordplugins/channelTabs/README.md create mode 100644 src/equicordplugins/characterCounter/README.md create mode 100644 src/equicordplugins/cleanChannelName/README.md create mode 100644 src/equicordplugins/clientSideBlock/README.md create mode 100644 src/equicordplugins/clipsEnhancements.discordDesktop/README.md create mode 100644 src/equicordplugins/commandPalette/README.md create mode 100644 src/equicordplugins/commandPalette/doc.md create mode 100644 src/equicordplugins/contentWarning/README.md create mode 100644 src/equicordplugins/copyProfileColors/README.md create mode 100644 src/equicordplugins/copyStatusUrls/README.md create mode 100644 src/equicordplugins/copyUserMention/README.md create mode 100644 src/equicordplugins/customFolderIcons/README.md create mode 100644 src/equicordplugins/customSounds/README.md create mode 100644 src/equicordplugins/customTimestamps/README.md create mode 100644 src/equicordplugins/customUserColors/README.md create mode 100644 src/equicordplugins/cuteAnimeBoys/README.md create mode 100644 src/equicordplugins/cuteNekos/README.md create mode 100644 src/equicordplugins/cutePats/README.md create mode 100644 src/equicordplugins/demonstration/README.md create mode 100644 src/equicordplugins/disableCameras/README.md create mode 100644 src/equicordplugins/discordDevBanner/README.md create mode 100644 src/equicordplugins/dontFilterMe/README.md create mode 100644 src/equicordplugins/downloadify/README.md create mode 100644 src/equicordplugins/equicordHelper/README.md create mode 100644 src/equicordplugins/equicordToolbox/README.md create mode 100644 src/equicordplugins/equissant/README.md create mode 100644 src/equicordplugins/exportContacts/README.md create mode 100644 src/equicordplugins/exportMessages/README.md create mode 100644 src/equicordplugins/fakeProfileThemesAndEffects/README.md create mode 100644 src/equicordplugins/fastDeleteChannels/README.md create mode 100644 src/equicordplugins/findReply/README.md create mode 100644 src/equicordplugins/fixFileExtensions/README.MD create mode 100644 src/equicordplugins/followVoiceUser/README.md create mode 100644 src/equicordplugins/fontLoader/README.md create mode 100644 src/equicordplugins/forwardAnywhere/README.md create mode 100644 src/equicordplugins/freaky/README.md create mode 100644 src/equicordplugins/frequentQuickSwitcher/README.md create mode 100644 src/equicordplugins/friendCloud/README.md create mode 100644 src/equicordplugins/friendCodes/README.md create mode 100644 src/equicordplugins/friendTags/README.md create mode 100644 src/equicordplugins/friendshipRanks/README.md create mode 100644 src/equicordplugins/fullVcPfp/README.md create mode 100644 src/equicordplugins/ignoreTerms/README.md create mode 100644 src/equicordplugins/messageLinkTooltip/README.md create mode 100644 src/equicordplugins/notificationTitle.discordDesktop/README.md rename src/equicordplugins/{notificationTitle => notificationTitle.discordDesktop}/index.tsx (100%) create mode 100644 src/equicordplugins/sidebarChat/README.md create mode 100644 src/equicordplugins/statusWhilePlaying.desktop/README.md create mode 100644 src/equicordplugins/teX/README.md create mode 100644 src/equicordplugins/timelessClips.desktop/README.md create mode 100644 src/equicordplugins/title/README.md create mode 100644 src/equicordplugins/unreadBadgeCount/README.md create mode 100644 src/equicordplugins/viewRawVariant/README.md create mode 100644 src/equicordplugins/voiceChatUtils/README.md create mode 100644 src/equicordplugins/wallpaperFree/README.md create mode 100644 src/equicordplugins/webpackTarball/README.md create mode 100644 src/plugins/accountPanelServerProfile/README.md create mode 100644 src/plugins/alwaysExpandRoles/README.md create mode 100644 src/plugins/appleMusic.desktop/README.md create mode 100644 src/plugins/betterFolders/README.md create mode 100644 src/plugins/betterRoleContext/README.md create mode 100644 src/plugins/betterSessions/README.md create mode 100644 src/plugins/betterSettings/README.md create mode 100644 src/plugins/clientTheme/README.md create mode 100644 src/plugins/consoleJanitor/README.md create mode 100644 src/plugins/copyEmojiMarkdown/README.md create mode 100644 src/plugins/copyFileContents/README.md create mode 100644 src/plugins/copyStickerLinks/README.md create mode 100644 src/plugins/customIdle/README.md create mode 100644 src/plugins/dearrow/README.md create mode 100644 src/plugins/decor/README.md create mode 100644 src/plugins/favEmojiFirst/README.md create mode 100644 src/plugins/favGifSearch/README.md create mode 100644 src/plugins/fixImagesQuality/README.md create mode 100644 src/plugins/fixYoutubeEmbeds.desktop/README.md create mode 100644 src/plugins/friendsSince/README.md create mode 100644 src/plugins/fullSearchContext/README.md create mode 100644 src/plugins/ignoreActivities/README.md create mode 100644 src/plugins/imageLink/README.md create mode 100644 src/plugins/imageZoom/README.md create mode 100644 src/plugins/implicitRelationships/README.md create mode 100644 src/plugins/mentionAvatars/README.md create mode 100644 src/plugins/messageClickActions/README.md create mode 100644 src/plugins/messageLatency/README.md create mode 100644 src/plugins/noMaskedUrlPaste/README.md create mode 100644 src/plugins/noUnblockToJump/README.md create mode 100644 src/plugins/notificationVolume/README.md rename src/plugins/notificationVolume/{index.tsx => index.ts} (100%) create mode 100644 src/plugins/onePingPerDM/README.md create mode 100644 src/plugins/openInApp/README.md create mode 100644 src/plugins/pauseInvitesForever/README.md create mode 100644 src/plugins/permissionFreeWill/README.md create mode 100644 src/plugins/previewMessage/README.md create mode 100644 src/plugins/quickMention/README.md create mode 100644 src/plugins/quickReply/README.md create mode 100644 src/plugins/replaceGoogleSearch/README.md create mode 100644 src/plugins/replyTimestamp/README.md create mode 100644 src/plugins/seeSummaries/README.md create mode 100644 src/plugins/serverInfo/README.md create mode 100644 src/plugins/showHiddenThings/README.md create mode 100644 src/plugins/showTimeoutDuration/README.md create mode 100644 src/plugins/stickerPaste/README.md create mode 100644 src/plugins/superReactionTweaks/README.md create mode 100644 src/plugins/themeAttributes/README.md create mode 100644 src/plugins/unlockedAvatarZoom/README.md create mode 100644 src/plugins/userMessagesPronouns/README.md create mode 100644 src/plugins/userVoiceShow/README.md create mode 100644 src/plugins/validReply/README.md create mode 100644 src/plugins/volumeBooster/README.md create mode 100644 src/plugins/whoReacted/README.md create mode 100644 src/plugins/xsOverlay/README.md create mode 100644 src/plugins/youtubeAdblock.desktop/README.md diff --git a/src/equicordplugins/ABSRPC/index.tsx b/src/equicordplugins/absRPC/index.tsx similarity index 100% rename from src/equicordplugins/ABSRPC/index.tsx rename to src/equicordplugins/absRPC/index.tsx diff --git a/src/equicordplugins/allCallTimers/README.md b/src/equicordplugins/allCallTimers/README.md new file mode 100644 index 000000000..3526200cf --- /dev/null +++ b/src/equicordplugins/allCallTimers/README.md @@ -0,0 +1,12 @@ +# AllCallTimers + +Adds a timer to each user in a guild voice call. + +Original author: [AllCallTimersDiscordPlugin](https://github.com/Max-Herbold/AllCallTimersDiscordPlugin) + +image +image +
+image +image +
diff --git a/src/equicordplugins/altKrispSwitch/README.md b/src/equicordplugins/altKrispSwitch/README.md new file mode 100644 index 000000000..04d0c3ccd --- /dev/null +++ b/src/equicordplugins/altKrispSwitch/README.md @@ -0,0 +1,3 @@ +# AltKrispSwitch + +Swaps between None and Krisp instead of Krisp and Standard diff --git a/src/equicordplugins/alwaysExpandProfiles/README.md b/src/equicordplugins/alwaysExpandProfiles/README.md new file mode 100644 index 000000000..e5e63c2c9 --- /dev/null +++ b/src/equicordplugins/alwaysExpandProfiles/README.md @@ -0,0 +1,3 @@ +# AlwaysExpandProfiles + +Always expands a user or bots profile skipping the popout diff --git a/src/equicordplugins/amITyping/README.md b/src/equicordplugins/amITyping/README.md new file mode 100644 index 000000000..3f157dbee --- /dev/null +++ b/src/equicordplugins/amITyping/README.md @@ -0,0 +1,3 @@ +# AmITyping + +Simply shows if you are typing. This is almost exactly what it will look like for other users, so it may show after you have finished typing. diff --git a/src/equicordplugins/anammox/README.md b/src/equicordplugins/anammox/README.md new file mode 100644 index 000000000..568b91021 --- /dev/null +++ b/src/equicordplugins/anammox/README.md @@ -0,0 +1,9 @@ +# Anammox + +Removes Nitro-exclusive stuff from various places: + +- The shops above private chats +- The gift button in the chat box +- The "billing" section in settings + +While this can be done with css, a plugin is able to do it much more thorougly — for example, the shops are still accessible via keyboard navigation. diff --git a/src/equicordplugins/atSomeone/README.md b/src/equicordplugins/atSomeone/README.md new file mode 100644 index 000000000..fe035ef79 --- /dev/null +++ b/src/equicordplugins/atSomeone/README.md @@ -0,0 +1,10 @@ +# AtSomeone + +Mention someone randomly + +Due to some limitations, it won't show on autocomplete in dms/groups but it still works. + +Do `@role*` to mention a random person with that role. + +![image](https://cdn.nest.rip/uploads/20743dd7-0484-4f86-a7ed-0d56229ce778.png) +![image](https://cdn.nest.rip/uploads/a96afd16-8dc9-429a-8d63-f915fc28bae0.png) diff --git a/src/equicordplugins/autoJump/README.md b/src/equicordplugins/autoJump/README.md new file mode 100644 index 000000000..e361254ca --- /dev/null +++ b/src/equicordplugins/autoJump/README.md @@ -0,0 +1,3 @@ +# AutoJump + +Jumps to the first message in new messages for a channel diff --git a/src/equicordplugins/bannersEverywhere/README.md b/src/equicordplugins/bannersEverywhere/README.md new file mode 100644 index 000000000..c0a8fbdda --- /dev/null +++ b/src/equicordplugins/bannersEverywhere/README.md @@ -0,0 +1,5 @@ +# BannersEverywhere + +Displays nitro and USRBG banners as a background in the member list + +![](https://cdn.nest.rip/uploads/450f3161-ad26-4965-8e69-33104a2e7cae.png) diff --git a/src/equicordplugins/betterActivities/README.md b/src/equicordplugins/betterActivities/README.md new file mode 100644 index 000000000..b328c6557 --- /dev/null +++ b/src/equicordplugins/betterActivities/README.md @@ -0,0 +1,7 @@ +# BetterActivities + +Shows activity icons in the member list and allows showing all activities + +![Shows activity icons next to the status line in the member list](https://cdn.nest.rip/uploads/2ebf9c64-fb58-4e6c-a916-630db88be8ca.png) +![Shows all activities in the profile via a carousel](https://cdn.nest.rip/uploads/1f33b612-3b36-4aba-961d-0e94a7b31757.png) +![Optionally show all activities as list](https://cdn.nest.rip/uploads/acd042b7-2a57-4875-b7d4-9c558bfb9a0c.png) diff --git a/src/equicordplugins/betterAudioPlayer/README.md b/src/equicordplugins/betterAudioPlayer/README.md new file mode 100644 index 000000000..593a7713c --- /dev/null +++ b/src/equicordplugins/betterAudioPlayer/README.md @@ -0,0 +1,3 @@ +# BetterAudioPlayer + +Adds visualizers to audio attachments diff --git a/src/equicordplugins/betterBanReasons/README.md b/src/equicordplugins/betterBanReasons/README.md new file mode 100644 index 000000000..f5fb10a17 --- /dev/null +++ b/src/equicordplugins/betterBanReasons/README.md @@ -0,0 +1,7 @@ +# BetterBanReasons + +* Create custom reasons to use in the Discord ban modal. +* Show a text input by default instead of the options + +Settings screenshot +Ban modal Screenshot diff --git a/src/equicordplugins/betterBlockedUsers/README.md b/src/equicordplugins/betterBlockedUsers/README.md new file mode 100644 index 000000000..2b580938e --- /dev/null +++ b/src/equicordplugins/betterBlockedUsers/README.md @@ -0,0 +1,3 @@ +# BetterBlockedUsers + +Adds searching and opening the profile of a blocked user diff --git a/src/equicordplugins/betterCommands/README.md b/src/equicordplugins/betterCommands/README.md new file mode 100644 index 000000000..c785ab060 --- /dev/null +++ b/src/equicordplugins/betterCommands/README.md @@ -0,0 +1,3 @@ +# BetterCommands + +Adds/Fixes various things about commands diff --git a/src/equicordplugins/betterInvites/README.md b/src/equicordplugins/betterInvites/README.md new file mode 100644 index 000000000..037a3cff2 --- /dev/null +++ b/src/equicordplugins/betterInvites/README.md @@ -0,0 +1,11 @@ +# BetterInvites + +![Inviter preview](https://cdn.nest.rip/uploads/0438e1e9-405f-479b-a6ec-ed36224c5b28.png) + +![Receiver preview](https://cdn.nest.rip/uploads/0c454567-31ea-48da-9684-adb619bc2098.png) + +# features + +- be able to see the inviter and inviter profile +- preview discoverable servers by clicking the server name +- be able to see the invite relative expiration date diff --git a/src/equicordplugins/betterPlusReacts/README.md b/src/equicordplugins/betterPlusReacts/README.md new file mode 100644 index 000000000..1772d89ab --- /dev/null +++ b/src/equicordplugins/betterPlusReacts/README.md @@ -0,0 +1,3 @@ +# BetterPlusReacts + + diff --git a/src/equicordplugins/betterplusreacts/index.ts b/src/equicordplugins/betterPlusReacts/index.ts similarity index 96% rename from src/equicordplugins/betterplusreacts/index.ts rename to src/equicordplugins/betterPlusReacts/index.ts index a590b0263..2e3f84d2b 100644 --- a/src/equicordplugins/betterplusreacts/index.ts +++ b/src/equicordplugins/betterPlusReacts/index.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -// https://github.com/Masterjoona/vc-betterplusreacts/blob/main/index.ts import { Devs } from "@utils/constants"; import definePlugin from "@utils/types"; import { MessageStore } from "@webpack/common"; diff --git a/src/equicordplugins/blockKrisp/README.md b/src/equicordplugins/blockKrisp/README.md new file mode 100644 index 000000000..fab38364b --- /dev/null +++ b/src/equicordplugins/blockKrisp/README.md @@ -0,0 +1,5 @@ +# BlockKrisp + +Prevent Krisp from loading + +![Screenshot](https://cdn.nest.rip/uploads/8e08689e-992f-4c48-9cdf-9166304182a5.png) diff --git a/src/equicordplugins/bypassPinPrompt/README.md b/src/equicordplugins/bypassPinPrompt/README.md new file mode 100644 index 000000000..8543fcb01 --- /dev/null +++ b/src/equicordplugins/bypassPinPrompt/README.md @@ -0,0 +1,3 @@ +# BypassPinPrompt + +Bypasses the prompt for pinning messages diff --git a/src/equicordplugins/bypassStatus/README.md b/src/equicordplugins/bypassStatus/README.md new file mode 100644 index 000000000..992401e93 --- /dev/null +++ b/src/equicordplugins/bypassStatus/README.md @@ -0,0 +1,3 @@ +# BypassStatus + +Bypass Status allows you to set specific guilds/channels to still receive notifications and calls from when in a set status. diff --git a/src/equicordplugins/channelBadges/README.md b/src/equicordplugins/channelBadges/README.md new file mode 100644 index 000000000..7f61fd971 --- /dev/null +++ b/src/equicordplugins/channelBadges/README.md @@ -0,0 +1,3 @@ +# ChannelBadges + +Badges for channels based on their type such as text, voice, forum, etc. diff --git a/src/equicordplugins/channelTabs/README.md b/src/equicordplugins/channelTabs/README.md new file mode 100644 index 000000000..d536eee7f --- /dev/null +++ b/src/equicordplugins/channelTabs/README.md @@ -0,0 +1,7 @@ +# ChannelTabs + +Group your commonly visited channels in tabs, like a browser + +![screenshot of the plugin showing how the tabs look, as well as notification dots, typing indicators and a compact tab](https://cdn.nest.rip/uploads/d4435b4b-dbe4-4378-b984-923fba99b79f.png) +![bookmark bar with folders and various channel types](https://cdn.nest.rip/uploads/b2a1b5be-463a-4961-a654-3fb1c8803164.png) +![used together with a custom theme, tabs and bookmarks being dragged around](https://cdn.nest.rip/uploads/09d5af8b-5049-45d1-806c-1a5f702a4647.gif) diff --git a/src/equicordplugins/characterCounter/README.md b/src/equicordplugins/characterCounter/README.md new file mode 100644 index 000000000..2efee28d5 --- /dev/null +++ b/src/equicordplugins/characterCounter/README.md @@ -0,0 +1,3 @@ +# CharacterCounter + +Character counter for chat input diff --git a/src/equicordplugins/cleanChannelName/README.md b/src/equicordplugins/cleanChannelName/README.md new file mode 100644 index 000000000..3211ca4dd --- /dev/null +++ b/src/equicordplugins/cleanChannelName/README.md @@ -0,0 +1,3 @@ +# CleanChannelName + +Cleans emojis and other things out of channel names diff --git a/src/equicordplugins/clientSideBlock/README.md b/src/equicordplugins/clientSideBlock/README.md new file mode 100644 index 000000000..23d9f69a9 --- /dev/null +++ b/src/equicordplugins/clientSideBlock/README.md @@ -0,0 +1,3 @@ +# ClientSideBlock + +Blocks a user client side so they don't know they're blocked diff --git a/src/equicordplugins/clientSideBlock/index.tsx b/src/equicordplugins/clientSideBlock/index.tsx index 91053f293..52ea80223 100644 --- a/src/equicordplugins/clientSideBlock/index.tsx +++ b/src/equicordplugins/clientSideBlock/index.tsx @@ -36,14 +36,6 @@ const settings = definePluginSettings( restartNeeded: true, default: true }, - /* - hideNewUsers: { - type: OptionType.BOOLEAN, - description: "Should content from users with the \"I'm new here, say hi!\" badge be blocked\"", - restartNeeded: true, - default: true - }, - */ blockedReplyDisplay: { type: OptionType.SELECT, description: "What should display instead of the message when someone replies to someone you have hidden", diff --git a/src/equicordplugins/clipsEnhancements.discordDesktop/README.md b/src/equicordplugins/clipsEnhancements.discordDesktop/README.md new file mode 100644 index 000000000..591a84284 --- /dev/null +++ b/src/equicordplugins/clipsEnhancements.discordDesktop/README.md @@ -0,0 +1,3 @@ +# ClipsEnhancements + +Add more Clip FPS and duration options, plus RPC tagging! diff --git a/src/equicordplugins/commandPalette/README.md b/src/equicordplugins/commandPalette/README.md new file mode 100644 index 000000000..81abfe714 --- /dev/null +++ b/src/equicordplugins/commandPalette/README.md @@ -0,0 +1,3 @@ +# CommandPalette + +Navigating with a keyboard diff --git a/src/equicordplugins/commandPalette/doc.md b/src/equicordplugins/commandPalette/doc.md new file mode 100644 index 000000000..014b92832 --- /dev/null +++ b/src/equicordplugins/commandPalette/doc.md @@ -0,0 +1,49 @@ +# Command Palette Actions Guide + +Welcome to the Command Palette Actions Guide! This guide serves to inform you on how to implement your own actions to the CommandPalette plugin. To do so you have two options: hardcoding or utilizing the `registerAction` function. This guide will focus on using the function as its best practice and is what should be used in most situations. + +> [!IMPORTANT] +> While both methods hardcoding and using `registerAction` offer similar implementations, it's recommended to refrain from Hardcoding unless you solely plan on using this locally. + +### Implementing Multiple Choice Modals + +```ts +registerAction({ + id: 'multipleChoiceCommand', + label: 'Multiple Choice', + callback: async () => { + // Open a modal with multiple choices + const choice = await openMultipleChoice([ + { id: 'test1', label: 'Test 1' }, + { id: 'test2', label: 'Test 2' }, + ]); + + // Log the selected choice with its label and ID + console.log(`Selected ${choice.label} with the ID ${choice.id}`); + }, +}); +``` + +- **ID**: A unique identifier for the command/action, ensuring uniqueness across specific options in that modal instance. +- **Label**: The text displayed in the command palette for user recognition. +- **Callback (optional)**: The function executed when the command is triggered. + +Inside the callback, the `openMultipleChoice` function opens a modal with a list of choices. Users can select an option, and upon choosing, a `ButtonAction` type is returned. The user's choice is then logged for reference. + +### Implementing String Input Modals + +```ts +registerAction({ + id: 'stringInputCommand', + label: 'String Input', + callback: async () => { + // Open a modal with a text input + const text = await openSimpleTextInput(); + + // Log the inputted text to console + console.log(`They typed: ${text}`); + }, +}); +``` + +When the `stringInputCommand` is triggered, a modal with a simple text input field appears. Users can input text, and the entered string is returned. In this case, we log their input to console. diff --git a/src/equicordplugins/contentWarning/README.md b/src/equicordplugins/contentWarning/README.md new file mode 100644 index 000000000..156381b42 --- /dev/null +++ b/src/equicordplugins/contentWarning/README.md @@ -0,0 +1,3 @@ +# ContentWarning + +Specify certain words to blur diff --git a/src/equicordplugins/contentWarning/index.tsx b/src/equicordplugins/contentWarning/index.tsx index cf753d783..b1f21118e 100644 --- a/src/equicordplugins/contentWarning/index.tsx +++ b/src/equicordplugins/contentWarning/index.tsx @@ -133,7 +133,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "ContentWarning", authors: [EquicordDevs.camila314], - description: "Allows you to specify certain trigger words that will be blurred by default. Clicking on the blurred content will reveal it.", + description: "Allows you to specify certain trigger words that will be blurred by default. Hovering on the blurred content will reveal it.", settings, patches: [ { diff --git a/src/equicordplugins/copyProfileColors/README.md b/src/equicordplugins/copyProfileColors/README.md new file mode 100644 index 000000000..c098a1ed4 --- /dev/null +++ b/src/equicordplugins/copyProfileColors/README.md @@ -0,0 +1,3 @@ +# CopyProfileColors + +Allows copying profile colors diff --git a/src/equicordplugins/copyStatusUrls/README.md b/src/equicordplugins/copyStatusUrls/README.md new file mode 100644 index 000000000..102e03086 --- /dev/null +++ b/src/equicordplugins/copyStatusUrls/README.md @@ -0,0 +1,3 @@ +# CopyStatusUrls + +Allows copying status urls diff --git a/src/equicordplugins/copyUserMention/README.md b/src/equicordplugins/copyUserMention/README.md new file mode 100644 index 000000000..3b458fc85 --- /dev/null +++ b/src/equicordplugins/copyUserMention/README.md @@ -0,0 +1,3 @@ +# CopyUserMention + +Allows copying a users mention diff --git a/src/equicordplugins/customFolderIcons/README.md b/src/equicordplugins/customFolderIcons/README.md new file mode 100644 index 000000000..043226ac2 --- /dev/null +++ b/src/equicordplugins/customFolderIcons/README.md @@ -0,0 +1,11 @@ +# CustomFolderIcons + +Allows you to set custom images/svgs as folder icons + +Available as "Change Icon" in the folder context menu + +![the context menu with the "Change Icon" button](https://cdn.nest.rip/uploads/17256db9-8b5c-40a2-acdc-b37449eb9c0f.png) + +![an image of some example custom folder icons](https://cdn.nest.rip/uploads/109163ab-384d-4638-a035-f8743248147f.png) + +![the modal used to customize the folder icons](https://cdn.nest.rip/uploads/594d0792-0961-4766-bca8-74ae56e914fd.png) diff --git a/src/equicordplugins/customSounds/README.md b/src/equicordplugins/customSounds/README.md new file mode 100644 index 000000000..f3b502b68 --- /dev/null +++ b/src/equicordplugins/customSounds/README.md @@ -0,0 +1,3 @@ +# CustomSounds + +Allows customizing discords sounds diff --git a/src/equicordplugins/customTimestamps/README.md b/src/equicordplugins/customTimestamps/README.md new file mode 100644 index 000000000..ce9029eb3 --- /dev/null +++ b/src/equicordplugins/customTimestamps/README.md @@ -0,0 +1,3 @@ +# CustomTimestamps + +Allows custom timestamp formatting diff --git a/src/equicordplugins/customUserColors/README.md b/src/equicordplugins/customUserColors/README.md new file mode 100644 index 000000000..ec9a78515 --- /dev/null +++ b/src/equicordplugins/customUserColors/README.md @@ -0,0 +1,3 @@ +# CustomUserColors + +Like IrcColors except allows setting custom user colors diff --git a/src/equicordplugins/cuteAnimeBoys/README.md b/src/equicordplugins/cuteAnimeBoys/README.md new file mode 100644 index 000000000..68558f80f --- /dev/null +++ b/src/equicordplugins/cuteAnimeBoys/README.md @@ -0,0 +1,3 @@ +# CuteAnimeBoys + +Adds the cuteanimeboys command diff --git a/src/equicordplugins/cuteNekos/README.md b/src/equicordplugins/cuteNekos/README.md new file mode 100644 index 000000000..92e2d3b87 --- /dev/null +++ b/src/equicordplugins/cuteNekos/README.md @@ -0,0 +1,3 @@ +# CuteNekos + +Adds the nekos command diff --git a/src/equicordplugins/cutePats/README.md b/src/equicordplugins/cutePats/README.md new file mode 100644 index 000000000..f05bc65b3 --- /dev/null +++ b/src/equicordplugins/cutePats/README.md @@ -0,0 +1,3 @@ +# CutePats + +Adds the pat command diff --git a/src/equicordplugins/demonstration/README.md b/src/equicordplugins/demonstration/README.md new file mode 100644 index 000000000..431fcf8ab --- /dev/null +++ b/src/equicordplugins/demonstration/README.md @@ -0,0 +1,3 @@ +# Demonstration + +Helps for taking theme screenshots diff --git a/src/equicordplugins/disableCameras/README.md b/src/equicordplugins/disableCameras/README.md new file mode 100644 index 000000000..06d8bc53d --- /dev/null +++ b/src/equicordplugins/disableCameras/README.md @@ -0,0 +1,3 @@ +# DisableCameras + +Disables cameras by default when in a voice call diff --git a/src/equicordplugins/discordDevBanner/README.md b/src/equicordplugins/discordDevBanner/README.md new file mode 100644 index 000000000..4661627a4 --- /dev/null +++ b/src/equicordplugins/discordDevBanner/README.md @@ -0,0 +1,3 @@ +# DiscordDevBanner + +Enables developer banner and adds custom options to display in it diff --git a/src/equicordplugins/dontFilterMe/README.md b/src/equicordplugins/dontFilterMe/README.md new file mode 100644 index 000000000..abf74daf0 --- /dev/null +++ b/src/equicordplugins/dontFilterMe/README.md @@ -0,0 +1,3 @@ +# DontFilterMe + +Warns you if automod might flag your message diff --git a/src/equicordplugins/downloadify/README.md b/src/equicordplugins/downloadify/README.md new file mode 100644 index 000000000..e07224448 --- /dev/null +++ b/src/equicordplugins/downloadify/README.md @@ -0,0 +1,3 @@ +# Downloadify + +Improved downloading for various assets diff --git a/src/equicordplugins/equicordHelper/README.md b/src/equicordplugins/equicordHelper/README.md new file mode 100644 index 000000000..9c1dcab8a --- /dev/null +++ b/src/equicordplugins/equicordHelper/README.md @@ -0,0 +1,3 @@ +# EquicordHelper + +Fixes various issues for crashing and adds other items that might help with support diff --git a/src/equicordplugins/equicordToolbox/README.md b/src/equicordplugins/equicordToolbox/README.md new file mode 100644 index 000000000..1729ba0be --- /dev/null +++ b/src/equicordplugins/equicordToolbox/README.md @@ -0,0 +1,3 @@ +# EquicordToolbox + +Adds a toolbox for various control options diff --git a/src/equicordplugins/equissant/README.md b/src/equicordplugins/equissant/README.md new file mode 100644 index 000000000..d6922399f --- /dev/null +++ b/src/equicordplugins/equissant/README.md @@ -0,0 +1,3 @@ +# Equissant + +Plays crossant every 10 clicks diff --git a/src/equicordplugins/exportContacts/README.md b/src/equicordplugins/exportContacts/README.md new file mode 100644 index 000000000..620b8a296 --- /dev/null +++ b/src/equicordplugins/exportContacts/README.md @@ -0,0 +1,3 @@ +# ExportContacts + +Exports your friends list onto your clipboard diff --git a/src/equicordplugins/exportMessages/README.md b/src/equicordplugins/exportMessages/README.md new file mode 100644 index 000000000..27f6f7ba0 --- /dev/null +++ b/src/equicordplugins/exportMessages/README.md @@ -0,0 +1,3 @@ +# ExportMessages + +Allows exporting a message to a file diff --git a/src/equicordplugins/fakeProfileThemesAndEffects/README.md b/src/equicordplugins/fakeProfileThemesAndEffects/README.md new file mode 100644 index 000000000..91ceedbea --- /dev/null +++ b/src/equicordplugins/fakeProfileThemesAndEffects/README.md @@ -0,0 +1,3 @@ +# FakeProfileThemesAndEffects + +Expanded FakeProfileThemes with effects diff --git a/src/equicordplugins/fastDeleteChannels/README.md b/src/equicordplugins/fastDeleteChannels/README.md new file mode 100644 index 000000000..260d3f074 --- /dev/null +++ b/src/equicordplugins/fastDeleteChannels/README.md @@ -0,0 +1,3 @@ +# FastDeleteChannels + +Adds a trash can icon when pressing specific keybinds to delete a channel/channels instantly diff --git a/src/equicordplugins/findReply/README.md b/src/equicordplugins/findReply/README.md new file mode 100644 index 000000000..d2b7dd18b --- /dev/null +++ b/src/equicordplugins/findReply/README.md @@ -0,0 +1,5 @@ +# FindReply + +Lets you jump to the replies to a message. Makes it easier to follow past conversations/Q&A's. + +![](https://cdn.nest.rip/uploads/1a49e773-acae-49aa-9444-f340b828cbba.gif) diff --git a/src/equicordplugins/fixFileExtensions/README.MD b/src/equicordplugins/fixFileExtensions/README.MD new file mode 100644 index 000000000..dd3148499 --- /dev/null +++ b/src/equicordplugins/fixFileExtensions/README.MD @@ -0,0 +1,3 @@ +# FixFileExtensions + +Fixes file extensions by renaming them to work on discord diff --git a/src/equicordplugins/followVoiceUser/README.md b/src/equicordplugins/followVoiceUser/README.md new file mode 100644 index 000000000..c6d298b78 --- /dev/null +++ b/src/equicordplugins/followVoiceUser/README.md @@ -0,0 +1,3 @@ +# FollowVoiceUser + +Allows following a friend through voice chats diff --git a/src/equicordplugins/fontLoader/README.md b/src/equicordplugins/fontLoader/README.md new file mode 100644 index 000000000..5fb04a62b --- /dev/null +++ b/src/equicordplugins/fontLoader/README.md @@ -0,0 +1,3 @@ +# FontLoader + +Allows loading and using different fonts from Google diff --git a/src/equicordplugins/forwardAnywhere/README.md b/src/equicordplugins/forwardAnywhere/README.md new file mode 100644 index 000000000..23bf9ca07 --- /dev/null +++ b/src/equicordplugins/forwardAnywhere/README.md @@ -0,0 +1,3 @@ +# ForwardAnywhere + +Allows forwarding a message anywhere diff --git a/src/equicordplugins/freaky/README.md b/src/equicordplugins/freaky/README.md new file mode 100644 index 000000000..7ea56617d --- /dev/null +++ b/src/equicordplugins/freaky/README.md @@ -0,0 +1,3 @@ +# Freaky + +Changes your text to look like this 𝓔𝓺𝓾𝓲𝓬𝓸𝓻𝓭 diff --git a/src/equicordplugins/frequentQuickSwitcher/README.md b/src/equicordplugins/frequentQuickSwitcher/README.md new file mode 100644 index 000000000..b463d4d47 --- /dev/null +++ b/src/equicordplugins/frequentQuickSwitcher/README.md @@ -0,0 +1,3 @@ +# FrequentQuickSwitcher + +Changes quick switcher to be your most frequent channels diff --git a/src/equicordplugins/friendCloud/README.md b/src/equicordplugins/friendCloud/README.md new file mode 100644 index 000000000..4e6b12fd7 --- /dev/null +++ b/src/equicordplugins/friendCloud/README.md @@ -0,0 +1,7 @@ +# FriendCloud + +Visualize the users you interact with most in a cloud! + +Simply run the `/friendcloud` command to generate a cloud of your friends based on your interactions with them. + +Use the `count` option to set the number of users to display. diff --git a/src/equicordplugins/friendCodes/README.md b/src/equicordplugins/friendCodes/README.md new file mode 100644 index 000000000..6a6ac36c6 --- /dev/null +++ b/src/equicordplugins/friendCodes/README.md @@ -0,0 +1,3 @@ +# FriendCodes + +Easily generate friend codes diff --git a/src/equicordplugins/friendTags/README.md b/src/equicordplugins/friendTags/README.md new file mode 100644 index 000000000..f2fa98cf4 --- /dev/null +++ b/src/equicordplugins/friendTags/README.md @@ -0,0 +1,3 @@ +# FriendTags + +Allows filtering friends in quick switcher diff --git a/src/equicordplugins/friendshipRanks/README.md b/src/equicordplugins/friendshipRanks/README.md new file mode 100644 index 000000000..0efc4b738 --- /dev/null +++ b/src/equicordplugins/friendshipRanks/README.md @@ -0,0 +1,3 @@ +# FriendshipRanks + +Adds badges to show how long you've been friends with a user diff --git a/src/equicordplugins/fullVcPfp/README.md b/src/equicordplugins/fullVcPfp/README.md new file mode 100644 index 000000000..d25ed8258 --- /dev/null +++ b/src/equicordplugins/fullVcPfp/README.md @@ -0,0 +1,3 @@ +# FullVCPFP + +Makes profile pictures show fully diff --git a/src/equicordplugins/ignoreTerms/README.md b/src/equicordplugins/ignoreTerms/README.md new file mode 100644 index 000000000..d2215771e --- /dev/null +++ b/src/equicordplugins/ignoreTerms/README.md @@ -0,0 +1,5 @@ +# IgnoreTerms + +Ignore Discord's new terms of service + +![Screenshot](https://cdn.nest.rip/uploads/aa0b1baf-6256-4c31-9bee-73cdaa24aed5.png) diff --git a/src/equicordplugins/messageLinkTooltip/README.md b/src/equicordplugins/messageLinkTooltip/README.md new file mode 100644 index 000000000..6fa4e038a --- /dev/null +++ b/src/equicordplugins/messageLinkTooltip/README.md @@ -0,0 +1,3 @@ +# MessageLinkTooltip + +MessageLinkEmbed but with space constrants diff --git a/src/equicordplugins/notificationTitle.discordDesktop/README.md b/src/equicordplugins/notificationTitle.discordDesktop/README.md new file mode 100644 index 000000000..a885e8c1a --- /dev/null +++ b/src/equicordplugins/notificationTitle.discordDesktop/README.md @@ -0,0 +1,3 @@ +# NotificationTitle + +Improved Desktop Notifications diff --git a/src/equicordplugins/notificationTitle/index.tsx b/src/equicordplugins/notificationTitle.discordDesktop/index.tsx similarity index 100% rename from src/equicordplugins/notificationTitle/index.tsx rename to src/equicordplugins/notificationTitle.discordDesktop/index.tsx diff --git a/src/equicordplugins/sidebarChat/README.md b/src/equicordplugins/sidebarChat/README.md new file mode 100644 index 000000000..3fdf2412b --- /dev/null +++ b/src/equicordplugins/sidebarChat/README.md @@ -0,0 +1,5 @@ +# SidebarChat + +View another channel, dm or group dm as a resizable sidebar. + +![vencord and aliucord support channels both open](https://cdn.nest.rip/uploads/bc92a8bd-fc25-4775-9744-360de3f1b872.png) diff --git a/src/equicordplugins/statusWhilePlaying.desktop/README.md b/src/equicordplugins/statusWhilePlaying.desktop/README.md new file mode 100644 index 000000000..5e3abf287 --- /dev/null +++ b/src/equicordplugins/statusWhilePlaying.desktop/README.md @@ -0,0 +1,5 @@ +# StatusWhilePlaying + +This plugin automatically updates your online status (online, idle, dnd) when launching games. + +It will change your status back to the prior status when the game is closed. diff --git a/src/equicordplugins/teX/README.md b/src/equicordplugins/teX/README.md new file mode 100644 index 000000000..128f5608d --- /dev/null +++ b/src/equicordplugins/teX/README.md @@ -0,0 +1,8 @@ +# TeX + +Renders TeX formulas. +`` `$\sin x$` `` renders as $\sin x$, +`` `$$x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$$` `` renders as $\displaystyle x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$. +The latter is intentionally set to an inline-block instead of a block, to save space. + +This plugin requires external libraries, which will not work in the userscript version of Vencord without additional workarounds. diff --git a/src/equicordplugins/timelessClips.desktop/README.md b/src/equicordplugins/timelessClips.desktop/README.md new file mode 100644 index 000000000..94a328f5b --- /dev/null +++ b/src/equicordplugins/timelessClips.desktop/README.md @@ -0,0 +1,3 @@ +# TimelessClips + +Custom clip length diff --git a/src/equicordplugins/title/README.md b/src/equicordplugins/title/README.md new file mode 100644 index 000000000..62e991345 --- /dev/null +++ b/src/equicordplugins/title/README.md @@ -0,0 +1,3 @@ +# Title + +In the browser version, the window/tab title is always prefixed with "Discord -". This plugin lets you change or remove this prefix. diff --git a/src/equicordplugins/unreadBadgeCount/README.md b/src/equicordplugins/unreadBadgeCount/README.md new file mode 100644 index 000000000..21e362f77 --- /dev/null +++ b/src/equicordplugins/unreadBadgeCount/README.md @@ -0,0 +1,7 @@ +# UnreadCountBadge + +Show a badge in the channel list for unread messages. + +you have to load the channel or get new messages in that channel for it show idk why maybe sometime ill figure it out + +![image](https://why-are-you.gay/HpPVuwSV) diff --git a/src/equicordplugins/viewRawVariant/README.md b/src/equicordplugins/viewRawVariant/README.md new file mode 100644 index 000000000..20d9b56fc --- /dev/null +++ b/src/equicordplugins/viewRawVariant/README.md @@ -0,0 +1,3 @@ +# ViewRawVariant + +Improved ViewRaw with more features and places diff --git a/src/equicordplugins/voiceChatUtils/README.md b/src/equicordplugins/voiceChatUtils/README.md new file mode 100644 index 000000000..7f0345312 --- /dev/null +++ b/src/equicordplugins/voiceChatUtils/README.md @@ -0,0 +1,5 @@ +# VoiceChatUtilities + +Allows you to perform multiple actions on an entire channel (move, mute, disconnect, etc.) + +![Screenshot](https://cdn.nest.rip/uploads/9d6d2b72-0346-4951-90f3-6cab963728eb.png) diff --git a/src/equicordplugins/wallpaperFree/README.md b/src/equicordplugins/wallpaperFree/README.md new file mode 100644 index 000000000..dcb59fbf2 --- /dev/null +++ b/src/equicordplugins/wallpaperFree/README.md @@ -0,0 +1,7 @@ +# WallpaperFree + +Recreation of the old DM wallpaper experiment; Set a background image for any channel, user or server. + + + +*ignore the Discord wallpaper picker it is removed* diff --git a/src/equicordplugins/webpackTarball/README.md b/src/equicordplugins/webpackTarball/README.md new file mode 100644 index 000000000..23c64c3ed --- /dev/null +++ b/src/equicordplugins/webpackTarball/README.md @@ -0,0 +1,5 @@ +# WebpackTarball + +Adds an option to the Vencord Toolbox that lets you grab Discord's webpack as a tarball, which is very convenient for inspecting the code. + +The reason it's a tarball instead of for example a zip is just simplicity — the tar file writer is only like 60ish lines. diff --git a/src/plugins/accountPanelServerProfile/README.md b/src/plugins/accountPanelServerProfile/README.md new file mode 100644 index 000000000..f837864b7 --- /dev/null +++ b/src/plugins/accountPanelServerProfile/README.md @@ -0,0 +1,7 @@ +# AccountPanelServerProfile + +Right click your account panel in the bottom left to view your profile in the current server + +![](https://github.com/user-attachments/assets/3228497d-488f-479c-93d2-a32ccdb08f0f) + +![](https://github.com/user-attachments/assets/6fc45363-d95f-4810-812f-2f9fb28b41b5) diff --git a/src/plugins/alwaysExpandRoles/README.md b/src/plugins/alwaysExpandRoles/README.md new file mode 100644 index 000000000..344268cbf --- /dev/null +++ b/src/plugins/alwaysExpandRoles/README.md @@ -0,0 +1,3 @@ +# Always Expand Roles + +Always expands the role list in profile popouts diff --git a/src/plugins/appleMusic.desktop/README.md b/src/plugins/appleMusic.desktop/README.md new file mode 100644 index 000000000..52ab93bfd --- /dev/null +++ b/src/plugins/appleMusic.desktop/README.md @@ -0,0 +1,9 @@ +# AppleMusicRichPresence + +This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.) + +![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0) + +## Configuration + +For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name. diff --git a/src/plugins/betterFolders/README.md b/src/plugins/betterFolders/README.md new file mode 100644 index 000000000..658c6f1db --- /dev/null +++ b/src/plugins/betterFolders/README.md @@ -0,0 +1,11 @@ +# Better Folders + +Better Folders offers a variety of options to improve your folder experience + +Always show the folder icon, regardless of if the folder is open or not + +Only have one folder open at a time + +Open folders in a sidebar: + +![A folder open in a separate sidebar](https://github.com/user-attachments/assets/432d3146-8091-4bae-9c1e-c19046c72947) diff --git a/src/plugins/betterRoleContext/README.md b/src/plugins/betterRoleContext/README.md new file mode 100644 index 000000000..e54e1e313 --- /dev/null +++ b/src/plugins/betterRoleContext/README.md @@ -0,0 +1,6 @@ +# BetterRoleContext + +Adds options to copy role color, edit role and view role icon when right clicking roles in the user profile + +![](https://github.com/Vendicated/Vencord/assets/45497981/354220a4-09f3-4c5f-a28e-4b19ca775190) + diff --git a/src/plugins/betterSessions/README.md b/src/plugins/betterSessions/README.md new file mode 100644 index 000000000..cf13e6c50 --- /dev/null +++ b/src/plugins/betterSessions/README.md @@ -0,0 +1,5 @@ +# BetterSessions + +Enhances the sessions (devices) menu. Allows you to view exact timestamps, give each session a custom name, and receive notifications about new sessions. + +![](https://github.com/Vendicated/Vencord/assets/9750071/4a44b617-bb8f-4dcb-93f1-b7d2575ed3d8) diff --git a/src/plugins/betterSettings/README.md b/src/plugins/betterSettings/README.md new file mode 100644 index 000000000..127c6ce76 --- /dev/null +++ b/src/plugins/betterSettings/README.md @@ -0,0 +1,9 @@ +# BetterSettings + +Improves Discord's Settings via multiple (toggleable) changes: +- makes opening settings much faster +- removes the scuffed transition animation +- organises the settings cog context menu into categories + +![](https://github.com/Vendicated/Vencord/assets/45497981/e8d67a95-3909-4be5-8281-8cf9d2f1c30e) + diff --git a/src/plugins/clientTheme/README.md b/src/plugins/clientTheme/README.md new file mode 100644 index 000000000..e2723f8bc --- /dev/null +++ b/src/plugins/clientTheme/README.md @@ -0,0 +1,7 @@ +# Client Theme + +Revival of the old client theme experiment (The one that came before the sucky one that we actually got) + +![the ClientTheme theme colour picker](https://user-images.githubusercontent.com/37855219/230238053-e90b7098-373a-459a-bb8c-c24e82f69270.png) + +https://github.com/Vendicated/Vencord/assets/45497981/6c1bcb3b-e0c7-4a02-b0b8-c4c5cd954f38 diff --git a/src/plugins/consoleJanitor/README.md b/src/plugins/consoleJanitor/README.md new file mode 100644 index 000000000..9737f53d1 --- /dev/null +++ b/src/plugins/consoleJanitor/README.md @@ -0,0 +1,5 @@ +# ConsoleJanitor + +Disables annoying console messages/errors. This plugin mainly removes errors/warnings that happen all the time and Discord logger messages. + +One of the disabled messages is the "Window state not initialized" warning, for example. diff --git a/src/plugins/copyEmojiMarkdown/README.md b/src/plugins/copyEmojiMarkdown/README.md new file mode 100644 index 000000000..9e62e6635 --- /dev/null +++ b/src/plugins/copyEmojiMarkdown/README.md @@ -0,0 +1,5 @@ +# CopyEmojiMarkdown + +Allows you to copy emojis as formatted string. Custom emojis will be copied as `<:trolley:1024751352028602449>`, default emojis as `🛒` + +![](https://github.com/Vendicated/Vencord/assets/45497981/417f345a-7031-4fe7-8e42-e238870cd547) diff --git a/src/plugins/copyFileContents/README.md b/src/plugins/copyFileContents/README.md new file mode 100644 index 000000000..18dc2d3df --- /dev/null +++ b/src/plugins/copyFileContents/README.md @@ -0,0 +1,5 @@ +# CopyFileContents + +Adds a button to text file attachments to copy their contents. + +![](https://github.com/user-attachments/assets/b1a0f6f4-106f-4953-94d9-4c5ef5810bca) diff --git a/src/plugins/copyStickerLinks/README.md b/src/plugins/copyStickerLinks/README.md new file mode 100644 index 000000000..40e632322 --- /dev/null +++ b/src/plugins/copyStickerLinks/README.md @@ -0,0 +1,5 @@ +# CopyStickerLinks + +Adds "Copy Link" and "Open Link" options to the context menu of stickers! + +![](https://github.com/user-attachments/assets/a0982d5c-ab83-458b-9ca3-834803e0782e) diff --git a/src/plugins/customIdle/README.md b/src/plugins/customIdle/README.md new file mode 100644 index 000000000..63bf87d89 --- /dev/null +++ b/src/plugins/customIdle/README.md @@ -0,0 +1,5 @@ +# CustomIdle + +Lets you change the time until your status gets automatically set to idle. You can also prevent idling altogether. + +![Plugin Configuration](https://github.com/Vendicated/Vencord/assets/45801973/4e5259b2-18e0-42e5-b69f-efc672ce1e0b) diff --git a/src/plugins/dearrow/README.md b/src/plugins/dearrow/README.md new file mode 100644 index 000000000..81a762c70 --- /dev/null +++ b/src/plugins/dearrow/README.md @@ -0,0 +1,5 @@ +# Dearrow + +Makes YouTube embed titles and thumbnails less sensationalist, powered by [Dearrow](https://dearrow.ajay.app/) + +https://github.com/Vendicated/Vencord/assets/45497981/7bf81108-102d-47c5-8ba5-357db4db1283 diff --git a/src/plugins/decor/README.md b/src/plugins/decor/README.md new file mode 100644 index 000000000..467a61457 --- /dev/null +++ b/src/plugins/decor/README.md @@ -0,0 +1,17 @@ +# Decor + +Custom avatar decorations! + +![Custom decorations in chat](https://github.com/Vendicated/Vencord/assets/30497388/b0c4c4c8-8723-42a8-b50f-195ad4e26136) + +Create and use your own custom avatar decorations, or pick your favorite from the presets. + +You'll be able to see the custom avatar decorations of other users of this plugin, and they'll be able to see your custom avatar decoration. + +You can select and manage your custom avatar decorations under the "Profiles" page in settings, or in the plugin settings. + +![Custom decorations management](https://github.com/Vendicated/Vencord/assets/30497388/74fe8a9e-a2a2-4b29-bc10-9eaa58208ad4) + +Review the [guidelines](https://github.com/decor-discord/.github/blob/main/GUIDELINES.md) before creating your own custom avatar decoration. + +Join the [Discord server](https://discord.gg/dXp2SdxDcP) for support and notifications on your decoration's review. diff --git a/src/plugins/favEmojiFirst/README.md b/src/plugins/favEmojiFirst/README.md new file mode 100644 index 000000000..10a62a6be --- /dev/null +++ b/src/plugins/favEmojiFirst/README.md @@ -0,0 +1,6 @@ +# FavoriteEmojiFirst + +Puts your favorite emoji first in the emoji autocomplete. + +![a screenshot of the favourite emojis section](https://github.com/Vendicated/Vencord/assets/45497981/419c8c16-1afc-46e0-9cc2-20b9c3489711) +![a comparison of the emoji picker before and after enabling this plugin](https://github.com/Vendicated/Vencord/assets/45497981/4f57626d-cfc6-4155-a47c-2eac191231bb) diff --git a/src/plugins/favGifSearch/README.md b/src/plugins/favGifSearch/README.md new file mode 100644 index 000000000..af120dace --- /dev/null +++ b/src/plugins/favGifSearch/README.md @@ -0,0 +1,5 @@ +# FavoriteGifSearch + +Adds a search bar to favorite gifs. + +![Screenshot](https://github.com/Vendicated/Vencord/assets/45497981/19552adc-d921-4153-976e-e9361dc8fdaf) diff --git a/src/plugins/fixImagesQuality/README.md b/src/plugins/fixImagesQuality/README.md new file mode 100644 index 000000000..8e3cf6548 --- /dev/null +++ b/src/plugins/fixImagesQuality/README.md @@ -0,0 +1,3 @@ +# Fix Images Quality + +Prevents images from being loaded as webp, which can cause quality loss diff --git a/src/plugins/fixYoutubeEmbeds.desktop/README.md b/src/plugins/fixYoutubeEmbeds.desktop/README.md new file mode 100644 index 000000000..875de9e28 --- /dev/null +++ b/src/plugins/fixYoutubeEmbeds.desktop/README.md @@ -0,0 +1,5 @@ +# FixYoutubeEmbeds + +Bypasses youtube videos being blocked from display on Discord (for example by UMG) + +![](https://github.com/Vendicated/Vencord/assets/45497981/7a5fdcaa-217c-4c63-acae-f0d6af2f79be) diff --git a/src/plugins/friendsSince/README.md b/src/plugins/friendsSince/README.md new file mode 100644 index 000000000..ccbb09b67 --- /dev/null +++ b/src/plugins/friendsSince/README.md @@ -0,0 +1,5 @@ +# FriendsSince + +Shows when you became friends with someone in the user popout + +![](https://github.com/Vendicated/Vencord/assets/45497981/bb258188-ab48-4c4d-9858-1e90ba41e926) diff --git a/src/plugins/fullSearchContext/README.md b/src/plugins/fullSearchContext/README.md new file mode 100644 index 000000000..4bc5feb5a --- /dev/null +++ b/src/plugins/fullSearchContext/README.md @@ -0,0 +1,5 @@ +# FullSearchContext + +Makes the message context menu in message search results have all options you'd expect. + +![](https://github.com/user-attachments/assets/472d1327-3935-44c7-b7c4-0978b5348550) diff --git a/src/plugins/ignoreActivities/README.md b/src/plugins/ignoreActivities/README.md new file mode 100644 index 000000000..abc75720f --- /dev/null +++ b/src/plugins/ignoreActivities/README.md @@ -0,0 +1,13 @@ +# IgnoreActivities + +Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings. + +![](https://github.com/user-attachments/assets/f0c19060-0ecf-4f1c-8165-a5aa40143c82) + +![](https://github.com/user-attachments/assets/73c3fa7a-5b90-41ee-a4d6-91fa76458b74) + +![](https://github.com/user-attachments/assets/1ab3fe73-3911-48d1-8a08-e976af614b41) + +The activity stays showing as a detected game even if ignored, differently from the stock Toggle Detection button from Discord: + +![](https://github.com/user-attachments/assets/08ea60c3-3a31-42de-ae4c-7535fbf1b45a) diff --git a/src/plugins/imageLink/README.md b/src/plugins/imageLink/README.md new file mode 100644 index 000000000..add657948 --- /dev/null +++ b/src/plugins/imageLink/README.md @@ -0,0 +1,3 @@ +# ImageLink + +If a message consists of only a link to an image, Discord hides the link and shows only the image embed. This plugin makes the link show regardless. diff --git a/src/plugins/imageZoom/README.md b/src/plugins/imageZoom/README.md new file mode 100644 index 000000000..75cfd4fd8 --- /dev/null +++ b/src/plugins/imageZoom/README.md @@ -0,0 +1,6 @@ +# ImageZoom + +Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size + +![the plugin in action](https://github.com/Vendicated/Vencord/assets/45497981/408cd77d-c5f4-40bc-8de2-f977a31b3e5f) +![the context menu options offered by the plugin](https://github.com/Vendicated/Vencord/assets/45497981/3bede636-f1ce-493f-af46-788b920cb81c) diff --git a/src/plugins/implicitRelationships/README.md b/src/plugins/implicitRelationships/README.md new file mode 100644 index 000000000..b560bf2c4 --- /dev/null +++ b/src/plugins/implicitRelationships/README.md @@ -0,0 +1,7 @@ +# ImplicitRelationships + +Shows your implicit relationships in the Friends tab. + +Implicit relationships on Discord are people with whom you've frecently interacted and don't have a relationship with. Even though Discord thinks you should be friends with them, you haven't added them as friends! + +![](https://camo.githubusercontent.com/6927161ee0c933f7ef6d61f243cca3e6ea4c8db9d1becd8cbf73c45e1bd0d127/68747470733a2f2f692e646f6c66692e65732f7055447859464662674d2e706e673f6b65793d736e3950343936416c32444c7072) diff --git a/src/plugins/mentionAvatars/README.md b/src/plugins/mentionAvatars/README.md new file mode 100644 index 000000000..5e553419e --- /dev/null +++ b/src/plugins/mentionAvatars/README.md @@ -0,0 +1,6 @@ +# MentionAvatars + +Shows user avatars and role icons inside mentions + +![](https://github.com/user-attachments/assets/fc76ea47-5e19-4063-a592-c57785a75cc7) +![](https://github.com/user-attachments/assets/76c4c3d9-7cde-42db-ba84-903cbb40c163) diff --git a/src/plugins/messageClickActions/README.md b/src/plugins/messageClickActions/README.md new file mode 100644 index 000000000..5162105a1 --- /dev/null +++ b/src/plugins/messageClickActions/README.md @@ -0,0 +1,5 @@ +# MessageClickActions + +Allows you to double click to edit/reply to a message or delete it if you hold the backspace key + +![](https://github.com/Vendicated/Vencord/assets/55940580/6885aca2-4021-4910-b636-bb40f877a816) diff --git a/src/plugins/messageLatency/README.md b/src/plugins/messageLatency/README.md new file mode 100644 index 000000000..8d2a776cd --- /dev/null +++ b/src/plugins/messageLatency/README.md @@ -0,0 +1,31 @@ +# MessageLatency + +Displays an indicator for messages that took ≥n seconds to send. + +> **NOTE** +> +> - This plugin only applies to messages received after opening the channel +> - False positives can exist if the user's system clock has drifted. +> - Grouped messages only display latency of the first message + +## Demo + +### Chat View + +![chat-view](https://github.com/Vendicated/Vencord/assets/82430093/69430881-60b3-422f-aa3d-c62953837566) + +### Clock -ve Drift + +![pissbot-on-top](https://github.com/Vendicated/Vencord/assets/82430093/d9248b66-e761-4872-8829-e8bf4fea6ec8) + +### Clock +ve Drift + +![dumb-ai](https://github.com/Vendicated/Vencord/assets/82430093/0e9783cf-51d5-4559-ae10-42399e7d4099) + +### Connection Delay + +![who-this](https://github.com/Vendicated/Vencord/assets/82430093/fd68873d-8630-42cc-a166-e9063d2718b2) + +### Icons + +![icons](https://github.com/Vendicated/Vencord/assets/82430093/17630bd9-44ee-4967-bcdf-3315eb6eca85) diff --git a/src/plugins/noMaskedUrlPaste/README.md b/src/plugins/noMaskedUrlPaste/README.md new file mode 100644 index 000000000..36707ac93 --- /dev/null +++ b/src/plugins/noMaskedUrlPaste/README.md @@ -0,0 +1,3 @@ +# NoMaskedUrlPaste + +Pasting a link while you have text selected will NOT paste your link as a masked link. diff --git a/src/plugins/noUnblockToJump/README.md b/src/plugins/noUnblockToJump/README.md new file mode 100644 index 000000000..630ef7dc2 --- /dev/null +++ b/src/plugins/noUnblockToJump/README.md @@ -0,0 +1,5 @@ +# No Unblock To Jump + +Removes the popup preventing you to jump to a message from a blocked/ignored user or likely spammer (eg: in search results) + +![A modal popup telling you to unblock a user to jump their message](https://github.com/user-attachments/assets/0e4b859d-f3b3-4101-9a83-829afb473d1e) diff --git a/src/plugins/notificationVolume/README.md b/src/plugins/notificationVolume/README.md new file mode 100644 index 000000000..05e5af4e2 --- /dev/null +++ b/src/plugins/notificationVolume/README.md @@ -0,0 +1,3 @@ +# NotificationVolume + +Set a separate volume for notifications and in-app sounds (e.g. messages, call sound, mute/unmute) helping your ears stay healthy for many years to come. diff --git a/src/plugins/notificationVolume/index.tsx b/src/plugins/notificationVolume/index.ts similarity index 100% rename from src/plugins/notificationVolume/index.tsx rename to src/plugins/notificationVolume/index.ts diff --git a/src/plugins/onePingPerDM/README.md b/src/plugins/onePingPerDM/README.md new file mode 100644 index 000000000..43f89b72c --- /dev/null +++ b/src/plugins/onePingPerDM/README.md @@ -0,0 +1,7 @@ +# OnePingPerDM +If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit + +## Purpose +- Prevents ping audio spam in DMs +- Be able to distinguish more than one ping as multiple users +- Be less annoyed while gaming diff --git a/src/plugins/openInApp/README.md b/src/plugins/openInApp/README.md new file mode 100644 index 000000000..2b1385c62 --- /dev/null +++ b/src/plugins/openInApp/README.md @@ -0,0 +1,11 @@ +# OpenInApp + +Open links in their respective apps instead of your browser + +## Currently supports: + +- Spotify +- Steam +- EpicGames +- Tidal +- Apple Music (iTunes) diff --git a/src/plugins/pauseInvitesForever/README.md b/src/plugins/pauseInvitesForever/README.md new file mode 100644 index 000000000..7ccf0b72f --- /dev/null +++ b/src/plugins/pauseInvitesForever/README.md @@ -0,0 +1,5 @@ +# PauseInvitesForever + +Adds a button to the Security Actions modal to to pause invites indefinitely. + +![](https://github.com/Vendicated/Vencord/assets/47677887/e5ba40a3-cb08-462a-8615-fb74dd54c790) diff --git a/src/plugins/permissionFreeWill/README.md b/src/plugins/permissionFreeWill/README.md new file mode 100644 index 000000000..ca30575ff --- /dev/null +++ b/src/plugins/permissionFreeWill/README.md @@ -0,0 +1,9 @@ +# PermissionFreeWill + +Removes the client-side restrictions that prevent editing channel permissions, such as permission lockouts ("Pretty sure +you don't want to do this") and onboarding requirements ("Making this change will make your server incompatible [...]") + +## Warning + +This plugin will let you create permissions in servers that **WILL** lock you out of channels until an administrator +can resolve it for you. Please be careful with the overwrites you are making and check carefully. diff --git a/src/plugins/previewMessage/README.md b/src/plugins/previewMessage/README.md new file mode 100644 index 000000000..34eee3f61 --- /dev/null +++ b/src/plugins/previewMessage/README.md @@ -0,0 +1,6 @@ +# PreviewMessage + +Lets you preview your message before sending it. + +![the plugin in action](https://github.com/Vendicated/Vencord/assets/45497981/3ce32860-e5cd-4ea2-bdab-e121f1703579) + diff --git a/src/plugins/quickMention/README.md b/src/plugins/quickMention/README.md new file mode 100644 index 000000000..fd862610d --- /dev/null +++ b/src/plugins/quickMention/README.md @@ -0,0 +1,5 @@ +# QuickMention + +Adds a mention icon to the messages action bar + +![](https://github.com/Vendicated/Vencord/assets/55940580/82d3fec7-4196-4917-b3c2-6e652b2aff9e) diff --git a/src/plugins/quickReply/README.md b/src/plugins/quickReply/README.md new file mode 100644 index 000000000..bd3067910 --- /dev/null +++ b/src/plugins/quickReply/README.md @@ -0,0 +1,6 @@ +# QuickReply + +Reply to (ctrl + up/down) and edit (ctrl + shift + up/down) messages via keybinds + +![](https://github.com/Vendicated/Vencord/assets/55940580/df79a27a-6529-4c70-8870-3c17d3637e4f) + diff --git a/src/plugins/replaceGoogleSearch/README.md b/src/plugins/replaceGoogleSearch/README.md new file mode 100644 index 000000000..1ab30212e --- /dev/null +++ b/src/plugins/replaceGoogleSearch/README.md @@ -0,0 +1,5 @@ +# ReplaceGoogleSearch + +Replaces the Google search with different Engines + +![Visualization](https://github.com/Vendicated/Vencord/assets/61953774/8b8158d2-0407-4d7b-9dff-a8b9bdc1a122) diff --git a/src/plugins/replyTimestamp/README.md b/src/plugins/replyTimestamp/README.md new file mode 100644 index 000000000..b7952bf3a --- /dev/null +++ b/src/plugins/replyTimestamp/README.md @@ -0,0 +1,5 @@ +# ReplyTimestamp + +Shows timestamps on the previews of replied-to messages. Pretty simple. + +![](https://github.com/Vendicated/Vencord/assets/1547062/62e2b67a-e567-4c7a-884d-4640f897f7e0) diff --git a/src/plugins/seeSummaries/README.md b/src/plugins/seeSummaries/README.md new file mode 100644 index 000000000..1c8b6c793 --- /dev/null +++ b/src/plugins/seeSummaries/README.md @@ -0,0 +1,9 @@ +# Summaries + +Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations. + +Read more about summaries in the [official Discord help article](https://support.discord.com/hc/en-us/articles/12926016807575-In-Channel-Conversation-Summaries)! + +Note that this plugin can't fetch old summaries, it can only display ones created while your Discord is running with the plugin enabled. + +![](https://github.com/Vendicated/Vencord/assets/45497981/bd931b0c-2e85-4c10-9f7c-8ba01eb55745) diff --git a/src/plugins/serverInfo/README.md b/src/plugins/serverInfo/README.md new file mode 100644 index 000000000..98c9013e0 --- /dev/null +++ b/src/plugins/serverInfo/README.md @@ -0,0 +1,7 @@ +# ServerInfo + +Allows you to view info about servers and see friends and blocked users + +![](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580) +![](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630) +![Available as "Server Profile" option in the server context menu](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54) diff --git a/src/plugins/showHiddenThings/README.md b/src/plugins/showHiddenThings/README.md new file mode 100644 index 000000000..753e5c148 --- /dev/null +++ b/src/plugins/showHiddenThings/README.md @@ -0,0 +1,19 @@ +# ShowHiddenThings + +Displays various hidden & moderator-only things regardless of permissions. + +## Features + +- Show member timeout icons in chat +![](https://github.com/Vendicated/Vencord/assets/47677887/75e1f6ba-8921-4188-9c2d-c9c3f9d07101) + +- Show the invites paused tooltip in the server list +![](https://github.com/Vendicated/Vencord/assets/47677887/b6a923d2-ac55-40d9-b4f8-fa6fc117148b) + +- Show the member mod view context menu item in all servers + +![](https://github.com/Vendicated/Vencord/assets/47677887/3dac95dd-841c-4c15-ad87-2db7bd1e4dab) + +- Disable filters in Server Discovery search that hide servers that don't meet discovery criteria + +- Disable filters in Server Discovery search that hide NSFW & disallowed servers diff --git a/src/plugins/showTimeoutDuration/README.md b/src/plugins/showTimeoutDuration/README.md new file mode 100644 index 000000000..137802473 --- /dev/null +++ b/src/plugins/showTimeoutDuration/README.md @@ -0,0 +1,8 @@ +# ShowTimeoutDuration + +Displays how much longer a user's timeout will last. +Either in the timeout icon tooltip, or next to it, configurable via settings! + +![indicator in tooltip](https://github.com/Vendicated/Vencord/assets/45497981/606588a3-2646-40d9-8800-b6307f650136) + +![indicator next to timeout icon](https://github.com/Vendicated/Vencord/assets/45497981/ab9d2101-0fdc-4143-9310-9488f056eeee) diff --git a/src/plugins/stickerPaste/README.md b/src/plugins/stickerPaste/README.md new file mode 100644 index 000000000..a29170272 --- /dev/null +++ b/src/plugins/stickerPaste/README.md @@ -0,0 +1,3 @@ +# StickerPaste + +Makes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending. diff --git a/src/plugins/superReactionTweaks/README.md b/src/plugins/superReactionTweaks/README.md new file mode 100644 index 000000000..624ab8669 --- /dev/null +++ b/src/plugins/superReactionTweaks/README.md @@ -0,0 +1,11 @@ +# Super Reaction Tweaks + +This plugin applies configurable various tweaks to super reactions. + +![Screenshot](https://user-images.githubusercontent.com/22851444/281598795-58f07116-9f95-4f64-940b-23a5499f2302.png) + +## Features: + +**Super React By Default** - The reaction picker will default to super reactions instead of normal reactions. + +**Super Reaction Play Limit** - Allows you to decide how many super reaction animations can play at once, including removing the limit entirely. diff --git a/src/plugins/themeAttributes/README.md b/src/plugins/themeAttributes/README.md new file mode 100644 index 000000000..89001aae4 --- /dev/null +++ b/src/plugins/themeAttributes/README.md @@ -0,0 +1,29 @@ +# ThemeAttributes + +This plugin adds data attributes and CSS variables to various elements inside Discord + +This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible + +## Available Attributes + +### All Tab Bars (User Settings, Server Settings, etc) + +`data-tab-id` contains the id of that tab + +![image](https://github.com/Vendicated/Vencord/assets/45497981/1263b782-f673-4f09-820c-4cc366d062ad) + +### Chat Messages + +- `data-author-id` contains the id of the author +- `data-author-username` contains the username of the author +- `data-is-self` is a boolean indicating whether this is the current user's message + +![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122) + +## CSS Variables + +### Avatars + +`--avatar-url-` contains a URL for the users avatar with the size attribute adjusted for the resolutions `128, 256, 512, 1024, 2048, 4096`. + +![image](https://github.com/Vendicated/Vencord/assets/26598490/192ddac0-c827-472f-9933-fa99ff36f723) diff --git a/src/plugins/unlockedAvatarZoom/README.md b/src/plugins/unlockedAvatarZoom/README.md new file mode 100644 index 000000000..9ecd0b1a4 --- /dev/null +++ b/src/plugins/unlockedAvatarZoom/README.md @@ -0,0 +1,5 @@ +# UnlockedAvatarZoom + +Allows you to zoom in further in the image crop tool when changing your avatar + +![](https://raw.githubusercontent.com/Vencord/plugin-assets/main/UnlockedAvatarZoom/demo.avif) diff --git a/src/plugins/userMessagesPronouns/README.md b/src/plugins/userMessagesPronouns/README.md new file mode 100644 index 000000000..455f9c443 --- /dev/null +++ b/src/plugins/userMessagesPronouns/README.md @@ -0,0 +1,5 @@ +User Messages Pronouns + +Adds pronouns to chat user messages + +![](https://github.com/user-attachments/assets/34dc373d-faf4-4420-b49b-08b2647baa3b) diff --git a/src/plugins/userVoiceShow/README.md b/src/plugins/userVoiceShow/README.md new file mode 100644 index 000000000..a0eb7c43e --- /dev/null +++ b/src/plugins/userVoiceShow/README.md @@ -0,0 +1,7 @@ +# User Voice Show + +Shows an indicator when a user is in a Voice Channel + +![a preview of the indicator in the user profile](https://github.com/user-attachments/assets/48f825e4-fad5-40d7-bb4f-41d5e595aae0) + +![a preview of the indicator in the member list](https://github.com/user-attachments/assets/51be081d-7bbb-45c5-8533-d565228e50c1) diff --git a/src/plugins/validReply/README.md b/src/plugins/validReply/README.md new file mode 100644 index 000000000..49e313cf5 --- /dev/null +++ b/src/plugins/validReply/README.md @@ -0,0 +1,7 @@ +# ValidReply + +Fixes referenced (replied to) messages showing as "Message could not be loaded". + +Hover the text to load the message! + +![](https://github.com/Vendicated/Vencord/assets/45801973/d3286acf-e822-4b7f-a4e7-8ced18f581af) diff --git a/src/plugins/volumeBooster/README.md b/src/plugins/volumeBooster/README.md new file mode 100644 index 000000000..62bd5ab52 --- /dev/null +++ b/src/plugins/volumeBooster/README.md @@ -0,0 +1,9 @@ +# Volume Booster + +Allows you to boost the volume over 200% on desktop and over 100% on other clients. + +Works on users, bots, and streams! + +![the volume being moved up to 270% on vesktop](https://github.com/user-attachments/assets/793e012e-c069-4fa4-a3d5-61c2f55edd3e) + +![the volume being moved up to 297% on a stream](https://github.com/user-attachments/assets/77463eb9-2537-4821-a3ab-82f60633ccbc) diff --git a/src/plugins/whoReacted/README.md b/src/plugins/whoReacted/README.md new file mode 100644 index 000000000..ee4227ab8 --- /dev/null +++ b/src/plugins/whoReacted/README.md @@ -0,0 +1,5 @@ +# WhoReacted + +Next to each reaction, display each user's avatar. Each avatar can be clicked and will open the profile. + +![](https://github.com/Vendicated/Vencord/assets/57493648/97fec9e8-396f-4f5e-916e-1ec21445113d) diff --git a/src/plugins/xsOverlay/README.md b/src/plugins/xsOverlay/README.md new file mode 100644 index 000000000..7f517c173 --- /dev/null +++ b/src/plugins/xsOverlay/README.md @@ -0,0 +1,14 @@ +# XSOverlay Notifier + +Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR. + +## Preview + +![Resulting notification inside XSOverlay](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40) + +![Test notification inside XSOverlay](https://github.com/user-attachments/assets/d3b0c387-1d67-4697-a470-d4a927e228f4) + +## Usage +- Enable this plugin +- Set port and plugin settings as desired (defaults should work fine) +- Open SteamVR and XSOverlay diff --git a/src/plugins/youtubeAdblock.desktop/README.md b/src/plugins/youtubeAdblock.desktop/README.md new file mode 100644 index 000000000..d6235f36e --- /dev/null +++ b/src/plugins/youtubeAdblock.desktop/README.md @@ -0,0 +1,7 @@ +# WatchTogetherAdblock + +Block ads in YouTube embeds and the WatchTogether activity via AdGuard + +Note that this only works for yourself, other users in the activity will still see ads. + +Powered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut) From 71f1a29840fc4e924c22e0ff148b7b208800749c Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:23:44 -0400 Subject: [PATCH 056/881] Fix Patches --- .../alwaysExpandProfiles/index.ts | 16 +++++----------- src/equicordplugins/downloadify/index.tsx | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/equicordplugins/alwaysExpandProfiles/index.ts b/src/equicordplugins/alwaysExpandProfiles/index.ts index b52b3296d..f54c2904a 100644 --- a/src/equicordplugins/alwaysExpandProfiles/index.ts +++ b/src/equicordplugins/alwaysExpandProfiles/index.ts @@ -17,18 +17,12 @@ export default definePlugin({ authors: [EquicordDevs.thororen], patches: [ { - find: '"UserProfilePopout"})', + find: '"view-profile"', replacement: { - match: /(?<=user:(\i).*?"PRESS_VIEW_PROFILE".{0,150})return/, - replace: "return $self.openUserProfile($1);" - } - }, - { - find: '"BotUserProfilePopout"', - replacement: { - match: /(?<=user:(\i).*?"PRESS_VIEW_PROFILE".{0,100})return/, - replace: "return $self.openUserProfile($1);" - } + match: /(user:(\i).*?"PRESS_VIEW_PROFILE".{0,200})return/, + replace: "$1return $self.openUserProfile($2);" + }, + all: true }, ], openUserProfile(user: User) { diff --git a/src/equicordplugins/downloadify/index.tsx b/src/equicordplugins/downloadify/index.tsx index 6c4216f9b..7a09fc6c4 100644 --- a/src/equicordplugins/downloadify/index.tsx +++ b/src/equicordplugins/downloadify/index.tsx @@ -33,7 +33,7 @@ export default definePlugin({ patches: [ { // Pass the guild ID to the profile modal context. Used by the next patch. - find: "animate}):null", + find: "clanTagChiplet}),", replacement: { match: /("right",)(avatarUrl:null)/, replace: "$1guildId:arguments[0]?.channel?.guild_id,$2" From 610565c0c38ed50d10d9ebe93d246c9dbe19434e Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:24:42 -0700 Subject: [PATCH 057/881] Add some READMEs. (#357) * Add READMEs. * Add READMEs. --- src/equicordplugins/downloadify/README.md | 12 +++- src/equicordplugins/questify/README.md | 68 +++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/equicordplugins/questify/README.md diff --git a/src/equicordplugins/downloadify/README.md b/src/equicordplugins/downloadify/README.md index e07224448..f816a56f9 100644 --- a/src/equicordplugins/downloadify/README.md +++ b/src/equicordplugins/downloadify/README.md @@ -1,3 +1,13 @@ # Downloadify -Improved downloading for various assets +Download various assets directly in Discord without having to open a browser or dig through HTML. + +## Features +1. Download non-image files such as ZIP files directly inside of Discord. + - Allow or disallow unicode characters in file names. + - Various image assets are available in multiple formats as Discord makes converted copies. + - APNG and AWEBP are presented with the `.apng` and `.awebp` extensions during download in order to differentiate them from static PNG and WEBP variants, but upon download their extensions are converted back to `.png` and `.webp` respectively. +2. Show a notification when downloads start and finish with a customizable duration. +3. Add a download button to voice messages. +4. Set a default directory or pick each time. + - Pick whether to overwrite existing files or not. \ No newline at end of file diff --git a/src/equicordplugins/questify/README.md b/src/equicordplugins/questify/README.md new file mode 100644 index 000000000..d58dbd5c3 --- /dev/null +++ b/src/equicordplugins/questify/README.md @@ -0,0 +1,68 @@ +# Questify + +Enhance your Quest experience with a suite of features, or disable them entirely if they're not your thing. + +![An example of the Restyle and Reorder features.](https://github.com/user-attachments/assets/452ba894-d0e0-46d5-894a-af901c42d8e5) + +## Features +1. Quest Features + - Modify specific Quest features. + - Allows for disabling the following Quest related features: + - Everything — All of the below is disabled. + - Discovery Tab — Quests tab becomes hidden. + - Fetching Quests — Will prevent fetching Quests resulting in a blank Quests page. + - Badge on User Profiles — Hides the profile badge which doubles as a shortcut to the Quests page. + - Popup Above User Panel — Hides the new Quest promotion that appears above your profile panel. + - Gift Inventory Relocation Notice — Hides the text in the gift inventory which doubles as a shortcut to the Quests page. + - Friends List Active Now Promotion — Hides the promotion that displays when your friends play a game which has an active Quest. + - Allows for modifying the following Quest related features: + - Complete Video Quests in Background — Click the watch button on video Quests and wait for the video duration to pass. The first click will start the Quest without opening the video modal. Subsequent clicks will show the modal as normal. + - Complete Game Quests in Background — Click the accept button on game Quests and wait for the play duration to pass. + - Make Mobile Quests Desktop Compatible — Make mobile-only video Quests completable on Desktop. +2. Quests Button + - Show a Quest button in the server list with an optional indicator for unclaimed Quests. + - Button Visibility + - Always — Always display the button. + - Unclaimed — Only display the button when you have unclaimed Quests. + - Never — Never display the button. + - Unclaimed Indicator + - Pill — Shows an "unread" pill when there are unclaimed Quests. + - Badge — Shows the number of unclaimed Quests as a "ping" badge. + - Both — Both of the above. + - None — Show no indication of unclaimed Quests. + - Badge Color + - Use the color picker to pick a custom badge color or use the default Discord ping badge color by clicking `Disable`. + - Left / Middle / Right Click Actions + - Open Quests — Navigate to the Quests tab in Discovery. + - Plugin Settings — Open the plugin settings. + - Context Menu — Show the options `Mark All Ignored`, `Reset Ignored List`, and `Fetch Quests`. + - Nothing — Do nothing. +3. Fetching Quests + - Configure how often to fetch Quests from Discord and set up alerts for new Quests. + - By default, Discord only fetches Quests on load and when visiting the Quests page. This means that without a fetch interval defined below, this plugin will become unaware of new Quests added throughout the day. + - This relies on the Quest Button being enabled and set to either `Unclaimed`, or set to `Always` with unclaimed `Pill`, `Badge`, or `Both` indicators enabled. Otherwise, there is no reason to periodically fetch Quests. + - Also, if `Fetching Quests` is blocked in the `Quest Features` setting, this will not work. + - Fetch Interval + - Any time between 30 minutes and 12 hours. Defaults to every 45 minutes. + - Alert Sound + - Pick a default Discord sound to play when new Quests are fetched, or provide a custom audio URL. The audio must be hosted on a domain whitelisted by the Vencord CSP, such as `catbox`. + - If the provided URL is not on a supported domain, you will receive an error. If it *is* on a supported domain, but the audio file is invalid, it will silently fail. To ensure your audio file plays fine, click the audio preview button in the plugin settings. +4. Restyle Quests + - Highlight Quests with optional theme colors for visibility. + - Unclaimed / Claimed / Ignored / Expired + - Use the color picker to pick a custom Quest tile color or use the default Discord coloring by clicking `Disable`. + - Gradient Style + - Intense Restyle Gradient — A more intense gradient than default in order to hide hard edges under the gradient. + - Default Restyle Gradient — The default gradient intensity with the restyle color. + - Subtle Black Gradient — A much less intense black gradient for contrast with the Quest assets. + - No Gradient — Remove the gradient altogether. May cause the Quest assets to be difficult to see. + - Asset Preload + - Load All Quest Assets On Page Load — As soon as the Quest page is opened, load all Quest assets on the page. + - Load Quest Assets During Page Scroll — Keep the default behavior of loading Quest assets as they come into view. +5. Reorder Quests + - Sort Quests by their status. + - Comma-separated list must contain all of: `UNCLAIMED, CLAIMED, IGNORED, EXPIRED`. + - Subsort status groups by Quest added date, expiring date, or claimed date. + - Ignored Quest Profile + - Share ignored quests between accounts on a single client or keep them separate. + - Ignore Quests by opening their context menu and selecting `Mark as Ignored`. \ No newline at end of file From b432730c1d82bc3041aa2c0005427685b1938b93 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:43:11 -0400 Subject: [PATCH 058/881] I forgot a fi --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e710003dc..17224758a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,6 +36,7 @@ jobs: echo "release=true" >> $GITHUB_OUTPUT else echo "release=false" >> $GITHUB_OUTPUT + fi - name: Stop if not newer if: steps.check.outputs.release == 'false' From 622df6a680e097126c6b82270aa35bab94ddba37 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:47:02 -0400 Subject: [PATCH 059/881] Look for just v* --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 17224758a..457338b4a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: - name: Get latest Git tag id: tag run: | - latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + latest_tag=$(git tag --list 'v*' --sort=-v:refname | head -n 1 || echo "v0.0.0") echo "latest=$latest_tag" >> $GITHUB_OUTPUT - name: Check if package.json version is newer From 4629f993cc0b828aae35ed5fad9257965fd5534f Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:51:55 -0400 Subject: [PATCH 060/881] =?UTF-8?q?=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 457338b4a..28b30bc2f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -45,27 +45,34 @@ jobs: exit 0 - name: Create Tag + if: steps.check.outputs.release == 'true' uses: butlerlogic/action-autotag@1.1.2 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" with: tag_prefix: "v" - - uses: pnpm/action-setup@v3 + - name: Setup PNPM + if: steps.check.outputs.release == 'true' + uses: pnpm/action-setup@v3 - name: Use Node.js 20 + if: steps.check.outputs.release == 'true' uses: actions/setup-node@v4 with: node-version: 20 cache: "pnpm" - name: Install dependencies + if: steps.check.outputs.release == 'true' run: pnpm install --frozen-lockfile - name: Build web + if: steps.check.outputs.release == 'true' run: pnpm buildWebStandalone - name: Publish extension + if: steps.check.outputs.release == 'true' run: | cd dist/browser/chromium-unpacked pnpx chrome-webstore-upload-cli@2.1.0 upload --auto-publish From 5583f7af54d7a4e11e97170d97f10d5f6e60071f Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:44:16 -0700 Subject: [PATCH 061/881] SilentTyping: Fix setting name side-effects. (#358) * Fix variable name side-effects. * Language. --- src/plugins/silentTyping/index.tsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/plugins/silentTyping/index.tsx b/src/plugins/silentTyping/index.tsx index 29b5fcf83..5e976008c 100644 --- a/src/plugins/silentTyping/index.tsx +++ b/src/plugins/silentTyping/index.tsx @@ -27,7 +27,7 @@ import { Channel } from "@vencord/discord-types"; import { ChannelStore, FluxDispatcher, Menu, React } from "@webpack/common"; const settings = definePluginSettings({ - enabled: { + enabledGlobally: { type: OptionType.BOOLEAN, description: "Toggle functionality of your own typing indicator globally.", default: true, @@ -51,7 +51,7 @@ const settings = definePluginSettings({ type: OptionType.SELECT, description: "What to do when left clicking the chat icon.", options: [ - { label: "Toggle Plugin Functionality", value: "global" }, + { label: "Toggle Typing Globally", value: "global" }, { label: "Toggle Typing in Channel", value: "channel", default: true }, { label: "Toggle Typing in Guild", value: "guild" }, { label: "Open Plugin Settings", value: "settings" } @@ -61,7 +61,7 @@ const settings = definePluginSettings({ type: OptionType.SELECT, description: "What to do when middle clicking the chat icon.", options: [ - { label: "Toggle Plugin Functionality", value: "global" }, + { label: "Toggle Typing Globally", value: "global" }, { label: "Toggle Typing in Channel", value: "channel" }, { label: "Toggle Typing in Guild", value: "guild" }, { label: "Open Plugin Settings", value: "settings", default: true } @@ -71,7 +71,7 @@ const settings = definePluginSettings({ type: OptionType.SELECT, description: "What to do when right clicking the chat icon.", options: [ - { label: "Toggle Plugin Functionality", value: "global", default: true }, + { label: "Toggle Typing Globally", value: "global", default: true }, { label: "Toggle Typing in Channel", value: "channel" }, { label: "Toggle Typing in Guild", value: "guild" }, { label: "Open Plugin Settings", value: "settings" } @@ -100,7 +100,7 @@ const settings = definePluginSettings({ }); function toggleGlobal(): void { - settings.store.enabled = !settings.store.enabled; + settings.store.enabledGlobally = !settings.store.enabledGlobally; } function toggleLocation(locationId: string, effectiveList: string[], defaultHidden: boolean): void { @@ -119,7 +119,7 @@ function toggleLocation(locationId: string, effectiveList: string[], defaultHidd const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { const { - enabled, + enabledGlobally, chatIcon, defaultHidden, enabledLocations, @@ -128,7 +128,7 @@ const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { chatIconMiddleClickAction, chatIconRightClickAction, } = settings.use([ - "enabled", + "enabledGlobally", "chatIcon", "defaultHidden", "enabledLocations", @@ -143,10 +143,10 @@ const SilentTypingChatToggle: ChatBarButtonFactory = ({ channel, type }) => { if (!validChat || !chatIcon) return null; const effectiveList = getEffectiveList(); - const enabledLocally = enabled && checkEnabled(channel); + const enabledLocally = enabledGlobally && checkEnabled(channel); const location = channel.guild_id && effectiveList.includes(channel.guild_id) ? "Guild" : effectiveList.includes(channel.id) ? "Channel" : "Global"; - const tooltip = enabled ? ( + const tooltip = enabledGlobally ? ( enabledLocally ? `Typing Hidden (${location})` : `Typing Visible (${location})` ) : "Typing Visible (Global)"; @@ -220,7 +220,7 @@ function getEffectiveList(): string[] { } function checkEnabled(channel: string | Channel): boolean { - if (!settings.store.enabled) return false; + if (!settings.store.enabledGlobally) return false; const channelId = typeof channel === "string" ? channel : channel.id; const guildId = typeof channel === "string" ? ChannelStore.getChannel(channelId)?.guild_id : channel.guild_id; @@ -237,14 +237,14 @@ const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => { const { chatIcon, chatContextMenu, - enabled, + enabledGlobally, defaultHidden, hideChatBoxTypingIndicators, hideMembersListTypingIndicators } = settings.use([ "chatIcon", "chatContextMenu", - "enabled", + "enabledGlobally", "defaultHidden", "hideChatBoxTypingIndicators", "hideMembersListTypingIndicators" @@ -260,8 +260,8 @@ const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => { group.splice(idx >= 0 ? idx : 0, 0, - settings.store.enabled = !settings.store.enabled} /> + settings.store.enabledGlobally = !settings.store.enabledGlobally} /> settings.store.hideChatBoxTypingIndicators = !settings.store.hideChatBoxTypingIndicators} /> Date: Fri, 22 Aug 2025 17:58:20 -0400 Subject: [PATCH 062/881] Update Questify Patch --- src/equicordplugins/questify/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 70baf9d43..6d619ad19 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -1069,8 +1069,8 @@ export default definePlugin({ { // When the Quest Accepted button which has been enabled again by the above patch is // clicked, resume the automatic completion of the Quest and disable the button again. - match: /(disabled:\i.\i.\i\[)/, - replace: "onClick:()=>{$self.processQuestForAutoComplete(arguments[0].quest)},$1", + match: /(START_QUEST_CTA.*?,{variant:"secondary",)/, + replace: "$1onClick:()=>{$self.processQuestForAutoComplete(arguments[0].quest)},", }, { // The "Quest Accepted" text is changed to "Resume" if the Quest is in progress but not active. From 865097b0dc13550596842d174394a578bafb78b9 Mon Sep 17 00:00:00 2001 From: Jake <134332787+justjxke@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:59:43 +0100 Subject: [PATCH 063/881] feat(MessageLogger): add diffs to messagelogger (#356) * fix(MessageLogger): add diffs to messagelogger >> fix multiple line edits not working and updating to the latest diff >> using character LCS algorithm >> Fix HistoryModal.tsx diff display logic >> update diff colors and deleteStyleOverlay.css * remove light colors, literally no need lol * add settings option * make the setting restart required * fix some duplication errors in logging with diffs * comment fixes * add the ability to disable/enable diff styles on a message * Update Setting --------- Co-authored-by: justjxke Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/plugins/messageLogger/HistoryModal.tsx | 2 +- .../messageLogger/deleteStyleOverlay.css | 4 +- src/plugins/messageLogger/diffUtils.ts | 115 ++++++++++++++++++ src/plugins/messageLogger/index.tsx | 111 +++++++++++++---- src/plugins/messageLogger/messageLogger.css | 21 +++- 5 files changed, 224 insertions(+), 29 deletions(-) create mode 100644 src/plugins/messageLogger/diffUtils.ts diff --git a/src/plugins/messageLogger/HistoryModal.tsx b/src/plugins/messageLogger/HistoryModal.tsx index 5c49d72a7..c0b4d3dea 100644 --- a/src/plugins/messageLogger/HistoryModal.tsx +++ b/src/plugins/messageLogger/HistoryModal.tsx @@ -84,7 +84,7 @@ export function HistoryModal({ modalProps, message }: { modalProps: ModalProps;
- {parseEditContent(contents[currentTab], message)} + {parseEditContent(contents[currentTab], message, currentTab === contents.length - 1 ? undefined : contents[contents.length - 1])}
diff --git a/src/plugins/messageLogger/deleteStyleOverlay.css b/src/plugins/messageLogger/deleteStyleOverlay.css index 59e4ac666..9fca958b2 100644 --- a/src/plugins/messageLogger/deleteStyleOverlay.css +++ b/src/plugins/messageLogger/deleteStyleOverlay.css @@ -1,3 +1,3 @@ .messagelogger-deleted { - background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important; -} + background-color: hsl(3deg 86% 68%) !important; +} \ No newline at end of file diff --git a/src/plugins/messageLogger/diffUtils.ts b/src/plugins/messageLogger/diffUtils.ts new file mode 100644 index 000000000..3713058a3 --- /dev/null +++ b/src/plugins/messageLogger/diffUtils.ts @@ -0,0 +1,115 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export interface DiffPart { + type: "added" | "removed" | "unchanged"; + text: string; +} + +export function createWordDiff(oldText: string, newText: string): DiffPart[] { + // suffix shit, if oldText is shorter than newText and newText starts with oldText + if (oldText.length < newText.length && newText.startsWith(oldText)) { + const added = newText.slice(oldText.length); + const parts: DiffPart[] = []; + if (oldText.length > 0) { + parts.push({ type: "unchanged", text: oldText }); + } + parts.push({ type: "added", text: added }); + return parts; + } + + // same as above + if (oldText.length < newText.length && newText.endsWith(oldText)) { + const added = newText.slice(0, newText.length - oldText.length); + const parts: DiffPart[] = []; + parts.push({ type: "added", text: added }); + if (oldText.length > 0) { + parts.push({ type: "unchanged", text: oldText }); + } + return parts; + } + + // Handle simple prefix removal: "watch this" -> "watch" + if (newText.length < oldText.length && oldText.startsWith(newText)) { + const removed = oldText.slice(newText.length); + const parts: DiffPart[] = []; + if (newText.length > 0) { + parts.push({ type: "unchanged", text: newText }); + } + parts.push({ type: "removed", text: removed }); + return parts; + } + + // Handle simple suffix removal: "prefix watch" -> "watch" + if (newText.length < oldText.length && oldText.endsWith(newText)) { + const removed = oldText.slice(0, oldText.length - newText.length); + const parts: DiffPart[] = []; + parts.push({ type: "removed", text: removed }); + if (newText.length > 0) { + parts.push({ type: "unchanged", text: newText }); + } + return parts; + } + + // For complex cases, fall back to LCS algorithm + const oldChars = oldText.split(""); + const newChars = newText.split(""); + + const result: DiffPart[] = []; + const dp: number[][] = []; + + for (let i = 0; i <= oldChars.length; i++) { + dp[i] = []; + for (let j = 0; j <= newChars.length; j++) { + dp[i][j] = 0; + } + } + + for (let i = 1; i <= oldChars.length; i++) { + for (let j = 1; j <= newChars.length; j++) { + if (oldChars[i - 1] === newChars[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + let i = oldChars.length; + let j = newChars.length; + + const diffParts: DiffPart[] = []; + + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && oldChars[i - 1] === newChars[j - 1]) { + diffParts.unshift({ type: "unchanged", text: oldChars[i - 1] }); + i--; + j--; + } else if (j > 0 && (i === 0 || dp[i][j - 1] > dp[i - 1][j])) { + diffParts.unshift({ type: "added", text: newChars[j - 1] }); + j--; + } else if (i > 0) { + diffParts.unshift({ type: "removed", text: oldChars[i - 1] }); + i--; + } + } + + const groupedParts: DiffPart[] = []; + for (const part of diffParts) { + const lastPart = groupedParts[groupedParts.length - 1]; + if (lastPart && lastPart.type === part.type) { + lastPart.text += part.text; + } else { + groupedParts.push(part); + } + } + + return groupedParts; +} + +export function createMessageDiff(previousContent: string, currentContent: string): DiffPart[] { + return createWordDiff(previousContent, currentContent); +} diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 7f2de1e67..57ab3f10b 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -21,20 +21,25 @@ import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { Message } from "@vencord/discord-types"; import { findByPropsLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, Menu, MessageStore, Parser, React, SelectedChannelStore, Timestamp, UserStore, useStateFromStores } from "@webpack/common"; import overlayStyle from "./deleteStyleOverlay.css?managed"; import textStyle from "./deleteStyleText.css?managed"; +import { createMessageDiff, DiffPart } from "./diffUtils"; import { openHistoryModal } from "./HistoryModal"; interface MLMessage extends Message { deleted?: boolean; editHistory?: { timestamp: Date; content: string; }[]; firstEditTimestamp?: Date; + diffViewDisabled?: boolean; } const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessage"); +// track messages where the user disabled diffs for this session +const disabledDiffMessages = new Set(); + function addDeleteStyle() { if (Settings.plugins.MessageLogger.deleteStyle === "text") { enableStyle(textStyle); @@ -47,6 +52,7 @@ function addDeleteStyle() { const REMOVE_HISTORY_ID = "ml-remove-history"; const TOGGLE_DELETE_STYLE_ID = "ml-toggle-style"; +const TOGGLE_DIFF_VIEW_ID = "ml-toggle-diff"; const patchMessageContextMenu: NavContextMenuPatchCallback = ( children, props, @@ -74,6 +80,28 @@ const patchMessageContextMenu: NavContextMenuPatchCallback = ( ); } + // toggle per-message diff rendering when the message has an edit history + if (editHistory?.length) { + const isDisabled = disabledDiffMessages.has(id); + children.push( + { + if (isDisabled) disabledDiffMessages.delete(id); + else disabledDiffMessages.add(id); + // also toggle a CSS class on the message element for an immediate visual effect (basically shows things work lol) + const domElement = document.getElementById(`chat-messages-${channel_id}-${id}`); + domElement?.classList.toggle("messagelogger-diff-disabled", disabledDiffMessages.has(id)); + // force a re-render without mutating message fields + updateMessage(channel_id, id); + }} + />, + ); + } + let label; if (!Vencord.Plugins.isPluginEnabled("MessageLoggerEnhanced")) { @@ -137,7 +165,32 @@ const patchChannelContextMenu: NavContextMenuPatchCallback = ( ); }; -export function parseEditContent(content: string, message: Message) { +function renderDiffParts(diffParts: DiffPart[]) { + return diffParts.map((part, index) => { + if (part.type === "unchanged") { + return React.createElement("span", { key: index }, part.text); + } else if (part.type === "added") { + return React.createElement("span", { + key: index, + className: "messagelogger-diff-added" + }, part.text); + } else if (part.type === "removed") { + return React.createElement("span", { + key: index, + className: "messagelogger-diff-removed" + }, part.text); + } + return React.createElement("span", { key: index }, part.text); + }); +} + +export function parseEditContent(content: string, message: Message, previousContent?: string) { + const perMessageDiffEnabled = !disabledDiffMessages.has(message.id); + if (previousContent && content !== previousContent && Settings.plugins.MessageLogger.showEditDiffs && perMessageDiffEnabled) { + const diffParts = createMessageDiff(content, previousContent); + return renderDiffParts(diffParts); + } + return Parser.parse(content, true, { channelId: message.channel_id, messageId: message.id, @@ -177,24 +230,33 @@ export default definePlugin({ [MessageStore], () => MessageStore.getMessage(channelId, messageId) as MLMessage, null, - (oldMsg, newMsg) => oldMsg?.editHistory === newMsg?.editHistory, + (oldMsg, newMsg) => + oldMsg?.editHistory === newMsg?.editHistory && + oldMsg?.diffViewDisabled === newMsg?.diffViewDisabled && + oldMsg === newMsg, ); return Settings.plugins.MessageLogger.inlineEdits && ( - <> - {message.editHistory?.map((edit, idx) => ( -
- {parseEditContent(edit.content, message)} - - {" "}({getIntlMessage("MESSAGE_EDITED")}) - -
- ))} - + + {message.editHistory?.map((edit, idx) => { + const nextContent = idx === (message.editHistory?.length ?? 0) - 1 + ? message.content + : message.editHistory?.[idx + 1]?.content; + + return ( +
+ {parseEditContent(edit.content, message, nextContent)} + + {" "}({getIntlMessage("MESSAGE_EDITED")}) + +
+ ); + })} +
); }, { noop: true }), @@ -262,6 +324,11 @@ export default definePlugin({ description: "Comma-separated list of guild IDs to ignore", default: "", }, + showEditDiffs: { + type: OptionType.BOOLEAN, + description: "Show visual differences between edited message versions", + default: false, + }, }, handleDelete( @@ -327,7 +394,7 @@ export default definePlugin({ ) || (isEdit ? !logEdits : !logDeletes) || ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) || - // Ignore Venbot in the support channels + // Ignore Venbot in the support channels (love you venbot!!!) (message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === VC_SUPPORT_CATEGORY_ID)); }, @@ -427,7 +494,8 @@ export default definePlugin({ "this.customRenderedContent = $1.customRenderedContent," + "this.deleted = $1.deleted || false," + "this.editHistory = $1.editHistory || []," + - "this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp,", + "this.firstEditTimestamp = $1.firstEditTimestamp || this.editedTimestamp || this.timestamp," + + "this.diffViewDisabled = $1.diffViewDisabled || false,", }, ], }, @@ -441,7 +509,7 @@ export default definePlugin({ match: /(?<=null!=\i\.edited_timestamp\)return )\i\(\i,\{reactions:(\i)\.reactions.{0,50}\}\)/, replace: - "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp })", + "Object.assign($&,{ deleted:$1.deleted, editHistory:$1.editHistory, firstEditTimestamp:$1.firstEditTimestamp, diffViewDisabled:$1.diffViewDisabled })", }, { @@ -461,7 +529,8 @@ export default definePlugin({ "})())," + "deleted: arguments[1]?.deleted," + "editHistory: arguments[1]?.editHistory," + - "firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)", + "firstEditTimestamp: new Date(arguments[1]?.firstEditTimestamp ?? $2.editedTimestamp ?? $2.timestamp)," + + "diffViewDisabled: arguments[1]?.diffViewDisabled", }, { // Preserve deleted attribute on attachments diff --git a/src/plugins/messageLogger/messageLogger.css b/src/plugins/messageLogger/messageLogger.css index a76e98888..5f4e5bb13 100644 --- a/src/plugins/messageLogger/messageLogger.css +++ b/src/plugins/messageLogger/messageLogger.css @@ -2,15 +2,12 @@ display: none; } -.messagelogger-deleted -:is( - .messagelogger-deleted-attachment, +.messagelogger-deleted:is(.messagelogger-deleted-attachment, .emoji, [data-type="sticker"], [class*="embedIframe"], [class*="embedSpotify"], - [class*="imageContainer"] -) { + [class*="imageContainer"]) { filter: grayscale(1) !important; transition: 150ms filter ease-in-out; @@ -48,3 +45,17 @@ flex-wrap: wrap; gap: 16px; } + +.messagelogger-diff-added { + color: #85E89D; +} + +.messagelogger-diff-removed { + color: #F47068; +} + +/* when diffs are disabled for a specific message, remove diff colors */ +.messagelogger-diff-disabled .messagelogger-diff-added, +.messagelogger-diff-disabled .messagelogger-diff-removed { + color: inherit; +} From 03e9c2b501710430a442cffc7d5f778e8ea9d468 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:07:01 -0700 Subject: [PATCH 064/881] Improve patch. (#359) --- src/equicordplugins/questify/index.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index 6d619ad19..fd5558b2c 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -1066,16 +1066,12 @@ export default definePlugin({ match: /(START_QUEST_CTA.{0,400}?disabled:)(!0)/, replace: "$1$self.shouldDisableQuestAcceptedButton(arguments[0].quest)??$2" }, - { - // When the Quest Accepted button which has been enabled again by the above patch is - // clicked, resume the automatic completion of the Quest and disable the button again. - match: /(START_QUEST_CTA.*?,{variant:"secondary",)/, - replace: "$1onClick:()=>{$self.processQuestForAutoComplete(arguments[0].quest)},", - }, { // The "Quest Accepted" text is changed to "Resume" if the Quest is in progress but not active. + // When the Quest Accepted button which has been enabled again by the above patch is clicked, + // resume the automatic completion of the Quest and disable the button again. match: /(\i.intl.string\(\i.\i#{intl::QUEST_ACCEPTED}\))/, - replace: "$self.getQuestAcceptedButtonText(arguments[0].quest)??$1" + replace: "$self.getQuestAcceptedButtonText(arguments[0].quest)??$1,onClick:()=>{$self.processQuestForAutoComplete(arguments[0].quest)}" } ] }, From 708bce9a91aa74388d119b97bb92fe4f5f7d979e Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:41:52 -0400 Subject: [PATCH 065/881] Redo ChannelBadges --- src/equicordplugins/channelBadges/index.ts | 140 ------------------ src/equicordplugins/channelBadges/index.tsx | 91 ++++++++++++ .../channelBadges/settings.tsx | 105 +++++++------ .../fastDeleteChannels/index.tsx | 2 + 4 files changed, 143 insertions(+), 195 deletions(-) delete mode 100644 src/equicordplugins/channelBadges/index.ts create mode 100644 src/equicordplugins/channelBadges/index.tsx diff --git a/src/equicordplugins/channelBadges/index.ts b/src/equicordplugins/channelBadges/index.ts deleted file mode 100644 index 3c0f5838a..000000000 --- a/src/equicordplugins/channelBadges/index.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./style.css"; - -import { EquicordDevs } from "@utils/constants"; -import { getCurrentGuild } from "@utils/discord"; -import definePlugin from "@utils/types"; -import { Channel, Guild } from "@vencord/discord-types"; -import { ChannelStore, SelectedGuildStore } from "@webpack/common"; - -import { isEnabled, returnChannelBadge, settings } from "./settings"; - -let observer: MutationObserver | null = null; -let currentGuild: Guild | undefined | null = null; - -function addBadgesToChannel(element: HTMLElement, channelId: string) { - const parentContainer: HTMLElement | null = element.querySelector('[class*="linkTop"]'); - if (!parentContainer) return; - - const channel: Channel | undefined = ChannelStore.getChannel(channelId); - if (!channel || !isEnabled(channel.type)) return; - - const { type, nsfw, threadMetadata } = channel; - const isPrivate = channel.isPrivate() || threadMetadata?.locked || channel.isArchivedThread(); - const isNSFW = nsfw || channel.isNSFW(); - - let badgeContainer: HTMLElement | null = parentContainer.querySelector(".badge-container"); - if (!badgeContainer) { - badgeContainer = document.createElement("div"); - badgeContainer.classList.add("badge-container"); - parentContainer.appendChild(badgeContainer); - } - - const badgeConditions = [ - { id: 6101, condition: isPrivate, title: "This channel is locked." }, - { id: 6100, condition: isNSFW, title: "This channel is marked as NSFW." }, - { id: 6102, condition: currentGuild?.rulesChannelId === channel.id, title: "This channel is the server rules channel." }, - ]; - - badgeConditions.forEach(({ id, condition, title }) => { - if (condition && isEnabled(id)) addBadge(badgeContainer!, id, title); - }); - - addBadge(badgeContainer!, type, returnChannelBadge(type).label); -} - -function addBadge(container: HTMLElement, id: number, title: string) { - const { css, label, color } = returnChannelBadge(id); - const badge = document.createElement("div"); - badge.classList.add("channel-badge", `channel-badge-${css}`); - badge.textContent = label; - badge.title = title; - - if (color && color !== "") { - badge.style.backgroundColor = color; - } - - container.appendChild(badge); -} - -function deleteAllBadges() { - document.querySelectorAll(".channel-badge").forEach(badge => badge.remove()); - document.querySelectorAll('[data-list-item-id^="channels___"][data-scanned]').forEach(element => { - element.removeAttribute("data-scanned"); - }); -} - -export function reloadBadges() { - deleteAllBadges(); - document.querySelectorAll('[data-list-item-id^="channels___"]').forEach(element => { - const channelId = element.getAttribute("data-list-item-id")?.split("___")[1]; - if (channelId && /^\d+$/.test(channelId)) { - addBadgesToChannel(element as HTMLElement, channelId); - element.setAttribute("data-scanned", "true"); - } - }); -} - -function observeDomChanges() { - if (observer) observer.disconnect(); - - const handleMutations = (mutations: MutationRecord[]) => { - const addedElements = new Set(); - mutations.forEach(mutation => { - mutation.addedNodes.forEach(node => { - if (node.nodeType === Node.ELEMENT_NODE) { - const element = node as Element; - element.querySelectorAll('[data-list-item-id^="channels___"]:not([data-scanned])').forEach(child => { - addedElements.add(child); - }); - } - }); - }); - - addedElements.forEach(element => { - const channelId = element.getAttribute("data-list-item-id")?.split("___")[1]; - if (channelId && /^\d+$/.test(channelId)) { - addBadgesToChannel(element as HTMLElement, channelId); - element.setAttribute("data-scanned", "true"); - } - }); - }; - - observer = new MutationObserver(handleMutations); - observer.observe(document.body, { childList: true, subtree: true }); -} - -function onGuildChange() { - const newGuild: Guild | undefined | null = getCurrentGuild(); - if (newGuild !== currentGuild) { - currentGuild = newGuild; - } -} - -export default definePlugin({ - name: "ChannelBadges", - description: "Adds badges to channels based on their type", - authors: [EquicordDevs.creations], - settings, - - async start() { - currentGuild = getCurrentGuild(); - observeDomChanges(); - reloadBadges(); - SelectedGuildStore.addChangeListener(onGuildChange); - }, - - stop() { - if (observer) { - observer.disconnect(); - observer = null; - } - deleteAllBadges(); - SelectedGuildStore.removeChangeListener(onGuildChange); - } -}); diff --git a/src/equicordplugins/channelBadges/index.tsx b/src/equicordplugins/channelBadges/index.tsx new file mode 100644 index 000000000..b1a2ddb28 --- /dev/null +++ b/src/equicordplugins/channelBadges/index.tsx @@ -0,0 +1,91 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./style.css"; + +import { EquicordDevs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { Channel } from "@vencord/discord-types"; +import { GuildStore, React, SelectedGuildStore } from "@webpack/common"; +import { JSX } from "react"; + +import { isEnabled, returnChannelBadge, settings } from "./settings"; + +function renderBadge(id: number, title: string) { + const { css, label, color } = returnChannelBadge(id); + + return ( +
+ {label} +
+ ); +} + +export default definePlugin({ + name: "ChannelBadges", + description: "Adds badges to channels based on their type", + authors: [EquicordDevs.creations], + settings, + patches: [ + // TY TypingIndicator + // Normal Channels + { + find: "UNREAD_IMPORTANT:", + replacement: { + match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/, + replace: "$&,$self.renderChannelBadges($1)" + } + }, + // Threads + { + find: "spineWithGuildIcon]:", + replacement: { + match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/, + replace: "$&,$self.renderChannelBadges($1)" + } + } + ], + renderChannelBadges(channel: Channel) { + if (!channel || !isEnabled(channel.type)) return null; + + const { type, nsfw, threadMetadata } = channel; + const isPrivate = channel.isPrivate() || threadMetadata?.locked || channel.isArchivedThread(); + const isNSFW = nsfw || channel.isNSFW(); + + const selectedGuildId = SelectedGuildStore.getGuildId(); + const guild = selectedGuildId ? GuildStore.getGuild(selectedGuildId) : null; + + const badgeConditions = [ + { id: 6101, condition: isPrivate, title: "This channel is locked." }, + { id: 6100, condition: isNSFW, title: "This channel is marked as NSFW." }, + { id: 6102, condition: guild?.rulesChannelId === channel.id, title: "This channel is the server rules channel." }, + ]; + + let badges: JSX.Element[] = []; + + if (settings.store.oneBadgePerChannel) { + const first = badgeConditions.find(({ id, condition }) => condition && isEnabled(id)); + if (first) { + badges.push(renderBadge(first.id, first.title)); + } else { + badges.push(renderBadge(type, returnChannelBadge(type).label)); + } + } else { + badges = badgeConditions + .filter(({ id, condition }) => condition && isEnabled(id)) + .map(({ id, title }) => renderBadge(id, title)); + + badges.push(renderBadge(type, returnChannelBadge(type).label)); + } + + return
{badges}
; + } +}); diff --git a/src/equicordplugins/channelBadges/settings.tsx b/src/equicordplugins/channelBadges/settings.tsx index c0283bd53..baa38992d 100644 --- a/src/equicordplugins/channelBadges/settings.tsx +++ b/src/equicordplugins/channelBadges/settings.tsx @@ -7,276 +7,273 @@ import { definePluginSettings } from "@api/Settings"; import { OptionType } from "@utils/types"; -import { reloadBadges } from "./index"; - -const settings = definePluginSettings({ +export const settings = definePluginSettings({ oneBadgePerChannel: { type: OptionType.BOOLEAN, default: false, description: "Show only one badge per channel", - onChange: reloadBadges, + restartNeeded: true, }, showTextBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Text badge", - onChange: reloadBadges, + restartNeeded: true, }, showVoiceBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Voice badge", - onChange: reloadBadges, + restartNeeded: true, }, showCategoryBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Category badge", - onChange: reloadBadges, + restartNeeded: true, }, showDirectoryBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Directory badge", - onChange: reloadBadges, + restartNeeded: true, }, showAnnouncementThreadBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Announcement Thread badge", - onChange: reloadBadges, + restartNeeded: true, }, showPublicThreadBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Public Thread badge", - onChange: reloadBadges, + restartNeeded: true, }, showPrivateThreadBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Private Thread badge", - onChange: reloadBadges, + restartNeeded: true, }, showStageBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Stage badge", - onChange: reloadBadges, + restartNeeded: true, }, showAnnouncementBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Announcement badge", - onChange: reloadBadges, + restartNeeded: true, }, showForumBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Forum badge", - onChange: reloadBadges, + restartNeeded: true, }, showMediaBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Media badge", - onChange: reloadBadges, + restartNeeded: true, }, showNSFWBadge: { type: OptionType.BOOLEAN, default: true, description: "Show NSFW badge", - onChange: reloadBadges, + restartNeeded: true, }, showLockedBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Locked badge", - onChange: reloadBadges, + restartNeeded: true, }, showRulesBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Rules badge", - onChange: reloadBadges, + restartNeeded: true, }, showUnknownBadge: { type: OptionType.BOOLEAN, default: true, description: "Show Unknown badge", - onChange: reloadBadges, + restartNeeded: true, }, textBadgeLabel: { type: OptionType.STRING, default: "Text", description: "Text badge label", - onChange: reloadBadges, + restartNeeded: true, }, voiceBadgeLabel: { type: OptionType.STRING, default: "Voice", description: "Voice badge label", - onChange: reloadBadges, + restartNeeded: true, }, categoryBadgeLabel: { type: OptionType.STRING, default: "Category", description: "Category badge label", - onChange: reloadBadges, + restartNeeded: true, }, announcementBadgeLabel: { type: OptionType.STRING, default: "News", description: "Announcement badge label", - onChange: reloadBadges, + restartNeeded: true, }, announcementThreadBadgeLabel: { type: OptionType.STRING, default: "News Thread", description: "Announcement Thread badge label", - onChange: reloadBadges, + restartNeeded: true, }, publicThreadBadgeLabel: { type: OptionType.STRING, default: "Thread", description: "Public Thread badge label", - onChange: reloadBadges, + restartNeeded: true, }, privateThreadBadgeLabel: { type: OptionType.STRING, default: "Private Thread", description: "Private Thread badge label", - onChange: reloadBadges, + restartNeeded: true, }, stageBadgeLabel: { type: OptionType.STRING, default: "Stage", description: "Stage badge label", - onChange: reloadBadges, + restartNeeded: true, }, directoryBadgeLabel: { type: OptionType.STRING, default: "Directory", description: "Directory badge label", - onChange: reloadBadges, + restartNeeded: true, }, forumBadgeLabel: { type: OptionType.STRING, default: "Forum", description: "Forum badge label", - onChange: reloadBadges, + restartNeeded: true, }, mediaBadgeLabel: { type: OptionType.STRING, default: "Media", description: "Media badge label", - onChange: reloadBadges, + restartNeeded: true, }, nsfwBadgeLabel: { type: OptionType.STRING, default: "NSFW", description: "NSFW badge label", - onChange: reloadBadges, + restartNeeded: true, }, lockedBadgeLabel: { type: OptionType.STRING, default: "Locked", description: "Locked badge label", - onChange: reloadBadges, + restartNeeded: true, }, rulesBadgeLabel: { type: OptionType.STRING, default: "Rules", description: "Rules badge label", - onChange: reloadBadges, + restartNeeded: true, }, unknownBadgeLabel: { type: OptionType.STRING, default: "Unknown", description: "Unknown badge label", - onChange: reloadBadges, + restartNeeded: true, }, - textBadgeColor: { type: OptionType.STRING, description: "Text badge color", - onChange: reloadBadges, + restartNeeded: true, }, voiceBadgeColor: { type: OptionType.STRING, description: "Voice badge color", - onChange: reloadBadges, + restartNeeded: true, }, categoryBadgeColor: { type: OptionType.STRING, description: "Category badge color", - onChange: reloadBadges, + restartNeeded: true, }, announcementBadgeColor: { type: OptionType.STRING, description: "Announcement badge color", - onChange: reloadBadges, + restartNeeded: true, }, announcementThreadBadgeColor: { type: OptionType.STRING, description: "Announcement Thread badge color", - onChange: reloadBadges, + restartNeeded: true, }, publicThreadBadgeColor: { type: OptionType.STRING, description: "Public Thread badge color", - onChange: reloadBadges, + restartNeeded: true, }, privateThreadBadgeColor: { type: OptionType.STRING, description: "Private Thread badge color", - onChange: reloadBadges, + restartNeeded: true, }, stageBadgeColor: { type: OptionType.STRING, description: "Stage badge color", - onChange: reloadBadges, + restartNeeded: true, }, directoryBadgeColor: { type: OptionType.STRING, description: "Directory badge color", - onChange: reloadBadges, + restartNeeded: true, }, forumBadgeColor: { type: OptionType.STRING, description: "Forum badge color", - onChange: reloadBadges, + restartNeeded: true, }, mediaBadgeColor: { type: OptionType.STRING, description: "Media badge color", - onChange: reloadBadges, + restartNeeded: true, }, nsfwBadgeColor: { type: OptionType.STRING, description: "NSFW badge color", - onChange: reloadBadges, + restartNeeded: true, }, lockedBadgeColor: { type: OptionType.STRING, description: "Locked badge color", - onChange: reloadBadges, + restartNeeded: true, }, rulesBadgeColor: { type: OptionType.STRING, description: "Rules badge color", - onChange: reloadBadges, + restartNeeded: true, }, unknownBadgeColor: { type: OptionType.STRING, description: "Unknown badge color", - onChange: reloadBadges, + restartNeeded: true, }, }); -const defaultValues = { +export const defaultValues = { showTextBadge: true, showVoiceBadge: true, showCategoryBadge: true, @@ -314,7 +311,7 @@ const defaultValues = { nsfwBadgeTooltip: "This channel is marked as NSFW.", }; -function isEnabled(type: number) { +export function isEnabled(type: number) { const fromValues = settings.store; switch (type) { @@ -351,7 +348,7 @@ function isEnabled(type: number) { } } -function returnChannelBadge(type: number) { +export function returnChannelBadge(type: number) { switch (type) { case 0: return { css: "text", label: settings.store.textBadgeLabel, color: settings.store.textBadgeColor }; @@ -385,5 +382,3 @@ function returnChannelBadge(type: number) { return { css: "unknown", label: settings.store.unknownBadgeLabel, color: settings.store.unknownBadgeColor }; } } - -export { defaultValues, isEnabled, returnChannelBadge, settings }; diff --git a/src/equicordplugins/fastDeleteChannels/index.tsx b/src/equicordplugins/fastDeleteChannels/index.tsx index 2a026500a..b112ca46a 100644 --- a/src/equicordplugins/fastDeleteChannels/index.tsx +++ b/src/equicordplugins/fastDeleteChannels/index.tsx @@ -78,6 +78,7 @@ export default definePlugin({ settings, patches: [ // TY TypingIndicator + // Normal Channels { find: "UNREAD_IMPORTANT:", replacement: { @@ -85,6 +86,7 @@ export default definePlugin({ replace: "$&,$self.TrashIcon($1)" } }, + // Threads { find: "spineWithGuildIcon]:", replacement: { From e5a945932df2f2ab337c57f4ab73cfcbdff2ac8b Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 22 Aug 2025 19:30:48 -0400 Subject: [PATCH 066/881] Rewrite CharacterCounter --- src/equicordplugins/channelBadges/index.tsx | 2 +- .../characterCounter/index.tsx | 160 ++++-------------- .../characterCounter/style.css | 21 +-- 3 files changed, 44 insertions(+), 139 deletions(-) diff --git a/src/equicordplugins/channelBadges/index.tsx b/src/equicordplugins/channelBadges/index.tsx index b1a2ddb28..dba79c867 100644 --- a/src/equicordplugins/channelBadges/index.tsx +++ b/src/equicordplugins/channelBadges/index.tsx @@ -32,7 +32,7 @@ function renderBadge(id: number, title: string) { export default definePlugin({ name: "ChannelBadges", description: "Adds badges to channels based on their type", - authors: [EquicordDevs.creations], + authors: [EquicordDevs.creations, EquicordDevs.thororen], settings, patches: [ // TY TypingIndicator diff --git a/src/equicordplugins/characterCounter/index.tsx b/src/equicordplugins/characterCounter/index.tsx index bebabcc40..59b5f348d 100644 --- a/src/equicordplugins/characterCounter/index.tsx +++ b/src/equicordplugins/characterCounter/index.tsx @@ -9,142 +9,56 @@ import "./style.css"; import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { waitFor } from "@webpack"; -import { SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common"; - -let ChannelTextAreaClasses: Record | null = null; -let shouldShowColorEffects: boolean; -let position: boolean; +import { UserStore } from "@webpack/common"; const settings = definePluginSettings({ colorEffects: { type: OptionType.BOOLEAN, description: "Turn on or off color effects for getting close to the character limit", default: true, - onChange: value => { - shouldShowColorEffects = value; - } }, - position: { - type: OptionType.BOOLEAN, - description: "Move the character counter to the left side of the chat input", - default: false, - onChange: value => { - position = value; - - const charCounterDiv = document.querySelector(".vc-char-counter"); - if (charCounterDiv) { - if (value) charCounterDiv.classList.add("left"); - else charCounterDiv.classList.remove("left"); - } - } - } }); -const updateCharCounterOnChannelChange = async () => { - await waitForChatInput(); - addCharCounter(); -}; - -const waitForChatInput = async () => { - return waitFor([`${ChannelTextAreaClasses?.channelTextArea}`], () => document.querySelector(`.${ChannelTextAreaClasses?.channelTextArea}`)); -}; - -const addCharCounter = () => { - const chatTextArea: HTMLElement | null = document.querySelector(`.${ChannelTextAreaClasses?.channelTextArea}`); - if (!chatTextArea) return; - - let charCounterDiv: HTMLElement | null = document.querySelector(".vc-char-counter"); - if (!charCounterDiv) { - charCounterDiv = document.createElement("div"); - charCounterDiv.classList.add("vc-char-counter"); - - if (position) charCounterDiv.classList.add("left"); - - const premiumType = (UserStore.getCurrentUser().premiumType ?? 0); - const charMax = premiumType === 2 ? 4000 : 2000; - - charCounterDiv.innerHTML = `0/${charMax}`; - charCounterDiv.style.opacity = "0"; - } - - const chatInputContainer: HTMLElement | null = chatTextArea.closest("form"); - if (chatInputContainer && !chatInputContainer.contains(charCounterDiv)) { - chatTextArea.style.display = "flex"; - chatTextArea.style.flexDirection = "column"; - chatCounterPositionUpdate(chatInputContainer, chatTextArea, charCounterDiv); - - chatTextArea.appendChild(charCounterDiv); - } - - const chatInput: HTMLElement | null = chatTextArea.querySelector('div[contenteditable="true"]'); - - const updateCharCount = () => { - const text = chatInput?.textContent?.replace(/[\uFEFF\xA0]/g, "") || ""; - const charCount = text.trim().length; - - if (charCount !== 0) charCounterDiv.style.opacity = "1"; - else charCounterDiv.style.opacity = "0"; - - const charCountSpan: HTMLElement | null = charCounterDiv!.querySelector(".vc-char-count"); - charCountSpan!.textContent = `${charCount}`; - - const bottomPos = chatInputContainer!.offsetHeight; - charCounterDiv.style.bottom = `${bottomPos.toString()}px`; - - if (shouldShowColorEffects) { - const premiumType = (UserStore.getCurrentUser().premiumType ?? 0); - const charMax = premiumType === 2 ? 4000 : 2000; - const percentage = (charCount / charMax) * 100; - let color; - if (percentage < 50) { - color = "var(--text-muted)"; - } else if (percentage < 75) { - color = "var(--yellow-330)"; - } else if (percentage < 90) { - color = "var(--orange-330)"; - } else { - color = "var(--red-360)"; - } - charCountSpan!.style.color = color; - } - }; - - chatInput?.addEventListener("input", updateCharCount); - chatInput?.addEventListener("keydown", () => setTimeout(updateCharCount, 0)); - chatInput?.addEventListener("paste", () => setTimeout(updateCharCount, 0)); -}; - -const chatCounterPositionUpdate = (chatInputContainer: HTMLElement, chatTextArea: HTMLElement, charCounterDiv: HTMLElement) => { - chatTextArea.style.justifyContent = "flex-end"; - charCounterDiv.style.position = "absolute"; - - const bottomPos = (chatInputContainer.offsetHeight) - 10; // onload 68px, minus 10 to fix - charCounterDiv.style.bottom = `${bottomPos.toString()}px`; -}; - export default definePlugin({ name: "CharacterCounter", description: "Adds a character counter to the chat input", - authors: [EquicordDevs.creations, EquicordDevs.Panniku], - settings: settings, + authors: [EquicordDevs.creations, EquicordDevs.Panniku, EquicordDevs.thororen], + settings, + patches: [ + { + find: ".CREATE_FORUM_POST||", + replacement: { + match: /(textValue:(\i).{0,50}channelId:\i\.id\}\)),\i/, + replace: "$1,$self.getCharCounter($2)" + } + }, + { + find: "#{intl::PREMIUM_MESSAGE_LENGTH_UPSELL_TOOLTIP}", + replacement: { + match: /return \i\?\i\(\):\i\(\)/, + replace: "return null" + } + } + ], + getCharCounter(text: string) { + const premiumType = (UserStore.getCurrentUser().premiumType ?? 0); + const charMax = premiumType === 2 ? 4000 : 2000; + const { length } = text; - start: async () => { - shouldShowColorEffects = settings.store.colorEffects; - position = settings.store.position; + let color = "var(--primary-330)"; + if (settings.store.colorEffects) { + const percentage = (length / charMax) * 100; + if (percentage < 50) color = "var(--text-muted)"; + else if (percentage < 75) color = "var(--yellow-330)"; + else if (percentage < 90) color = "var(--orange-330)"; + else color = "var(--red-360)"; + } - waitFor(["buttonContainer", "channelTextArea"], m => (ChannelTextAreaClasses = m)); - await updateCharCounterOnChannelChange(); - - SelectedChannelStore.addChangeListener(updateCharCounterOnChannelChange); - SelectedGuildStore.addChangeListener(updateCharCounterOnChannelChange); - }, - - stop() { - const charCounterDiv = document.querySelector(".vc-char-counter"); - if (charCounterDiv) charCounterDiv.remove(); - - SelectedChannelStore.removeChangeListener(updateCharCounterOnChannelChange); - SelectedGuildStore.removeChangeListener(updateCharCounterOnChannelChange); + return ( +
+ {length}/ + {charMax} +
+ ); } }); diff --git a/src/equicordplugins/characterCounter/style.css b/src/equicordplugins/characterCounter/style.css index 066a1b9fc..6dcdb79ff 100644 --- a/src/equicordplugins/characterCounter/style.css +++ b/src/equicordplugins/characterCounter/style.css @@ -1,22 +1,13 @@ .vc-char-counter { font-size: 12px; color: var(--text-muted); - text-align: right; position: absolute; - right: 0; + bottom: 4px; + right: 6px; pointer-events: none; - z-index: 1; - margin: 0; - padding: 6px; - background-color: var(--background-tertiary); - border-radius: var(--radius-sm); - transition: opacity 0.3s ease; -} - -.vc-char-counter.left { - text-align: left; - right: auto; - left: 0; + z-index: 2; + background: transparent; + transition: color 0.3s ease; } .vc-char-counter .vc-char-count { @@ -25,4 +16,4 @@ .vc-char-counter .vc-char-max { color: var(--primary-330); -} +} \ No newline at end of file From 4a35cf1769f2f6bc178451b5641140ef06e478b5 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 23 Aug 2025 01:57:27 +0200 Subject: [PATCH 067/881] Toolbox: fix & move to the titlebar --- src/plugins/vencordToolbox/index.tsx | 31 ++++++++++------------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/plugins/vencordToolbox/index.tsx b/src/plugins/vencordToolbox/index.tsx index af9fbc87f..1bc97cf56 100644 --- a/src/plugins/vencordToolbox/index.tsx +++ b/src/plugins/vencordToolbox/index.tsx @@ -94,7 +94,7 @@ function VencordPopoutIcon(isShown: boolean) { ); } -function VencordPopoutButton() { +function VencordPopoutButton({ buttonClass }: { buttonClass: string; }) { const buttonRef = useRef(null); const [show, setShow] = useState(false); @@ -111,7 +111,7 @@ function VencordPopoutButton() { {(_, { isShown }) => ( setShow(v => !v)} tooltip={isShown ? null : "Vencord Toolbox"} icon={() => VencordPopoutIcon(isShown)} @@ -122,33 +122,24 @@ function VencordPopoutButton() { ); } -function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) { - children.splice( - children.length - 1, 0, - - - - ); - - return <>{children}; -} - export default definePlugin({ name: "VencordToolbox", - description: "Adds a button next to the inbox button in the channel header that houses Vencord quick actions", + description: "Adds a button to the titlebar that houses Vencord quick actions", authors: [Devs.Ven, Devs.AutumnVN], patches: [ { - find: ".controlButtonWrapper,", + find: '"M9 3v18"', replacement: { - match: /(?<=toolbar:function\(.{0,100}\()\i.Fragment,/, - replace: "$self.ToolboxFragmentWrapper," + match: /focusSectionProps:"HELP".{0,20},className:(\i\.button)\}\),/, + replace: "$& $self.renderVencordPopoutButton($1)," } } ], - ToolboxFragmentWrapper: ErrorBoundary.wrap(ToolboxFragmentWrapper, { - fallback: () =>

Failed to render :(

- }) + renderVencordPopoutButton: (buttonClass: string) => ( + + + + ) }); From abe910d80d2848127e5ee1857cfccedfbdbbbf07 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 23 Aug 2025 02:02:02 +0200 Subject: [PATCH 068/881] fix not being able to dismiss Vencord Notices --- src/plugins/_api/notices.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/_api/notices.ts b/src/plugins/_api/notices.ts index 2384a10e4..3b43c31ac 100644 --- a/src/plugins/_api/notices.ts +++ b/src/plugins/_api/notices.ts @@ -32,9 +32,16 @@ export default definePlugin({ match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g, replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" }, + + // TODO: remove this compat eventually { match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/, - replace: (m, notice) => `if(${notice}?.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}` + replace: (m, notice) => `if(${notice}?.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`, + noWarn: true, + }, + { + match: /(?<=function (\i)\(\i\){)return null!=(\i)(?=.+?NOTICE_DISMISS:\1)/, + replace: (m, _, notice) => `if(${notice}?.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}` } ] } From 76a60e07e9d299953461640b3409aff13045295d Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 23 Aug 2025 02:16:32 +0200 Subject: [PATCH 069/881] fix SuperReactionTweaks --- src/plugins/superReactionTweaks/index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 22e7d6f3c..2edf99e81 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -42,8 +42,13 @@ export default definePlugin({ { find: ",BURST_REACTION_EFFECT_PLAY", replacement: { - match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=\d+?(?=\))/, - replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})` + /* + * var limit = 5 + * ... + * if (calculatePlayingCount(a,b) >= limit) return; + */ + match: /(?<=(\i)=5.+?)if\((.{0,20}?)>=\1\)return;/, + replace: "if(!$self.shouldPlayBurstReaction($2))return;" } }, { @@ -58,8 +63,7 @@ export default definePlugin({ shouldPlayBurstReaction(playingCount: number) { if (settings.store.unlimitedSuperReactionPlaying) return true; - if (settings.store.superReactionPlayingLimit === 0) return false; - if (playingCount <= settings.store.superReactionPlayingLimit) return true; + if (settings.store.superReactionPlayingLimit > playingCount) return true; return false; }, From df48710b60e267c40df632638eda9f452cd7f7a2 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:16:42 -0700 Subject: [PATCH 070/881] Fix patch. (#360) --- src/equicordplugins/questify/index.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/equicordplugins/questify/index.tsx b/src/equicordplugins/questify/index.tsx index fd5558b2c..e0a4a9646 100644 --- a/src/equicordplugins/questify/index.tsx +++ b/src/equicordplugins/questify/index.tsx @@ -992,20 +992,20 @@ export default definePlugin({ }, { // Sorts the "All Quests" tab Quest tiles. - // Also sets mobile-only Quests as desktop compatible if the setting is enabled. - find: ".ALL);return(", - replacement: { - match: /(quests:(\i).{0,100}?quests:)\i/, - replace: "$1$self.preprocessQuests($2)" - } - }, - { // Sorts the "Claimed Quests" tab Quest tiles. + // Also sets mobile-only Quests as desktop compatible if the setting is enabled. find: ".ALL)}):(", - replacement: { - match: /(claimedQuests:(\i).{0,50}?;)/, - replace: "$1$2=$self.preprocessQuests($2);" - } + group: true, + replacement: [ + { + match: /(claimedQuests:(\i).{0,50}?;)/, + replace: "$1$2=$self.preprocessQuests($2);" + }, + { + match: /(quests:(\i).{0,50}?;)/, + replace: "$1$2=$self.preprocessQuests($2);" + } + ] }, { // Whether preloading assets is enabled or not, the placeholders loading From 2541de0e929414e068b2f3cb09d05dc636dd0dc6 Mon Sep 17 00:00:00 2001 From: panbread <93918332+Panniku@users.noreply.github.com> Date: Sat, 23 Aug 2025 18:05:48 +0530 Subject: [PATCH 071/881] fix(messageLogger): add transparency? (#362) * nyaboom (#4) * revert to previous previous --- src/plugins/messageLogger/deleteStyleOverlay.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/messageLogger/deleteStyleOverlay.css b/src/plugins/messageLogger/deleteStyleOverlay.css index 9fca958b2..59e4ac666 100644 --- a/src/plugins/messageLogger/deleteStyleOverlay.css +++ b/src/plugins/messageLogger/deleteStyleOverlay.css @@ -1,3 +1,3 @@ .messagelogger-deleted { - background-color: hsl(3deg 86% 68%) !important; -} \ No newline at end of file + background-color: hsla(var(--red-430-hsl, 0 85% 61%) / 15%) !important; +} From 62ef6c62b64c365b59f4f0c7b8694abdfc154217 Mon Sep 17 00:00:00 2001 From: Mocha <74067110+M0chaCat@users.noreply.github.com> Date: Sat, 23 Aug 2025 08:41:58 -0400 Subject: [PATCH 072/881] Animalese plugin! (#361) * Animalese plugin! * rm readme and license :3 * oopsies :3c * lint * import devs :3 * Rewind time * Update index.ts --------- Co-authored-by: mocha Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/animalese/README.md | 21 +++ src/equicordplugins/animalese/index.ts | 209 ++++++++++++++++++++++++ src/utils/constants.ts | 8 + 3 files changed, 238 insertions(+) create mode 100644 src/equicordplugins/animalese/README.md create mode 100644 src/equicordplugins/animalese/index.ts diff --git a/src/equicordplugins/animalese/README.md b/src/equicordplugins/animalese/README.md new file mode 100644 index 000000000..02a59c96b --- /dev/null +++ b/src/equicordplugins/animalese/README.md @@ -0,0 +1,21 @@ +## vencord-animalese +animalese yapping in your discord client + +> [!WARNING] +> don't go to Vencord support if you can't install this. + +### demo + +https://github.com/user-attachments/assets/3f1fa925-bbd2-451d-b63e-ea14dbe4be44 + +### settings + +##### settings used in the video +![CleanShot 2024-12-26 at 22 11 49@2x](https://github.com/user-attachments/assets/35895fcc-d703-42cb-b0ef-bdef39ab15be) + + +you can change the settings in the settings but otherwise, enjoy the yap + +### credits + +- [sounds: animalese-generator by equalo-official](https://github.com/equalo-official/animalese-generator/tree/master) diff --git a/src/equicordplugins/animalese/index.ts b/src/equicordplugins/animalese/index.ts new file mode 100644 index 000000000..2ec6ca338 --- /dev/null +++ b/src/equicordplugins/animalese/index.ts @@ -0,0 +1,209 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { FluxDispatcher } from "@webpack/common"; + +const settings = definePluginSettings({ + volume: { + type: OptionType.SLIDER, + description: "Volume of the animalese sound", + default: 0.5, + markers: [0, 0.25, 0.5, 0.75, 1], + }, + speed: { + type: OptionType.SLIDER, + description: "Speed of the animalese sound", + default: 1, + markers: [0.5, 0.75, 1, 1.25, 1.5], + }, +}); + +let audioContext: AudioContext | null = null; +let currentChannelId: string | null = null; + +// better than my old hardcoded garbage +const highSounds = Array.from( + { length: 30 }, + (_, i) => `sound${String(i + 1).padStart(2, "0")}.wav` +); +const soundBuffers: Record = {}; + +// todo implement other pitch sounds but theoretically plugging this in from said repo should work, right? +const BASE_URL_HIGH = + "https://github.com/Equicord/Equibored/raw/refs/heads/main/sounds/animalese/high"; + +async function initSoundBuffers() { + if (!audioContext) audioContext = new AudioContext(); + for (const file of highSounds) { + const nameWithoutExt = file.replace(".wav", ""); + soundBuffers[nameWithoutExt] = await loadSound( + `${BASE_URL_HIGH}/${file}` + ); + } +} + +async function loadSound(url: string): Promise { + if (!audioContext) audioContext = new AudioContext(); + const response = await fetch(url); + if (!response.ok) throw new Error("Network response was not OK"); + const arrayBuffer = await response.arrayBuffer(); + return audioContext.decodeAudioData(arrayBuffer); +} + +async function generateAnimalese(text: string): Promise { + if (!audioContext) audioContext = new AudioContext(); + + const soundIndices: string[] = []; + const text_lower = text.toLowerCase(); + + for (let i = 0; i < text_lower.length; i++) { + const char = text_lower[i]; + if (char === "s" && text_lower[i + 1] === "h") { + soundIndices.push("sound27"); + i++; + } else if (char === "t" && text_lower[i + 1] === "h") { + soundIndices.push("sound28"); + i++; + } else if ( + char === "h" && + (text_lower[i - 1] === "s" || text_lower[i - 1] === "t") + ) { + continue; + } else if (char === "," || char === "?") { + soundIndices.push("sound30"); + } else if (char === text_lower[i - 1]) { + continue; + } else if (char.match(/[a-z]/)) { + const index = char.charCodeAt(0) - 96; + soundIndices.push(`sound${String(index).padStart(2, "0")}`); + } + } + + // No valid characters? Just return null + if (soundIndices.length === 0) { + return null; + } + + const totalDuration = soundIndices.length * 0.1; + const frameCount = Math.max(1, Math.floor(audioContext.sampleRate * totalDuration)); + + const outputBuffer = audioContext.createBuffer( + 1, + frameCount, + audioContext.sampleRate + ); + const outputData = outputBuffer.getChannelData(0); + + let offset = 0; + for (let i = 0; i < soundIndices.length; i++) { + const soundIndex = soundIndices[i]; + const buffer = soundBuffers[soundIndex]; + if (!buffer) continue; + + const variation = 0.15; + let pitchShift = 2.8 + Math.random() * variation; + + const isQuestion = text_lower.endsWith("?"); + if (isQuestion && i >= soundIndices.length * 0.8) { + const progress = + (i - soundIndices.length * 0.8) / (soundIndices.length * 0.2); + pitchShift += progress * 0.1 + 0.1; + } + + const inputData = buffer.getChannelData(0); + const inputLength = inputData.length; + const outputLength = Math.floor(inputLength / pitchShift); + + for (let j = 0; j < outputLength; j++) { + const inputIndex = Math.floor(j * pitchShift); + if (inputIndex < inputLength && offset + j < outputData.length) { + outputData[offset + j] = inputData[inputIndex]; + } + } + offset += outputLength; + } + + return outputBuffer; +} + + +async function playSound(buffer: AudioBuffer, volume: number) { + if (!audioContext) audioContext = new AudioContext(); + const source = audioContext.createBufferSource(); + const gainNode = audioContext.createGain(); + + source.buffer = buffer; + gainNode.gain.value = volume; + + source.connect(gainNode); + gainNode.connect(audioContext.destination); + + source.start(); +} + +export default definePlugin({ + name: "Animalese", + description: "Plays animalese (they yap a lot) on message sent", + authors: [EquicordDevs.ryanamay, EquicordDevs.Mocha], + settings, + + start() { + // Only subscribe once! + const init = async () => { + if (!audioContext) { + audioContext = new AudioContext(); + await initSoundBuffers(); + } + document.removeEventListener("click", init); + }; + document.addEventListener("click", init); + + this.channelSelectListener = ({ channelId }) => { + currentChannelId = channelId; + }; + FluxDispatcher.subscribe("CHANNEL_SELECT", this.channelSelectListener); + + this.messageCreateListener = async ({ optimistic, type, message }) => { + // DO NOT REMOVE THIS OR EELSE IT WILL BE YAP CENTRAL + if (optimistic || type !== "MESSAGE_CREATE") return; + + if ( + !message?.content || + message.author?.bot || + message.channel_id !== currentChannelId + ) { + return; + } + + try { + const buffer = await generateAnimalese(message.content); + await playSound(buffer, settings.store.volume); + } catch (err) { + console.error("[Animalese]", err); + } + }; + FluxDispatcher.subscribe("MESSAGE_CREATE", this.messageCreateListener); + }, + + stop() { + FluxDispatcher.unsubscribe( + "CHANNEL_SELECT", + this.channelSelectListener + ); + FluxDispatcher.unsubscribe( + "MESSAGE_CREATE", + this.messageCreateListener + ); + if (audioContext) { + audioContext.close(); + audioContext = null; + } + currentChannelId = null; + }, +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d13e44f1c..4b111e8d0 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1129,6 +1129,14 @@ export const EquicordDevs = Object.freeze({ name: "DiabeloDEV", id: 1231342375465390100n }, + ryanamay: { + name: "ryanamay", + id: 1262793452236570667n + }, + Mocha: { + name: "Mocha", + id: 808802000224518264n + }, } satisfies Record); // iife so #__PURE__ works correctly From 14a1c90c46edf62982e122ee0896bc3b86b63a85 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 23 Aug 2025 08:44:09 -0400 Subject: [PATCH 073/881] Fix Lint --- src/equicordplugins/animalese/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/animalese/index.ts b/src/equicordplugins/animalese/index.ts index 2ec6ca338..8bb52ceb4 100644 --- a/src/equicordplugins/animalese/index.ts +++ b/src/equicordplugins/animalese/index.ts @@ -183,7 +183,7 @@ export default definePlugin({ try { const buffer = await generateAnimalese(message.content); - await playSound(buffer, settings.store.volume); + if (buffer) await playSound(buffer, settings.store.volume); } catch (err) { console.error("[Animalese]", err); } From 2325fd97231972c0177485b37b4b864d9d417b73 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:30:58 -0300 Subject: [PATCH 074/881] Fix IgnoreActivities --- src/plugins/_api/notices.ts | 2 +- src/plugins/ignoreActivities/index.tsx | 22 ++++++++++++++---- src/plugins/superReactionTweaks/index.ts | 29 ++++++++++++++++-------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/plugins/_api/notices.ts b/src/plugins/_api/notices.ts index 3b43c31ac..43ef2acc0 100644 --- a/src/plugins/_api/notices.ts +++ b/src/plugins/_api/notices.ts @@ -33,7 +33,7 @@ export default definePlugin({ replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" }, - // TODO: remove this compat eventually + // FIXME(Bundler minifier change related): Remove the non used compability once enough time has passed { match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/, replace: (m, notice) => `if(${notice}?.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`, diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 88906c385..014c1abd9 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -56,15 +56,15 @@ function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string ); } -const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable Activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill); -const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill); +const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill); +const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill); function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) { const s = settings.use(["ignoredActivities"]); const { ignoredActivities } = s; if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)"); - return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)"); + return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--interactive-normal)"); } function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity) { @@ -253,15 +253,29 @@ export default definePlugin({ replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));` } }, + + // FIXME(Bundler minifier change related): Remove the non used compability once enough time has passed { find: "#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}", replacement: { // let { ... nowPlaying: a = !1 ... // let { overlay: b ... } = Props match: /#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}.+?}\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)/, - replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` + replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`, + noWarn: true, } }, + { + find: "#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}", + replacement: { + // let { ... nowPlaying: a = !1 ... + // let { overlay: b ... } = Props ... + // ToggleOverLayButton(), nowPlaying && ... RemoveGameButton() + match: /\.gameNameLastPlayed.+?,\i\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)(?=\1&&)/, + replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`, + } + }, + // Activities from the apps launcher in the bottom right of the chat bar { find: ".promotedLabelWrapperNonBanner,children", diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 2edf99e81..a8827903f 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -41,21 +41,30 @@ export default definePlugin({ patches: [ { find: ",BURST_REACTION_EFFECT_PLAY", - replacement: { - /* - * var limit = 5 - * ... - * if (calculatePlayingCount(a,b) >= limit) return; - */ - match: /(?<=(\i)=5.+?)if\((.{0,20}?)>=\1\)return;/, - replace: "if(!$self.shouldPlayBurstReaction($2))return;" - } + replacement: [ + // FIXME(Bundler minifier change related): Remove the non used compability once enough time has passed + { + // if (inlinedCalculatePlayingCount(a,b) >= limit) return; + match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=5+?(?=\))/, + replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})`, + noWarn: true, + }, + { + /* + * var limit = 5 + * ... + * if (calculatePlayingCount(a,b) >= limit) return; + */ + match: /((\i)=5.+?)if\((.{0,20}?)>=\2\)return;/, + replace: (_, rest, playingCount) => `${rest}if(!$self.shouldPlayBurstReaction(${playingCount}))return;` + } + ] }, { find: ".EMOJI_PICKER_CONSTANTS_EMOJI_CONTAINER_PADDING_HORIZONTAL)", replacement: { match: /(openPopoutType:void 0(?=.+?isBurstReaction:(\i).+?(\i===\i\.\i.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/, - replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` + replace: (_, rest, _isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` } } ], From c4471aa6f57821297eefba98064d5022c982e080 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 23 Aug 2025 13:56:18 -0400 Subject: [PATCH 075/881] Fix Animalese --- src/equicordplugins/animalese/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/animalese/index.ts b/src/equicordplugins/animalese/index.ts index 8bb52ceb4..9b7bdd449 100644 --- a/src/equicordplugins/animalese/index.ts +++ b/src/equicordplugins/animalese/index.ts @@ -36,7 +36,7 @@ const soundBuffers: Record = {}; // todo implement other pitch sounds but theoretically plugging this in from said repo should work, right? const BASE_URL_HIGH = - "https://github.com/Equicord/Equibored/raw/refs/heads/main/sounds/animalese/high"; + "https://github.com/Equicord/Equibored/raw/main/sounds/animalese/high"; async function initSoundBuffers() { if (!audioContext) audioContext = new AudioContext(); From a970296c90804b8c9033e74b238b0f438444ee52 Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Sat, 23 Aug 2025 14:38:58 -0700 Subject: [PATCH 076/881] MessageLogger: Only show diffs option if enabled + switch to definePluginSettings. (#365) * Only show diffs option if enabled + switch to definePluginSettings. * Only show diffs option if enabled + switch to definePluginSettings. --- src/plugins/messageLogger/index.tsx | 154 ++++++++++++++-------------- 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index 57ab3f10b..6abc73432 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -11,7 +11,7 @@ import { NavContextMenuPatchCallback, } from "@api/ContextMenu"; import { updateMessage } from "@api/MessageUpdater"; -import { Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, VC_SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants"; @@ -41,7 +41,7 @@ const styles = findByPropsLazy("edited", "communicationDisabled", "isSystemMessa const disabledDiffMessages = new Set(); function addDeleteStyle() { - if (Settings.plugins.MessageLogger.deleteStyle === "text") { + if (settings.store.deleteStyle === "text") { enableStyle(textStyle); disableStyle(overlayStyle); } else { @@ -80,8 +80,9 @@ const patchMessageContextMenu: NavContextMenuPatchCallback = ( ); } - // toggle per-message diff rendering when the message has an edit history - if (editHistory?.length) { + // toggle per-message diff rendering when the message + // has an edit history and the setting is enabled + if (editHistory?.length && settings.store.showEditDiffs) { const isDisabled = disabledDiffMessages.has(id); children.push( { if (isDisabled) disabledDiffMessages.delete(id); else disabledDiffMessages.add(id); - // also toggle a CSS class on the message element for an immediate visual effect (basically shows things work lol) + // Also toggle a CSS class on the message element for immediate visual effect const domElement = document.getElementById(`chat-messages-${channel_id}-${id}`); domElement?.classList.toggle("messagelogger-diff-disabled", disabledDiffMessages.has(id)); - // force a re-render without mutating message fields + // Force a re-render without mutating message fields updateMessage(channel_id, id); }} />, @@ -186,7 +187,7 @@ function renderDiffParts(diffParts: DiffPart[]) { export function parseEditContent(content: string, message: Message, previousContent?: string) { const perMessageDiffEnabled = !disabledDiffMessages.has(message.id); - if (previousContent && content !== previousContent && Settings.plugins.MessageLogger.showEditDiffs && perMessageDiffEnabled) { + if (previousContent && content !== previousContent && settings.store.showEditDiffs && perMessageDiffEnabled) { const diffParts = createMessageDiff(content, previousContent); return renderDiffParts(diffParts); } @@ -202,11 +203,76 @@ export function parseEditContent(content: string, message: Message, previousCont }); } +const settings = definePluginSettings({ + deleteStyle: { + type: OptionType.SELECT, + description: "The style of deleted messages", + default: "text", + options: [ + { label: "Red text", value: "text", default: true }, + { label: "Red overlay", value: "overlay" }, + ], + onChange: () => addDeleteStyle(), + }, + logDeletes: { + type: OptionType.BOOLEAN, + description: "Whether to log deleted messages", + default: true, + }, + collapseDeleted: { + type: OptionType.BOOLEAN, + description: "Whether to collapse deleted messages, similar to blocked messages", + default: false, + restartNeeded: true, + }, + logEdits: { + type: OptionType.BOOLEAN, + description: "Whether to log edited messages", + default: true, + }, + inlineEdits: { + type: OptionType.BOOLEAN, + description: "Whether to display edit history as part of message content", + default: true, + }, + ignoreBots: { + type: OptionType.BOOLEAN, + description: "Whether to ignore messages by bots", + default: false, + }, + ignoreSelf: { + type: OptionType.BOOLEAN, + description: "Whether to ignore messages by yourself", + default: false, + }, + ignoreUsers: { + type: OptionType.STRING, + description: "Comma-separated list of user IDs to ignore", + default: "", + }, + ignoreChannels: { + type: OptionType.STRING, + description: "Comma-separated list of channel IDs to ignore", + default: "", + }, + ignoreGuilds: { + type: OptionType.STRING, + description: "Comma-separated list of guild IDs to ignore", + default: "", + }, + showEditDiffs: { + type: OptionType.BOOLEAN, + description: "Show visual differences between edited message versions", + default: false, + }, +}); + export default definePlugin({ name: "MessageLogger", description: "Temporarily logs deleted and edited messages.", authors: [Devs.rushii, Devs.Ven, Devs.AutumnVN, Devs.Nickyux, Devs.Kyuuhachi], dependencies: ["MessageUpdaterAPI"], + settings, contextMenus: { message: patchMessageContextMenu, @@ -236,7 +302,9 @@ export default definePlugin({ oldMsg === newMsg, ); - return Settings.plugins.MessageLogger.inlineEdits && ( + const { showEditDiffs, inlineEdits } = settings.use(["showEditDiffs", "inlineEdits"]); + + return inlineEdits && ( {message.editHistory?.map((edit, idx) => { const nextContent = idx === (message.editHistory?.length ?? 0) - 1 @@ -267,70 +335,6 @@ export default definePlugin({ }; }, - options: { - deleteStyle: { - type: OptionType.SELECT, - description: "The style of deleted messages", - default: "text", - options: [ - { label: "Red text", value: "text", default: true }, - { label: "Red overlay", value: "overlay" }, - ], - onChange: () => addDeleteStyle(), - }, - logDeletes: { - type: OptionType.BOOLEAN, - description: "Whether to log deleted messages", - default: true, - }, - collapseDeleted: { - type: OptionType.BOOLEAN, - description: "Whether to collapse deleted messages, similar to blocked messages", - default: false, - restartNeeded: true, - }, - logEdits: { - type: OptionType.BOOLEAN, - description: "Whether to log edited messages", - default: true, - }, - inlineEdits: { - type: OptionType.BOOLEAN, - description: "Whether to display edit history as part of message content", - default: true, - }, - ignoreBots: { - type: OptionType.BOOLEAN, - description: "Whether to ignore messages by bots", - default: false, - }, - ignoreSelf: { - type: OptionType.BOOLEAN, - description: "Whether to ignore messages by yourself", - default: false, - }, - ignoreUsers: { - type: OptionType.STRING, - description: "Comma-separated list of user IDs to ignore", - default: "", - }, - ignoreChannels: { - type: OptionType.STRING, - description: "Comma-separated list of channel IDs to ignore", - default: "", - }, - ignoreGuilds: { - type: OptionType.STRING, - description: "Comma-separated list of guild IDs to ignore", - default: "", - }, - showEditDiffs: { - type: OptionType.BOOLEAN, - description: "Show visual differences between edited message versions", - default: false, - }, - }, - handleDelete( cache: any, data: { ids: string[]; id: string; mlDeleted?: boolean; }, @@ -381,7 +385,7 @@ export default definePlugin({ ignoreGuilds, logEdits, logDeletes, - } = Settings.plugins.MessageLogger; + } = settings.store; const myId = UserStore.getCurrentUser().id; return ( @@ -616,7 +620,7 @@ export default definePlugin({ match: /if\((\i)\.blocked\)return \i\.\i\.MESSAGE_GROUP_BLOCKED;/, replace: '$&else if($1.deleted) return"MESSAGE_GROUP_DELETED";', }, - predicate: () => Settings.plugins.MessageLogger.collapseDeleted, + predicate: () => settings.store.collapseDeleted, }, { // Message group rendering @@ -631,7 +635,7 @@ export default definePlugin({ replace: '$&$1.type==="MESSAGE_GROUP_DELETED"?$self.DELETED_MESSAGE_COUNT:', }, ], - predicate: () => Settings.plugins.MessageLogger.collapseDeleted, + predicate: () => settings.store.collapseDeleted, }, ], }); From fb7115dd511379f55f4395684344e701df811d5d Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 24 Aug 2025 09:38:08 -0400 Subject: [PATCH 077/881] Some Bug Fixes --- .../bannersEverywhere/index.tsx | 17 ++--------------- .../spotify/lyrics/providers/store.ts | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/equicordplugins/bannersEverywhere/index.tsx b/src/equicordplugins/bannersEverywhere/index.tsx index 7b30c7e59..b6f5b8984 100644 --- a/src/equicordplugins/bannersEverywhere/index.tsx +++ b/src/equicordplugins/bannersEverywhere/index.tsx @@ -116,19 +116,7 @@ export default definePlugin({ ); }, - async checkImageExists(url: string): Promise { - return new Promise(resolve => { - const img = new Image(); - img.onload = () => resolve(true); - img.onerror = () => resolve(false); - img.src = url; - }); - }, - async gifToPng(url: string): Promise { - const exists = await this.checkImageExists(url); - if (!exists) return ""; - return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; @@ -139,13 +127,12 @@ export default definePlugin({ const ctx = canvas.getContext("2d"); if (ctx) { ctx.drawImage(img, 0, 0); - const pngDataUrl = canvas.toDataURL("image/png"); - resolve(pngDataUrl); + resolve(canvas.toDataURL("image/png")); } else { reject(new Error("Failed to get canvas context.")); } }; - img.onerror = err => reject(err); + img.onerror = () => resolve(""); img.src = url; }); }, diff --git a/src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts b/src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts index 5f9049891..475820eba 100644 --- a/src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts +++ b/src/equicordplugins/musicControls/spotify/lyrics/providers/store.ts @@ -74,15 +74,28 @@ export const SpotifyLrcStore = proxyLazyWebpack(() => { } if (provider === Provider.Translated || provider === Provider.Romanized) { - const originalLyrics = currentInfo?.lyricsVersions[settings.store.LyricsProvider] || providers - .map(p => currentInfo?.lyricsVersions[p]) - .find(Boolean); + const originalLyrics = currentInfo?.lyricsVersions[settings.store.LyricsProvider] || + providers.map(p => currentInfo?.lyricsVersions[p]).find(Boolean); if (!originalLyrics || !currentInfo) { showNotif("No lyrics", `No lyrics to ${provider === Provider.Translated ? "translate" : "romanize"}`); return; } + const lyricsCheckText = originalLyrics.map(line => line.text).join(" "); + + if (provider === Provider.Romanized && !/[^\u0000-\u007F]/.test(lyricsCheckText)) { + lyricsInfo = { + ...currentInfo, + useLyric: settings.store.LyricsProvider, + lyricsVersions: { + ...currentInfo.lyricsVersions, + }, + }; + store.emitChange(); + return; + } + const fetchResult = await lyricsAlternativeFetchers[provider](originalLyrics); if (!fetchResult) { From 4f04250b63442d71dc1976f07ebe4b62afa5b363 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:53:09 -0400 Subject: [PATCH 078/881] Online Themes Custom Names --- src/api/Settings.ts | 2 + src/components/ThemeSettings/ThemesTab.tsx | 63 ++++++--- src/components/settings/AddonCard.css | 7 + src/components/settings/EditableText.tsx | 55 ++++++++ src/components/settings/OnlineThemeCard.tsx | 134 ++++++++++++++++++++ src/utils/themes/bd/index.ts | 2 + 6 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 src/components/settings/EditableText.tsx create mode 100644 src/components/settings/OnlineThemeCard.tsx diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 6ff1ccc10..ddf326b01 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -35,6 +35,7 @@ export interface Settings { eagerPatches: boolean; enabledThemes: string[]; enabledThemeLinks: string[]; + themeNames: Record; enableReactDevtools: boolean; themeLinks: string[]; frameless: boolean; @@ -94,6 +95,7 @@ const DefaultSettings: Settings = { eagerPatches: IS_REPORTER, enabledThemes: [], enabledThemeLinks: [], + themeNames: {}, enableReactDevtools: false, frameless: false, transparent: false, diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index 901a3e930..b9a821b75 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -25,6 +25,7 @@ import { Flex } from "@components/Flex"; import { CogWheel, DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PluginIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { Link } from "@components/Link"; import { AddonCard, openPluginModal, QuickAction, QuickActionCard, SettingsTab, wrapTab } from "@components/settings"; +import { OnlineThemeCard } from "@components/settings/OnlineThemeCard"; import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; @@ -85,6 +86,7 @@ interface OtherThemeCardProps { onChange: (enabled: boolean) => void; onDelete: () => void; showDeleteButton?: boolean; + onEditName?: (newName: string) => void; } interface UserCSSCardProps { @@ -148,9 +150,10 @@ function UserCSSThemeCard({ theme, enabled, onChange, onDelete, onSettingsReset ); } -function OtherThemeCard({ theme, enabled, onChange, onDelete, showDeleteButton }: OtherThemeCardProps) { +function OtherThemeCard({ theme, enabled, onChange, onDelete, showDeleteButton, onEditName }: OtherThemeCardProps) { return ( - { e.preventDefault(); - theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite")); + theme.invite != null && + openInviteModal(theme.invite).catch(() => + showToast("Invalid or expired invite") + ); }} > Discord Server @@ -180,6 +186,8 @@ function OtherThemeCard({ theme, enabled, onChange, onDelete, showDeleteButton } )} } + + onEditName={onEditName} /> ); } @@ -190,7 +198,7 @@ enum ThemeTab { } function ThemesTab() { - const settings = useSettings(["themeLinks", "enabledThemeLinks", "enabledThemes"]); + const settings = useSettings(["themeLinks", "enabledThemeLinks", "enabledThemes", "themeNames"]); const fileInputRef = useRef(null); const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); @@ -198,6 +206,9 @@ function ThemesTab() { const [themeLinkValid, setThemeLinkValid] = useState(false); const [userThemes, setUserThemes] = useState(null); const [onlineThemes, setOnlineThemes] = useState<(UserThemeHeader & { link: string; })[] | null>(null); + const [themeNames, setThemeNames] = useState>(() => { + return settings.themeNames ?? {}; + }); const [themeDir, , themeDirPending] = useAwaiter(VencordNative.themes.getThemesDir); useEffect(() => { @@ -444,6 +455,12 @@ function ThemesTab() { } function OnlineThemes() { + + const themes = (onlineThemes ?? []).map(theme => ({ + ...theme, + customName: themeNames[theme.link] ?? null, + })); + return ( <>; @@ -457,26 +474,36 @@ function ThemesTab() {
- {onlineThemes?.map(rawLink => { + {themes.map(theme => { const { label, link } = (() => { - const match = /^@(light|dark) (.*)/.exec(rawLink.link); - if (!match) return { label: rawLink, link: rawLink }; + const match = /^@(light|dark) (.*)/.exec(theme.link); + if (!match) return { label: theme, link: theme }; const [, mode, link] = match; return { label: `[${mode} mode only] ${link}`, link }; })(); - return onThemeLinkEnabledChange(rawLink.link, enabled)} - onDelete={async () => { - onThemeLinkEnabledChange(rawLink.link, false); - deleteThemeLink(rawLink.link); - }} - showDeleteButton - theme={rawLink} - />; + return ( + onThemeLinkEnabledChange(theme.link, enabled)} + onDelete={async () => { + onThemeLinkEnabledChange(theme.link, false); + deleteThemeLink(theme.link); + }} + showDeleteButton + onEditName={newName => { + const updatedNames = { ...themeNames, [theme.link]: newName }; + setThemeNames(updatedNames); + settings.themeNames = { + ...settings.themeNames, + [theme.link]: newName, + }; + }} + /> + ); })}
diff --git a/src/components/settings/AddonCard.css b/src/components/settings/AddonCard.css index d5e3b4d6b..ffc083c4c 100644 --- a/src/components/settings/AddonCard.css +++ b/src/components/settings/AddonCard.css @@ -94,4 +94,11 @@ .vc-addon-title:hover { overflow: visible; animation: vc-addon-title var(--duration) linear infinite; +} + +.vc-addon-editable { + border: none; + background: transparent; + color: inherit; + font: inherit; } \ No newline at end of file diff --git a/src/components/settings/EditableText.tsx b/src/components/settings/EditableText.tsx new file mode 100644 index 000000000..dbadfbdcd --- /dev/null +++ b/src/components/settings/EditableText.tsx @@ -0,0 +1,55 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { React, Text, useEffect, useRef, useState } from "@webpack/common"; + +interface EditableTextProps { + value: string; + onChange: (newValue: string) => void; + className?: string; +} + +export function EditableText({ value, onChange, className }: EditableTextProps) { + const [editing, setEditing] = useState(false); + const [tempValue, setTempValue] = useState(value); + const inputRef = useRef(null); + + useEffect(() => { + if (editing && inputRef.current) { + inputRef.current.focus(); + } + }, [editing]); + + return editing ? ( + setTempValue(e.target.value)} + onBlur={() => { + setEditing(false); + onChange(tempValue); + }} + onKeyDown={e => { + if (e.key === "Enter") { + setEditing(false); + onChange(tempValue); + } else if (e.key === "Escape") { + setEditing(false); + setTempValue(value); + } + }} + /> + ) : ( + setEditing(true)} + style={{ cursor: "pointer" }} + > + {value} + + ); +} diff --git a/src/components/settings/OnlineThemeCard.tsx b/src/components/settings/OnlineThemeCard.tsx new file mode 100644 index 000000000..fce5d3cc9 --- /dev/null +++ b/src/components/settings/OnlineThemeCard.tsx @@ -0,0 +1,134 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 . +*/ + +import "./AddonCard.css"; + +import { classNameFactory } from "@api/Styles"; +import { AddonBadge } from "@components/settings/PluginBadge"; +import { Switch } from "@components/settings/Switch"; +import { Text, useRef } from "@webpack/common"; +import type { MouseEventHandler, ReactNode } from "react"; + +import { EditableText } from "./EditableText"; +import { useTruncatedText } from "./tabs/plugins/components/truncateText"; + +const cl = classNameFactory("vc-addon-"); + +interface Props { + name: ReactNode; + description: ReactNode; + enabled: boolean; + setEnabled: (enabled: boolean) => void; + disabled?: boolean; + isNew?: boolean; + onMouseEnter?: MouseEventHandler; + onMouseLeave?: MouseEventHandler; + + infoButton?: ReactNode; + footer?: ReactNode; + author?: ReactNode; + + customName?: string; + customDescription?: string; + onEditName?: (newName: string) => void; + onEditDescription?: (newDescription: string) => void; +} + +export function OnlineThemeCard({ + disabled, + isNew, + name, + infoButton, + footer, + author, + enabled, + setEnabled, + description, + onMouseEnter, + onMouseLeave, + customName, + onEditName, +}: Props) { + const titleRef = useRef(null); + const titleContainerRef = useRef(null); + const { truncated, containerRef } = useTruncatedText(description ? description.toString() : ""); + + return ( +
+
+
+ +
+
{ + 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`); + }} + > + {onEditName ? ( + + ) : ( + customName || name + )} +
+
+ {isNew && } +
+ + {!!author && ( + + {author} + + )} +
+ + {infoButton} + + +
+ +
+ {truncated} +
+ + {footer} +
+ ); +} diff --git a/src/utils/themes/bd/index.ts b/src/utils/themes/bd/index.ts index 0751663f0..95a2a7595 100644 --- a/src/utils/themes/bd/index.ts +++ b/src/utils/themes/bd/index.ts @@ -31,6 +31,8 @@ export interface UserThemeHeader { source?: string; website?: string; invite?: string; + + customName?: string; } function makeHeader(fileName: string, opts: Partial = {}): UserThemeHeader { From 87d28a4a85857fd32b98147ef66e85e105a3248a Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:06:08 -0400 Subject: [PATCH 079/881] Fix Lint --- src/components/ThemeSettings/ThemesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ThemeSettings/ThemesTab.tsx b/src/components/ThemeSettings/ThemesTab.tsx index b9a821b75..16262aafc 100644 --- a/src/components/ThemeSettings/ThemesTab.tsx +++ b/src/components/ThemeSettings/ThemesTab.tsx @@ -198,7 +198,7 @@ enum ThemeTab { } function ThemesTab() { - const settings = useSettings(["themeLinks", "enabledThemeLinks", "enabledThemes", "themeNames"]); + const settings = useSettings(["themeLinks", "enabledThemeLinks", "enabledThemes"]); const fileInputRef = useRef(null); const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL); From 53e7bd12df7e63eef857942d889b56d8b729da12 Mon Sep 17 00:00:00 2001 From: Mocha <74067110+M0chaCat@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:45:22 -0400 Subject: [PATCH 080/881] Animalese Updates (#366) * More settings, better description, dont yap URLs * Lint * i do NOT remember changing this * Update index.ts * pitch + oops * oops :3 * Edits * Update index.ts * Update index.ts --------- Co-authored-by: mocha Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/animalese/index.ts | 138 +++++++++++++++---------- 1 file changed, 86 insertions(+), 52 deletions(-) diff --git a/src/equicordplugins/animalese/index.ts b/src/equicordplugins/animalese/index.ts index 9b7bdd449..265c2e0d6 100644 --- a/src/equicordplugins/animalese/index.ts +++ b/src/equicordplugins/animalese/index.ts @@ -7,14 +7,14 @@ import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { FluxDispatcher } from "@webpack/common"; +import { SelectedChannelStore, UserStore } from "@webpack/common"; const settings = definePluginSettings({ volume: { type: OptionType.SLIDER, description: "Volume of the animalese sound", default: 0.5, - markers: [0, 0.25, 0.5, 0.75, 1], + markers: [0, 0.1, 0.25, 0.5, 0.6, 0.75, 1], }, speed: { type: OptionType.SLIDER, @@ -22,10 +22,48 @@ const settings = definePluginSettings({ default: 1, markers: [0.5, 0.75, 1, 1.25, 1.5], }, + pitch: { + type: OptionType.SLIDER, + description: "Pitch multiplier", + default: 1, + markers: [0.75, 0.8, 0.85, 1, 1.15, 1.25, 1.35, 1.5], + }, + messageLengthLimit: { + type: OptionType.NUMBER, + description: "Maximum length of message to process", + default: 50, + }, + processOwnMessages: { + type: OptionType.BOOLEAN, + description: "Enable to yap your own messages too", + default: true, + }, + soundQuality: { + type: OptionType.SELECT, + description: "Quality of sound to use", + options: [ + { + label: "High", + value: "high", + default: true + }, + { + label: "Medium", + value: "med" + }, + { + label: "Low", + value: "low" + }, + { + label: "Lowest", + value: "low" + } + ] + } }); let audioContext: AudioContext | null = null; -let currentChannelId: string | null = null; // better than my old hardcoded garbage const highSounds = Array.from( @@ -34,16 +72,15 @@ const highSounds = Array.from( ); const soundBuffers: Record = {}; -// todo implement other pitch sounds but theoretically plugging this in from said repo should work, right? -const BASE_URL_HIGH = - "https://github.com/Equicord/Equibored/raw/main/sounds/animalese/high"; +const BASE_URL_HIGH = "https://raw.githubusercontent.com/Equicord/Equibored/main/sounds/animalese"; async function initSoundBuffers() { if (!audioContext) audioContext = new AudioContext(); + const quality = settings.store.soundQuality; for (const file of highSounds) { const nameWithoutExt = file.replace(".wav", ""); soundBuffers[nameWithoutExt] = await loadSound( - `${BASE_URL_HIGH}/${file}` + `${BASE_URL_HIGH}/${quality}/${file}` ); } } @@ -101,13 +138,15 @@ async function generateAnimalese(text: string): Promise { const outputData = outputBuffer.getChannelData(0); let offset = 0; + const baseLetterDuration = audioContext.sampleRate * (0.09 / settings.store.speed); + // ~90ms per letter at 1x speed (tweakable!) + for (let i = 0; i < soundIndices.length; i++) { - const soundIndex = soundIndices[i]; - const buffer = soundBuffers[soundIndex]; + const buffer = soundBuffers[soundIndices[i]]; if (!buffer) continue; const variation = 0.15; - let pitchShift = 2.8 + Math.random() * variation; + let pitchShift = (2.8 * settings.store.pitch) + (Math.random() * variation); const isQuestion = text_lower.endsWith("?"); if (isQuestion && i >= soundIndices.length * 0.8) { @@ -120,25 +159,29 @@ async function generateAnimalese(text: string): Promise { const inputLength = inputData.length; const outputLength = Math.floor(inputLength / pitchShift); + // copy sound into the slot for (let j = 0; j < outputLength; j++) { const inputIndex = Math.floor(j * pitchShift); - if (inputIndex < inputLength && offset + j < outputData.length) { - outputData[offset + j] = inputData[inputIndex]; + const targetIndex = offset + j; + if (inputIndex < inputLength && targetIndex < outputData.length) { + outputData[targetIndex] = inputData[inputIndex]; } } - offset += outputLength; + + // instead of stacking lengths, jump forward a *fixed slot* per character + offset += Math.floor(baseLetterDuration); } return outputBuffer; } - async function playSound(buffer: AudioBuffer, volume: number) { if (!audioContext) audioContext = new AudioContext(); const source = audioContext.createBufferSource(); const gainNode = audioContext.createGain(); source.buffer = buffer; + source.playbackRate.value = settings.store.pitch; gainNode.gain.value = volume; source.connect(gainNode); @@ -149,10 +192,36 @@ async function playSound(buffer: AudioBuffer, volume: number) { export default definePlugin({ name: "Animalese", - description: "Plays animalese (they yap a lot) on message sent", + description: "Plays animal crossing animalese for every message sent (they yap a lot)", authors: [EquicordDevs.ryanamay, EquicordDevs.Mocha], settings, + flux: { + async MESSAGE_CREATE({ optimistic, type, message, channelId }) { + if (optimistic || type !== "MESSAGE_CREATE") return; + if (message.state === "SENDING") return; + if (!message.content || message.author?.bot || channelId !== SelectedChannelStore.getChannelId()) return; + + const urlPattern = /https?:\/\/[^\s]+/; + const maxLength = settings.store.messageLengthLimit || 100; + const processOwnMessages = settings.store.processOwnMessages ?? true; + + if ( + urlPattern.test(message.content) + || message.content.length > maxLength + || !processOwnMessages + && String(message.author.id) === String(UserStore.getCurrentUser().id) + ) return; + + try { + const buffer = await generateAnimalese(message.content); + if (buffer) await playSound(buffer, settings.store.volume); + } catch (err) { + console.error("[Animalese]", err); + } + } + }, + start() { // Only subscribe once! const init = async () => { @@ -160,50 +229,15 @@ export default definePlugin({ audioContext = new AudioContext(); await initSoundBuffers(); } - document.removeEventListener("click", init); + init(); }; - document.addEventListener("click", init); - - this.channelSelectListener = ({ channelId }) => { - currentChannelId = channelId; - }; - FluxDispatcher.subscribe("CHANNEL_SELECT", this.channelSelectListener); - - this.messageCreateListener = async ({ optimistic, type, message }) => { - // DO NOT REMOVE THIS OR EELSE IT WILL BE YAP CENTRAL - if (optimistic || type !== "MESSAGE_CREATE") return; - - if ( - !message?.content || - message.author?.bot || - message.channel_id !== currentChannelId - ) { - return; - } - - try { - const buffer = await generateAnimalese(message.content); - if (buffer) await playSound(buffer, settings.store.volume); - } catch (err) { - console.error("[Animalese]", err); - } - }; - FluxDispatcher.subscribe("MESSAGE_CREATE", this.messageCreateListener); + init(); }, stop() { - FluxDispatcher.unsubscribe( - "CHANNEL_SELECT", - this.channelSelectListener - ); - FluxDispatcher.unsubscribe( - "MESSAGE_CREATE", - this.messageCreateListener - ); if (audioContext) { audioContext.close(); audioContext = null; } - currentChannelId = null; }, }); From ee415f8e3443a6391e436c78efa36ffe45e2e235 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:24:37 -0400 Subject: [PATCH 081/881] Freaky: Make Ending Optional --- src/equicordplugins/freaky/index.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/freaky/index.tsx b/src/equicordplugins/freaky/index.tsx index 47b2f1aed..fe9a08c3d 100644 --- a/src/equicordplugins/freaky/index.tsx +++ b/src/equicordplugins/freaky/index.tsx @@ -5,9 +5,10 @@ */ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption } from "@api/Commands"; +import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import { sendMessage } from "@utils/discord"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; const charMap: Record = { q: "𝓺", w: "𝔀", e: "𝓮", r: "𝓻", t: "𝓽", y: "𝔂", u: "𝓾", i: "𝓲", o: "𝓸", p: "𝓹", @@ -23,14 +24,23 @@ const mapCharacters = (text: string, map: Record) => function makeFreaky(text: string) { text = mapCharacters(text.trim() || "freaky", charMap); - text += Math.random() < 0.25 ? " 👅" : " ❤️"; + if (settings.store.addFreakyEnding) text += Math.random() < 0.25 ? " 👅" : " ❤️"; return text; } +const settings = definePluginSettings({ + addFreakyEnding: { + type: OptionType.BOOLEAN, + description: "Add 👅 or ❤️ at the end", + default: true + } +}); + export default definePlugin({ name: "Freaky", description: "freaky.", authors: [EquicordDevs.nyx], + settings, commands: [ { name: "freaky", From a72ba28d683e373a875f8486a31ec065b17786f3 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:16:23 -0400 Subject: [PATCH 082/881] Fix Broken Patches --- src/components/settings/tabs/plugins/index.tsx | 1 - src/equicordplugins/themeLibrary/components/ThemeTab.tsx | 1 - src/webpack/common/components.ts | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 9ab3cdcbd..273a8731e 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -530,7 +530,6 @@ export default function PluginSettings() { select={onStatusChange} isSelected={v => v === searchValue.status} closeOnSelect={true} - className={InputStyles.input} /> diff --git a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx index 1e42a6691..f94fc1d77 100644 --- a/src/equicordplugins/themeLibrary/components/ThemeTab.tsx +++ b/src/equicordplugins/themeLibrary/components/ThemeTab.tsx @@ -224,7 +224,6 @@ function ThemeTab() { clearable={false} onChange={v => onStatusChange(v as SearchStatus)} closeOnSelect={true} - className={InputStyles.input} /> diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d5e8dad15..8a8a781b0 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -55,8 +55,8 @@ export const TextInput = waitForComponent("TextInput", filters.comp export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:")); export const Text = waitForComponent("Text", filters.componentByCode('case"always-white"')); export const Heading = waitForComponent("Heading", filters.componentByCode(">6?{", "variant:")); -export const Select = waitForComponent("Select", filters.componentByCode('.selectPositionTop]:"top"===', '"Escape"===')); -export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('.selectPositionTop]:"top"===', ".multi]:")); +export const Select = waitForComponent("Select", filters.componentByCode("#{intl::SELECT}", "#{intl::CLEAR}", '"Escape"===')); +export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode("setSelectionRange", ".multi]:")); export const Slider = waitForComponent("Slider", filters.componentByCode('"markDash".concat(')); export const Popout = waitForComponent("Popout", filters.componentByCode("ref:this.ref,", "renderPopout:this.renderPopout,")); export const Dialog = waitForComponent("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1')); From ee48cc173a5d6077247ec86c1ff98e59bc9eb1b4 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:22:05 -0400 Subject: [PATCH 083/881] Better Patches --- src/webpack/common/components.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 8a8a781b0..9715619a3 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -55,8 +55,8 @@ export const TextInput = waitForComponent("TextInput", filters.comp export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:")); export const Text = waitForComponent("Text", filters.componentByCode('case"always-white"')); export const Heading = waitForComponent("Heading", filters.componentByCode(">6?{", "variant:")); -export const Select = waitForComponent("Select", filters.componentByCode("#{intl::SELECT}", "#{intl::CLEAR}", '"Escape"===')); -export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode("setSelectionRange", ".multi]:")); +export const Select = waitForComponent("Select", filters.componentByCode('="bottom",', ".select,", '"Escape"===')); +export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode(".setSelectionRange(", ".multi]:")); export const Slider = waitForComponent("Slider", filters.componentByCode('"markDash".concat(')); export const Popout = waitForComponent("Popout", filters.componentByCode("ref:this.ref,", "renderPopout:this.renderPopout,")); export const Dialog = waitForComponent("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1')); From b8bc5210031e9034ced05192624c1aaba4f80b90 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:55:38 -0400 Subject: [PATCH 084/881] BetterInvites: Fix Patch --- src/equicordplugins/betterInvites/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/betterInvites/index.tsx b/src/equicordplugins/betterInvites/index.tsx index 1e12adad7..eaeca3262 100644 --- a/src/equicordplugins/betterInvites/index.tsx +++ b/src/equicordplugins/betterInvites/index.tsx @@ -40,7 +40,7 @@ export default definePlugin({ find: "#{intl::HUB_INVITE_ANOTHER_SCHOOL_LINK}", replacement: [ { - match: /,(\i)&&(\(.{0,50}\i\.tooltipContainer.+)(\i\.\i\.string\(\i\.\i#{intl::GUEST_MEMBERSHIP_EXPLANATION}\))/, + match: /,(\i)&&(\(.{0,50}asContainer.+)(\i\.\i\.string\(\i\.\i#{intl::GUEST_MEMBERSHIP_EXPLANATION}\))/, replace: ",($1||((!$1)&&arguments[0].invite.expires_at)) && $2$self.RenderTip($1, $3, arguments[0].invite.expires_at)" }, { From 4bcf4eb1509739d0fd288a6a29287fe3803a5c16 Mon Sep 17 00:00:00 2001 From: thororen <78185467+thororen1234@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:43:15 -0400 Subject: [PATCH 085/881] fix failing to find the Select component (#3631) --- src/components/settings/tabs/plugins/index.tsx | 1 - src/webpack/common/components.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 6faa63682..4db9699ac 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -262,7 +262,6 @@ function PluginSettings() { select={onStatusChange} isSelected={v => v === searchValue.status} closeOnSelect={true} - className={InputStyles.input} /> diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d5e8dad15..9715619a3 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -55,8 +55,8 @@ export const TextInput = waitForComponent("TextInput", filters.comp export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:")); export const Text = waitForComponent("Text", filters.componentByCode('case"always-white"')); export const Heading = waitForComponent("Heading", filters.componentByCode(">6?{", "variant:")); -export const Select = waitForComponent("Select", filters.componentByCode('.selectPositionTop]:"top"===', '"Escape"===')); -export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('.selectPositionTop]:"top"===', ".multi]:")); +export const Select = waitForComponent("Select", filters.componentByCode('="bottom",', ".select,", '"Escape"===')); +export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode(".setSelectionRange(", ".multi]:")); export const Slider = waitForComponent("Slider", filters.componentByCode('"markDash".concat(')); export const Popout = waitForComponent("Popout", filters.componentByCode("ref:this.ref,", "renderPopout:this.renderPopout,")); export const Dialog = waitForComponent("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1')); From 5dee7469864f042cd221deff5ab1495b092acd24 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 23 Aug 2025 11:30:58 -0300 Subject: [PATCH 086/881] Fix IgnoreActivities --- src/plugins/_api/notices.ts | 2 +- src/plugins/ignoreActivities/index.tsx | 22 ++++++++++++++---- src/plugins/superReactionTweaks/index.ts | 29 ++++++++++++++++-------- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/plugins/_api/notices.ts b/src/plugins/_api/notices.ts index 3b43c31ac..43ef2acc0 100644 --- a/src/plugins/_api/notices.ts +++ b/src/plugins/_api/notices.ts @@ -33,7 +33,7 @@ export default definePlugin({ replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" }, - // TODO: remove this compat eventually + // FIXME(Bundler minifier change related): Remove the non used compability once enough time has passed { match: /(?<=,NOTICE_DISMISS:function\(\i\){)return null!=(\i)/, replace: (m, notice) => `if(${notice}?.id=="VencordNotice")return(${notice}=null,Vencord.Api.Notices.nextNotice(),true);${m}`, diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index 88906c385..014c1abd9 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -56,15 +56,15 @@ function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string ); } -const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable Activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill); -const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable Activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill); +const ToggleIconOn = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Disable activity", "M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z", fill); +const ToggleIconOff = (activity: IgnoredActivity, fill: string) => ToggleIcon(activity, "Enable activity", "m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z", fill); function ToggleActivityComponent(activity: IgnoredActivity, isPlaying = false) { const s = settings.use(["ignoredActivities"]); const { ignoredActivities } = s; if (ignoredActivities.some(act => act.id === activity.id)) return ToggleIconOff(activity, "var(--status-danger)"); - return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--primary-400)"); + return ToggleIconOn(activity, isPlaying ? "var(--green-300)" : "var(--interactive-normal)"); } function handleActivityToggle(e: React.MouseEvent, activity: IgnoredActivity) { @@ -253,15 +253,29 @@ export default definePlugin({ replace: (m, runningGames) => `${m}${runningGames}=${runningGames}.filter(({id,name})=>$self.isActivityNotIgnored({type:0,application_id:id,name}));` } }, + + // FIXME(Bundler minifier change related): Remove the non used compability once enough time has passed { find: "#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}", replacement: { // let { ... nowPlaying: a = !1 ... // let { overlay: b ... } = Props match: /#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}.+?}\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)/, - replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),` + replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`, + noWarn: true, } }, + { + find: "#{intl::SETTINGS_GAMES_TOGGLE_OVERLAY}", + replacement: { + // let { ... nowPlaying: a = !1 ... + // let { overlay: b ... } = Props ... + // ToggleOverLayButton(), nowPlaying && ... RemoveGameButton() + match: /\.gameNameLastPlayed.+?,\i\(\),(?<=nowPlaying:(\i)=!1,.+?overlay:\i,[^}]+?\}=(\i).+?)(?=\1&&)/, + replace: (m, nowPlaying, props) => `${m}$self.renderToggleGameActivityButton(${props},${nowPlaying}),`, + } + }, + // Activities from the apps launcher in the bottom right of the chat bar { find: ".promotedLabelWrapperNonBanner,children", diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 2edf99e81..a8827903f 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -41,21 +41,30 @@ export default definePlugin({ patches: [ { find: ",BURST_REACTION_EFFECT_PLAY", - replacement: { - /* - * var limit = 5 - * ... - * if (calculatePlayingCount(a,b) >= limit) return; - */ - match: /(?<=(\i)=5.+?)if\((.{0,20}?)>=\1\)return;/, - replace: "if(!$self.shouldPlayBurstReaction($2))return;" - } + replacement: [ + // FIXME(Bundler minifier change related): Remove the non used compability once enough time has passed + { + // if (inlinedCalculatePlayingCount(a,b) >= limit) return; + match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.+?if\()(\(\(\i,\i\)=>.+?\(\i,\i\))>=5+?(?=\))/, + replace: (_, rest, playingCount) => `${rest}!$self.shouldPlayBurstReaction(${playingCount})`, + noWarn: true, + }, + { + /* + * var limit = 5 + * ... + * if (calculatePlayingCount(a,b) >= limit) return; + */ + match: /((\i)=5.+?)if\((.{0,20}?)>=\2\)return;/, + replace: (_, rest, playingCount) => `${rest}if(!$self.shouldPlayBurstReaction(${playingCount}))return;` + } + ] }, { find: ".EMOJI_PICKER_CONSTANTS_EMOJI_CONTAINER_PADDING_HORIZONTAL)", replacement: { match: /(openPopoutType:void 0(?=.+?isBurstReaction:(\i).+?(\i===\i\.\i.REACTION)).+?\[\2,\i\]=\i\.useState\().+?\)/, - replace: (_, rest, isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` + replace: (_, rest, _isBurstReactionVariable, isReactionIntention) => `${rest}$self.shouldSuperReactByDefault&&${isReactionIntention})` } } ], From 19c1eaed18cdc484ed0c910c8ce0743cef6d5531 Mon Sep 17 00:00:00 2001 From: thororen <78185467+thororen1234@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:43:15 -0400 Subject: [PATCH 087/881] fix failing to find the Select component (#3631) --- src/components/settings/tabs/plugins/index.tsx | 1 - src/webpack/common/components.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 6faa63682..4db9699ac 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -262,7 +262,6 @@ function PluginSettings() { select={onStatusChange} isSelected={v => v === searchValue.status} closeOnSelect={true} - className={InputStyles.input} /> diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index d5e8dad15..9715619a3 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -55,8 +55,8 @@ export const TextInput = waitForComponent("TextInput", filters.comp export const TextArea = waitForComponent("TextArea", filters.componentByCode("this.getPaddingRight()},id:")); export const Text = waitForComponent("Text", filters.componentByCode('case"always-white"')); export const Heading = waitForComponent("Heading", filters.componentByCode(">6?{", "variant:")); -export const Select = waitForComponent("Select", filters.componentByCode('.selectPositionTop]:"top"===', '"Escape"===')); -export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode('.selectPositionTop]:"top"===', ".multi]:")); +export const Select = waitForComponent("Select", filters.componentByCode('="bottom",', ".select,", '"Escape"===')); +export const SearchableSelect = waitForComponent("SearchableSelect", filters.componentByCode(".setSelectionRange(", ".multi]:")); export const Slider = waitForComponent("Slider", filters.componentByCode('"markDash".concat(')); export const Popout = waitForComponent("Popout", filters.componentByCode("ref:this.ref,", "renderPopout:this.renderPopout,")); export const Dialog = waitForComponent("Dialog", filters.componentByCode('role:"dialog",tabIndex:-1')); From 6bdded4360e7d7edb1193cd4b59a349a552700a9 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:20:51 -0400 Subject: [PATCH 088/881] VoiceJoinMessages: Fix Empty String --- src/equicordplugins/voiceJoinMessages/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/equicordplugins/voiceJoinMessages/index.ts b/src/equicordplugins/voiceJoinMessages/index.ts index 8f7d74e1b..845dd6e84 100644 --- a/src/equicordplugins/voiceJoinMessages/index.ts +++ b/src/equicordplugins/voiceJoinMessages/index.ts @@ -10,7 +10,7 @@ import { humanFriendlyJoin } from "@utils/text"; import definePlugin, { OptionType } from "@utils/types"; import { Message, User } from "@vencord/discord-types"; import { findByCodeLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore, VoiceStateStore } from "@webpack/common"; +import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; const createBotMessage = findByCodeLazy('username:"Clyde"'); @@ -126,12 +126,12 @@ export default definePlugin({ const selfInChannel = SelectedChannelStore.getVoiceChannelId() === channelId; let memberListContent = ""; if (settings.store.friendDirectMessagesShowMembers || settings.store.friendDirectMessagesShowMemberCount) { - const voiceState = VoiceStateStore.getVoiceStatesForChannel(channelId); - const sortedVoiceStates: User[] = Object.values(voiceState as { [key: string]: VoiceState; }) - .filter((voiceState: VoiceState) => { voiceState.user && voiceState.user.id !== userId; }) - .map((voiceState: VoiceState) => voiceState.user); - console.log(sortedVoiceStates); - const otherMembers = sortedVoiceStates.filter(s => s.id !== userId); + const users = Object.values(voiceStates) + .filter(vs => vs.channelId === channelId) + .map(vs => UserStore.getUser(vs.userId)) + .filter(user => user != null); + + const otherMembers = users.filter(s => s.id !== userId); const otherMembersCount = otherMembers.length; if (otherMembersCount <= 0) { memberListContent += ", nobody else is in the voice channel"; From 557ff6085a848f705fd8f6653101ee1ad1d02a51 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:37:47 -0400 Subject: [PATCH 089/881] Fix MutualGroupDMs Patch --- src/plugins/mutualGroupDMs/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 8d81e43db..0d350a8d7 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -109,14 +109,14 @@ export default definePlugin({ }, // User Profile Modal v2 { - find: ".tabBarPanel,children:", + find: ".WIDGETS?(", replacement: [ { match: /items:(\i),.+?(?=return\(0,\i\.jsxs?\)\("div)/, replace: "$&$self.pushSection($1,arguments[0].user);" }, { - match: /\.tabBarPanel,children:(?=.+?section:(\i))/, + match: /(?:\.tabBarPanel|\.WIDGETS\}\)),children:(?=.+?section:(\i))/, replace: "$&$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):" }, // Make the gap between each item smaller so our tab can fit. From c5888c25f70ad85793ba33bead19604d50469265 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 29 Aug 2025 02:50:32 +0200 Subject: [PATCH 090/881] bump to v1.12.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09a822932..3f0e3371f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.12.12", + "version": "1.12.13", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From 6f3cda2cb19fe1caf55c1e76eacd88df3cf84d23 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:18:57 -0400 Subject: [PATCH 091/881] Patch Fixes --- src/equicordplugins/downloadify/index.tsx | 4 ++-- src/plugins/showHiddenChannels/index.tsx | 2 +- src/plugins/showMeYourName/index.tsx | 26 +++++++++++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/equicordplugins/downloadify/index.tsx b/src/equicordplugins/downloadify/index.tsx index 7a09fc6c4..09205532a 100644 --- a/src/equicordplugins/downloadify/index.tsx +++ b/src/equicordplugins/downloadify/index.tsx @@ -91,8 +91,8 @@ export default definePlugin({ replacement: [ { // Make use of the download function. - match: /(let{item:\i}=(\i).{0,450}?)(await \i.\i.saveImage\(\i,\i.contentType,\i.\i\)),/, - replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$3});", + match: /(let{item:\i}=(\i).{0,450}?)(let \i=)(await \i.\i.saveImage\(\i,\i.contentType,\i.\i\))/, + replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$4});$3$4", }, { // Disable default success toast. diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index 755aa4530..e6acc0994 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -264,7 +264,7 @@ export default definePlugin({ replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;` }, { - match: /(?<="renderHeaderBar",\i=>{.+?hideSearch:(\i)\.isDirectory\(\))/, + match: /(?<="renderHeaderBar",\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/, replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 5eca3a0b3..cd82c9c4d 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -768,19 +768,19 @@ export default definePlugin({ patches: [ { find: '="SYSTEM_TAG"', - group: true, - replacement: [ - { - // Replace names in messages and replies. - match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/, - replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" - }, - { - // Animate gradients for message authors. - match: /(let{setAnimate:\i}=(\i);)/, - replace: "$1if($2.animate){$self.addHoveringMessage(arguments[0].message.id)}else{$self.removeHoveringMessage(arguments[0].message.id)};" - } - ] + replacement: { + // Replace names in messages and replies. + match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/, + replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" + } + }, + { + find: /setAnimate.{0,50}\.ANIMATE_CHAT_AVATAR,/, + replacement: { + // Animate gradients for message authors. + match: /(let{setAnimate:\i}=(\i);)/, + replace: "$1if($2.animate){$self.addHoveringMessage(arguments[0].message.id)}else{$self.removeHoveringMessage(arguments[0].message.id)};" + } }, { // Replace names in mentions. From 508dce22eb881a2349b4f770831c8af6fcce6db9 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:08:04 -0400 Subject: [PATCH 092/881] Fix Downloadify --- src/equicordplugins/downloadify/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/equicordplugins/downloadify/index.tsx b/src/equicordplugins/downloadify/index.tsx index 09205532a..f27bdf8d4 100644 --- a/src/equicordplugins/downloadify/index.tsx +++ b/src/equicordplugins/downloadify/index.tsx @@ -92,11 +92,15 @@ export default definePlugin({ { // Make use of the download function. match: /(let{item:\i}=(\i).{0,450}?)(let \i=)(await \i.\i.saveImage\(\i,\i.contentType,\i.\i\))/, - replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$4});$3$4", + replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$4})", + }, + { + match: /if\(\i===.{0,100}\(\i\)\);/, + replace: "" }, { // Disable default success toast. - match: /(\(0,.{0,85}?ToastType.SUCCESS\)\))/, + match: /\i===\i\.\i\.SAVED&&\(\(0,.{0,85}?ToastType.SUCCESS\)\)\)/, replace: "", }, { From 943c6fe0249af2345496a673aab724074d6d032d Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 29 Aug 2025 06:04:24 -0700 Subject: [PATCH 093/881] Fix patches. (#367) --- src/equicordplugins/downloadify/index.tsx | 10 +++----- src/plugins/showMeYourName/index.tsx | 29 +++++++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/equicordplugins/downloadify/index.tsx b/src/equicordplugins/downloadify/index.tsx index f27bdf8d4..4e612f2f6 100644 --- a/src/equicordplugins/downloadify/index.tsx +++ b/src/equicordplugins/downloadify/index.tsx @@ -91,16 +91,12 @@ export default definePlugin({ replacement: [ { // Make use of the download function. - match: /(let{item:\i}=(\i).{0,450}?)(let \i=)(await \i.\i.saveImage\(\i,\i.contentType,\i.\i\))/, - replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$4})", - }, - { - match: /if\(\i===.{0,100}\(\i\)\);/, - replace: "" + match: /(let{item:\i}=(\i).{0,450}?)let \i=(await \i.\i.saveImage\(\i,\i.contentType,\i.\i\)).{0,110}?&&/, + replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$3});", }, { // Disable default success toast. - match: /\i===\i\.\i\.SAVED&&\(\(0,.{0,85}?ToastType.SUCCESS\)\)\)/, + match: /\(\(0,.{0,85}?ToastType.SUCCESS\)\)\)/, replace: "", }, { diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index cd82c9c4d..3700cef94 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -768,18 +768,27 @@ export default definePlugin({ patches: [ { find: '="SYSTEM_TAG"', - replacement: { - // Replace names in messages and replies. - match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/, - replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" - } + group: true, + replacement: [ + { + // Replace names in messages and replies. + match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/, + replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)" + }, + { + // Pass the message object to the should-animate checker. + match: /(\(\{)(shouldSubscribe)/, + replace: "$1message:arguments[0].message,$2" + } + ] }, { find: /setAnimate.{0,50}\.ANIMATE_CHAT_AVATAR,/, replacement: { - // Animate gradients for message authors. + // Track hovering on messages to animate names since Discord's + // built-in hover tracking is buggy. Used by a patch below. match: /(let{setAnimate:\i}=(\i);)/, - replace: "$1if($2.animate){$self.addHoveringMessage(arguments[0].message.id)}else{$self.removeHoveringMessage(arguments[0].message.id)};" + replace: "$1if(arguments[0].message){if($2.animate){$self.addHoveringMessage(arguments[0].message.id)}else{$self.removeHoveringMessage(arguments[0].message.id)}};" } }, { @@ -810,9 +819,9 @@ export default definePlugin({ // Track hovering over second-level messages to animate gradients. // Tack on the groupId, which is how the client groups messages from // the same author together, to the message temporarily so that mentions - // across a group can animate together. By default, Discord handles this - // by animating the second-level message if you are hovering the first-level - // message, but not the other way around. This patch allows both. + // across a group can animate together. By default, Discord handles this, + // but moving between messages can cause a race condition where the animation + // stops on one of the messages in the group. This patch fixes that. find: "CUSTOM_GIFT?\"\":", replacement: { match: /(\(\i,\i,\i\);)(let \i=\i.id===\i(?:.{0,500}?)hovering:(\i))/, From 67aff64fed0422092961f2255691504e92b680e8 Mon Sep 17 00:00:00 2001 From: thororen <78185467+thororen1234@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:01:27 -0400 Subject: [PATCH 094/881] MutualGroupDMs: Fix broken patch (#3634) --- src/plugins/mutualGroupDMs/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/mutualGroupDMs/index.tsx b/src/plugins/mutualGroupDMs/index.tsx index 8d81e43db..288899237 100644 --- a/src/plugins/mutualGroupDMs/index.tsx +++ b/src/plugins/mutualGroupDMs/index.tsx @@ -109,14 +109,14 @@ export default definePlugin({ }, // User Profile Modal v2 { - find: ".tabBarPanel,children:", + find: ".tabBarPanel,{[", replacement: [ { match: /items:(\i),.+?(?=return\(0,\i\.jsxs?\)\("div)/, replace: "$&$self.pushSection($1,arguments[0].user);" }, { - match: /\.tabBarPanel,children:(?=.+?section:(\i))/, + match: /\.tabBarPanel,.+?children:(?=.+?section:(\i))/, replace: "$&$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):" }, // Make the gap between each item smaller so our tab can fit. From 1c4055711cd2ea9fc2b718dd298b173f1dfffccc Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:01:45 -0400 Subject: [PATCH 095/881] Fix Tagging --- .github/workflows/publish.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 28b30bc2f..4117a2065 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,11 @@ on: branches: - main +env: + FORCE_COLOR: true + +permissions: write-all + jobs: release: runs-on: ubuntu-latest From aca30bcb9a37425c4cdecda6f0a5b94a57225112 Mon Sep 17 00:00:00 2001 From: sadan4 <117494111+sadan4@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:02:45 -0400 Subject: [PATCH 096/881] ShowHiddenChannels: Fix broken patch (#3635) --- src/plugins/showHiddenChannels/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index b4fcd7136..5f83a3f5d 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -239,7 +239,7 @@ export default definePlugin({ replace: (m, channel) => `${m}if($self.isHiddenChannel(${channel}))break;` }, { - match: /(?<="renderHeaderBar",\i=>{.+?hideSearch:(\i)\.isDirectory\(\))/, + match: /(?<="renderHeaderBar",\(\)=>{.+?hideSearch:(\i)\.isDirectory\(\))/, replace: (_, channel) => `||$self.isHiddenChannel(${channel})` }, { From 8807564053c7b4cc05c763e2dc7171f5d61e39c7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:23:23 -0300 Subject: [PATCH 097/881] ConsoleJanitor: Fix outdated settings margin --- .../settings/tabs/plugins/components/Common.tsx | 2 +- src/plugins/consoleJanitor/index.tsx | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/settings/tabs/plugins/components/Common.tsx b/src/components/settings/tabs/plugins/components/Common.tsx index 3d84859bf..61c3da529 100644 --- a/src/components/settings/tabs/plugins/components/Common.tsx +++ b/src/components/settings/tabs/plugins/components/Common.tsx @@ -36,7 +36,7 @@ export function resolveError(isValidResult: boolean | string) { interface SettingsSectionProps extends PropsWithChildren { name: string; description: string; - error: string | null; + error?: string | null; inlineSetting?: boolean; } diff --git a/src/plugins/consoleJanitor/index.tsx b/src/plugins/consoleJanitor/index.tsx index 4981ac2ca..fb4117c1e 100644 --- a/src/plugins/consoleJanitor/index.tsx +++ b/src/plugins/consoleJanitor/index.tsx @@ -6,11 +6,10 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; +import { SettingsSection } from "@components/settings/tabs/plugins/components/Common"; import { Devs } from "@utils/constants"; -import { Margins } from "@utils/margins"; import definePlugin, { defineDefault, OptionType, StartAt } from "@utils/types"; -import { Checkbox, Forms, Text } from "@webpack/common"; +import { Checkbox, Text } from "@webpack/common"; const Noop = () => { }; const NoopLogger = { @@ -58,15 +57,13 @@ function AllowLevelSetting({ settingKey }: AllowLevelSettingProps) { const AllowLevelSettings = ErrorBoundary.wrap(() => { return ( - - Filter List - Always allow loggers of these types - + +
{Object.keys(settings.store.allowLevel).map(key => ( ))} - - +
+
); }); From b10b238e05fef78bd3b133809eaea5ee1b093eda Mon Sep 17 00:00:00 2001 From: Etorix <92535668+EtorixDev@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:16:13 -0700 Subject: [PATCH 098/881] Downloadify: Simplify patches. (#369) * Simplify patches. * Simplify patches. --- src/equicordplugins/downloadify/index.tsx | 16 +++------------- .../downloadify/utils/handlers.tsx | 13 +++---------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/equicordplugins/downloadify/index.tsx b/src/equicordplugins/downloadify/index.tsx index 4e612f2f6..3b0fb0618 100644 --- a/src/equicordplugins/downloadify/index.tsx +++ b/src/equicordplugins/downloadify/index.tsx @@ -91,23 +91,13 @@ export default definePlugin({ replacement: [ { // Make use of the download function. - match: /(let{item:\i}=(\i).{0,450}?)let \i=(await \i.\i.saveImage\(\i,\i.contentType,\i.\i\)).{0,110}?&&/, + match: /(let{item:\i}=(\i).{0,450}?)(\i\(!0\);try{.{0,400}?finally{\i\(!1\)})/, replace: "$1await $self.handleExpandedModalDownloadButtonClicked($2,async()=>{$3});", }, - { - // Disable default success toast. - match: /\(\(0,.{0,85}?ToastType.SUCCESS\)\)\)/, - replace: "", - }, - { - // Disable default failure toast. - match: /(\(0,.{0,85}?ToastType.FAILURE\)\))/, - replace: "", - }, { // Prevent videos from opening in browser. - match: /(let{item:\i}=\i.{0,350}?)(SAVE_MEDIA_PRESSED\)).{0,100}?(\i\(!0\);)/, - replace: "$1$2,true){$3" + match: /(let{item:\i}=\i.{0,350}?)(SAVE_MEDIA_PRESSED\)).{0,100}?type\){/, + replace: "$1$2,true){" } ], } diff --git a/src/equicordplugins/downloadify/utils/handlers.tsx b/src/equicordplugins/downloadify/utils/handlers.tsx index 42dd32fc7..43f336578 100644 --- a/src/equicordplugins/downloadify/utils/handlers.tsx +++ b/src/equicordplugins/downloadify/utils/handlers.tsx @@ -677,7 +677,7 @@ export async function handleHoverDownloadButtonClicked(props: HoverDownloadProps if (!originalItem) { // Fallback to default behavior. DownloadifyLogger.info(`[${getFormattedNow()}] [UNRECOGNIZED HOVER DOWNLOAD ITEM]`, props.item); - displayStatus && showToast("File Opened in Browser", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); + displayStatus && showToast("Unknown Media Item. Falling Back to Default Behavior", Toasts.Type.MESSAGE, { duration: statusDuration * 1250 }); window.open(downloadUrl, "_blank"); } else { contentType ??= (await DownloadifyNative.queryURL(downloadUrl) || ""); @@ -700,15 +700,8 @@ export async function handleExpandedModalDownloadButtonClicked(props: ExpandedMo if (!url && !original) { // Fallback to default behavior. DownloadifyLogger.info(`[${getFormattedNow()}] [UNRECOGNIZED EXPANDED MODAL DOWNLOAD ITEM]`, props.item); - displayStatus && showToast("Download Started", Toasts.Type.MESSAGE, { duration: statusDuration * 1000 }); - - try { - await fallback(); - displayStatus && showToast("Download Success", Toasts.Type.SUCCESS, { duration: statusDuration * 1000 }); - } catch (error) { - DownloadifyLogger.info(`[${getFormattedNow()}] [FALLBACK DOWNLOAD FAILED]`, error); - displayStatus && showToast("Download Failed", Toasts.Type.FAILURE, { duration: statusDuration * 1000 }); - } + displayStatus && showToast("Unknown Media Item. Falling Back to Default Behavior", Toasts.Type.MESSAGE, { duration: statusDuration * 1250 }); + await fallback(); } else { contentType ??= (await DownloadifyNative.queryURL(url) || ""); let aliasBasename = sourceMetadata?.identifier.title ?? null; From 2c7de5ff7f6ec3d460729be8f072a538d3d3d6f1 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:15:51 -0400 Subject: [PATCH 099/881] Add UserProfileBadge Back For Comp --- src/plugins/index.ts | 8 ++++++++ src/utils/types.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 53c2cd770..b2b066c49 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -125,6 +125,14 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) { dep.isDependency = true; }); + if (p.userProfileBadge) { + if (!p.userProfileBadges) { + p.userProfileBadges = [p.userProfileBadge]; + } else { + p.userProfileBadges = [p.userProfileBadge, ...p.userProfileBadges]; + } + } + if (p.commands?.length) neededApiPlugins.add("CommandsAPI"); if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI"); if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI"); diff --git a/src/utils/types.ts b/src/utils/types.ts index 462686357..0f81d7bdf 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -171,6 +171,7 @@ export interface PluginDef { */ managedStyle?: string; + userProfileBadge?: ProfileBadge; userProfileBadges?: ProfileBadge[]; onMessageClick?: MessageClickListener; From e85e87c4e832ad19d996389af47fde5661b3719a Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 30 Aug 2025 12:32:16 -0400 Subject: [PATCH 100/881] Downloadify Desktop Restriction --- .../{downloadify => downloadify.desktop}/README.md | 0 .../{downloadify => downloadify.desktop}/index.tsx | 1 - .../{downloadify => downloadify.desktop}/native.ts | 0 .../{downloadify => downloadify.desktop}/settings.tsx | 0 .../{downloadify => downloadify.desktop}/style.css | 0 .../{downloadify => downloadify.desktop}/utils/definitions.tsx | 0 .../{downloadify => downloadify.desktop}/utils/handlers.tsx | 0 .../{downloadify => downloadify.desktop}/utils/misc.tsx | 0 8 files changed, 1 deletion(-) rename src/equicordplugins/{downloadify => downloadify.desktop}/README.md (100%) rename src/equicordplugins/{downloadify => downloadify.desktop}/index.tsx (99%) rename src/equicordplugins/{downloadify => downloadify.desktop}/native.ts (100%) rename src/equicordplugins/{downloadify => downloadify.desktop}/settings.tsx (100%) rename src/equicordplugins/{downloadify => downloadify.desktop}/style.css (100%) rename src/equicordplugins/{downloadify => downloadify.desktop}/utils/definitions.tsx (100%) rename src/equicordplugins/{downloadify => downloadify.desktop}/utils/handlers.tsx (100%) rename src/equicordplugins/{downloadify => downloadify.desktop}/utils/misc.tsx (100%) diff --git a/src/equicordplugins/downloadify/README.md b/src/equicordplugins/downloadify.desktop/README.md similarity index 100% rename from src/equicordplugins/downloadify/README.md rename to src/equicordplugins/downloadify.desktop/README.md diff --git a/src/equicordplugins/downloadify/index.tsx b/src/equicordplugins/downloadify.desktop/index.tsx similarity index 99% rename from src/equicordplugins/downloadify/index.tsx rename to src/equicordplugins/downloadify.desktop/index.tsx index 3b0fb0618..880e39e13 100644 --- a/src/equicordplugins/downloadify/index.tsx +++ b/src/equicordplugins/downloadify.desktop/index.tsx @@ -16,7 +16,6 @@ export default definePlugin({ name: "Downloadify", description: "Download various assets directly in Discord without having to open a browser or dig through HTML.", authors: [EquicordDevs.Etorix], - hidden: IS_WEB, settings, VoiceMessageDownloadButton, diff --git a/src/equicordplugins/downloadify/native.ts b/src/equicordplugins/downloadify.desktop/native.ts similarity index 100% rename from src/equicordplugins/downloadify/native.ts rename to src/equicordplugins/downloadify.desktop/native.ts diff --git a/src/equicordplugins/downloadify/settings.tsx b/src/equicordplugins/downloadify.desktop/settings.tsx similarity index 100% rename from src/equicordplugins/downloadify/settings.tsx rename to src/equicordplugins/downloadify.desktop/settings.tsx diff --git a/src/equicordplugins/downloadify/style.css b/src/equicordplugins/downloadify.desktop/style.css similarity index 100% rename from src/equicordplugins/downloadify/style.css rename to src/equicordplugins/downloadify.desktop/style.css diff --git a/src/equicordplugins/downloadify/utils/definitions.tsx b/src/equicordplugins/downloadify.desktop/utils/definitions.tsx similarity index 100% rename from src/equicordplugins/downloadify/utils/definitions.tsx rename to src/equicordplugins/downloadify.desktop/utils/definitions.tsx diff --git a/src/equicordplugins/downloadify/utils/handlers.tsx b/src/equicordplugins/downloadify.desktop/utils/handlers.tsx similarity index 100% rename from src/equicordplugins/downloadify/utils/handlers.tsx rename to src/equicordplugins/downloadify.desktop/utils/handlers.tsx diff --git a/src/equicordplugins/downloadify/utils/misc.tsx b/src/equicordplugins/downloadify.desktop/utils/misc.tsx similarity index 100% rename from src/equicordplugins/downloadify/utils/misc.tsx rename to src/equicordplugins/downloadify.desktop/utils/misc.tsx From 7f3dd6ef87adfc7fab924e0bf294e09e6a07ef44 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 30 Aug 2025 22:56:41 -0400 Subject: [PATCH 101/881] Add Dev List --- .github/workflows/build.yml | 1 + .github/workflows/nixosBuild.yml | 1 + package.json | 1 + scripts/generateDevsList.ts | 131 +++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 scripts/generateDevsList.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77250b9d3..2050ad109 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,7 @@ jobs: pnpm generatePluginJson dist/plugins.json pnpm generateEquicordPluginJson dist/equicordplugins.json pnpm generateVencordPluginJson dist/vencordplugins.json + pnpm generateDevsList dist/devs.json - name: Collect files to be released run: | diff --git a/.github/workflows/nixosBuild.yml b/.github/workflows/nixosBuild.yml index 6890f7748..50d7638ff 100644 --- a/.github/workflows/nixosBuild.yml +++ b/.github/workflows/nixosBuild.yml @@ -39,6 +39,7 @@ jobs: pnpm generatePluginJson dist/plugins.json pnpm generateEquicordPluginJson dist/equicordplugins.json pnpm generateVencordPluginJson dist/vencordplugins.json + pnpm generateDevsList dist/devs.json - name: Collect files to be released run: | diff --git a/package.json b/package.json index cf0a7f373..c62a68020 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "watch": "pnpm build --watch", "dev": "pnpm watch", "watchWeb": "pnpm buildWeb --watch", + "generateDevsList": "tsx scripts/generateDevsList.ts", "generatePluginJson": "tsx scripts/generatePluginList.ts", "generateEquicordPluginJson": "tsx scripts/generateEquicordPluginList.ts", "generateVencordPluginJson": "tsx scripts/generateVencordPluginList.ts", diff --git a/scripts/generateDevsList.ts b/scripts/generateDevsList.ts new file mode 100644 index 000000000..472358728 --- /dev/null +++ b/scripts/generateDevsList.ts @@ -0,0 +1,131 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 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 . +*/ + +import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs"; +import { access, readFile } from "fs/promises"; +import { join, sep } from "path"; +import { normalize as posixNormalize, sep as posixSep } from "path/posix"; +import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, PropertyAssignment, ScriptTarget, StringLiteral, SyntaxKind } from "typescript"; + +import { getPluginTarget } from "./utils.mjs"; + +interface Dev { + name: string; + id: string; +} + +interface Command { + name: string; + description: string; +} + +const devs = {} as Record; +const equicordDevs = {} as Record; + +function getName(node: NamedDeclaration) { + return node.name && isIdentifier(node.name) ? node.name.text : undefined; +} + +function hasName(node: NamedDeclaration, name: string) { + return getName(node) === name; +} + +function getObjectProp(node: ObjectLiteralExpression, name: string) { + const prop = node.properties.find(p => hasName(p, name)); + if (prop && isPropertyAssignment(prop)) return prop.initializer; + return prop; +} + +function parseDevs() { + const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest); + + for (const child of file.getChildAt(0).getChildren()) { + if (!isVariableStatement(child)) continue; + + const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs")); + if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue; + + const value = devsDeclaration.initializer.arguments[0]; + + if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error("Failed to parse devs: not an object literal"); + + for (const prop of value.expression.properties) { + const name = (prop.name as Identifier).text; + const value = isPropertyAssignment(prop) ? prop.initializer : prop; + + if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`); + + devs[name] = { + name: (getObjectProp(value, "name") as StringLiteral).text, + id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1) + }; + } + + return; + } + + throw new Error("Could not find Devs constant"); +} + +function parseEquicordDevs() { + const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest); + + for (const child of file.getChildAt(0).getChildren()) { + if (!isVariableStatement(child)) continue; + + const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "EquicordDevs")); + if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue; + + const value = devsDeclaration.initializer.arguments[0]; + + if (!isSatisfiesExpression(value) || !isObjectLiteralExpression(value.expression)) throw new Error("Failed to parse EquicordDevs: not an object literal"); + + for (const prop of value.expression.properties) { + const name = (prop.name as Identifier).text; + const value = isPropertyAssignment(prop) ? prop.initializer : prop; + + if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse EquicordDevs: ${name} is not an object literal`); + + equicordDevs[name] = { + name: (getObjectProp(value, "name") as StringLiteral).text, + id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1) + }; + } + + return; + } + + throw new Error("Could not find EquicordDevs constant"); +} + +(async () => { + parseDevs(); + parseEquicordDevs(); + + const allDevs = { + equicord: equicordDevs, + vencord: devs + }; + + const data = JSON.stringify(allDevs, null, 2); + if (process.argv.length > 2) { + writeFileSync(process.argv[2], data); + } else { + console.log(data); + } +})(); From 7744f5189291a8e416e18bfa96167a005207b109 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 30 Aug 2025 23:04:56 -0400 Subject: [PATCH 102/881] Remove Unused Imports --- scripts/generateDevsList.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/scripts/generateDevsList.ts b/scripts/generateDevsList.ts index 472358728..e810d66d2 100644 --- a/scripts/generateDevsList.ts +++ b/scripts/generateDevsList.ts @@ -16,24 +16,14 @@ * along with this program. If not, see . */ -import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs"; -import { access, readFile } from "fs/promises"; -import { join, sep } from "path"; -import { normalize as posixNormalize, sep as posixSep } from "path/posix"; -import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, PropertyAssignment, ScriptTarget, StringLiteral, SyntaxKind } from "typescript"; - -import { getPluginTarget } from "./utils.mjs"; +import { readFileSync, writeFileSync } from "fs"; +import { BigIntLiteral, createSourceFile, Identifier, isCallExpression, isIdentifier, isObjectLiteralExpression, isPropertyAssignment, isSatisfiesExpression, isVariableStatement, NamedDeclaration, ObjectLiteralExpression, ScriptTarget, StringLiteral } from "typescript"; interface Dev { name: string; id: string; } -interface Command { - name: string; - description: string; -} - const devs = {} as Record; const equicordDevs = {} as Record; @@ -118,8 +108,8 @@ function parseEquicordDevs() { parseEquicordDevs(); const allDevs = { + vencord: devs, equicord: equicordDevs, - vencord: devs }; const data = JSON.stringify(allDevs, null, 2); From 17b90beee1af377becbf92764cf954910037f140 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 31 Aug 2025 05:18:40 +0200 Subject: [PATCH 103/881] add context menu options to vencord badges --- src/api/Badges.ts | 39 +++++++++++++++------- src/plugins/_api/badges/index.tsx | 54 +++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 18 deletions(-) diff --git a/src/api/Badges.ts b/src/api/Badges.ts index f79ca0995..1345b26d6 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -34,7 +34,9 @@ export interface ProfileBadge { image?: string; link?: string; /** Action to perform when you click the badge */ - onClick?(event: React.MouseEvent, props: BadgeUserArgs): void; + onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void; + /** Action to perform when you right click the badge */ + onContextMenu?(event: React.MouseEvent, props: BadgeUserArgs & BadgeUserArgs): void; /** Should the user display this badge? */ shouldShow?(userInfo: BadgeUserArgs): boolean; /** Optional props (e.g. style) for the badge, ignored for component badges */ @@ -76,21 +78,34 @@ export function removeProfileBadge(badge: ProfileBadge) { export function _getBadges(args: BadgeUserArgs) { const badges = [] as ProfileBadge[]; for (const badge of Badges) { - if (!badge.shouldShow || badge.shouldShow(args)) { - const b = badge.getBadges - ? badge.getBadges(args).map(b => { - b.component &&= ErrorBoundary.wrap(b.component, { noop: true }); - return b; - }) - : [{ ...badge, ...args }]; + if (badge.shouldShow && !badge.shouldShow(args)) { + continue; + } - badge.position === BadgePosition.START - ? badges.unshift(...b) - : badges.push(...b); + const b = badge.getBadges + ? badge.getBadges(args).map(badge => ({ + ...args, + ...badge, + component: badge.component && ErrorBoundary.wrap(badge.component, { noop: true }) + })) + : [{ ...args, ...badge }]; + + if (badge.position === BadgePosition.START) { + badges.unshift(...b); + } else { + badges.push(...b); } } + const donorBadges = BadgeAPIPlugin.getDonorBadges(args.userId); - if (donorBadges) badges.unshift(...donorBadges); + if (donorBadges) { + badges.unshift( + ...donorBadges.map(badge => ({ + ...args, + ...badge, + })) + ); + } return badges; } diff --git a/src/plugins/_api/badges/index.tsx b/src/plugins/_api/badges/index.tsx index 433c30807..4118435af 100644 --- a/src/plugins/_api/badges/index.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -27,11 +27,11 @@ import { openContributorModal } from "@components/settings/tabs"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; -import { shouldShowContributorBadge } from "@utils/misc"; +import { copyWithToast, shouldShowContributorBadge } from "@utils/misc"; import { closeModal, ModalContent, ModalFooter, ModalHeader, ModalRoot, openModal } from "@utils/modal"; import definePlugin from "@utils/types"; import { User } from "@vencord/discord-types"; -import { Forms, Toasts, UserStore } from "@webpack/common"; +import { ContextMenuApi, Forms, Menu, Toasts, UserStore } from "@webpack/common"; const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64"; @@ -56,9 +56,35 @@ async function loadBadges(noCache = false) { let intervalId: any; +function BadgeContextMenu({ badge }: { badge: ProfileBadge & BadgeUserArgs; }) { + console.log(badge); + return ( + + {badge.description && ( + copyWithToast(badge.description!)} + /> + )} + {badge.image && ( + copyWithToast(badge.image!)} + /> + )} + + ); +} + export default definePlugin({ name: "BadgeAPI", - description: "API to add badges to users.", + description: "API to add badges to users", authors: [Devs.Megu, Devs.Ven, Devs.TheSun], required: true, patches: [ @@ -80,10 +106,10 @@ export default definePlugin({ match: /(?<="aria-label":(\i)\.description,.{0,200})children:/, replace: "children:$1.component?$self.renderBadgeComponent({...$1}) :" }, - // conditionally override their onClick with badge.onClick if it exists + // handle onClick and onContextMenu { match: /href:(\i)\.link/, - replace: "...($1.onClick&&{onClick:vcE=>$1.onClick(vcE,$1)}),$&" + replace: "...$self.getBadgeMouseEventHandlers($1),$&" } ] } @@ -137,6 +163,19 @@ export default definePlugin({ }, { noop: true }), + getBadgeMouseEventHandlers(badge: ProfileBadge & BadgeUserArgs) { + const handlers = {} as Record void>; + + if (!badge) return handlers; // sanity check + + const { onClick, onContextMenu } = badge; + + if (onClick) handlers.onClick = e => onClick(e, badge); + if (onContextMenu) handlers.onContextMenu = e => onContextMenu(e, badge); + + return handlers; + }, + getDonorBadges(userId: string) { return DonorBadges[userId]?.map(badge => ({ image: badge.badge, @@ -148,6 +187,9 @@ export default definePlugin({ transform: "scale(0.9)" // The image is a bit too big compared to default badges } }, + onContextMenu(event, badge) { + ContextMenuApi.openContextMenu(event, () => ); + }, onClick() { const modalKey = openModal(props => ( { @@ -203,6 +245,6 @@ export default definePlugin({ )); }, - })); + } satisfies ProfileBadge)); } }); From 43b0c7217fc22d80bceee7be6cb0e08140a77c4f Mon Sep 17 00:00:00 2001 From: KiCKinTheBucket Date: Sun, 31 Aug 2025 07:00:53 -0400 Subject: [PATCH 104/881] Fixed soggy default image url, again (#370) * Moved soggy default image source into Equicord Due to the original image source seemingly being inconsistent and the domain not having https enabled. * Or I could just be stupid https is supported, and my previous merge was overwritten, I'm just dumb * Update index.tsx --------- Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/soggy/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/equicordplugins/soggy/index.tsx b/src/equicordplugins/soggy/index.tsx index 9af3e9252..5c37506a9 100644 --- a/src/equicordplugins/soggy/index.tsx +++ b/src/equicordplugins/soggy/index.tsx @@ -109,7 +109,7 @@ const settings = definePluginSettings({ imageLink: { description: "URL for the image (button and modal)", type: OptionType.STRING, - default: "https://soggy.cat/assets/images/soggycat.webp", + default: "https://cdn.nest.rip/uploads/a0df62f0-39ee-4d2d-86a9-033ede2156f0.webp", }, songLink: { description: "URL for the song to play", From c23a82e06ec90e87e00999ee8a0f6ed2fd47b5df Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:27:03 -0400 Subject: [PATCH 105/881] Remove Support 100+ Plugins Cancel Text --- src/plugins/_core/supportHelper.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index 235e141ef..b943dca9e 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -178,8 +178,7 @@ function generatePluginList() { We do not handle support for users who use 100+ plugins issue could be plugin confliction try removing some plugins and see if it fixes! - , - cancelText: "Okay continue" + }); } From 3787622f994932b3b1356216d1f55defbc7b8b7d Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Mon, 1 Sep 2025 07:14:47 -0400 Subject: [PATCH 106/881] CloudTab: Modify Description --- src/components/settings/tabs/sync/CloudTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings/tabs/sync/CloudTab.tsx b/src/components/settings/tabs/sync/CloudTab.tsx index 93e8071fb..07489cb62 100644 --- a/src/components/settings/tabs/sync/CloudTab.tsx +++ b/src/components/settings/tabs/sync/CloudTab.tsx @@ -124,7 +124,7 @@ function CloudTab() { - Equicord comes with the Vencord cloud integration that adds goodies like settings sync across devices. + Equicord comes with a cloud integration that adds goodies like settings sync across devices. It respects your privacy, and the source code is AGPL 3.0 licensed so you can host it yourself. From 3cd45b4182874299b7e030f0b6ce5906175817ef Mon Sep 17 00:00:00 2001 From: creations Date: Mon, 1 Sep 2025 16:03:58 -0400 Subject: [PATCH 107/881] ChatButtonsPlus Plugin (#371) * delete meow and woof and make a new plugin called ChatButtonsPlus * rename css file * update to only add once if plugins were enabled before --- src/equicordplugins/chatButtonsPlus/index.tsx | 284 ++++++++++++++++++ .../chatButtonsPlus/styles.css | 52 ++++ src/equicordplugins/meow/index.tsx | 32 -- src/equicordplugins/woof/index.tsx | 31 -- 4 files changed, 336 insertions(+), 63 deletions(-) create mode 100644 src/equicordplugins/chatButtonsPlus/index.tsx create mode 100644 src/equicordplugins/chatButtonsPlus/styles.css delete mode 100644 src/equicordplugins/meow/index.tsx delete mode 100644 src/equicordplugins/woof/index.tsx diff --git a/src/equicordplugins/chatButtonsPlus/index.tsx b/src/equicordplugins/chatButtonsPlus/index.tsx new file mode 100644 index 000000000..3634d67d1 --- /dev/null +++ b/src/equicordplugins/chatButtonsPlus/index.tsx @@ -0,0 +1,284 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { ChatBarButton } from "@api/ChatButtons"; +import { DataStore } from "@api/index"; +import { definePluginSettings, Settings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { getCurrentChannel, sendMessage } from "@utils/discord"; +import { useForceUpdater } from "@utils/react"; +import definePlugin, { OptionType } from "@utils/types"; +import { Button, Forms, React, Switch, TextInput } from "@webpack/common"; + +type ButtonEntry = { + id: string; + label: string; + message: string; + svg: string; + enabled: boolean; +}; + +let buttonEntries: ButtonEntry[] = []; +const BUTTON_ENTRIES_KEY = "ChatButtonsPlus_buttonEntries"; + +function generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); +} + +async function handleButtonClick(context: string) { + const currentChannel = getCurrentChannel(); + + if (!currentChannel) return; + + sendMessage(currentChannel.id, { content: context }); +} + +async function addButtonEntry(forceUpdate: () => void) { + try { + buttonEntries.push({ + id: generateId(), + label: "New Button", + message: "Hello!", + svg: '', + enabled: true + }); + await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries); + forceUpdate(); + } catch (error) { + console.error("ChatButtonsPlus: Failed to add button entry:", error); + } +} + +async function removeButtonEntry(id: string, forceUpdate: () => void) { + try { + const index = buttonEntries.findIndex(entry => entry.id === id); + if (index !== -1) { + buttonEntries.splice(index, 1); + await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries); + forceUpdate(); + } + } catch (error) { + console.error("ChatButtonsPlus: Failed to remove button entry:", error); + } +} + +function ButtonEntries() { + const update = useForceUpdater(); + + async function setLabel(id: string, value: string) { + try { + const index = buttonEntries.findIndex(entry => entry.id === id); + if (index !== -1) { + buttonEntries[index].label = value.trim() || "Button"; + await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries); + update(); + } + } catch (error) { + console.error("ChatButtonsPlus: Failed to update label:", error); + } + } + + async function setMessage(id: string, value: string) { + try { + const index = buttonEntries.findIndex(entry => entry.id === id); + if (index !== -1) { + buttonEntries[index].message = value.trim() || "Hello!"; + await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries); + update(); + } + } catch (error) { + console.error("ChatButtonsPlus: Failed to update message:", error); + } + } + + async function setSvg(id: string, value: string) { + try { + const index = buttonEntries.findIndex(entry => entry.id === id); + if (index !== -1 && isValidSvg(value.trim())) { + buttonEntries[index].svg = value.trim(); + await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries); + update(); + } + } catch (error) { + console.error("ChatButtonsPlus: Failed to update SVG:", error); + } + } + + async function setEnabled(id: string, value: boolean) { + try { + const index = buttonEntries.findIndex(entry => entry.id === id); + if (index !== -1) { + buttonEntries[index].enabled = value; + await DataStore.set(BUTTON_ENTRIES_KEY, buttonEntries); + update(); + } + } catch (error) { + console.error("ChatButtonsPlus: Failed to update enabled state:", error); + } + } + + function isValidSvg(svg: string): boolean { + if (!svg) return true; + + const dangerousPatterns = [ + /