diff --git a/src/equicordplugins/copyProfileColors/index.tsx b/src/equicordplugins/copyProfileColors/index.tsx deleted file mode 100644 index 256e86994..000000000 --- a/src/equicordplugins/copyProfileColors/index.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { copyToClipboard } from "@utils/clipboard"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Menu, Toasts, UserProfileStore } from "@webpack/common"; - -function getProfileColors(userId) { - try { - const profile = UserProfileStore.getUserProfile(userId); - - if (!profile || !profile.themeColors || profile.themeColors.length < 2) { - return null; - } - - const primaryColor = profile.themeColors[0].toString(16).padStart(6, "0"); - const secondaryColor = profile.themeColors[1].toString(16).padStart(6, "0"); - - return { primaryColor, secondaryColor }; - } catch (e) { - console.error("Failed to get profile colors:", e); - return null; - } -} - -function copyProfileColors(userId) { - const colors = getProfileColors(userId); - - if (!colors) { - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "No profile colors found!", - id: Toasts.genId() - }); - return; - } - - const { primaryColor, secondaryColor } = colors; - - // Formatting - const formattedColors = `Primary-color #${primaryColor}, Secondary-Color #${secondaryColor}`; - - try { - copyToClipboard(formattedColors); - Toasts.show({ - type: Toasts.Type.SUCCESS, - message: "Profile colors copied to clipboard!", - id: Toasts.genId() - }); - } catch (e) { - console.error("Failed to copy to clipboard:", e); - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Error copying profile colors!", - id: Toasts.genId() - }); - } -} - -export function ColorIcon() { - return ( - - - - ); -} -// spawn in the context menu -const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }) => { - if (!user) return; - children.push( - copyProfileColors(user.id)} - /> - ); -}; - -export default definePlugin({ - name: "CopyProfileColors", - description: "A plugin to copy people's profile gradient colors to clipboard.", - authors: [EquicordDevs.Crxa, EquicordDevs.Cortex], - contextMenus: { - "user-context": userContextMenuPatch, - "user-profile-actions": userContextMenuPatch - } -}); diff --git a/src/equicordplugins/equissant/index.ts b/src/equicordplugins/equissant/index.ts deleted file mode 100644 index 188316213..000000000 --- a/src/equicordplugins/equissant/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer"; -import { definePluginSettings } from "@api/Settings"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -let clickCount = 0; -let croissant: AudioPlayerInterface | null = null; - -function play() { - const triggerAmount = settings.store.amount; - clickCount++; - - if (clickCount % triggerAmount === 0) { - croissant?.play(); - clickCount = 0; - } -} - -const settings = definePluginSettings({ - amount: { - type: OptionType.NUMBER, - description: "amount of clicks to trigger crossant", - default: 10, - } -}); - -export default definePlugin({ - name: "Equissant", - description: "Crossant every specified amount of clicks :trolley:", - authors: [EquicordDevs.SomeAspy, Devs.thororen], - dependencies: ["AudioPlayerAPI"], - settings, - start() { - croissant = createAudioPlayer("https://github.com/Equicord/Equibored/raw/main/sounds/equissant/croissant.mp3", { persistent: true }); - document.addEventListener("click", play); - }, - stop() { - croissant?.delete(); - document.removeEventListener("click", play); - } -}); diff --git a/src/equicordplugins/exitSounds/index.tsx b/src/equicordplugins/exitSounds/index.tsx deleted file mode 100644 index db67fe6ed..000000000 --- a/src/equicordplugins/exitSounds/index.tsx +++ /dev/null @@ -1,147 +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 { playAudio } from "@api/AudioPlayer"; -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { Button } from "@components/Button"; -import { Devs } from "@utils/constants"; -import { classNameFactory } from "@utils/css"; -import definePlugin, { OptionType } from "@utils/types"; -import { findComponentByCodeLazy } from "@webpack"; -import { ChannelActions, Constants, GuildStore, IconUtils, MediaEngineStore, Menu, RestAPI, SearchableSelect, SelectedChannelStore, TextInput, Toasts } from "@webpack/common"; - -const cl = classNameFactory("vc-exitsounds-"); - -const PlayIcon = findComponentByCodeLazy("4.96v14.08c0"); - -function GuildSelector() { - const { soundGuildId } = settings.use(["soundGuildId"]); - const options = Object.values(GuildStore.getGuilds()).map(g => ({ - value: g.id, - label: g.name - })); - - return ( - o.value === soundGuildId)?.value} - placeholder="Select a server..." - maxVisibleItems={6} - closeOnSelect={true} - onChange={v => settings.store.soundGuildId = v} - renderOptionPrefix={o => { - const guild = GuildStore.getGuild(o?.value); - if (!guild?.icon) return null; - return ( - - ); - }} - /> - ); -} - -function SoundIdInput() { - const { soundId } = settings.use(["soundId"]); - - return ( -
-
- settings.store.soundId = v} - placeholder="Enter sound ID..." - /> -
- -
- ); -} - -const settings = definePluginSettings({ - soundGuildId: { - type: OptionType.COMPONENT, - description: "Select the server containing the sound.", - component: GuildSelector - }, - soundId: { - type: OptionType.COMPONENT, - description: "Enter the ID of the sound you want to play.", - component: SoundIdInput - } -}); - -const SoundButtonContext: NavContextMenuPatchCallback = (children, { sound }: { sound: { soundId: string; guildId: string; }; }) => { - children.splice(1, 0, - - { - settings.store.soundGuildId = sound.guildId; - settings.store.soundId = sound.soundId; - }} - /> - - ); -}; - -let original: typeof ChannelActions.selectVoiceChannel; - -export default definePlugin({ - name: "ExitSounds", - description: "Play soundboard sounds when you disconnect from voice.", - authors: [Devs.prism], - dependencies: ["AudioPlayerAPI"], - settings, - - contextMenus: { - "sound-button-context": SoundButtonContext - }, - - start() { - original = ChannelActions.selectVoiceChannel; - ChannelActions.selectVoiceChannel = async (id: string | null, ...args: unknown[]) => { - const { soundGuildId, soundId } = settings.store; - const voiceId = SelectedChannelStore.getVoiceChannelId(); - - if (soundGuildId && soundId && voiceId !== id && !MediaEngineStore.isDeaf()) { - try { - await RestAPI.post({ - url: Constants.Endpoints.SEND_SOUNDBOARD_SOUND(voiceId), - body: { - sound_id: soundId, - source_guild_id: soundGuildId - } - }); - await new Promise(r => setTimeout(r, 500)); - } catch { - Toasts.show({ - message: "Oops! Something went wrong.", - id: Toasts.genId(), - type: Toasts.Type.FAILURE - }); - } - } - - return original(id, ...args); - }; - }, - - stop() { - ChannelActions.selectVoiceChannel = original; - } -}); diff --git a/src/equicordplugins/exitSounds/styles.css b/src/equicordplugins/exitSounds/styles.css deleted file mode 100644 index 246817838..000000000 --- a/src/equicordplugins/exitSounds/styles.css +++ /dev/null @@ -1,17 +0,0 @@ -.vc-exitsounds-input-row { - display: flex; - gap: 8px; - align-items: center; -} - -.vc-exitsounds-input-wrapper { - flex: 1; -} - -.vc-exitsounds-guild-icon { - width: 24px; - height: 24px; - border-radius: 4px; - margin-right: 8px; - vertical-align: middle; -} diff --git a/src/equicordplugins/fastDeleteChannels/index.tsx b/src/equicordplugins/fastDeleteChannels/index.tsx deleted file mode 100644 index e4b20c200..000000000 --- a/src/equicordplugins/fastDeleteChannels/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Constants, PermissionsBits, PermissionStore, React, RestAPI, useCallback, useEffect, useState } from "@webpack/common"; - -const validKeycodes = [ - "Backspace", "Tab", "Enter", "ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "Pause", "CapsLock", - "Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "PrintScreen", "Insert", - "Delete", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "KeyA", "KeyB", "KeyC", - "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", - "KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ", "MetaLeft", "MetaRight", "ContextMenu", "Numpad0", "Numpad1", "Numpad2", "Numpad3", - "Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "NumpadMultiply", "NumpadAdd", "NumpadSubtract", "NumpadDecimal", - "NumpadDivide", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "NumLock", "ScrollLock", -]; - -function showIcon() { - const [show, setShow] = useState(false); - const { keyBind, reqCtrl, reqShift, reqAlt } = settings.store; - - const handleKeys = useCallback((e: KeyboardEvent) => { - const isMatchingKey = - e.code === keyBind && - (!reqCtrl || e.ctrlKey) && - (!reqShift || e.shiftKey) && - (!reqAlt || e.altKey); - - setShow(isMatchingKey); - }, [keyBind, reqCtrl, reqShift, reqAlt]); - - useEffect(() => { - window.addEventListener("keydown", handleKeys); - window.addEventListener("keyup", handleKeys); - - return () => { - window.removeEventListener("keydown", handleKeys); - window.removeEventListener("keyup", handleKeys); - }; - }, [handleKeys]); - - return show; -} - -// TY ToggleVideoBind -const settings = definePluginSettings({ - keyBind: { - description: "The key to toggle trash when pressed.", - type: OptionType.STRING, - default: "KeyZ", - isValid: (value: string) => validKeycodes.includes(value), - }, - reqCtrl: { - description: "Require control to be held.", - type: OptionType.BOOLEAN, - default: true, - }, - reqShift: { - description: "Require shift to be held.", - type: OptionType.BOOLEAN, - default: true, - }, - reqAlt: { - description: "Require alt to be held.", - type: OptionType.BOOLEAN, - default: false, - }, -}); - -export default definePlugin({ - name: "FastDeleteChannels", - description: "Adds a trash icon to delete channels", - authors: [Devs.thororen], - settings, - patches: [ - // TY TypingIndicator - // Normal Channels - { - find: "UNREAD_IMPORTANT:", - replacement: { - match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/, - replace: "$&,$self.TrashIcon($1)" - } - }, - // Threads - { - find: "18V16H9v2H6Zm3", - replacement: { - match: /mentionsCount:\i.+?null(?<=channel:(\i).+?)/, - replace: "$&,$self.TrashIcon($1)" - } - } - ], - TrashIcon: channel => { - const show = showIcon(); - - if (!show || !PermissionStore.can(PermissionsBits.MANAGE_CHANNELS, channel)) return null; - - return ( - RestAPI.del({ url: Constants.Endpoints.CHANNEL(channel.id) })} - > - - - - - - ); - } -}); diff --git a/src/equicordplugins/followVoiceUser/index.tsx b/src/equicordplugins/followVoiceUser/index.tsx deleted file mode 100644 index 18ef15244..000000000 --- a/src/equicordplugins/followVoiceUser/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { Notice } from "@components/Notice"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Channel, User, VoiceState } from "@vencord/discord-types"; -import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Menu, React, VoiceStateStore } from "@webpack/common"; - -type TFollowedUserInfo = { - lastChannelId: string; - userId: string; -} | null; - -interface UserContextProps { - channel: Channel; - user: User; - guildId?: string; -} - -let followedUserInfo: TFollowedUserInfo = null; - -const voiceChannelAction = findByPropsLazy("selectVoiceChannel"); -const UserStore = findStoreLazy("UserStore"); -const RelationshipStore = findStoreLazy("RelationshipStore"); - -const settings = definePluginSettings({ - onlyWhenInVoice: { - type: OptionType.BOOLEAN, - default: true, - description: "Only follow the user when you are in a voice channel" - }, - leaveWhenUserLeaves: { - type: OptionType.BOOLEAN, - default: false, - description: "Leave the voice channel when the user leaves. (That can cause you to sometimes enter infinite leave/join loop)" - } -}); - -const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { channel, user }: UserContextProps) => { - if (UserStore.getCurrentUser().id === user.id || !RelationshipStore.getFriendIDs().includes(user.id)) return; - - const [checked, setChecked] = React.useState(followedUserInfo?.userId === user.id); - - children.push( - , - { - if (followedUserInfo?.userId === user.id) { - followedUserInfo = null; - setChecked(false); - return; - } - - followedUserInfo = { - lastChannelId: UserStore.getCurrentUser().id, - userId: user.id - }; - setChecked(true); - }} - > - ); -}; - -export default definePlugin({ - name: "FollowVoiceUser", - description: "Follow a friend in voice chat.", - authors: [EquicordDevs.TheArmagan], - settings, - settingsAboutComponent: () => ( - - This Plugin is used to follow a Friend/Friends into voice chat(s). - - ), - flux: { - async VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - if (!followedUserInfo) return; - if (!RelationshipStore.getFriendIDs().includes(followedUserInfo.userId)) return; - - if ( - settings.store.onlyWhenInVoice - && !VoiceStateStore.getVoiceStateForUser(UserStore.getCurrentUser().id) - ) return; - - voiceStates.forEach(voiceState => { - if ( - voiceState.userId === followedUserInfo!.userId - && voiceState.channelId - && voiceState.channelId !== followedUserInfo!.lastChannelId - ) { - followedUserInfo!.lastChannelId = voiceState.channelId; - voiceChannelAction.selectVoiceChannel(followedUserInfo!.lastChannelId); - } else if ( - voiceState.userId === followedUserInfo!.userId - && !voiceState.channelId - && settings.store.leaveWhenUserLeaves - ) { - voiceChannelAction.selectVoiceChannel(null); - } - }); - } - }, - contextMenus: { - "user-context": UserContextMenuPatch - } -}); diff --git a/src/equicordplugins/fontLoader/index.tsx b/src/equicordplugins/fontLoader/index.tsx deleted file mode 100644 index f81c1d9a5..000000000 --- a/src/equicordplugins/fontLoader/index.tsx +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./styles.css"; - -import { definePluginSettings, migratePluginSetting } from "@api/Settings"; -import { Card } from "@components/Card"; -import { HeadingSecondary, HeadingTertiary } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { debounce } from "@shared/debounce"; -import { EquicordDevs } from "@utils/constants"; -import { Margins } from "@utils/margins"; -import { classes } from "@utils/misc"; -import definePlugin, { OptionType } from "@utils/types"; -import { React, TextInput } from "@webpack/common"; - -interface GoogleFontMetadata { - family: string; - displayName: string; - authors: string[]; - category?: number; - popularity?: number; - variants: Array<{ - axes: Array<{ - tag: string; - min: number; - max: number; - }>; - }>; -} - -const createGoogleFontUrl = (family: string, options = "") => - `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}${options}&display=swap`; - -const loadFontStyle = (url: string) => { - document.head.insertAdjacentHTML("beforeend", ``); - return document.createElement("style"); -}; - -async function searchGoogleFonts(query: string) { - try { - const response = await fetch("https://fonts.google.com/$rpc/fonts.fe.catalog.actions.metadata.MetadataService/FontSearch", { - method: "POST", - headers: { - "content-type": "application/json+protobuf", - "x-user-agent": "grpc-web-javascript/0.1" - }, - body: JSON.stringify([[query, null, null, null, null, null, 1], [5], null, 16]) - }); - - const data = await response.json(); - if (!data?.[1]) return []; - return data[1].map(([_, fontData]: [string, any[]]) => ({ - family: fontData[0], - displayName: fontData[1], - authors: fontData[2], - category: fontData[3], - variants: fontData[6].map((variant: any[]) => ({ - axes: variant[0].map(([tag, min, max]: [string, number, number]) => ({ - tag, min, max - })) - })) - })); - } catch (err) { - console.error("Failed to fetch fonts:", err); - return []; - } -} - -const preloadFont = (family: string) => - loadFontStyle(createGoogleFontUrl(family, "&text=The quick brown fox jumps over the lazy dog")); - -let styleElement: HTMLStyleElement | null = null; - -const applyFont = async (fontFamily: string) => { - if (!fontFamily) { - styleElement?.remove(); - styleElement = null; - return; - } - - try { - if (!styleElement) { - styleElement = document.createElement("style"); - document.head.appendChild(styleElement); - } - - loadFontStyle(createGoogleFontUrl(fontFamily, ":wght@300;400;500;600;700")); - styleElement.textContent = ` - * { - --font-primary: '${fontFamily}', sans-serif !important; - --font-display: '${fontFamily}', sans-serif !important; - --font-headline: '${fontFamily}', sans-serif !important; - ${settings.store.applyOnCodeBlocks ? "--font-code: '${fontFamily}', monospace !important;" : ""} - } - `; - } catch (err) { - console.error("Failed to load font:", err); - } -}; - -function GoogleFontSearch({ onSelect }: { onSelect: (font: GoogleFontMetadata) => void; }) { - const [query, setQuery] = React.useState(""); - const [results, setResults] = React.useState([]); - const [loading, setLoading] = React.useState(false); - const previewStyles = React.useRef([]); - - React.useEffect(() => () => { - previewStyles.current.forEach(style => style.remove()); - }, []); - - const debouncedSearch = debounce(async (value: string) => { - setLoading(true); - if (!value) { - setResults([]); - setLoading(false); - return; - } - - const fonts = await searchGoogleFonts(value); - previewStyles.current.forEach(style => style.remove()); - previewStyles.current = await Promise.all(fonts.map(f => preloadFont(f.family))); - setResults(fonts); - setLoading(false); - }, 300); - - const handleSearch = (e: string) => { - setQuery(e); - debouncedSearch(e); - }; - - return ( -
- Search Google Fonts - Click on any font to apply it. - - handleSearch(e)} - placeholder="Search fonts..." - disabled={loading} - className={Margins.bottom16} - /> - - {results.length > 0 && ( -
- {results.map(font => ( - onSelect(font)} - > -
- {font.displayName} - The quick brown fox jumps over the lazy dog -
- {font.authors?.length && ( - - by {font.authors.join(", ")} - - )} -
- ))} -
- )} -
- ); -} - -migratePluginSetting("FontLoader", "applyOnCodeBlocks", "applyOnClodeBlocks"); -const settings = definePluginSettings({ - selectedFont: { - type: OptionType.STRING, - description: "Currently selected font", - default: "", - hidden: true - }, - fontSearch: { - type: OptionType.COMPONENT, - description: "Search and select Google Fonts", - component: () => ( - { - settings.store.selectedFont = font.family; - applyFont(font.family); - }} - /> - ) - }, - applyOnCodeBlocks: { - type: OptionType.BOOLEAN, - description: "Apply the font to code blocks", - default: false - } -}); - -export default definePlugin({ - name: "FontLoader", - description: "Loads any font from Google Fonts", - authors: [EquicordDevs.vmohammad], - settings, - - async start() { - const savedFont = settings.store.selectedFont; - if (savedFont) { - await applyFont(savedFont); - } - }, - - stop() { - if (styleElement) { - styleElement.remove(); - styleElement = null; - } - } -}); diff --git a/src/equicordplugins/fontLoader/styles.css b/src/equicordplugins/fontLoader/styles.css deleted file mode 100644 index 7ca42e5ea..000000000 --- a/src/equicordplugins/fontLoader/styles.css +++ /dev/null @@ -1,23 +0,0 @@ -.eq-googlefonts-results { - max-height: 400px; - overflow-y: auto; -} - -.eq-googlefonts-card { - padding: 16px; - cursor: pointer !important; - background: var(--background-secondary-alt); - border: 1px solid var(--background-base-low); -} - -.eq-googlefonts-card:hover { - background: var(--background-mod-subtle); - cursor: pointer !important; -} - -.eq-googlefonts-preview { - display: flex; - flex-direction: column; - gap: 8px; - cursor: pointer !important; -} \ No newline at end of file diff --git a/src/equicordplugins/frequentQuickSwitcher/index.tsx b/src/equicordplugins/frequentQuickSwitcher/index.tsx deleted file mode 100644 index c2d61a11e..000000000 --- a/src/equicordplugins/frequentQuickSwitcher/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { ChannelStore, UserSettingsActionCreators } from "@webpack/common"; - -function generateSearchResults(query) { - const frequentChannelsWithQuery = Object.entries(UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue().guildAndChannelFrecency.guildAndChannels) - .map(([key, value]) => key) - .filter(id => ChannelStore.getChannel(id) != null) - .filter(id => ChannelStore.getChannel(id).name.includes(query)) - .sort((id1, id2) => { - const channel1 = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue().guildAndChannelFrecency.guildAndChannels[id1]; - const channel2 = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue().guildAndChannelFrecency.guildAndChannels[id2]; - return channel2.totalUses - channel1.totalUses; - }) - .slice(0, 20); - - return frequentChannelsWithQuery.map(channelID => { - const channel = ChannelStore.getChannel(channelID); - return ( - { - "type": "TEXT_CHANNEL", - "record": channel, - "score": 20, - "comparator": query, - "sortable": query - } - ); - }); -} - -export default definePlugin({ - name: "FrequentQuickSwitcher", - description: "Rewrites and filters the quick switcher results to be your most frequent channels", - authors: [Devs.Samwich], - generateSearchResults: generateSearchResults, - patches: [ - { - find: "#{intl::QUICKSWITCHER_PLACEHOLDER}", - replacement: { - match: /let{selectedIndex:\i,results:\i}/, - replace: "this.props.results = $self.generateSearchResults(this.state.query);$&" - }, - } - ] -}); diff --git a/src/equicordplugins/friendTags/index.tsx b/src/equicordplugins/friendTags/index.tsx deleted file mode 100644 index 0f893cdb8..000000000 --- a/src/equicordplugins/friendTags/index.tsx +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "styles.css?managed"; - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { DataStore } from "@api/index"; -import { definePluginSettings } from "@api/Settings"; -import { BaseText } from "@components/BaseText"; -import { Divider } from "@components/Divider"; -import { Devs } from "@utils/constants"; -import { useForceUpdater } from "@utils/react"; -import definePlugin, { OptionType } from "@utils/types"; -import { Button, ChannelStore, Menu, RelationshipStore, TextInput, useEffect, UserStore, useState } from "@webpack/common"; - -interface UserTagData { - tagName: string; - userIds: string[]; -} - -let SavedData: UserTagData[] = []; -const tagStoreName = "vc-friendtags-tags"; - -function parseUsertags(text: string): string[] { - const matches = text.match(/&([^&]+)/g); - if (!matches) return []; - const tags = matches.map(match => match.substring(1).trim()); - return tags.filter(tag => tag !== ""); -} - -function queryFriendTags(query) { - const tags = parseUsertags(query); - if (!tags.length) return []; - - const filteredTagObjects = SavedData - .filter(data => data.tagName.length && data.userIds.length) - .filter(data => tags.some(tag => tag.toLowerCase() === data.tagName.toLowerCase())); - if (!filteredTagObjects.length) return []; - - const users = Array.from(new Set([...ChannelStore.getDMUserIds(), ...RelationshipStore.getFriendIDs()])) - .filter(user => filteredTagObjects.some(tag => tag.userIds.includes(user))); - - return users.map(user => { - const userObject: any = UserStore.getUser(user); - return { - type: "USER", - record: userObject, - score: 20, - comparator: userObject.globalName || userObject.username, - sortable: userObject.globalName || userObject.username - }; - }); -} - -async function SetData() { - const fetchData = await DataStore.get(tagStoreName); - if (SavedData !== fetchData) { - await DataStore.set(tagStoreName, JSON.stringify(SavedData)); - } - return true; -} - -async function GetData() { - const fetchData = await DataStore.get(tagStoreName); - if (!fetchData) { - DataStore.set(tagStoreName, JSON.stringify([])); - SavedData = []; - return; - } - SavedData = JSON.parse(fetchData); -} - -function TagConfigCard(props) { - const { tag } = props; - const [tagName, setTagName] = useState(tag.tagName); - const [userIds, setUserIDs] = useState(tag.userIds.join(", ")); - const update = useForceUpdater(); - - useEffect(() => { - const dataTag = SavedData.find(obj => obj.tagName === tag.tagName); - if (dataTag) { - dataTag.tagName = tagName; - } - SetData(); - update(); - }, [tagName]); - - useEffect(() => { - const dataTag = SavedData.find(obj => obj.userIds === tag.userIds); - if (dataTag) { - dataTag.userIds = userIds.split(", "); - } - SetData(); - update(); - }, [userIds]); - - return ( - <> - Name - - Users (Seperated by comma) - -
- User List (Click A User To Remove) -
- { - userIds.split(", ").map(user => { - const userData: any = UserStore.getUser(user); - if (!userData) return null; - return ( -
- - setUserIDs(userIds.replace(`, ${user}`, "").replace(user, ""))}>{userData.globalName || userData.username} -
- ); - }) - } -
-
- - - ); -} - -function TagConfigurationComponent() { - const update = useForceUpdater(); - - return ( - <> - - { - SavedData?.map(e => ( - <> - - - - )) - } - - - ); -} - -const settings = definePluginSettings({ - tagConfiguration: { - type: OptionType.COMPONENT, - description: "The tag configuration component", - component: () => { - return ( - - ); - } - } -}); - -function UserToTagID(user, tag, remove) { - if (remove) { - SavedData.filter(e => e.tagName === tag)[0].userIds = SavedData.filter(e => e.tagName === tag)[0].userIds.filter(e => e !== user); - } - else { - SavedData.filter(e => e.tagName === tag)[0]?.userIds.push(user); - } - SetData(); -} - -const userPatch: NavContextMenuPatchCallback = (children, { user }) => { - const buttonElement = - - {SavedData.map(tag => { - const isTagged = SavedData.filter(e => e.tagName === tag.tagName)[0].userIds.includes(user.id); - - return ( - { UserToTagID(user.id, tag.tagName, isTagged); }} - /> - ); - })} - ; - - children.push({ ...buttonElement }); -}; - -export default definePlugin({ - name: "FriendTags", - description: "Allows you to filter by custom tags in the quick switcher by starting a search with &", - authors: [Devs.Samwich], - settings, - contextMenus: { - "user-context": userPatch - }, - patches: [ - { - find: "#{intl::QUICKSWITCHER_PLACEHOLDER}", - replacement: { - match: /let{selectedIndex:\i,results:\i}/, - replace: "if(this.state.query.includes(\"&\")){ this.props.results = $self.queryFriendTags(this.state.query); }$&" - }, - } - ], - async start() { - GetData(); - }, - queryFriendTags, -}); diff --git a/src/equicordplugins/friendTags/styles.css b/src/equicordplugins/friendTags/styles.css deleted file mode 100644 index c214d5d9c..000000000 --- a/src/equicordplugins/friendTags/styles.css +++ /dev/null @@ -1,11 +0,0 @@ -.vc-permviewer-user-header-container { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.vc-permviewer-user-header-btns { - display: flex; - gap: 8px; -} \ No newline at end of file diff --git a/src/equicordplugins/friendshipRanks/index.tsx b/src/equicordplugins/friendshipRanks/index.tsx deleted file mode 100644 index 9294c9f61..000000000 --- a/src/equicordplugins/friendshipRanks/index.tsx +++ /dev/null @@ -1,154 +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 { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges"; -import { Badges } from "@api/index"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Flex } from "@components/Flex"; -import { Paragraph } from "@components/Paragraph"; -import { Devs } from "@utils/constants"; -import { classNameFactory } from "@utils/css"; -import { ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import definePlugin from "@utils/types"; -import { Forms, RelationshipStore } from "@webpack/common"; - -interface rankInfo { - title: string; - description: string; - requirement: number; - iconSrc: string; -} - -const cl = classNameFactory("vc-friendship-ranks-"); - -function daysSince(dateString: string): number { - const date = new Date(dateString); - const currentDate = new Date(); - - const differenceInMs = currentDate.getTime() - date.getTime(); - - const days = differenceInMs / (1000 * 60 * 60 * 24); - - return Math.floor(days); -} - -const ranks: rankInfo[] = - [ - { - title: "Sprout", - description: "Your friendship is just starting", - requirement: 0, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/sprout.png" - }, - { - title: "Blooming", - description: "Your friendship is getting there! (1 Month)", - requirement: 30, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/blooming.png" - }, - { - title: "Burning", - description: "Your friendship has reached terminal velocity (3 Months)", - requirement: 90, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/burning.png" - }, - { - title: "Fighter", - description: "Your friendship is strong (6 Months)", - requirement: 182.5, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/fighter.png" - }, - { - title: "Star", - description: "Your friendship has been going on for a WHILE (1 Year)", - requirement: 365, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/star.png" - }, - { - title: "Royal", - description: "Your friendship has gone through thick and thin- a whole 2 years!", - requirement: 730, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/royal.png" - }, - { - title: "Besties", - description: "How do you even manage this??? (5 Years)", - requirement: 1826.25, - iconSrc: "https://equicord.org/assets/plugins/friendshipRanks/besties.png" - } - ]; - -function openRankModal(rank: rankInfo) { - openModal(props => ( - - - - - - rank icon - {rank.title} - - - - -
- - {rank.description} - -
-
-
-
- )); -} - -function shouldShowBadge(userId: string, requirement: number, index: number) { - if (!RelationshipStore.isFriend(userId)) return false; - - const days = daysSince(RelationshipStore.getSince(userId)); - - if (ranks[index + 1] == null) return days > requirement; - - return (days > requirement && days < ranks[index + 1].requirement); -} - -function getBadgesToApply() { - const badgesToApply: ProfileBadge[] = ranks.map((rank, index) => { - return ({ - description: rank.title, - iconSrc: rank.iconSrc, - position: BadgePosition.END, - onClick: () => openRankModal(rank), - shouldShow: (info: BadgeUserArgs) => shouldShowBadge(info.userId, rank.requirement, index), - props: { - style: { - borderRadius: "50%", - transform: "scale(0.9)" - } - }, - }); - }); - - return badgesToApply; -} - -export default definePlugin({ - name: "FriendshipRanks", - description: "Adds badges showcasing how long you have been friends with a user for", - authors: [Devs.Samwich], - start() { - getBadgesToApply().forEach(b => Badges.addProfileBadge(b)); - - }, - stop() { - getBadgesToApply().forEach(b => Badges.removeProfileBadge(b)); - }, -}); diff --git a/src/equicordplugins/friendshipRanks/styles.css b/src/equicordplugins/friendshipRanks/styles.css deleted file mode 100644 index b331f601e..000000000 --- a/src/equicordplugins/friendshipRanks/styles.css +++ /dev/null @@ -1,19 +0,0 @@ -.vc-friendship-ranks-flex { - width: 100%; - justify-content: center; - height: 18px; -} - -.vc-friendship-ranks-img { - display: flex; - width: 100%; - text-align: center; - place-content: center center; - margin: 0; - gap: 4px; -} - -.vc-friendship-ranks-text { - padding: 1em; - text-align: center; -} \ No newline at end of file diff --git a/src/equicordplugins/ghosted/index.tsx b/src/equicordplugins/ghosted/index.tsx index 845a34f6b..13c1b167d 100644 --- a/src/equicordplugins/ghosted/index.tsx +++ b/src/equicordplugins/ghosted/index.tsx @@ -7,179 +7,197 @@ import "./styles.css"; import { findGroupChildrenByChildId } from "@api/ContextMenu"; -import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; +import { + addServerListElement, + removeServerListElement, + ServerListRenderPosition, +} from "@api/ServerList"; import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs, EquicordDevs } from "@utils/constants"; import { classNameFactory } from "@utils/css"; import { closeModal, openModal } from "@utils/modal"; import definePlugin, { OptionType } from "@utils/types"; -import { Channel } from "@vencord/discord-types"; +import type { Channel } from "@vencord/discord-types"; import { Menu, Tooltip, useEffect, useState } from "@webpack/common"; -import { Boo, clearChannelFromGhost, getBooCount, getGhostedChannels, onBooCountChange } from "./Boo"; -import { getChannelDisplayName, GhostedUsersModal } from "./GhostedUsersModal"; +import { + Boo, + clearChannelFromGhost, + getBooCount, + getGhostedChannels, + onBooCountChange, +} from "./Boo"; +import { getChannelDisplayName,GhostedUsersModal } from "./GhostedUsersModal"; import { IconGhost } from "./IconGhost"; export const cl = classNameFactory("vc-boo-"); export const settings = definePluginSettings({ - showIndicator: { - type: OptionType.BOOLEAN, - description: "Show the ghost counter at the top of the server list", - default: true, - restartNeeded: false - }, - showDmIcons: { - type: OptionType.BOOLEAN, - description: "Show ghost icons next to individual DMs", - default: true, - restartNeeded: false - }, - ignoreGroupDms: { - type: OptionType.BOOLEAN, - description: "Exclude all group dms from ghosting", - default: false - }, - exemptedChannels: { - type: OptionType.STRING, - description: "Comma-separated list of channel IDs to exempt from ghosting (right-click a DM channel to copy its ID)", - default: "", - restartNeeded: false - }, - ignoreBots: { - type: OptionType.BOOLEAN, - description: "Ignore DMs from bots", - default: true, - restartNeeded: false - } + showIndicator: { + type: OptionType.BOOLEAN, + description: "Show the ghost counter at the top of the server list", + default: true, + restartNeeded: false, + }, + showDmIcons: { + type: OptionType.BOOLEAN, + description: "Show ghost icons next to individual DMs", + default: true, + restartNeeded: false, + }, + ignoreGroupDms: { + type: OptionType.BOOLEAN, + description: "Exclude all group dms from ghosting", + default: false, + }, + exemptedChannels: { + type: OptionType.STRING, + description: + "Comma-separated list of channel IDs to exempt from ghosting (right-click a DM channel to copy its ID)", + default: "", + restartNeeded: false, + }, + ignoreBots: { + type: OptionType.BOOLEAN, + description: "Ignore DMs from bots", + default: true, + restartNeeded: false, + }, }); function BooIndicator() { - const [count, setCount] = useState(getBooCount()); - const [showJumpscare, setShowJumpscare] = useState(false); + const [count, setCount] = useState(getBooCount()); - useEffect(() => { - const unsubscribe = onBooCountChange(newCount => { - setCount(newCount); - }); + useEffect(() => { + const unsubscribe = onBooCountChange(newCount => { + setCount(newCount); + }); - return () => { - unsubscribe(); - }; - }, []); + return () => { + unsubscribe(); + }; + }, []); - if (!settings.store.showIndicator && !showJumpscare) return null; + if (!settings.store.showIndicator) return null; - const handleClick = () => { - const ghostedChannels = getGhostedChannels(); - const modalKey = openModal(modalProps => ( - - closeModal(modalKey)} - onClearGhost={clearChannelFromGhost} - /> - - )); - }; + const handleClick = () => { + const ghostedChannels = getGhostedChannels(); + const modalKey = openModal(modalProps => ( + + closeModal(modalKey)} + onClearGhost={clearChannelFromGhost} + /> + + )); + }; - const getTooltipText = () => { - const ghostedChannels = getGhostedChannels(); - if (ghostedChannels.length === 0) { - return "No Ghosted Users"; - } - if (ghostedChannels.length <= 5) { - return ghostedChannels - .map(id => getChannelDisplayName(id)) - .join(", "); - } - return `${ghostedChannels.length} Ghosted Users`; - }; + const getTooltipText = () => { + const ghostedChannels = getGhostedChannels(); + if (ghostedChannels.length === 0) { + return "No Ghosted Users"; + } + if (ghostedChannels.length <= 5) { + return ghostedChannels.map(id => getChannelDisplayName(id)).join(", "); + } + return `${ghostedChannels.length} Ghosted Users`; + }; - return ( - <> - {settings.store.showIndicator && getGhostedChannels().length > 0 && ( -
- - {({ onMouseEnter, onMouseLeave }) => ( -
- {count} -
- )} -
-
- )} - - ); + return ( + <> + {settings.store.showIndicator && getGhostedChannels().length > 0 && ( +
+ + {({ onMouseEnter, onMouseLeave }) => ( +
+ {count} +
+ )} +
+
+ )} + + ); } function makeContextItem(props) { - return { - clearChannelFromGhost(props.channel.id); - }} - />; + return ( + { + clearChannelFromGhost(props.channel.id); + }} + /> + ); } export default definePlugin({ - name: "Ghosted", - description: "A cute ghost will appear if you don't answer their DMs", - authors: [EquicordDevs.vei, Devs.sadan, EquicordDevs.justjxke, EquicordDevs.iamme], - settings, - dependencies: ["AudioPlayerAPI", "ServerListAPI"], - contextMenus: { - "gdm-context": (menuItems, props) => { - const group = findGroupChildrenByChildId("leave", menuItems, true); - group?.unshift(makeContextItem(props)); - }, - "user-context": (menuItems, props) => { - const group = findGroupChildrenByChildId("close-dm", menuItems); - group?.push(makeContextItem(props)); - } - }, + name: "Ghosted", + description: "A cute ghost will appear if you don't answer their DMs", + authors: [ + EquicordDevs.vei, + Devs.sadan, + EquicordDevs.justjxke, + EquicordDevs.iamme, + ], + settings, + dependencies: ["AudioPlayerAPI", "ServerListAPI"], + contextMenus: { + "gdm-context": (menuItems, props) => { + const group = findGroupChildrenByChildId("leave", menuItems, true); + group?.unshift(makeContextItem(props)); + }, + "user-context": (menuItems, props) => { + const group = findGroupChildrenByChildId("close-dm", menuItems); + group?.push(makeContextItem(props)); + }, + }, - patches: [ - { - find: "PrivateChannel.renderAvatar", - replacement: { - match: /\]:\i\|\|\i.{0,50}children:\[/, - replace: "$&$self.renderBoo(arguments[0])," - } - }, - ], + patches: [ + { + find: "PrivateChannel.renderAvatar", + replacement: { + match: /\]:i\|\|i.{0,50}children:\[/, + replace: "$&$self.renderBoo(arguments[0]),", + }, + }, + ], - renderBoo(props: { channel: Channel; }) { - return ( - - - - ); - }, + renderBoo(props: { channel: Channel }) { + return ( + + + + ); + }, - renderIndicator() { - return ( - - - - ); - }, + renderIndicator() { + return ( + + + + ); + }, - start() { - addServerListElement(ServerListRenderPosition.Above, this.renderIndicator); - }, + start() { + addServerListElement(ServerListRenderPosition.Above, this.renderIndicator); + }, - stop() { - removeServerListElement(ServerListRenderPosition.Above, this.renderIndicator); - }, + stop() { + removeServerListElement( + ServerListRenderPosition.Above, + this.renderIndicator, + ); + }, }); diff --git a/src/equicordplugins/ghosted/styles.css b/src/equicordplugins/ghosted/styles.css index bb5d05bbf..a1db4c98f 100644 --- a/src/equicordplugins/ghosted/styles.css +++ b/src/equicordplugins/ghosted/styles.css @@ -53,93 +53,6 @@ gap: 2px; } -/* Jumpscare overlay styling */ -.vc-boo-jumpscare-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgb(0 0 0 / 50%); - display: flex; - align-items: center; - justify-content: center; - z-index: 9999; - cursor: pointer; -} - -.vc-boo-jumpscare-image { - max-width: 90%; - max-height: 90%; - object-fit: contain; -} - -/* job application */ -@keyframes boo-jumpscare-fade { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes boo-jumpscare-fade-out { - from { - opacity: 1; - } - - to { - opacity: 0; - } -} - -@keyframes boo-jumpscare-shake { - - /* stylelint-disable-next-line rule-empty-line-before */ - 0%, - 100% { - transform: translate(0, 0) rotate(0deg); - } - - 10% { - transform: translate(-5px, -5px) rotate(-2deg); - } - - 20% { - transform: translate(5px, 5px) rotate(2deg); - } - - 30% { - transform: translate(-5px, 5px) rotate(-1deg); - } - - 40% { - transform: translate(5px, -5px) rotate(1deg); - } - - 50% { - transform: translate(-3px, -3px) rotate(-1deg); - } - - 60% { - transform: translate(3px, 3px) rotate(1deg); - } - - 70% { - transform: translate(-3px, 3px) rotate(-0.5deg); - } - - 80% { - transform: translate(3px, -3px) rotate(0.5deg); - } - - 90% { - transform: translate(-2px, -2px) rotate(-0.5deg); - } -} - .vc-boo-clear { margin-right: 16px; } diff --git a/src/equicordplugins/hopOn/index.tsx b/src/equicordplugins/hopOn/index.tsx deleted file mode 100644 index cd0de30ed..000000000 --- a/src/equicordplugins/hopOn/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Message } from "@vencord/discord-types"; -import { RelationshipStore, SelectedChannelStore } from "@webpack/common"; - -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - channelId: string; - message: Message; -} - -const settings = definePluginSettings({ - regex: { - type: OptionType.STRING, - description: "Regex to trigger on", - default: "hop on (?:fortnite|fn)" - }, - url: { - type: OptionType.STRING, - description: "URL to open", - default: "com.epicgames.launcher://apps/fn%3A4fe75bbc5a674f4f9b356b5c90567da5%3AFortnite?action=launch&silent=true" - } -}); -export default definePlugin({ - name: "HopOn", - description: "Hop on! Opens a configurable URL when a message matches a custom regex in the current channel.", - authors: [Devs.ImLvna], - settings, - flux: { - async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (RelationshipStore.isBlocked(message.author?.id)) return; - if (channelId !== SelectedChannelStore.getChannelId()) return; - if (!message.content?.match(new RegExp(settings.store.regex, "i"))) return; - - VencordNative.native.openExternal(settings.store.url); - } - } -}); diff --git a/src/equicordplugins/husk/index.tsx b/src/equicordplugins/husk/index.tsx deleted file mode 100644 index 25aad8e37..000000000 --- a/src/equicordplugins/husk/index.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* eslint-disable simple-header/header */ - -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 your mom lol - * - * 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 } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import { classes } from "@utils/misc"; -import definePlugin, { OptionType } from "@utils/types"; -import { ChannelStore, EmojiStore, RestAPI } from "@webpack/common"; -import type { SVGProps } from "react"; -// eslint-disable-next-line no-duplicate-imports -import { PropsWithChildren } from "react"; - -interface BaseIconProps extends IconProps { - viewBox: string; -} - -interface IconProps extends SVGProps { - className?: string; - height?: string | number; - width?: string | number; -} - -function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren) { - return ( - - {children} - - ); -} - -export function Husk(props: IconProps) { - return ( - - - - ); -} - -const settings = definePluginSettings({ - findInServer: { - description: "Attempt to find emoji of same name in server before using ID in settings (useful if no nitro)", - type: OptionType.BOOLEAN, - default: true - }, - emojiName: { - description: "Emoji name (default (from Vencord Server): husk)", - type: OptionType.STRING, - default: "husk" - }, - emojiID: { - description: "Emoji ID (default (from Vencord Server): 1026532993923293184)", - type: OptionType.BIGINT, - default: 1026532993923293184n - } -}); - -function getEmojiIdThatShouldBeUsed(guildId: string) { - if (!settings.store.findInServer || guildId === "") return settings.store.emojiID; - let id = ""; - EmojiStore.getGuildEmoji(guildId).forEach(emoji => { - if (emoji.name === settings.store.emojiName) { - id = emoji.id; - } - }); - return id !== "" ? id : settings.store.emojiID; -} - -export default definePlugin({ - name: "Husk", - description: "Adds Husk button (check settings to change used emoji)", - authors: [Devs.nin0dev], - settings, - messagePopoverButton: { - icon: Husk, - render(msg) { - return { - label: "Husk", - icon: Husk, - message: msg, - channel: ChannelStore.getChannel(msg.channel_id), - onClick: () => { - const guildId = ChannelStore.getChannel(msg.channel_id).guild_id !== null ? ChannelStore.getChannel(msg.channel_id).guild_id : ""; - RestAPI.put({ - url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${settings.store.emojiName}:${getEmojiIdThatShouldBeUsed(guildId)}/@me` - } - ); - } - }; - }, - } -}); diff --git a/src/equicordplugins/iRememberYou/components/data.tsx b/src/equicordplugins/iRememberYou/components/data.tsx deleted file mode 100644 index 4d75730bb..000000000 --- a/src/equicordplugins/iRememberYou/components/data.tsx +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { DataStore } from "@api/index"; -import { Guild, User } from "@vencord/discord-types"; -import { ChannelStore, GuildMemberStore, GuildStore, InviteActions, MessageStore, PermissionsBits, PermissionStore, UserStore, } from "@webpack/common"; - -export interface IUserExtra { - isOwner?: boolean; - updatedAt?: number; -} - -export interface IStorageUser { - id: string; - username: string, - tag: string, - iconURL?: string; - extra?: IUserExtra; -} - -export interface GroupData { - id: string; - users: { [key: string]: IStorageUser; }; - name: string; - inviteLink?: string; -} - -export class Data { - declare usersCollection: Record; - declare _storageAutoSaveProtocol_interval; - declare _onMessagePreSend_preSend; - - withStart() { - return this; - } - - onMessagePreSend(channelId, message, extra) { - const target: Set<{ user: User; source?: Guild, extra: IUserExtra; }> = new Set(); - const now = Date.now(); - const { replyOptions } = extra; - - const guild = (() => { - const channel = ChannelStore.getChannel(channelId); - return GuildStore.getGuild(channel.guild_id) || undefined; - })(); - - if (replyOptions.messageReference) { - const { channel_id, message_id } = replyOptions.messageReference; - const message = MessageStore.getMessage(channel_id, message_id); - if (!message) { - return; - } - const { author } = message; - - target.add({ user: author, source: guild, extra: { updatedAt: now } }); - } - - if (message.content) { - const { content } = message; - const ids = [...content.matchAll(/<@!?(?\d{17,23})>/g)].map( - ({ groups }) => groups.id - ); - - const users = ids - .map(id => UserStore.getUser(id)) - .filter(Boolean); - for (const user of users) { - target.add({ user, source: guild, extra: { updatedAt: now } }); - } - } - - this.processUsersToCollection([...target]); - } - - async processUsersToCollection( - array: { user: User; source?: Guild; extra?: IUserExtra; }[] - ) { - const target = this.usersCollection; - const processedGuilds = new Set(); - - for (const { user, source, extra } of array) { - if (user.bot) { - continue; - } - - const groupKey = source?.id ?? "dm"; - const group = (target[groupKey] ||= { - name: source?.name || "dm", - id: source?.id || user.id, - users: {}, - inviteLink: undefined - }); - const usersField = group.users; - const previouExtra = usersField[user.id]?.extra ?? {}; - const { id, username } = user; - - usersField[id] = { - id, - username, - tag: user.discriminator === "0" ? user.username : user.tag, - extra: { ...previouExtra, ...extra }, - iconURL: user.getAvatarURL(), - }; - - if (source && !processedGuilds.has(source.id) && this.hasInvitePermissions(source.id)) { - this.collectInviteLink(source.id); - processedGuilds.add(source.id); - } - } - } - - async updateStorage() { - await DataStore.set("irememberyou.data", this.usersCollection); - } - - async initializeUsersCollection() { - const data = await DataStore.get("irememberyou.data"); - this.usersCollection = data ?? {}; - } - - writeMembersFromUserGuildsToCollection() { - const target: Set<{ user: User; source?: Guild, extra: IUserExtra; }> = - new Set(); - - const now = Date.now(); - const LIMIT = 1_000; - - const clientId = UserStore.getCurrentUser().id; - if (!clientId) { - return; - } - for (const guild of Object.values(GuildStore.getGuilds())) { - const { ownerId } = guild; - if (ownerId !== clientId) { - continue; - } - - const members = GuildMemberStore.getMembers(guild.id); - if (members.length > LIMIT) { - members.length = LIMIT; - } - for (const member of members) { - const user = UserStore.getUser(member.userId); - target.add({ user, source: guild, extra: { updatedAt: now } }); - } - - this.processUsersToCollection([...target]); - - if (guild && this.hasInvitePermissions(guild.id)) { - this.collectInviteLink(guild.id); - } - } - } - - writeGuildsOwnersToCollection() { - const target: Set<{ user: User; source?: Guild; extra: IUserExtra; }> = - new Set(); - const now = Date.now(); - - for (const guild of Object.values(GuildStore.getGuilds())) { - const { ownerId } = guild; - const owner = UserStore.getUser(ownerId); - if (!owner) { - continue; - } - target.add({ - user: owner, - source: guild, - extra: { isOwner: true, updatedAt: now }, - }); - } - - this.processUsersToCollection([...target]); - } - - hasInvitePermissions(guildId: string): boolean { - const guild = GuildStore.getGuild(guildId); - if (!guild) return false; - - const currentUser = UserStore.getCurrentUser(); - if (!currentUser) return false; - - if (guild.ownerId === currentUser.id) return true; - - const member = GuildMemberStore.getMember(guildId, currentUser.id); - if (!member) return false; - - return PermissionStore.can(PermissionsBits.CREATE_INSTANT_INVITE, guild); - } - - async collectInviteLink(guildId: string) { - try { - const invites = await InviteActions.getInvites(guildId); - if (invites && invites.length > 0) { - const invite = invites[0]; - const inviteCode = invite.code; - const inviteLink = `https://discord.gg/${inviteCode}`; - - if (this.usersCollection[guildId]) { - this.usersCollection[guildId].inviteLink = inviteLink; - } - } - } catch (error) { - console.warn("Failed to collect invite link for guild:", guildId, error); - } - } - - storageAutoSaveProtocol() { - this._storageAutoSaveProtocol_interval = setInterval( - this.updateStorage.bind(this), - 60_000 * 3 - ); - } -} diff --git a/src/equicordplugins/iRememberYou/components/ui.tsx b/src/equicordplugins/iRememberYou/components/ui.tsx deleted file mode 100644 index ba79e6da6..000000000 --- a/src/equicordplugins/iRememberYou/components/ui.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { BaseText } from "@components/BaseText"; -import { Card } from "@components/Card"; -import { HeadingPrimary } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { SettingsTab, wrapTab } from "@components/settings"; -import { classNameFactory } from "@utils/css"; -import { copyWithToast, openUserProfile } from "@utils/discord"; -import { Avatar, Clickable, ContextMenuApi, Menu, React, TextInput, Tooltip, UserStore } from "@webpack/common"; - -import { Data, IStorageUser } from "./data"; - -const cl = classNameFactory("vc-i-remember-you-"); - -function tooltipText(user: IStorageUser) { - const { updatedAt } = user.extra || {}; - const updatedAtContent = updatedAt ? new Intl.DateTimeFormat().format(updatedAt) : null; - return `${user.username ?? user.tag}, updated at ${updatedAtContent}`; -} - -function UsersCollectionRows({ usersCollection }: { usersCollection: Data["usersCollection"]; }) { - if (Object.keys(usersCollection).length === 0) return ( - - It's empty right now - - ); - - return ( - <> - {Object.entries(usersCollection) - .map(([_key, { users, name }]) => ({ name, users: Object.values(users) })) - .sort((a, b) => b.users.length - a.users.length) - .map(({ name, users }) => ( - - ))} - - ); -} - -function UserRow({ user, allowOwner = true }: { user: IStorageUser, allowOwner?: boolean; }) { - return ( -
-
- openUserProfile(user.id)}> - - - - -
- - {props => - - {user.tag} {allowOwner && user.extra?.isOwner && "(owner)"} - - } - - { - e.preventDefault(); - const userObj = UserStore.getUser(user.id); - if (userObj) { - ContextMenuApi.openContextMenu(e, () => ( - - copyWithToast(user.id, "User ID copied to clipboard")} - /> - openUserProfile(user.id)} - /> - - )); - } - }} - > - copyWithToast(user.id, "User ID copied to clipboard")}> - - {user.id} - - - -
-
-
- ); -} - -function SearchElement({ usersCollection }: { usersCollection: Data["usersCollection"]; }) { - const [current, setCurrent] = React.useState(""); - const list = Object.values(usersCollection).flatMap(col => Object.values(col.users)) as IStorageUser[]; - - return ( -
- - {current && ( -
- {list.filter(user => user.tag.includes(current) || user.username.includes(current)) - .map(user => )} -
- )} -
- ); -} - -export function DataUI({ usersCollection }: { usersCollection: Data["usersCollection"]; }) { - return ( - - - - Provides a list of users you have mentioned or replied to, or those who own the servers you belong to (owner*), or are members of your guild - - - -
- -
-
- ); -} - -export default wrapTab(DataUI, "IRememberYouTab"); diff --git a/src/equicordplugins/iRememberYou/index.tsx b/src/equicordplugins/iRememberYou/index.tsx deleted file mode 100644 index 8b4614331..000000000 --- a/src/equicordplugins/iRememberYou/index.tsx +++ /dev/null @@ -1,55 +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 { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { EyeIcon } from "@components/Icons"; -import SettingsPlugin from "@plugins/_core/settings"; -import { EquicordDevs } from "@utils/constants"; -import { removeFromArray } from "@utils/misc"; -import definePlugin from "@utils/types"; - -import { Data } from "./components/data"; -import DataUI from "./components/ui"; - -export default definePlugin({ - name: "IRememberYou", - description: "Locally saves everyone you've been communicating with (including servers), in case of lose", - authors: [EquicordDevs.zoodogood, EquicordDevs.keircn], - dependencies: ["MessageEventsAPI"], - - patches: [], - - async start() { - SettingsPlugin.customEntries.push({ - key: "equicord_i_remember_you", - title: "I Remember You", - Component: () => , - Icon: EyeIcon - }); - SettingsPlugin.settingsSectionMap.push(["EquicordIRememberYou", "equicord_i_remember_you"]); - - const data = (this.dataManager = await new Data().withStart()); - - await data.initializeUsersCollection(); - data.writeGuildsOwnersToCollection(); - data.writeMembersFromUserGuildsToCollection(); - data._onMessagePreSend_preSend = addMessagePreSendListener( - data.onMessagePreSend.bind(data) - ); - data.storageAutoSaveProtocol(); - }, - - stop() { - removeFromArray(SettingsPlugin.customEntries, e => e.key === "equicord_i_remember_you"); - removeFromArray(SettingsPlugin.settingsSectionMap, entry => entry[1] === "equicord_i_remember_you"); - - const dataManager = this.dataManager as Data; - removeMessagePreSendListener(dataManager._onMessagePreSend_preSend); - clearInterval(dataManager._storageAutoSaveProtocol_interval); - }, -}); diff --git a/src/equicordplugins/iRememberYou/styles.css b/src/equicordplugins/iRememberYou/styles.css deleted file mode 100644 index 3a0b283ba..000000000 --- a/src/equicordplugins/iRememberYou/styles.css +++ /dev/null @@ -1,78 +0,0 @@ -.vc-i-remember-you-header-container { - display: contents; - justify-content: space-between; - align-items: center; - flex-direction: column; -} - -.vc-i-remember-you-header-name { - margin-top: 16px; -} - -.vc-i-remember-you-header-btns { - display: grid; - grid-template-columns: repeat(2, minmax(0, 350px)); - gap: 10px; - margin-top: 8px; -} - -.vc-i-remember-you-header { - color: var(--text-default); -} - -.vc-i-remember-you-rows { - gap: 5px; - flex-direction: column; -} - -.vc-i-remember-you-search { - margin-top: 10px; -} - -.vc-i-remember-you-search-user { - flex-direction: column; - padding-top: 18px; -} - -.vc-i-remember-you-user-avatar { - cursor: pointer; - margin-right: 0; -} - -.vc-i-remember-you-user-row { - display: flex; - flex-direction: column; - gap: 6px; - padding: 4px; - align-items: flex-start; -} - -.vc-i-remember-you-user-id { - align-self: flex-start; - opacity: 0.75; - height: min-content; - cursor: pointer; -} - -.vc-i-remember-you-user-id:hover { - opacity: 1; -} - -.vc-i-remember-you-user { - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - width: 100%; - margin-top: 0; -} - -.vc-i-remember-you-user-username { - margin-top: 2px; -} - -.vc-i-remember-you-user-tooltip { - display: flex; - flex-direction: column; - align-items: flex-start; -} \ No newline at end of file diff --git a/src/equicordplugins/idleAutoRestart/index.tsx b/src/equicordplugins/idleAutoRestart/index.tsx deleted file mode 100644 index 2112f671f..000000000 --- a/src/equicordplugins/idleAutoRestart/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType } from "@utils/types"; -import { Menu, VoiceStateStore } from "@webpack/common"; - -const logger = new Logger("IdleAutoRestart"); -let lastActivity = 0; -let intervalId: ReturnType | null = null; - -const settings = definePluginSettings({ - isEnabled: { - description: "Enable automatic restart after idle", - type: OptionType.BOOLEAN, - default: true, - }, - idleMinutes: { - description: "Minutes of inactivity before restarting (when not in VC)", - type: OptionType.SLIDER, - markers: [5, 10, 15, 30, 60, 120], - default: 30, - stickToMarkers: false, - }, -}); - -function onActivity() { - lastActivity = Date.now(); -} - -export default definePlugin({ - name: "IdleAutoRestart", - description: "Automatically restarts the client after being idle for a configurable amount of time, but avoids restarting while you are in VC.", - authors: [EquicordDevs.SteelTech], - settings, - - toolboxActions() { - return ( - { - settings.store.isEnabled = !settings.store.isEnabled; - }} - /> - ); - }, - - start() { - lastActivity = Date.now(); - - document.addEventListener("mousemove", onActivity); - document.addEventListener("keydown", onActivity); - document.addEventListener("mousedown", onActivity); - document.addEventListener("wheel", onActivity, { passive: true }); - - if (intervalId) clearInterval(intervalId); - intervalId = setInterval(() => { - if (!settings.store.isEnabled || VoiceStateStore.isCurrentClientInVoiceChannel()) return; - - const idleMs = settings.store.idleMinutes * 60_000; - if (Date.now() - lastActivity >= idleMs) { - logger.info("Idle timeout reached, reloading client"); - location.reload(); - } - }, 30_000); - }, - - stop() { - if (intervalId) { - clearInterval(intervalId); - intervalId = null; - } - - document.removeEventListener("mousemove", onActivity); - document.removeEventListener("keydown", onActivity); - document.removeEventListener("mousedown", onActivity); - document.removeEventListener("wheel", onActivity); - }, -}); diff --git a/src/equicordplugins/inRole/RoleMembersModal.tsx b/src/equicordplugins/inRole/RoleMembersModal.tsx deleted file mode 100644 index 6483b105b..000000000 --- a/src/equicordplugins/inRole/RoleMembersModal.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import ErrorBoundary from "@components/ErrorBoundary"; -import { Paragraph } from "@components/Paragraph"; -import { classNameFactory } from "@utils/css"; -import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { GuildMember } from "@vencord/discord-types"; -import { Parser } from "@webpack/common"; - -const cl = classNameFactory("vc-inrole-"); - -export function showInRoleModal(members: GuildMember[], roleId: string, channelId: string) { - openModal(props => - <> - - - - Members of role { - Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId }) - } ({members.length}) - - - -
- { - members.length !== 0 ? members.map(member => - <> - - {Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })} - - - ) : Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users! - } -
-
-
-
- - ); -} diff --git a/src/equicordplugins/inRole/index.tsx b/src/equicordplugins/inRole/index.tsx deleted file mode 100644 index 755f9219c..000000000 --- a/src/equicordplugins/inRole/index.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 nin0dev - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./style.css"; - -import { ApplicationCommandInputType, ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; -import { getUserSettingLazy } from "@api/UserSettings"; -import { InfoIcon } from "@components/Icons"; -import { Paragraph } from "@components/Paragraph"; -import { Devs } from "@utils/constants"; -import { getCurrentChannel, getCurrentGuild } from "@utils/discord"; -import definePlugin from "@utils/types"; -import { GuildMember } from "@vencord/discord-types"; -import { GuildMemberStore, GuildRoleStore, Menu, Parser } from "@webpack/common"; - -import { showInRoleModal } from "./RoleMembersModal"; - -const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!; - -function getMembersInRole(roleId: string, guildId: string) { - const members = GuildMemberStore.getMembers(guildId); - const membersInRole: GuildMember[] = []; - members.forEach(member => { - if (member.roles.includes(roleId)) { - membersInRole.push(member); - } - }); - return membersInRole; -} - -export default definePlugin({ - name: "InRole", - description: "Know who is in a role with the role context menu or /inrole command (read plugin info!)", - authors: [Devs.nin0dev], - dependencies: ["UserSettingsAPI"], - start() { - // DeveloperMode needs to be enabled for the context menu to be shown - DeveloperMode.updateSetting(true); - }, - settingsAboutComponent: () => { - return ( - <> - {Parser.parse(":warning:")} Limitations - If you don't have mod permissions on the server, and that server is large (over 100 members), the plugin may be limited in the following ways: - • Offline members won't be listed - • Up to 100 members will be listed by default. To get more, scroll down in the member list to load more members. - • However, friends will always be shown regardless of their status. - - ); - }, - - commands: [ - { - name: "inrole", - description: "Know who is in a role", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "role", - description: "The role", - type: ApplicationCommandOptionType.ROLE, - required: true - }, - ], - execute: (args, ctx) => { - // Guild check - if (!ctx.guild) { - return sendBotMessage(ctx.channel.id, { content: "Make sure that you are in a server." }); - } - const role = args[0].value; - showInRoleModal(getMembersInRole(role, ctx.guild.id), role, ctx.channel.id); - } - } - ], - contextMenus: { - "dev-context"(children, { id }: { id: string; }) { - const guild = getCurrentGuild(); - if (!guild) return; - - const channel = getCurrentChannel(); - if (!channel) return; - - const role = GuildRoleStore.getRole(guild.id, id); - if (!role) return; - - children.push( - { - showInRoleModal(getMembersInRole(role.id, guild.id), role.id, channel.id); - }} - icon={InfoIcon} - /> - ); - }, - "message"(children, { message }: { message: any; }) { - const guild = getCurrentGuild(); - if (!guild) return; - - const roleMentions = message.content.match(/<@&(\d+)>/g); - if (!roleMentions?.length) return; - - const channel = getCurrentChannel(); - if (!channel) return; - - const roleIds = roleMentions.map(mention => mention.match(/<@&(\d+)>/)![1]); - - const role = GuildRoleStore.getRole(guild.id, roleIds); - if (!role) return; - - children.push( - { - showInRoleModal(getMembersInRole(role.id, guild.id), role.id, channel.id); - }} - icon={InfoIcon} - /> - ); - } - } -}); diff --git a/src/equicordplugins/inRole/style.css b/src/equicordplugins/inRole/style.css deleted file mode 100644 index c7d398a0c..000000000 --- a/src/equicordplugins/inRole/style.css +++ /dev/null @@ -1,29 +0,0 @@ -.vc-inrole-member-list { - max-height: 400px; - margin-top: 10px; - margin-bottom: 13px; - overflow-x: hidden; -} - -.vc-inrole-member-list::-webkit-scrollbar { - background-color: #fff1; - border-radius: 100px; - width: 10px; -} - -.vc-inrole-member-list::-webkit-scrollbar-thumb { - background-color: #fff3; - border-radius: 100px; -} - -.vc-inrole-modal-member { - margin: 11px 0; -} - -.vc-inrole-header { - padding-top: 15px; -} - -.vc-inrole-close { - margin-left: auto; -} diff --git a/src/equicordplugins/ingtoninator/index.tsx b/src/equicordplugins/ingtoninator/index.tsx deleted file mode 100644 index 43013fc4f..000000000 --- a/src/equicordplugins/ingtoninator/index.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; -import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; - -type WordMatch = { - word: string, - startIndex: number, -}; - -const settings = definePluginSettings({ - showIcon: { - type: OptionType.BOOLEAN, - default: true, - description: "Show a button to toggle the Ingtoninator plugin", - restartNeeded: true - }, - isEnabled: { - type: OptionType.BOOLEAN, - default: true, - description: "Enable or disable the Ingtoninator" - } -}); - -const isLegal = (word: string): boolean => { - if (word === "i" || word === "I") return false; - - const lastChar = word.slice(-1).toLowerCase(); - if (["a", "e", "o", "u", "y"].includes(lastChar)) return false; - - return true; -}; - -const getWords = (string: string): WordMatch[] => { - const linkRegex = /https?:\/\/[^\s]+\.[^\s]+/g; - const linkRanges = new Set(); - - for (const match of string.matchAll(linkRegex)) { - const start = match.index!; - const end = start + match[0].length; - for (let i = start; i < end; i++) { - linkRanges.add(i); - } - } - - return Array.from(string.matchAll(/[\p{L}]+/gu), match => ({ - word: match[0], - startIndex: match.index! - })).filter(match => !linkRanges.has(match.startIndex)); -}; - -const chooseRandomWord = (message: string): WordMatch | null => { - const words: WordMatch[] = getWords(message); - - while (words.length > 0) { - const index: number = Math.floor(Math.random() * words.length); - const wordMatch: WordMatch = words[index]; - - if (!isLegal(wordMatch.word)) { - words.splice(index, 1); - continue; - } - - return wordMatch; - } - - return null; -}; - -const ington = (word: string): string => { - if (word.endsWith("INGTON")) return ""; - if (word.endsWith("INGTO")) return "N"; - if (word.endsWith("INGT")) return "ON"; - if (word.endsWith("ING")) return "TON"; - if (word.endsWith("IN")) return "GTON"; - if (word.endsWith("I")) return "NGTON"; - return "INGTON"; -}; - -const handleMessage = ((channelId, message) => { - if (!settings.store.isEnabled) return; - - const msg = message.content; - if (!msg || !msg.trim()) return; - - const wordMatch: WordMatch | null = chooseRandomWord(msg); - if (wordMatch === null) return; - - const { word } = wordMatch; - const wordUpper = word.toUpperCase(); - const isUpper = word === wordUpper; - - let insertion = ington(wordUpper); - if (!isUpper) { - insertion = insertion.toLowerCase(); - } - - const idx: number = wordMatch.startIndex + word.length; - message.content = msg.slice(0, idx) + insertion + msg.slice(idx); -}); - -const IngtoninatorButton: ChatBarButtonFactory = ({ isMainChat }) => { - const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); - const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - - if (!isMainChat || !showIcon) return null; - - return ( - - {isEnabled ? ( - enabledIcon() - ) : ( - disabledIcon() - )} - - ); -}; - -function enabledIcon() { - return ( - - - - ); -} - -function disabledIcon() { - return ( - - - - ); -} - -export default definePlugin({ - name: "Ingtoninator", - description: "Suffixes 'ington' to a random word in your message", - authors: [EquicordDevs.zyqunix, EquicordDevs.BioTomateDE], - settings, - chatBarButton: { - icon: disabledIcon, - render: IngtoninatorButton - }, - start() { - addMessagePreSendListener(handleMessage); - }, - stop() { - removeMessagePreSendListener(handleMessage); - } -}); diff --git a/src/equicordplugins/invisibleChat.desktop/components/DecryptionModal.tsx b/src/equicordplugins/invisibleChat.desktop/components/DecryptionModal.tsx deleted file mode 100644 index b4946e28f..000000000 --- a/src/equicordplugins/invisibleChat.desktop/components/DecryptionModal.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 { Heading, HeadingTertiary } from "@components/Heading"; -import { - ModalContent, - ModalFooter, - ModalHeader, - ModalRoot, - openModal, -} from "@utils/modal"; -import { Button, React, TextInput } from "@webpack/common"; - -import { buildEmbed, decrypt } from "../index"; - -export function DecModal(props: any) { - const encryptedMessage: string = props?.message?.content; - const [password, setPassword] = React.useState("password"); - - return ( - - - Decrypt Message - - - - Message with Encryption - - Password - - - - - - - - - ); -} - -export function buildDecModal(msg: any): any { - openModal((props: any) => ); -} diff --git a/src/equicordplugins/invisibleChat.desktop/components/EncryptionModal.tsx b/src/equicordplugins/invisibleChat.desktop/components/EncryptionModal.tsx deleted file mode 100644 index 04a225d5c..000000000 --- a/src/equicordplugins/invisibleChat.desktop/components/EncryptionModal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { FormSwitch } from "@components/FormSwitch"; -import { Heading, HeadingTertiary } from "@components/Heading"; -import { insertTextIntoChatInputBox } from "@utils/discord"; -import { - ModalContent, - ModalFooter, - ModalHeader, - ModalProps, - ModalRoot, - openModal, -} from "@utils/modal"; -import { Button, React, TextInput } from "@webpack/common"; - -import { encrypt } from "../index"; - -function EncModal(props: ModalProps) { - const [secret, setSecret] = React.useState(""); - const [cover, setCover] = React.useState(""); - const [password, setPassword] = React.useState("password"); - const [noCover, setNoCover] = React.useState(false); - - const isValid = secret && (noCover || (cover && cover.trim().split(" ").length > 1)); - - return ( - - - Encrypt Message - - - - Secret - { - setSecret(e); - }} - /> - Cover (2 or more Words!!) - { - setCover(e); - }} - /> - Password - { - setPassword(e); - }} - /> - { - setNoCover(e); - }} - /> - - - - - - - - ); -} - -export function buildEncModal(): any { - openModal(props => ); -} diff --git a/src/equicordplugins/invisibleChat.desktop/index.tsx b/src/equicordplugins/invisibleChat.desktop/index.tsx deleted file mode 100644 index 5589f8dec..000000000 --- a/src/equicordplugins/invisibleChat.desktop/index.tsx +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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 { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; -import { updateMessage } from "@api/MessageUpdater"; -import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import { getStegCloak } from "@utils/dependencies"; -import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; -import { Message } from "@vencord/discord-types"; -import { ChannelStore, Constants, RestAPI, Tooltip } from "@webpack/common"; - -import { buildDecModal } from "./components/DecryptionModal"; -import { buildEncModal } from "./components/EncryptionModal"; - -let steggo: any; - -function PopOverIcon() { - return ( - - - - - ); -} - -function Indicator() { - return ( - - {({ onMouseEnter, onMouseLeave }) => ( - - )} - - - ); - -} - -const ChatBarRender: ChatBarButtonFactory = ({ isMainChat }) => { - if (!isMainChat) return null; - - return ( - buildEncModal()} - buttonProps={{ - "aria-haspopup": "dialog", - }} - > - {ChatBarIcon()} - - ); -}; - -function ChatBarIcon() { - return ( - - - - ); -} - -const settings = definePluginSettings({ - savedPasswords: { - type: OptionType.STRING, - default: "password, Password", - description: "Saved Passwords (Seperated with a , )" - } -}); - -const EMBED_API_URL = "https://embed.sammcheese.net"; -const INV_REGEX = new RegExp(/( \u200c|\u200d |[\u2060-\u2064])[^\u200b]/); -const URL_REGEX = new RegExp(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/); -function colorCodeFromNumber(color: number): string { - return `#${[color >> 16, color >> 8, color] - .map(x => (x & 0xFF).toString(16)) - .join("")}`; -} - -async function getEmbed(url: URL): Promise { - const { body } = await RestAPI.post({ - url: Constants.Endpoints.UNFURL_EMBED_URLS, - body: { - urls: [url] - } - }); - // The endpoint returns the color as a number, but Discord expects a string - body.embeds[0].color = colorCodeFromNumber(body.embeds[0].color); - return await body.embeds[0]; -} - -export async function buildEmbed(message: any, revealed: string): Promise { - const urlCheck = revealed.match(URL_REGEX); - - message.embeds.push({ - type: "rich", - rawTitle: "Decrypted Message", - color: "#45f5f5", - rawDescription: revealed, - footer: { - text: "Made with ❤️ by c0dine and Sammy!", - }, - }); - - if (urlCheck?.length) { - const embed = await getEmbed(new URL(urlCheck[0])); - if (embed) - message.embeds.push(embed); - } - - updateMessage(message.channel_id, message.id, { embeds: message.embeds }); -} - -export default definePlugin({ - name: "InvisibleChat", - description: "Encrypt your Messages in a non-suspicious way!", - authors: [Devs.SammCheese], - dependencies: ["MessageUpdaterAPI"], - reporterTestable: ReporterTestable.Patches, - settings, - - chatBarButton: { - icon: ChatBarIcon, - render: ChatBarRender - }, - - patches: [ - { - // Indicator - find: ".SEND_FAILED,", - replacement: { - match: /let\{className:\i,message:\i[^}]*\}=(\i)/, - replace: "try {$1 && $self.INV_REGEX.test($1.message.content) ? $1.content.push($self.indicator()) : null } catch {};$&" - } - }, - ], - INV_REGEX, - - async start() { - const { default: StegCloak } = await getStegCloak(); - steggo = new StegCloak(true, false); - }, - messagePopoverButton: { - icon: PopOverIcon, - render(message) { - return INV_REGEX.test(message?.content) - ? { - label: "Decrypt Message", - icon: PopOverIcon, - message: message, - channel: ChannelStore.getChannel(message.channel_id), - onClick: async () => { - const res = await iteratePasswords(message); - - if (res) - buildEmbed(message, res); - else - buildDecModal({ message }); - } - } - : null; - } - }, - - indicator: ErrorBoundary.wrap(Indicator, { noop: true }) -}); - -export function encrypt(secret: string, password: string, cover: string): string { - return steggo.hide(secret + "\u200b", password, cover); -} - -export function decrypt(encrypted: string, password: string, removeIndicator: boolean): string { - const decrypted = steggo.reveal(encrypted, password); - return removeIndicator ? decrypted.replace("\u200b", "") : decrypted; -} - -export function isCorrectPassword(result: string): boolean { - return result.endsWith("\u200b"); -} - -export async function iteratePasswords(message: Message): Promise { - const passwords = settings.store.savedPasswords.split(",").map(s => s.trim()); - - if (!message?.content || !passwords?.length) return false; - - let { content } = message; - - // we use an extra variable so we dont have to edit the message content directly - if (/^\W/.test(message.content)) content = `d ${message.content}d`; - - for (let i = 0; i < passwords.length; i++) { - const result = decrypt(content, passwords[i], false); - if (isCorrectPassword(result)) { - return result; - } - } - - return false; -} diff --git a/src/equicordplugins/jumpscare/index.tsx b/src/equicordplugins/jumpscare/index.tsx deleted file mode 100644 index 5a8e4d531..000000000 --- a/src/equicordplugins/jumpscare/index.tsx +++ /dev/null @@ -1,90 +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 { createAudioPlayer } from "@api/AudioPlayer"; -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { createRoot, FluxDispatcher, useCallback, useEffect, useState } from "@webpack/common"; -import { Root } from "react-dom/client"; - -let jumpscareRoot: Root | undefined; - -const settings = definePluginSettings({ - imageSource: { - type: OptionType.STRING, - description: "Sets the image url of the jumpscare", - default: "https://github.com/Equicord/Equibored/blob/main/icons/jumpscare/troll.gif?raw=true" - }, - audioSource: { - type: OptionType.STRING, - description: "Sets the audio url of the jumpscare", - default: "https://github.com/Equicord/Equibored/raw/main/sounds/jumpscare/trollolol.mp3?raw=true" - }, - chance: { - type: OptionType.NUMBER, - description: "The chance of a jumpscare happening (1 in X so: 100 = 1/100 or 1%, 50 = 1/50 or 2%, etc.)", - default: 1000 - } -}); - -function getJumpscareRoot(): Root { - if (!jumpscareRoot) { - const element = document.createElement("div"); - element.id = "jumpscare-root"; - element.classList.add("jumpscare-root"); - document.body.append(element); - jumpscareRoot = createRoot(element); - } - - return jumpscareRoot; -} - -export default definePlugin({ - name: "Jumpscare", - description: "Adds a configurable chance of jumpscaring you whenever you open a channel. Inspired by Geometry Dash Mega Hack", - authors: [Devs.surgedevs], - dependencies: ["AudioPlayerAPI"], - settings, - - start() { - getJumpscareRoot().render( - - ); - }, - - stop() { - jumpscareRoot?.unmount(); - jumpscareRoot = undefined; - }, - - JumpscareComponent() { - const [isPlaying, setIsPlaying] = useState(false); - const jumpscareAudio = createAudioPlayer(settings.store.audioSource, { volume: 100, onEnded: () => { setIsPlaying(false); } }); - - const jumpscare = useCallback(event => { - if (isPlaying) return; - - const chance = 1 / settings.store.chance; - if (Math.random() > chance) return; - - setIsPlaying(true); - jumpscareAudio.play(); - }, [isPlaying]); - - useEffect(() => { - FluxDispatcher.subscribe("CHANNEL_SELECT", jumpscare); - - return () => { - FluxDispatcher.unsubscribe("CHANNEL_SELECT", jumpscare); - }; - }, [jumpscare]); - - return ; - } -}); diff --git a/src/equicordplugins/jumpscare/styles.css b/src/equicordplugins/jumpscare/styles.css deleted file mode 100644 index 3bfd50c3c..000000000 --- a/src/equicordplugins/jumpscare/styles.css +++ /dev/null @@ -1,33 +0,0 @@ -.jumpscare-root { - pointer-events: none; -} - -.jumpscare-img { - position: absolute; - width: 100%; - height: 100%; - background-color: #000; - z-index: 99999; - object-fit: contain; - opacity: 0; -} - -.jumpscare-animate { - animation: jumpscare-animation 0.7s; -} - -@keyframes jumpscare-animation { - 0% { - transform: scale(0); - } - - 80% { - transform: scale(1); - opacity: 1; - } - - 100% { - transform: scale(0); - opacity: 0; - } -} diff --git a/src/equicordplugins/lastActive/index.tsx b/src/equicordplugins/lastActive/index.tsx deleted file mode 100644 index 9e8333a5e..000000000 --- a/src/equicordplugins/lastActive/index.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Menu, NavigationRouter, RestAPI, Toasts, UserStore } from "@webpack/common"; - -async function findLastMessageFromUser(guildId: string, channelId: string, userId: string) { - try { - const res = await RestAPI.get({ - url: `/guilds/${guildId}/messages/search?author_id=${userId}&channel_id=${channelId}&sort_by=timestamp&sort_order=desc&offset=0` - }); - - const allMessages = res.body.messages?.flat() || []; - const newestMessage = allMessages.find(msg => msg && msg.id); - - if (newestMessage) return newestMessage.id; - - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Couldn't find any recent messages from this user.", - id: Toasts.genId() - }); - return null; - } catch (error) { - console.error("Error finding last message:", error); - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Failed to find messages. Check console for details.", - id: Toasts.genId() - }); - return null; - } -} - -async function jumpToLastActive(channel: any, targetUserId?: string) { - try { - if (!channel) { - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Channel information not available.", - id: Toasts.genId() - }); - return; - } - const guildId = channel.guild_id !== null ? channel.guild_id : "@me"; - const channelId = channel.id; - let userId: string; - if (targetUserId) { - - userId = targetUserId; - } else { - const currentUser = UserStore.getCurrentUser(); - userId = currentUser.id; - } - const messageId = await findLastMessageFromUser(guildId, channelId, userId); - if (messageId) { - const url = `/channels/${guildId}/${channelId}/${messageId}`; - NavigationRouter.transitionTo(url); - } - } catch (error) { - console.error("Error in jumpToLastActive:", error); - Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Failed to jump to message. Check console for details.", - id: Toasts.genId() - }); - } -} -const ChannelContextMenuPatch: NavContextMenuPatchCallback = (children, { channel }) => { - children.push( - Your Last Message} - icon={LastActiveIcon} - action={() => { - jumpToLastActive(channel); - }} - /> - ); -}; -const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user, channel }) => { - if (!channel || !user?.id) return; - - children.push( - User's Last Message} - icon={UserLastActiveIcon} - action={() => { - jumpToLastActive(channel, user.id); - }} - /> - ); -}; - -export function UserLastActiveIcon() { - return ( - - - - - - - ); -} - -export function LastActiveIcon() { - return ( - - - - ); -} - -export default definePlugin({ - name: "LastActive", - description: "A plugin to jump to last active message from yourself or another user in a channel/server.", - authors: [EquicordDevs.Crxa], - contextMenus: { - "channel-context": ChannelContextMenuPatch, - "user-context": UserContextMenuPatch, - "thread-context": ChannelContextMenuPatch - } -}); diff --git a/src/equicordplugins/messageBurst/index.ts b/src/equicordplugins/messageBurst/index.ts deleted file mode 100644 index 0d60216c8..000000000 --- a/src/equicordplugins/messageBurst/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 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 { Channel, Message } from "@vencord/discord-types"; -import { ChannelStore, MessageActions, MessageStore, UserStore } from "@webpack/common"; - -function shouldEdit(channel: Channel, message: Message, timePeriod: number, shouldMergeWithAttachment: boolean) { - let should = true; - - if (channel.isGroupDM()) { - if (channel.name === message.content) { - should = false; - } - } - - if (message.author.id !== UserStore.getCurrentUser().id) { - should = false; - } - - if (document.querySelector('[class*="replyBar"]')) { - should = false; - } - - if (message.attachments.length > 0 && !shouldMergeWithAttachment) { - should = false; - } - - // @ts-ignore - const timestamp = new Date(message.timestamp); - const now = new Date(); - - if ((now.getTime() - timestamp.getTime()) > (timePeriod * 1000)) { - should = false; - } - - return { - should: should, - content: message.content - }; -} - -const settings = definePluginSettings({ - timePeriod: { - type: OptionType.NUMBER, - description: "The duration of bursts (in seconds).", - default: 3 - }, - shouldMergeWithAttachment: { - type: OptionType.BOOLEAN, - description: "Should the message be merged if the last message has an attachment?", - default: false - }, - useSpace: { - type: OptionType.BOOLEAN, - description: "Whether to add a space between messages when merging instead of new lines.", - default: false - } -}); - -export default definePlugin({ - name: "MessageBurst", - description: "Merges messages sent within a time period with your previous sent message if no one else sends a message before you.", - authors: [EquicordDevs.port22exposed], - settings, - onBeforeMessageSend(channelId, message) { - const messages = MessageStore.getMessages(channelId)._map; - - if (!messages) { - return; - } - - const entries = Object.entries(messages); - const [lastMessageId, lastMessage] = entries[entries.length - 1]; - - const channel = ChannelStore.getChannel(channelId); - - const { should, content } = shouldEdit(channel, lastMessage as Message, this.settings.store.timePeriod, this.settings.store.shouldMergeWithAttachment); - - if (should) { - const separator = settings.store.useSpace ? " " : "\n"; - const newContent = content + separator + message.content; - - MessageActions.editMessage(channelId, lastMessageId, { - content: newContent, - }); - - message.content = ""; - } - }, -}); diff --git a/src/equicordplugins/messageFetchTimer/index.tsx b/src/equicordplugins/messageFetchTimer/index.tsx deleted file mode 100644 index 139a35209..000000000 --- a/src/equicordplugins/messageFetchTimer/index.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import { getCurrentChannel } from "@utils/discord"; -import definePlugin, { OptionType } from "@utils/types"; -import { FluxDispatcher, React } from "@webpack/common"; - -interface FetchTiming { - channelId: string; - startTime: number; - endTime?: number; - duration?: number; - timestamp?: Date; -} - -let currentFetch: FetchTiming | null = null; -let currentChannelId: string | null = null; -const channelTimings: Map = new Map(); - -const settings = definePluginSettings({ - showIcon: { - type: OptionType.BOOLEAN, - description: "Show fetch time icon in message bar", - default: true, - }, - showMs: { - type: OptionType.BOOLEAN, - description: "Show milliseconds in timing", - default: true, - }, - iconColor: { - type: OptionType.STRING, - description: "Icon color (CSS color value)", - default: "#00d166", - } -}); - -const FetchTimeButton: ChatBarButtonFactory = ({ isMainChat }) => { - const { showMs, iconColor } = settings.use(["showMs", "iconColor"]); - - if (!isMainChat || !settings.store.showIcon || !currentChannelId) { - return null; - } - - const channelData = channelTimings.get(currentChannelId); - if (!channelData) { - return null; - } - - const { time, timestamp } = channelData; - const displayTime = showMs ? `${Math.round(time)}ms` : `${Math.round(time / 1000)}s`; - - if (!showMs && Math.round(time / 1000) === 0) { - return null; - } - - const timeAgo = formatTimeAgo(timestamp); - - return ( - { }} - > -
- {FetchTimeIcon()} - - {displayTime} - -
-
- ); -}; - -function FetchTimeIcon() { - return ( - - - - ); -} - -function formatTimeAgo(timestamp: Date): string { - const now = new Date(); - const diff = now.getTime() - timestamp.getTime(); - const seconds = Math.floor(diff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) { - return `${days} day${days > 1 ? "s" : ""} ago`; - } else if (hours > 0) { - return `${hours} hour${hours > 1 ? "s" : ""} ago`; - } else if (minutes > 0) { - return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; - } else { - return "just now"; - } -} - -function handleChannelSelect(data: any) { - if (data.channelId && data.channelId !== currentChannelId) { - currentChannelId = data.channelId; - currentFetch = { - channelId: data.channelId, - startTime: performance.now() - }; - } -} - -function handleMessageLoad(data: any) { - if (!currentFetch || data.channelId !== currentFetch.channelId) return; - - const existing = channelTimings.get(currentFetch.channelId); - if (existing) return; - - const endTime = performance.now(); - const duration = endTime - currentFetch.startTime; - - channelTimings.set(currentFetch.channelId, { - time: duration, - timestamp: new Date() - }); - - currentFetch = null; -} - -export default definePlugin({ - name: "MessageFetchTimer", - description: "Shows how long it took to fetch messages for the current channel", - authors: [EquicordDevs.GroupXyz], - settings, - - chatBarButton: { - icon: FetchTimeIcon, - render: FetchTimeButton - }, - - start() { - FluxDispatcher.subscribe("CHANNEL_SELECT", handleChannelSelect); - FluxDispatcher.subscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad); - FluxDispatcher.subscribe("MESSAGE_CREATE", handleMessageLoad); - - const currentChannel = getCurrentChannel(); - if (currentChannel) { - currentChannelId = currentChannel.id; - } - }, - - stop() { - FluxDispatcher.unsubscribe("CHANNEL_SELECT", handleChannelSelect); - FluxDispatcher.unsubscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad); - FluxDispatcher.unsubscribe("MESSAGE_CREATE", handleMessageLoad); - - currentFetch = null; - channelTimings.clear(); - currentChannelId = null; - } -}); diff --git a/src/equicordplugins/messageNotifier/index.ts b/src/equicordplugins/messageNotifier/index.ts deleted file mode 100644 index 7a41158d0..000000000 --- a/src/equicordplugins/messageNotifier/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Notifications } from "@api/index"; -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import { getCurrentChannel } from "@utils/discord"; -import definePlugin, { OptionType } from "@utils/types"; -import { MessageJSON } from "@vencord/discord-types"; -import { MessageType } from "@vencord/discord-types/enums"; -import { ChannelStore, GuildStore, NavigationRouter, RelationshipStore } from "@webpack/common"; - -interface MessageCreatePayload { - guildId: string; - channelId: string; - message: MessageJSON; -} - -const settings = definePluginSettings({ - users: { - type: OptionType.STRING, - description: "Comma separated list of user ids to get message toasts for", - default: "", - isValid(value: string) { - if (value === "") return true; - const userIds = value.split(",").map(id => id.trim()); - for (const id of userIds) - if (!/\d+/.test(id)) return `${id} isn't a valid user id`; - return true; - }, - }, -}); - -export default definePlugin({ - authors: [EquicordDevs.cassie, EquicordDevs.mochienya], - name: "MessageNotifier", - description: "Get toasts for when chosen users send a message", - settings, - flux: { - MESSAGE_CREATE({ message, channelId, guildId }: MessageCreatePayload) { - if (message.type !== MessageType.DEFAULT || getCurrentChannel()?.id === channelId) return; - - const userIds = settings.store.users.split(",").map(id => id.trim()); - if (!userIds.includes(message.author.id)) return; - - const username = RelationshipStore.getNickname(message.author.id) ?? message.author.globalName ?? message.author.username; - const guild = GuildStore.getGuild(guildId); - const channel = ChannelStore.getChannel(channelId); - const locationName = guild ? `${guild.name}#${channel.name}` : channel?.name ?? "their dms"; - - Notifications.showNotification({ - title: `${username} sent a message`, - body: `Click to jump to ${locationName}`, - onClick() { - NavigationRouter.transitionTo(`/channels/${guild?.id ?? "@me"}/${channel.id}/${message.id}`); - }, - }); - }, - }, -}); diff --git a/src/equicordplugins/moyai/index.ts b/src/equicordplugins/moyai/index.ts deleted file mode 100644 index 4c6d79391..000000000 --- a/src/equicordplugins/moyai/index.ts +++ /dev/null @@ -1,174 +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 } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import { sleep } from "@utils/misc"; -import definePlugin, { makeRange, OptionType } from "@utils/types"; -import { Message, ReactionEmoji } from "@vencord/discord-types"; -import { RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; - -interface IMessageCreate { - type: "MESSAGE_CREATE"; - optimistic: boolean; - isPushNotification: boolean; - channelId: string; - message: Message; -} - -interface IReactionAdd { - type: "MESSAGE_REACTION_ADD"; - optimistic: boolean; - channelId: string; - messageId: string; - messageAuthorId: string; - userId: "195136840355807232"; - emoji: ReactionEmoji; -} - -interface IVoiceChannelEffectSendEvent { - type: string; - emoji?: ReactionEmoji; // Just in case... - channelId: string; - userId: string; - animationType: number; - animationId: number; -} - -const MOYAI = "🗿"; -const MOYAI_URL = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3"; -const MOYAI_URL_HD = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav"; - -const settings = definePluginSettings({ - volume: { - description: "Volume of the 🗿🗿🗿", - type: OptionType.SLIDER, - markers: makeRange(0, 1, 0.1), - default: 0.5, - stickToMarkers: false - }, - quality: { - description: "Quality of the 🗿🗿🗿", - type: OptionType.SELECT, - options: [ - { label: "Normal", value: "Normal", default: true }, - { label: "HD", value: "HD" } - ], - }, - triggerWhenUnfocused: { - description: "Trigger the 🗿 even when the window is unfocused", - type: OptionType.BOOLEAN, - default: true - }, - ignoreBots: { - description: "Ignore bots", - type: OptionType.BOOLEAN, - default: true - }, - ignoreBlocked: { - description: "Ignore blocked users", - type: OptionType.BOOLEAN, - default: true - } -}); - -export default definePlugin({ - name: "Moyai", - authors: [Devs.Megu, Devs.Nuckyz], - description: "Plays a 🗿 sound effect whenever a moyai emoji is sent, reacted, or used as a voice effect in your current channel.", - settings, - - flux: { - async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { - if (optimistic || type !== "MESSAGE_CREATE") return; - if (message.state === "SENDING") return; - if (settings.store.ignoreBots && message.author?.bot) return; - if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(message.author?.id)) return; - if (!message.content) return; - if (channelId !== SelectedChannelStore.getChannelId()) return; - - const moyaiCount = getMoyaiCount(message.content); - - for (let i = 0; i < moyaiCount; i++) { - boom(); - await sleep(300); - } - }, - - MESSAGE_REACTION_ADD({ optimistic, type, channelId, userId, messageAuthorId, emoji }: IReactionAdd) { - if (optimistic || type !== "MESSAGE_REACTION_ADD") return; - if (settings.store.ignoreBots && UserStore.getUser(userId)?.bot) return; - if (settings.store.ignoreBlocked && RelationshipStore.isBlocked(messageAuthorId)) return; - if (channelId !== SelectedChannelStore.getChannelId()) return; - - const name = emoji.name.toLowerCase(); - if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; - - boom(); - }, - - VOICE_CHANNEL_EFFECT_SEND({ emoji }: IVoiceChannelEffectSendEvent) { - if (!emoji?.name) return; - const name = emoji.name.toLowerCase(); - if (name !== MOYAI && !name.includes("moyai") && !name.includes("moai")) return; - - boom(); - } - } -}); - -function countOccurrences(sourceString: string, subString: string) { - let i = 0; - let lastIdx = 0; - while ((lastIdx = sourceString.indexOf(subString, lastIdx) + 1) !== 0) - i++; - - return i; -} - -function countMatches(sourceString: string, pattern: RegExp) { - if (!pattern.global) - throw new Error("pattern must be global"); - - let i = 0; - while (pattern.test(sourceString)) - i++; - - return i; -} - -const customMoyaiRe = //gi; - -function getMoyaiCount(message: string) { - const count = countOccurrences(message, MOYAI) - + countMatches(message, customMoyaiRe); - - return Math.min(count, 10); -} - -function boom() { - if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return; - const audioElement = document.createElement("audio"); - - audioElement.src = settings.store.quality === "HD" - ? MOYAI_URL_HD - : MOYAI_URL; - - audioElement.volume = settings.store.volume; - audioElement.play(); -} diff --git a/src/equicordplugins/noRoleHeaders/index.ts b/src/equicordplugins/noRoleHeaders/index.ts deleted file mode 100644 index 424ed032f..000000000 --- a/src/equicordplugins/noRoleHeaders/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoRoleHeaders", - description: "We are all equal!! Removes the role headers in the member list.", - authors: [Devs.Samwich], - patches: [ - { - find: "._areActivitiesExperimentallyHidden=(", - replacement: { - match: /return \i===\i\.\i\.UNKNOWN/, - replace: "return null;$&" - } - } - ] -}); diff --git a/src/equicordplugins/notificationTitle.discordDesktop/index.tsx b/src/equicordplugins/notificationTitle.discordDesktop/index.tsx deleted file mode 100644 index 56d8b3fef..000000000 --- a/src/equicordplugins/notificationTitle.discordDesktop/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Devs } from "@utils/constants"; -import { getIntlMessage } from "@utils/discord"; -import definePlugin from "@utils/types"; -import { findByCodeLazy, findByPropsLazy } from "@webpack"; -import { ChannelStore, GuildStore, RelationshipStore, UserStore } from "@webpack/common"; - -const { getName } = findByPropsLazy("getName", "useName", "getNickname"); -const computeChannelName = findByCodeLazy(".isThread())return`\""); - -const ChannelTypes = findByPropsLazy("DM", "GUILD_TEXT", "PUBLIC_THREAD", "UNKNOWN"); -const ChannelTypesSets = findByPropsLazy("THREADS", "GUILD_TEXTUAL", "ALL_DMS"); -const MessageTypes = findByPropsLazy("REPLY", "STAGE_RAISE_HAND", "CHANNEL_NAME_CHANGE"); - -export default definePlugin({ - name: "NotificationTitle", - description: "Makes desktop notifications more informative", - authors: [Devs.Kyuuhachi], - - patches: [ - { - find: '"SystemMessageUtils.stringify(...) could not convert"', - replacement: { - match: /{icon:.{0,50}emoji:\i}/, - replace: "($self.makeTitle($&,...arguments))", - } - }, - ], - - makeTitle(result, channel, message, user) { - const username = getName(channel.guild_id, channel.id, user); - - let title = username; - if (message.type === MessageTypes.REPLY && message.referenced_message?.author) { - const replyUser = UserStore.getUser(message.referenced_message.author.id); - const replyUsername = getName(channel.guild_id, channel.id, replyUser); - title = getIntlMessage("CHANNEL_MESSAGE_REPLY_A11Y_LABEL", { - author: username, - repliedAuthor: replyUsername, - }); - } - - const guild = GuildStore.getGuild(channel.guild_id); - const parent = ChannelStore.getChannel(channel.parent_id); - - if (channel.type !== ChannelTypes.DM) { - let where = ChannelTypesSets.THREADS.has(channel.type) - ? `${channelName(channel)} in ${channelName(parent, true)}` - : `${channelName(channel, true)}`; - if (guild != null) - where += `, ${guild.name}`; - title += `\n(${where})`; - } - result.title = title; - - return result; - } -}); - -function channelName(channel, withPrefix = false) { - return computeChannelName(channel, UserStore, RelationshipStore, withPrefix); -} diff --git a/src/equicordplugins/partyMode/index.ts b/src/equicordplugins/partyMode/index.ts deleted file mode 100644 index f7cddbf97..000000000 --- a/src/equicordplugins/partyMode/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; -import { FluxDispatcher } from "@webpack/common"; - -const enum Intensity { - Normal, - Better, - ProjectX, -} - -const settings = definePluginSettings({ - superIntensePartyMode: { - description: "Party intensity", - type: OptionType.SELECT, - options: [ - { label: "Normal", value: Intensity.Normal, default: true }, - { label: "Better", value: Intensity.Better }, - { label: "Project X", value: Intensity.ProjectX }, - ], - restartNeeded: false, - onChange: setSettings - }, -}); - -export default definePlugin({ - name: "PartyMode", - description: "Allows you to use party mode cause the party never ends ✨", - authors: [Devs.UwUDev], - reporterTestable: ReporterTestable.None, - settings, - - start() { - setPoggerState(true); - setSettings(settings.store.superIntensePartyMode); - }, - - stop() { - setPoggerState(false); - }, -}); - -function setPoggerState(state: boolean) { - FluxDispatcher.dispatch({ - type: "POGGERMODE_SETTINGS_UPDATE", - settings: { - enabled: state, - settingsVisible: state - } - }); -} - -function setSettings(intensity: Intensity) { - const state = { - screenshakeEnabledLocations: { 0: true, 1: true, 2: true }, - shakeIntensity: 1, - confettiSize: 16, - confettiCount: 5, - combosRequiredCount: 1 - }; - - switch (intensity) { - case Intensity.Normal: { - Object.assign(state, { - screenshakeEnabledLocations: { 0: true, 1: false, 2: false }, - combosRequiredCount: 5 - }); - break; - } - case Intensity.Better: { - Object.assign(state, { - confettiSize: 12, - confettiCount: 8, - }); - break; - } - case Intensity.ProjectX: { - Object.assign(state, { - shakeIntensity: 20, - confettiSize: 25, - confettiCount: 15, - }); - break; - } - } - - FluxDispatcher.dispatch({ - type: "POGGERMODE_SETTINGS_UPDATE", - settings: state - }); -} diff --git a/src/equicordplugins/pingNotifications/index.tsx b/src/equicordplugins/pingNotifications/index.tsx deleted file mode 100644 index e6b0e45e2..000000000 --- a/src/equicordplugins/pingNotifications/index.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { showNotification } from "@api/Notifications"; -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; -import { - ChannelStore, - GuildStore, - NavigationRouter, - PresenceStore, - RelationshipStore, - SelectedChannelStore, - UserStore -} from "@webpack/common"; - -const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); - -const settings = definePluginSettings({ - friends: { - type: OptionType.BOOLEAN, - default: false, - description: "Notify when friends send messages in servers" - }, - mentions: { - type: OptionType.BOOLEAN, - default: true, - description: "Notify when someone @mentions you directly" - }, - dms: { - type: OptionType.BOOLEAN, - default: true, - description: "Notify for direct messages (DMs)" - }, - showInActive: { - type: OptionType.BOOLEAN, - default: false, - description: "Show notifications even for currently active channel" - }, - ignoreMuted: { - type: OptionType.BOOLEAN, - default: true, - description: "Skip notifications from muted servers, channels, or users" - } -}); - -function formatContent(message) { - let content = message.content || ""; - message.mentions?.forEach(user => { - content = content.replace(new RegExp(`<@!?${user.id}>`, "g"), `@${user.username}`); - }); - return content.slice(0, 200) + (content.length > 200 ? "..." : ""); -} - -function checkIfMuted(channel) { - if (!settings.store.ignoreMuted) return false; - if (!channel) return false; - - if (channel.isMuted?.()) return true; - - const isDM = [1, 3].includes(channel.type); - if (isDM) { - const recipientIds = channel.recipients || []; - for (const userId of recipientIds) { - if (RelationshipStore.isBlocked(userId)) return true; - } - - if (UserGuildSettingsStore?.getMutedChannels?.()?.includes?.(channel.id)) return true; - } - - if (channel.guild_id) { - const guild = GuildStore.getGuild(channel.guild_id); - if (UserGuildSettingsStore.isMuted(channel.guild_id)) return true; - - if (UserGuildSettingsStore?.isMuted?.(channel.guild_id)) return true; - if (UserGuildSettingsStore?.isChannelMuted?.(channel.guild_id, channel.id)) return true; - if (UserGuildSettingsStore?.isCategoryMuted?.(channel.guild_id, channel.id)) return true; - } - - return false; -} - -function isUserBlocked(userId) { - return settings.store.ignoreMuted && RelationshipStore.isBlocked?.(userId); -} - -export default definePlugin({ - name: "PingNotifications", - description: "Customizable notifications with improved mention formatting", - authors: [EquicordDevs.smuki], - settings, - - flux: { - MESSAGE_CREATE({ message }) { - try { - if (!message?.channel_id || message.state === "SENDING") return; - - const channel = ChannelStore.getChannel(message.channel_id); - const currentUser = UserStore.getCurrentUser(); - - if (!channel || !currentUser) return; - if (message.author?.id === currentUser.id) return; - - const isDM = [1, 3].includes(channel.type); - - if (checkIfMuted(channel)) return; - if (isUserBlocked(message.author.id)) return; - if (!settings.store.showInActive && channel.id === SelectedChannelStore.getChannelId()) return; - if (PresenceStore.getStatus(currentUser.id) === "dnd") return; - - const author = UserStore.getUser(message.author.id) || { username: "Unknown" }; - const channelName = channel.name || (isDM ? "DM" : "Group"); - const body = formatContent(message); - - let shouldNotify = false; - - if (settings.store.mentions && message.mentions?.some(u => u.id === currentUser.id)) { - shouldNotify = true; - } else if (settings.store.friends && RelationshipStore.isFriend(message.author.id) && !isDM) { - shouldNotify = true; - } else if (isDM && settings.store.dms) { - shouldNotify = true; - } - - if (shouldNotify) { - showNotification({ - title: `${author.username} in ${channelName}`, - body, - icon: author.getAvatarURL?.(undefined, 128), - onClick: () => NavigationRouter.transitionTo( - `/channels/${channel.guild_id || "@me"}/${channel.id}/${message.id}` - ) - }); - } - } catch (err) { - console.error("[PingNotifications] Error:", err); - } - } - } -}); diff --git a/src/equicordplugins/profileSets/components/confirmModal.tsx b/src/equicordplugins/profileSets/components/confirmModal.tsx deleted file mode 100644 index 76da537d0..000000000 --- a/src/equicordplugins/profileSets/components/confirmModal.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Button } from "@components/Button"; -import { Heading } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; -interface ConfirmModalProps extends ModalProps { - title: string; - message: string; - confirmText: string; - cancelText: string; - onConfirm: () => void; - onCancel: () => void; -} - -interface ImportProfilesModalProps extends ModalProps { - title: string; - message: string; - onOverride: () => void; - onMerge: () => void; - onCancel: () => void; -} - -export function ConfirmModal({ title, message, confirmText, cancelText, onConfirm, onCancel, onClose, transitionState }: ConfirmModalProps) { - const closeAfter = (action: () => void) => () => { - action(); - onClose(); - }; - - return ( - - - {title} - - - {message} - - - - - - - ); -} - -export function ImportProfilesModal({ title, message, onOverride, onMerge, onCancel, onClose, transitionState }: ImportProfilesModalProps) { - const closeAfter = (action: () => void) => () => { - action(); - onClose(); - }; - - return ( - - - {title} - - - {message} - - - - - - - - ); -} diff --git a/src/equicordplugins/profileSets/components/presetList.tsx b/src/equicordplugins/profileSets/components/presetList.tsx deleted file mode 100644 index 61bf18ce6..000000000 --- a/src/equicordplugins/profileSets/components/presetList.tsx +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { isNonNullish } from "@utils/guards"; -import { classes } from "@utils/misc"; -import { ProfilePreset } from "@vencord/discord-types"; -import { ContextMenuApi, Menu, React, TextInput } from "@webpack/common"; - -import { cl } from ".."; -import { deletePreset, movePreset, renamePreset, updatePresetField } from "../utils/actions"; -import { getCurrentProfile } from "../utils/profile"; -import { PresetSection, type ProfilePresetEx } from "../utils/storage"; - -interface PresetListProps { - presets: ProfilePresetEx[]; - allPresets: ProfilePresetEx[]; - avatarSize: number; - selectedPreset: number; - onLoad: (index: number) => void; - onUpdate: () => void; - guildId?: string; - section: PresetSection; - currentPage: number; - onPageChange: (page: number) => void; -} - -export function PresetList({ - presets, - allPresets, - avatarSize, - selectedPreset, - onLoad, - onUpdate, - guildId, - section, - currentPage, - onPageChange -}: PresetListProps) { - type EditableProfile = Omit; - const [renaming, setRenaming] = React.useState(-1); - const [renameText, setRenameText] = React.useState(""); - const isGuildProfile = section === "server"; - - return ( -
- {presets.map(preset => { - const actualIndex = allPresets.indexOf(preset); - const isRenaming = renaming === actualIndex; - const isSelected = !isRenaming && selectedPreset === actualIndex; - const date = new Date(preset.timestamp); - const formattedDate = date.toLocaleDateString(undefined, { - month: "short", - day: "numeric", - year: "numeric" - }); - const formattedTime = date.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit" - }); - - const commitRename = () => { - const nextName = renameText.trim(); - if (!nextName) return; - renamePreset(actualIndex, nextName, section, guildId); - onUpdate(); - }; - - return ( -
{ - if (!isRenaming) { - onLoad(actualIndex); - } - }} - onKeyDown={e => { - if (!isRenaming && (e.key === "Enter" || e.key === " ")) { - e.preventDefault(); - onLoad(actualIndex); - } - }} - className={classes(cl("row"), isSelected ? "selected" : "")} - > -
- {preset.avatarDataUrl && ( - - )} -
- {isRenaming ? ( - { - commitRename(); - setRenaming(-1); - }} - onKeyDown={e => { - if (e.key === "Enter") { - commitRename(); - setRenaming(-1); - } else if (e.key === "Escape") { - setRenaming(-1); - } - e.stopPropagation(); - }} - onClick={e => e.stopPropagation()} - autoFocus - /> - ) : ( - <> -
- {preset.name} -
-
- {formattedDate} at {formattedTime} -
- - )} -
-
-
- { - e.stopPropagation(); - const target = e.currentTarget; - ContextMenuApi.openContextMenu(e, () => ( - - { - setRenaming(actualIndex); - setRenameText(preset.name); - }} - /> - { - const profile = await getCurrentProfile(guildId, { isGuildProfile }); - await Promise.all( - (Object.entries(profile) as [keyof EditableProfile, EditableProfile[keyof EditableProfile]][]) - .filter(([, value]) => isNonNullish(value)) - .map(([key, value]) => updatePresetField(actualIndex, key, value, section, guildId)) - ); - onUpdate(); - }} - /> - - {actualIndex > 0 && ( - { - movePreset(actualIndex, actualIndex - 1, section, guildId); - onUpdate(); - }} - /> - )} - {actualIndex < allPresets.length - 1 && ( - { - movePreset(actualIndex, actualIndex + 1, section, guildId); - onUpdate(); - }} - /> - )} - {currentPage > 1 && ( - { - movePreset(actualIndex, 0, section, guildId); - onPageChange(1); - onUpdate(); - }} - /> - )} - - { - await deletePreset(actualIndex, section, guildId); - onUpdate(); - }} - /> - - )); - }} - > - - -
-
- ); - })} -
- ); -} diff --git a/src/equicordplugins/profileSets/components/presetManager.tsx b/src/equicordplugins/profileSets/components/presetManager.tsx deleted file mode 100644 index 74f2c5bc8..000000000 --- a/src/equicordplugins/profileSets/components/presetManager.tsx +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Button } from "@components/Button"; -import { Heading } from "@components/Heading"; -import { classes } from "@utils/misc"; -import { openModal } from "@utils/modal"; -import { React, SelectedGuildStore, TextInput, useStateFromStores } from "@webpack/common"; - -import { cl, settings } from "../index"; -import { exportPresets, ImportDecision, importPresets, savePreset } from "../utils/actions"; -import { loadPresetAsPending } from "../utils/profile"; -import { loadPresets, presets, PresetSection, setCurrentPresetIndex } from "../utils/storage"; -import { ImportProfilesModal } from "./confirmModal"; -import { PresetList } from "./presetList"; - -const PRESETS_PER_PAGE = 5; - -type PresetManagerProps = { - section?: PresetSection; - guildId?: string; -}; - -export function PresetManager({ section, guildId }: PresetManagerProps) { - const [presetName, setPresetName] = React.useState(""); - const [, forceUpdate] = React.useReducer(x => x + 1, 0); - const [isSaving, setIsSaving] = React.useState(false); - const [currentPage, setCurrentPage] = React.useState(1); - const [pageInput, setPageInput] = React.useState("1"); - const [selectedPreset, setSelectedPreset] = React.useState(-1); - const [searchMode, setSearchMode] = React.useState(false); - const lastRandomIndexRef = React.useRef(-1); - const resolvedSection: PresetSection = section ?? "main"; - const isServerSection = resolvedSection === "server"; - const lastSelectedGuildId = useStateFromStores( - [SelectedGuildStore], - () => SelectedGuildStore.getLastSelectedGuildId() ?? SelectedGuildStore.getGuildId() - ); - const resolvedGuildId = isServerSection ? (guildId ?? lastSelectedGuildId ?? undefined) : undefined; - const canUseGuild = !isServerSection || Boolean(resolvedGuildId); - - React.useEffect(() => { - let isActive = true; - (async () => { - await loadPresets(resolvedSection); - if (!isActive) return; - setSelectedPreset(-1); - setCurrentPage(1); - setPageInput("1"); - forceUpdate(); - })(); - return () => { - isActive = false; - }; - }, [resolvedGuildId, resolvedSection]); - - const filteredPresets = !searchMode - ? presets - : presets.filter(preset => preset.name.toLowerCase().includes(presetName.toLowerCase())); - - const totalPages = Math.ceil(filteredPresets.length / PRESETS_PER_PAGE); - const startIndex = (currentPage - 1) * PRESETS_PER_PAGE; - const currentPresets = filteredPresets.slice(startIndex, startIndex + PRESETS_PER_PAGE); - - const handlePageChange = (newPage: number) => { - if (newPage >= 1 && newPage <= totalPages) { - setCurrentPage(newPage); - setPageInput(String(newPage)); - } - }; - - const handleSavePreset = async () => { - if (!canUseGuild) return; - const trimmedName = presetName.trim(); - if (!trimmedName) return; - setIsSaving(true); - await savePreset(trimmedName, resolvedSection, resolvedGuildId); - setPresetName(""); - setIsSaving(false); - const newTotalPages = Math.ceil(presets.length / PRESETS_PER_PAGE); - handlePageChange(newTotalPages); - forceUpdate(); - }; - - const applyPreset = (index: number) => { - setSelectedPreset(index); - setCurrentPresetIndex(index); - loadPresetAsPending(presets[index], resolvedGuildId, { - isGuildProfile: resolvedSection === "server" - }); - forceUpdate(); - }; - - const handleLoadPreset = (index: number) => { - if (!canUseGuild) return; - applyPreset(index); - }; - - const selectRandomPreset = (sectionType: PresetSection) => { - const availablePresets = presets; - if (!availablePresets.length) return null; - const randomIndex = Math.floor(Math.random() * availablePresets.length); - return { preset: availablePresets[randomIndex], index: randomIndex, section: sectionType }; - }; - - const handleRandomPreset = () => { - if (!canUseGuild) return; - const selection = selectRandomPreset(resolvedSection); - if (!selection) return; - let nextIndex = selection.index; - if (presets.length > 1 && nextIndex === lastRandomIndexRef.current) { - let attempts = 0; - while (attempts < 5 && nextIndex === lastRandomIndexRef.current) { - nextIndex = Math.floor(Math.random() * presets.length); - attempts++; - } - } - lastRandomIndexRef.current = nextIndex; - applyPreset(nextIndex); - }; - - const showImportPrompt = (existingCount: number): Promise => { - return new Promise(resolve => { - openModal(props => ( - resolve("override")} - onMerge={() => resolve("merge")} - onCancel={() => resolve("cancel")} - /> - )); - }); - }; - - const { avatarSize } = settings.store; - const hasPresets = presets.length > 0; - const shouldShowPagination = filteredPresets.length > PRESETS_PER_PAGE; - - return ( -
- - Saved Profiles - - -
- -
- -
- {!searchMode && ( - - )} - {hasPresets && ( - - )} -
- - {hasPresets && ( - <> - { - const newTotal = Math.ceil(presets.length / PRESETS_PER_PAGE); - if (newTotal === 0) { - handlePageChange(1); - } else if (currentPage > newTotal) { - handlePageChange(newTotal); - } - forceUpdate(); - }} - guildId={resolvedGuildId} - section={resolvedSection} - currentPage={currentPage} - onPageChange={handlePageChange} - /> - - {shouldShowPagination && ( -
- -
- { - const { value } = e.target; - setPageInput(value); - const num = parseInt(value); - if (!isNaN(num) && num >= 1 && num <= totalPages) { - setCurrentPage(num); - } - }} - className={cl("page-input")} - /> - - / {totalPages} - -
- -
- )} - -
- - )} - -
- - - -
-
- {resolvedSection === "server" && ( -
- )} -
- ); -} diff --git a/src/equicordplugins/profileSets/index.tsx b/src/equicordplugins/profileSets/index.tsx deleted file mode 100644 index a2db97aa4..000000000 --- a/src/equicordplugins/profileSets/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./styles.css"; - -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import { classNameFactory } from "@utils/css"; -import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; - -import { PresetManager } from "./components/presetManager"; -import { loadPresets, PresetSection } from "./utils/storage"; - -export const cl = classNameFactory("vc-profile-presets-"); -export const settings = definePluginSettings({ - avatarSize: { - type: OptionType.SLIDER, - description: "Avatar size in preset list.", - markers: [56, 64, 72, 80, 88, 96], - default: 56, - stickToMarkers: true - }, -}); - -export default definePlugin({ - name: "ProfileSets", - description: "Allows you to save and load different profile presets, via the Profile Section in Settings.", - authors: [EquicordDevs.omaw, EquicordDevs.justjxke], - settings, - patches: [ - { - find: "DefaultCustomizationSections: user cannot be undefined", - replacement: { - match: /return.{0,50}children:\[(?<=\.getLegacyUsername\(\).*?)/, - replace: "$&$self.renderPresetSection(\"main\")," - } - }, - { - find: "USER_SETTINGS_GUILD_PROFILE)", - replacement: { - match: /guildId:(\i\.id),onChange:(\i)\}\)(?=.{0,25}profilePreviewTitle:)/, - replace: 'guildId:$1,onChange:$2}),$self.renderPresetSection("server",$1)' - } - } - ], - start() { - loadPresets("main"); - }, - renderPresetSection(section: PresetSection, guildId?: string) { - return ; - } -}); diff --git a/src/equicordplugins/profileSets/styles.css b/src/equicordplugins/profileSets/styles.css deleted file mode 100644 index 390c827b9..000000000 --- a/src/equicordplugins/profileSets/styles.css +++ /dev/null @@ -1,209 +0,0 @@ -.vc-profile-presets-section { - margin-bottom: 20px; - padding: 0; - background-color: transparent; - max-width: 660px; - width: 100%; - min-width: 0; - align-self: flex-start; -} - -.vc-profile-presets-section-server { - max-width: 660px; -} - -.vc-profile-presets-heading { - margin-bottom: 8px; - font-size: 16px; - font-weight: 600; -} - -.vc-profile-presets-text { - margin-bottom: 8px; -} - -.vc-profile-presets-text-input { - width: 100%; - max-width: 500px; - min-width: 0; -} - -.vc-profile-presets-search { - display: flex; - gap: 8px; - margin-bottom: 12px; - flex-wrap: wrap; -} - -.vc-profile-presets-search-button { - min-width: 120px; -} - -.vc-profile-presets-pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 8px; - margin-top: 12px; -} - -.vc-profile-presets-section-server .vc-profile-presets-pagination { - width: 300px; - max-width: 100%; -} - -.vc-profile-presets-page { - display: flex; - align-items: center; - gap: 4px; -} - -.vc-profile-presets-page-of { - font-size: 14px; - color: var(--text-normal); - font-weight: 500; -} - -.vc-profile-presets-import { - display: flex; - gap: 8px; - margin-top: 12px; - margin-bottom: 16px; -} - -.vc-profile-presets-random { - background: linear-gradient(135deg, #ff5f6d 0%, #ffc371 50%, #8a7bff 100%); - color: #fff; - border: none; -} - -.vc-profile-presets-random:hover { - filter: brightness(1.05); -} - -.vc-profile-presets-random-content { - display: inline-flex; - align-items: center; - gap: 6px; -} - -.vc-profile-presets-random-icon { - width: 16px; - height: 16px; - display: inline-block; -} - -.vc-profile-presets-block { - margin: 16px 0; - border: none; - border-top: 1px solid var(--background-modifier-accent); -} - -.vc-profile-presets-avatar-url { - display: flex; - align-items: center; - gap: 12px; - flex: 1; - min-width: 0; -} - -.vc-profile-presets-rename { - min-width: 0; - flex: 1; -} - -.vc-profile-presets-updated { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; -} - -.vc-profile-presets-list-container { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 12px; - padding: 12px; - background-color: var(--background-secondary); - border-radius: 8px; - border: 2px solid var(--background-modifier-accent); - min-width: 0; - width: 100%; - max-width: 660px; - align-self: flex-start; -} - -.vc-profile-presets-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 12px; - background-color: var(--background-primary); - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; - border: 2px solid transparent; - outline: none; - min-width: 0; - width: 100%; - max-width: 300px; - align-self: flex-start; -} - -.vc-profile-presets-row:hover { - background-color: var(--background-modifier-hover) !important; - border-color: var(--background-modifier-accent) !important; -} - -.vc-profile-presets-row:focus-visible { - border-color: var(--brand-experiment) !important; - box-shadow: 0 0 0 2px var(--brand-experiment-30a); -} - -.vc-profile-presets-row.selected { - background-color: var(--background-modifier-selected); -} - -.vc-profile-presets-avatar { - border-radius: 50%; - border: 2px solid var(--background-tertiary); - flex-shrink: 0; -} - -.vc-profile-presets-name { - font-size: 15px; - font-weight: 600; - color: var(--header-primary); - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.vc-profile-presets-timestamp { - font-size: 12px; - color: var(--text-normal); - font-weight: 500; - opacity: 0.8; -} - -.vc-profile-presets-menu-icon { - opacity: 0.7; - cursor: pointer; - transition: opacity 0.15s ease; -} - -.vc-profile-presets-menu-icon:hover { - opacity: 1; -} - -.vc-profile-presets-page-input { - width: 40px; - text-align: center; - padding: 4px; - background-color: var(--background-secondary); - border: 2px solid var(--background-modifier-accent); - border-radius: 4px; - color: var(--text-normal); -} diff --git a/src/equicordplugins/profileSets/utils/actions.ts b/src/equicordplugins/profileSets/utils/actions.ts deleted file mode 100644 index a43aa3e24..000000000 --- a/src/equicordplugins/profileSets/utils/actions.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { isNonNullish } from "@utils/guards"; -import { ProfilePreset } from "@vencord/discord-types"; -import { findStoreLazy } from "@webpack"; -import { showToast, Toasts } from "@webpack/common"; - -import { getCurrentProfile } from "./profile"; -import { addPreset, movePresetInArray, presets, PresetSection, type ProfilePresetEx, removePreset, replaceAllPresets, savePresetsData, updatePreset } from "./storage"; - -const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore"); - -function isImageInput(value: unknown): value is string | { imageUri: string; } { - if (typeof value === "string") return value.length > 0; - return typeof value === "object" && isNonNullish(value) && "imageUri" in value && typeof (value as { imageUri: unknown }).imageUri === "string"; -} - -function getFreshPendingAvatar(section: PresetSection, guildId?: string): string | null { - const pending = (section === "server" && guildId - ? UserProfileSettingsStore.getPendingChanges?.(guildId) - : UserProfileSettingsStore.getPendingChanges?.()) ?? {}; - const pendingObj = pending as Record; - - const candidates = [ - pendingObj.selectedAvatarRaw, - pendingObj.presetAvatarRaw, - pendingObj.pendingAvatar, - pendingObj.avatar, - pendingObj.selectedAvatarProcessed, - pendingObj.presetAvatarProcessed - ]; - const selected = candidates.find(isImageInput); - if (!selected) return null; - return typeof selected === "string" ? selected : selected.imageUri; -} - -export async function savePreset(name: string, section: PresetSection, guildId?: string) { - const profile = await getCurrentProfile(guildId, { isGuildProfile: section === "server" }); - const freshPendingAvatar = getFreshPendingAvatar(section, guildId); - const effectiveAvatar = freshPendingAvatar ?? profile.avatarDataUrl ?? null; - - const newPreset: ProfilePresetEx = { - name, - timestamp: Date.now(), - ...profile, - avatarDataUrl: effectiveAvatar, - }; - addPreset(newPreset); - await savePresetsData(section); -} - -export async function updatePresetField>( - index: number, - field: K, - value: Omit[K], - section: PresetSection, - guildId?: string -) { - if (index < 0 || index >= presets.length) return; - void guildId; - - const updatedPreset = { - ...presets[index], - [field]: value, - timestamp: Date.now() - }; - updatePreset(index, updatedPreset); - await savePresetsData(section); -} - -export async function deletePreset(index: number, section: PresetSection, guildId?: string) { - if (index < 0 || index >= presets.length) return; - - removePreset(index); - await savePresetsData(section); -} - -export async function movePreset(fromIndex: number, toIndex: number, section: PresetSection, guildId?: string) { - if (fromIndex < 0 || fromIndex >= presets.length || toIndex < 0 || toIndex >= presets.length) return; - - movePresetInArray(fromIndex, toIndex); - await savePresetsData(section); -} - -export async function renamePreset(index: number, newName: string, section: PresetSection, guildId?: string) { - if (index < 0 || index >= presets.length || !newName.trim()) return; - - const updatedPreset = { ...presets[index], name: newName.trim() }; - updatePreset(index, updatedPreset); - await savePresetsData(section); -} - -export function exportPresets(section: PresetSection) { - const dataStr = JSON.stringify(presets, null, 2); - const dataBlob = new Blob([dataStr], { type: "application/json" }); - const url = URL.createObjectURL(dataBlob); - const link = document.createElement("a"); - link.href = url; - link.download = `profile-presets-${section}-${Date.now()}.json`; - link.click(); - URL.revokeObjectURL(url); -} - -export type ImportDecision = "override" | "merge" | "cancel"; - -export async function importPresets( - forceUpdate: () => void, - onImportPrompt: (existingCount: number) => Promise, - section: PresetSection, - guildId?: string -) { - const input = document.createElement("input"); - input.type = "file"; - input.accept = "application/json"; - input.onchange = async (event: Event) => { - try { - const target = event.currentTarget as HTMLInputElement | null; - const file = target?.files?.[0]; - if (!file) return; - - const text = await file.text(); - const importedPresets = JSON.parse(text); - - if (!Array.isArray(importedPresets)) { - return; - } - - if (presets.length > 0) { - const decision = await onImportPrompt(presets.length); - if (decision === "cancel") return; - if (decision === "override") { - replaceAllPresets(importedPresets); - } else { - const combined = [...presets, ...importedPresets]; - replaceAllPresets(combined); - } - } else { - replaceAllPresets(importedPresets); - } - - await savePresetsData(section); - forceUpdate(); - } catch { - showToast("Failed to import presets. The file might be invalid.", Toasts.Type.FAILURE); - } - }; - input.click(); -} diff --git a/src/equicordplugins/profileSets/utils/profile.ts b/src/equicordplugins/profileSets/utils/profile.ts deleted file mode 100644 index 148081443..000000000 --- a/src/equicordplugins/profileSets/utils/profile.ts +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { getUserSettingLazy } from "@api/UserSettings"; -import { CustomStatus, DisplayNameStyles, Nameplate, ProfileEffect, ProfilePreset } from "@vencord/discord-types"; -import { findStoreLazy } from "@webpack"; -import { FluxDispatcher, GuildMemberStore, IconUtils, UserProfileStore, UserStore } from "@webpack/common"; - -const UserProfileSettingsStore = findStoreLazy("UserProfileSettingsStore"); -const CustomStatusSettings = getUserSettingLazy("status", "customStatus")!; - -type PendingChanges = Record & { - selectedAvatarRaw?: ImageInput; - presetAvatarRaw?: ImageInput; - selectedAvatarProcessed?: ImageInput; - presetAvatarProcessed?: ImageInput; - pendingAvatar?: ImageInput; - avatar?: ImageInput; - pendingBanner?: ImageInput; - banner?: ImageInput; - pendingAvatarDecoration?: { asset: string; skuId: string; } | null; - pendingProfileEffect?: ProfileEffect | null; - pendingNameplate?: Nameplate | null; - pendingDisplayNameStyles?: DisplayNameStyles | null; - pendingAccentColor?: number | null; - pendingThemeColors?: number[] | null; - pendingBio?: string | null; - pendingPronouns?: string | null; - pendingNickname?: string | null; - pendingGlobalName?: string | null; - pendingPrimaryGuildId?: string | null; -}; - -type ImageInput = string | { imageUri: string; } | null | undefined; -type DisplayNameStylesLike = DisplayNameStyles & { - fontId?: number; - effectId?: number; -}; - -type CurrentProfileOptions = { - isGuildProfile?: boolean; -}; - -type LoadPresetOptions = { - skipGlobalName?: boolean; - skipBio?: boolean; - skipPronouns?: boolean; - isGuildProfile?: boolean; -}; - -function dispatch(type: string, payload: Record) { - FluxDispatcher.dispatch({ type, ...payload }); -} - -function isNonEmptyString(value: unknown): value is string { - return typeof value === "string" && value.length > 0; -} - -function hasImageInput(value: ImageInput): boolean { - if (!value) return false; - if (typeof value === "string") return value.length > 0; - return typeof value === "object" && isNonEmptyString(value?.imageUri); -} - -function normalizeDisplayNameStyles(value: DisplayNameStylesLike | null | undefined): DisplayNameStylesLike | null { - if (!value) return null; - const fontId = value.fontId ?? value.font_id; - const effectId = value.effectId ?? value.effect_id; - if (typeof fontId !== "number" || typeof effectId !== "number") return null; - const colors = Array.isArray(value.colors) ? [...value.colors] : []; - - return { - fontId, - effectId, - font_id: fontId, - effect_id: effectId, - colors - }; -} - -export async function imageUrlToBase64(url: string): Promise { - try { - const response = await fetch(url); - const blob = await response.blob(); - return await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsDataURL(blob); - }); - } catch { - return null; - } -} - -async function processImage(imageData: ImageInput, userId: string, type: "avatar" | "banner", guildId?: string, useGuildPath?: boolean): Promise { - if (!imageData) return null; - - if (typeof imageData === "object" && isNonEmptyString(imageData?.imageUri)) { - return imageData.imageUri; - } - - if (typeof imageData === "string") { - if (imageData.startsWith("data:")) return imageData; - if (/^https?:\/\//.test(imageData)) { - return await imageUrlToBase64(imageData); - } - - const isAnimated = imageData.startsWith("a_"); - const size = type === "banner" ? 1024 : 512; - const urlPath = type === "banner" ? "banners" : "avatars"; - const guildPath = guildId ? `guilds/${guildId}/users/${userId}/${type === "banner" ? "banners" : "avatars"}` : urlPath; - const guildUrl = `https://cdn.discordapp.com/${guildPath}/${imageData}.${isAnimated ? "gif" : "png"}?size=${size}`; - const globalUrl = `https://cdn.discordapp.com/${urlPath}/${userId}/${imageData}.${isAnimated ? "gif" : "png"}?size=${size}`; - if (useGuildPath && guildId) { - const guildResult = await imageUrlToBase64(guildUrl); - if (guildResult) return guildResult; - } - return await imageUrlToBase64(globalUrl); - } - - return null; -} - -export async function getCurrentProfile(guildId?: string, options: CurrentProfileOptions = {}): Promise> { - const currentUser = UserStore.getCurrentUser(); - const baseProfile = UserProfileStore.getUserProfile(currentUser.id); - const isGuildProfile = options.isGuildProfile ?? Boolean(guildId); - const effectiveGuildId = isGuildProfile ? guildId : undefined; - const guildProfile = effectiveGuildId ? UserProfileStore.getGuildMemberProfile(currentUser.id, effectiveGuildId) : null; - const userProfile = guildProfile ?? baseProfile; - const userAny = currentUser; - const guildMember = effectiveGuildId ? GuildMemberStore.getMember(effectiveGuildId, currentUser.id) : null; - - const pendingChangesDefault: PendingChanges = UserProfileSettingsStore.getPendingChanges() ?? {}; - const pendingChangesForGuild: PendingChanges = UserProfileSettingsStore.getPendingChanges(effectiveGuildId) ?? {}; - const pendingChanges: PendingChanges = isGuildProfile && Object.keys(pendingChangesForGuild).length > 0 - ? pendingChangesForGuild - : pendingChangesDefault; - - const customStatusSetting = CustomStatusSettings.getSetting(); - const customStatus = isGuildProfile - ? null - : { - text: customStatusSetting?.text ?? "", - emojiId: customStatusSetting?.emojiId ?? "0", - emojiName: customStatusSetting?.emojiName ?? "", - expiresAtMs: customStatusSetting?.expiresAtMs ?? "0" - }; - - const avatarDecorationSource = pendingChanges.pendingAvatarDecoration ?? userAny.avatarDecorationData; - const avatarDecoration = avatarDecorationSource ? { - asset: avatarDecorationSource.asset, - skuId: avatarDecorationSource.skuId - } : null; - - let profileEffect: ProfileEffect | null = null; - const effectToUse = pendingChanges.pendingProfileEffect ?? userProfile?.profileEffect; - - if (effectToUse) { - if (effectToUse.skuId && effectToUse.effects) { - profileEffect = { - skuId: effectToUse.skuId, - title: effectToUse.title, - description: effectToUse.description, - accessibilityLabel: effectToUse.accessibilityLabel, - reducedMotionSrc: effectToUse.reducedMotionSrc, - thumbnailPreviewSrc: effectToUse.thumbnailPreviewSrc, - effects: effectToUse.effects, - animationType: effectToUse.animationType, - staticFrameSrc: effectToUse.staticFrameSrc, - type: effectToUse.type || 1 - }; - } else if (effectToUse.skuId) { - const collectibles = userProfile?.collectibles; - const collectible = collectibles?.find(c => c?.skuId === effectToUse.skuId); - if (collectible) { - profileEffect = { - skuId: collectible.skuId, - title: collectible.title, - description: collectible.description, - accessibilityLabel: collectible.accessibilityLabel, - reducedMotionSrc: collectible.reducedMotionSrc, - thumbnailPreviewSrc: collectible.thumbnailPreviewSrc, - effects: collectible.effects, - animationType: collectible.animationType, - staticFrameSrc: collectible.staticFrameSrc, - type: collectible.type || 1 - }; - } - } - } - - const nameplateToUse = pendingChanges.pendingNameplate ?? userAny.collectibles?.nameplate; - const nameplate = nameplateToUse ? { - skuId: nameplateToUse.skuId, - asset: nameplateToUse.asset, - label: nameplateToUse.label, - palette: typeof nameplateToUse.palette === "string" ? nameplateToUse.palette : undefined, - type: nameplateToUse.type || 2 - } : null; - - const savedDisplayNameStyles = isGuildProfile - ? (guildMember?.displayNameStyles ?? userAny.displayNameStyles) - : userAny.displayNameStyles; - const displayNameStylesToUse = pendingChanges.pendingDisplayNameStyles ?? savedDisplayNameStyles; - const displayNameStyles = normalizeDisplayNameStyles(displayNameStylesToUse); - - const pendingAvatar = pendingChanges.pendingAvatar ?? pendingChanges.avatar; - const avatarToUse: ImageInput = hasImageInput(pendingAvatar) - ? pendingAvatar - : (isGuildProfile ? (guildMember?.avatar ?? currentUser.avatar ?? null) : (currentUser.avatar ?? null)); - - const useGuildAvatar = !!(effectiveGuildId && isGuildProfile && guildMember?.avatar && avatarToUse === guildMember.avatar); - - const avatarInput: ImageInput = hasImageInput(avatarToUse) - ? avatarToUse - : IconUtils.getUserAvatarURL(currentUser, true, 512); - const avatarDataUrl = await processImage(avatarInput, currentUser.id, "avatar", effectiveGuildId, useGuildAvatar); - const resolvedAvatarDataUrl = avatarDataUrl ?? IconUtils.getDefaultAvatarURL(currentUser.id); - - const pendingBanner = pendingChanges.pendingBanner ?? pendingChanges.banner; - const bannerToUse: ImageInput = hasImageInput(pendingBanner) - ? pendingBanner - : (isGuildProfile ? (guildProfile?.banner ?? baseProfile?.banner) : baseProfile?.banner); - const useGuildBanner = !!(effectiveGuildId && isGuildProfile && guildProfile?.banner && bannerToUse === guildProfile?.banner); - - const bannerDataUrl = await processImage(bannerToUse, currentUser.id, "banner", effectiveGuildId, useGuildBanner); - - return { - avatarDataUrl: resolvedAvatarDataUrl, - bannerDataUrl, - bio: pendingChanges.pendingBio ?? userProfile?.bio ?? null, - accentColor: pendingChanges.pendingAccentColor ?? userProfile?.accentColor ?? null, - themeColors: pendingChanges.pendingThemeColors ?? userProfile?.themeColors ?? null, - globalName: isGuildProfile - ? (pendingChanges.pendingNickname ?? guildMember?.nick ?? null) - : (pendingChanges.pendingGlobalName ?? currentUser.globalName ?? null), - pronouns: isGuildProfile ? null : (pendingChanges.pendingPronouns ?? userProfile?.pronouns ?? null), - avatarDecoration, - profileEffect, - nameplate, - primaryGuildId: isGuildProfile - ? null - : (pendingChanges.pendingPrimaryGuildId ?? userAny.primaryGuild?.identityGuildId ?? null), - customStatus, - displayNameStyles - }; -} - -function jsonEq(a: unknown, b: unknown): boolean { - if (a === b) return true; - return JSON.stringify(a) === JSON.stringify(b); -} - -function customStatusEq(a: CustomStatus | null | undefined, b: CustomStatus | null | undefined): boolean { - if (a == null || b == null) return a == null && b == null; - return a.text === b.text - && String(a.emojiId ?? "") === String(b.emojiId ?? "") - && a.emojiName === b.emojiName - && String(a.expiresAtMs ?? "0") === String(b.expiresAtMs ?? "0"); -} - -function resolvePendingAvatar(pendingChanges: PendingChanges | null): ImageInput { - if (!pendingChanges) return null; - - const candidates: ImageInput[] = [ - pendingChanges.selectedAvatarRaw, - pendingChanges.presetAvatarRaw, - pendingChanges.pendingAvatar, - pendingChanges.avatar, - pendingChanges.selectedAvatarProcessed, - pendingChanges.presetAvatarProcessed - ]; - return candidates.find(hasImageInput) ?? null; -} - -function normalizeImageValue(value: unknown): string | null { - if (typeof value === "string") return value; - if (value && typeof value === "object" && "imageUri" in value) { - const { imageUri } = value as { imageUri: unknown; }; - return typeof imageUri === "string" ? imageUri : null; - } - return null; -} - -function collectibleEqBySku(a: { skuId?: string | number | null; } | null | undefined, b: { skuId?: string | number | null; } | null | undefined): boolean { - if (a == null || b == null) return a == null && b == null; - return String(a.skuId ?? "") === String(b.skuId ?? ""); -} - -function avatarDecorationEq(a: { skuId?: string | number | null; asset?: string | null; } | null | undefined, b: { skuId?: string | number | null; asset?: string | null; } | null | undefined): boolean { - if (a == null || b == null) return a == null && b == null; - return String(a.skuId ?? "") === String(b.skuId ?? "") && String(a.asset ?? "") === String(b.asset ?? ""); -} - -function nameplateEq(a: { skuId?: string | number | null; asset?: string | null; } | null | undefined, b: { skuId?: string | number | null; asset?: string | null; } | null | undefined): boolean { - if (a == null || b == null) return a == null && b == null; - return String(a.skuId ?? "") === String(b.skuId ?? "") && String(a.asset ?? "") === String(b.asset ?? ""); -} - -export async function loadPresetAsPending(preset: ProfilePreset, guildId?: string, options: LoadPresetOptions = {}) { - try { - const isGuild = options.isGuildProfile ?? Boolean(guildId); - if (isGuild && !guildId) return; - const currentUser = UserStore.getCurrentUser(); - const baseProfile = UserProfileStore.getUserProfile(currentUser.id); - const guildProfile = isGuild && guildId ? UserProfileStore.getGuildMemberProfile(currentUser.id, guildId) : null; - const equippedProfile = guildProfile ?? baseProfile; - const current = await getCurrentProfile(guildId, { - isGuildProfile: isGuild - }); - const pendingChanges = (isGuild && guildId - ? UserProfileSettingsStore.getPendingChanges(guildId) - : UserProfileSettingsStore.getPendingChanges()); - const equippedAvatarDecoration = currentUser.avatarDecorationData ?? null; - const equippedProfileEffect = equippedProfile?.profileEffect ?? null; - const equippedNameplate = currentUser.collectibles?.nameplate ?? null; - const pendingAvatarDecoration = pendingChanges?.pendingAvatarDecoration ?? null; - const pendingProfileEffect = pendingChanges?.pendingProfileEffect ?? null; - const pendingNameplate = pendingChanges?.pendingNameplate ?? null; - const setPending = (field: string, payload: Record) => { - const cleanPayload = Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== undefined)); - if (!Object.keys(cleanPayload).length) return; - dispatch(`USER_PROFILE_SETTINGS_SET_PENDING_${field}`, isGuild ? { ...cleanPayload, guildId } : cleanPayload); - }; - - if ("avatarDataUrl" in preset) { - const avatarValue = preset.avatarDataUrl; - const presetAvatar = normalizeImageValue(avatarValue); - const currentAvatar = normalizeImageValue(current.avatarDataUrl); - const pendingAvatar = normalizeImageValue(resolvePendingAvatar(pendingChanges)); - if (isGuild || (presetAvatar !== currentAvatar && presetAvatar !== pendingAvatar)) { - const avatarPayload = - typeof avatarValue === "string" && avatarValue.startsWith("data:") - ? { - assetOrigin: "NEW_ASSET", - imageUri: avatarValue, - description: `profilesets-${preset.name ?? "preset"}` - } - : avatarValue; - setPending("AVATAR", { avatar: avatarPayload }); - } - } - - if ("bannerDataUrl" in preset && preset.bannerDataUrl !== current.bannerDataUrl) { - setPending("BANNER", { banner: preset.bannerDataUrl }); - } - - if (!options.skipBio && preset?.bio !== current?.bio) { - const bioValue = preset.bio === null ? "" : preset.bio; - setPending("BIO", { bio: bioValue }); - } - - if (!isGuild && !options.skipPronouns && preset?.pronouns !== current?.pronouns) { - const pronounsValue = preset.pronouns === null ? "" : preset.pronouns; - setPending("PRONOUNS", { pronouns: pronounsValue }); - } - - if (!options.skipGlobalName && preset?.globalName !== current?.globalName) { - if (isGuild) { - setPending("NICKNAME", { nickname: preset.globalName }); - } else { - setPending("GLOBAL_NAME", { globalName: preset.globalName }); - } - } - - if (preset.avatarDecoration && ![pendingAvatarDecoration, equippedAvatarDecoration, current.avatarDecoration].some(d => avatarDecorationEq(preset.avatarDecoration, d))) { - setPending("COLLECTIBLES_ITEM", { - item: { type: 0, value: preset.avatarDecoration } - }); - } - - if (preset.profileEffect && ![pendingProfileEffect, equippedProfileEffect, current.profileEffect].some(d => collectibleEqBySku(preset.profileEffect, d))) { - setPending("COLLECTIBLES_ITEM", { - item: { type: 1, value: preset.profileEffect } - }); - } - - if (preset.nameplate && ![pendingNameplate, equippedNameplate, current.nameplate].some(d => nameplateEq(preset.nameplate, d))) { - setPending("COLLECTIBLES_ITEM", { - item: { type: 2, value: preset.nameplate } - }); - } - - if (preset.displayNameStyles) { - const presetDisplayNameStyles = normalizeDisplayNameStyles(preset.displayNameStyles); - if (!jsonEq(presetDisplayNameStyles, current.displayNameStyles)) { - setPending("DISPLAY_NAME_STYLES", { displayNameStyles: presetDisplayNameStyles }); - } - } - - if (preset.themeColors && !jsonEq(preset.themeColors, current.themeColors)) { - setPending("THEME_COLORS", { themeColors: preset.themeColors }); - } - - if (preset.primaryGuildId && !isGuild && preset.primaryGuildId !== current.primaryGuildId) { - dispatch("USER_SETTINGS_SET_PENDING_PRIMARY_GUILD_ID", { primaryGuildId: preset.primaryGuildId }); - } - - if (preset.customStatus && !isGuild && !customStatusEq(preset.customStatus, current.customStatus)) { - CustomStatusSettings.updateSetting({ - text: preset.customStatus?.text ?? "", - expiresAtMs: preset.customStatus?.expiresAtMs ?? "0", - emojiId: preset.customStatus?.emojiId ?? "0", - emojiName: preset.customStatus?.emojiName ?? "" - }); - } - } catch (err) { - throw err; - } -} diff --git a/src/equicordplugins/profileSets/utils/storage.ts b/src/equicordplugins/profileSets/utils/storage.ts deleted file mode 100644 index ff5cc965f..000000000 --- a/src/equicordplugins/profileSets/utils/storage.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { DataStore } from "@api/index"; -import { Logger } from "@utils/Logger"; -import { ProfilePreset } from "@vencord/discord-types"; -import { UserStore } from "@webpack/common"; - -const logger = new Logger("ProfilePresets"); -const LEGACY_PRESETS_KEY = "ProfileDataset"; -const MAIN_PRESETS_KEY = "ProfilePresets_v2_Main"; -const SERVER_PRESETS_KEY = "ProfilePresets_v2_Server"; - -export type PresetSection = "main" | "server"; - -export type ProfilePresetEx = ProfilePreset & { - avatarRaw?: string | null; -}; - -export let presets: ProfilePresetEx[] = []; -export let currentPresetIndex = -1; -let activeScopeKey: string | null = null; - -function resetPresets(nextPresets: ProfilePresetEx[] = []) { - presets = nextPresets; - currentPresetIndex = -1; -} - -function getPresetsKey(section: PresetSection, userId: string) { - const baseKey = section === "main" ? MAIN_PRESETS_KEY : SERVER_PRESETS_KEY; - return `${baseKey}:${userId}`; -} - -function getLegacyKey(userId: string) { - return `${LEGACY_PRESETS_KEY}:${userId}:main`; -} - -export async function loadPresets(section: PresetSection) { - try { - const currentUser = UserStore.getCurrentUser(); - const userId = currentUser!.id; - const key = getPresetsKey(section, userId); - activeScopeKey = key; - const stored = await DataStore.get(key); - if (stored && Array.isArray(stored)) { - resetPresets(stored); - return; - } - - if (section === "main") { - const legacyKey = getLegacyKey(userId); - const legacyStored = await DataStore.get(legacyKey); - const legacyBaseStored = await DataStore.get(LEGACY_PRESETS_KEY); - const legacyToUse = Array.isArray(legacyStored) - ? legacyStored - : (Array.isArray(legacyBaseStored) ? legacyBaseStored : null); - if (legacyToUse) { - resetPresets(legacyToUse); - await DataStore.set(key, legacyToUse); - await DataStore.del(legacyKey); - await DataStore.del(LEGACY_PRESETS_KEY); - return; - } - } - resetPresets(); - } catch (err) { - logger.error("Failed to load presets", err); - resetPresets(); - } -} - -export async function savePresetsData(section?: PresetSection) { - try { - if (!activeScopeKey && !section) return; - const currentUser = UserStore.getCurrentUser(); - const userId = currentUser!.id; - const key = section ? getPresetsKey(section, userId) : activeScopeKey!; - await DataStore.set(key, presets); - } catch (err) { - logger.error("Failed to save presets", err); - } -} - -export function setCurrentPresetIndex(index: number) { - currentPresetIndex = index; -} - -export function addPreset(preset: ProfilePresetEx) { - presets.push(preset); -} - -export function updatePreset(index: number, preset: ProfilePresetEx) { - if (index >= 0 && index < presets.length) { - presets[index] = preset; - } -} - -export function removePreset(index: number) { - if (index >= 0 && index < presets.length) { - presets.splice(index, 1); - if (currentPresetIndex === index) { - currentPresetIndex = -1; - } else if (currentPresetIndex > index) { - currentPresetIndex--; - } - } -} - -export function movePresetInArray(fromIndex: number, toIndex: number) { - if (fromIndex < 0 || fromIndex >= presets.length || toIndex < 0 || toIndex >= presets.length) return; - const [preset] = presets.splice(fromIndex, 1); - presets.splice(toIndex, 0, preset); -} - -export function replaceAllPresets(newPresets: ProfilePresetEx[]) { - presets = newPresets; -} diff --git a/src/equicordplugins/questionMarkReplacement/index.tsx b/src/equicordplugins/questionMarkReplacement/index.tsx deleted file mode 100644 index 1c9acf14f..000000000 --- a/src/equicordplugins/questionMarkReplacement/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings } from "@api/Settings"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const settings = definePluginSettings({ - replace: { - type: OptionType.STRING, - description: "Replace with", - default: ":face_with_monocle:" - }, -}); - -function replaceQuestionMarks(content: string): string { - const allQuestionMarks = content.split("").every(char => char === "?"); - - if (allQuestionMarks) { - return content.replace(/\?/g, settings.store.replace); - } else { - return content; - } -} - -export default definePlugin({ - name: "QuestionMarkReplacement", - description: "Replace all question marks with chosen string, if message only contains question marks.", - authors: [Devs.nyx], - - settings, - - start() { - this.preSend = addMessagePreSendListener((_, msg) => { - msg.content = replaceQuestionMarks(msg.content); - }); - }, - - stop() { - removeMessagePreSendListener(this.preSend); - } -}); diff --git a/src/equicordplugins/screenRecorder.equibop/index.tsx b/src/equicordplugins/screenRecorder.equibop/index.tsx deleted file mode 100644 index a24ef052b..000000000 --- a/src/equicordplugins/screenRecorder.equibop/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; -import { ScreenshareIcon } from "@components/Icons"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Menu, UploadHandler } from "@webpack/common"; - -let recoder: MediaRecorder; - -export default definePlugin({ - name: "ScreenRecorder", - description: "epic screen recorder lol", - authors: [Devs.AutumnVN], - contextMenus: { - "channel-attach": startRecording - } -}); - -function startRecording(children) { - children.push( - - -
Start Recording
- - } - action={async () => { - const stream = await navigator.mediaDevices.getDisplayMedia({ audio: true, video: { frameRate: { ideal: 60 } } }); - recoder = new MediaRecorder(stream); - recoder.start(); - removeContextMenuPatch("channel-attach", startRecording); - addContextMenuPatch("channel-attach", stopRecording); - }} - /> - ); -} - -function stopRecording(children, props) { - children.push( - - -
Stop Recording
- - } - action={() => { - recoder.addEventListener("dataavailable", e => { - const file = new File([e.data], "watch if cute.webm", { type: "video/webm" }); - UploadHandler.promptToUpload([file], props.channel, 0); - }); - recoder.stop(); - removeContextMenuPatch("channel-attach", stopRecording); - addContextMenuPatch("channel-attach", startRecording); - }} - /> - ); -} diff --git a/src/equicordplugins/serverSearch/index.tsx b/src/equicordplugins/serverSearch/index.tsx deleted file mode 100644 index 48cbe348f..000000000 --- a/src/equicordplugins/serverSearch/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated, camila314, and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./styles.css"; - -import { addServerListElement, removeServerListElement, ServerListRenderPosition } from "@api/ServerList"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { FluxDispatcher } from "@webpack/common"; -import { Tooltip } from "webpack/common/components"; - -function SearchIcon() { - return ( - - - - ); -} - -export default definePlugin({ - name: "ServerSearch", - authors: [EquicordDevs.camila314], - description: "Navigate your servers better with a quick search button", - - renderButton() { - return -
- - {({ onMouseEnter, onMouseLeave }) => ( -
- FluxDispatcher.dispatch({ - type: "QUICKSWITCHER_SHOW", - query: "", - queryMode: null - }) - }> - -
- )} -
-
-
; - }, - - start() { - addServerListElement(ServerListRenderPosition.Above, this.renderButton); - }, - - stop() { - removeServerListElement(ServerListRenderPosition.Above, this.renderButton); - } -}); diff --git a/src/equicordplugins/serverSearch/styles.css b/src/equicordplugins/serverSearch/styles.css deleted file mode 100644 index 545036734..000000000 --- a/src/equicordplugins/serverSearch/styles.css +++ /dev/null @@ -1,33 +0,0 @@ -#vc-searchbutton-container { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 8px; -} - -#vc-searchbutton { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: var(--background-primary); - height: 48px; - width: 48px; - border-radius: 100%; - transition: border-radius 0.15s ease-out, background-color 0.15s ease-out; -} - -#vc-searchbutton-icon { - color: var(--text-default); -} - -#vc-searchbutton:hover { - border-radius: 34%; - background-color: var(--green-360); - cursor: pointer; -} - -#vc-searchbutton:hover #vc-searchbutton-icon { - color: var(--white-500); -} \ No newline at end of file diff --git a/src/equicordplugins/signature/index.tsx b/src/equicordplugins/signature/index.tsx deleted file mode 100644 index 1a7d28605..000000000 --- a/src/equicordplugins/signature/index.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; -import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; -import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Menu, React } from "@webpack/common"; - -// Big thank you too slientTyping - -const settings = definePluginSettings( - { - name: { - type: OptionType.STRING, - description: "The signature that will be added to the end of your messages", - default: "a chronic discord user" - }, - textHeader: { - description: "What header to preface text with", - type: OptionType.STRING, - default: ">", - }, - showIcon: { - type: OptionType.BOOLEAN, - default: true, - description: "Show an icon for toggling the plugin in the chat bar", - restartNeeded: true, - }, - contextMenu: { - type: OptionType.BOOLEAN, - description: "Add option to toggle the functionality in the chat input context menu", - default: true - }, - isEnabled: { - type: OptionType.BOOLEAN, - description: "Toggle functionality", - default: true, - }, - }); - -const SignatureToggle: ChatBarButtonFactory = ({ isMainChat }) => { - const { isEnabled, showIcon } = settings.use(["isEnabled", "showIcon"]); - const toggle = () => settings.store.isEnabled = !settings.store.isEnabled; - - if (!isMainChat || !showIcon) return null; - - return ( - - - - {isEnabled && ( - <> - - - - - )} - - - ); -}; - -function SignatureIcon() { - return ( - - - - ); -} - -const ChatBarContextCheckbox: NavContextMenuPatchCallback = children => { - const { isEnabled, contextMenu } = settings.use(["isEnabled", "contextMenu"]); - if (!contextMenu) return; - - const group = findGroupChildrenByChildId("submit-button", children); - - if (!group) return; - - const idx = group.findIndex(c => c?.props?.id === "submit-button"); - - group.splice(idx + 1, 0, - settings.store.isEnabled = !settings.store.isEnabled} - /> - ); -}; - -export default definePlugin({ - name: "Signature", - description: "Automated fingerprint/end text", - authors: [Devs.Ven, Devs.Rini, Devs.ImBanana, EquicordDevs.KrystalSkull], - onBeforeMessageSend(channelId, msg) { - if (!settings.store.isEnabled) { - msg.content = msg.content; - } else { - msg.content = textProcessing(msg.content); - } - }, - start: () => { - if (settings.store.isEnabled) true; - }, - stop: () => { - if (settings.store.isEnabled) false; - }, - - chatBarButton: { - icon: SignatureIcon, - render: SignatureToggle - }, - - settings, - - contextMenus: { - "textarea-context": ChatBarContextCheckbox - }, - - commands: [ - { - name: "signature", - description: "Toggle your signature", - inputType: ApplicationCommandInputType.BUILT_IN, - options: [ - { - name: "value", - description: "Toggle your signature (default is toggle)", - required: false, - type: ApplicationCommandOptionType.BOOLEAN, - }, - ], - execute: async (args, ctx) => { - settings.store.isEnabled = !!findOption(args, "value", !settings.store.isEnabled); - sendBotMessage(ctx.channel.id, { - content: settings.store.isEnabled ? "Signature enabled!" : "Signature disabled!", - }); - }, - } - ], -}); - -// text processing injection processor -function textProcessing(input: string) { - return `${input}\n${settings.store.textHeader ? settings.store.textHeader + " " : ""}${settings.store.name}`; -} diff --git a/src/equicordplugins/snowfall/index.tsx b/src/equicordplugins/snowfall/index.tsx deleted file mode 100644 index 7f103e9f4..000000000 --- a/src/equicordplugins/snowfall/index.tsx +++ /dev/null @@ -1,490 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { Heading } from "@components/Heading"; -import { Paragraph } from "@components/Paragraph"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { createRoot, React } from "@webpack/common"; -import type { Root } from "react-dom/client"; - -const SnowfallCSS = ` -#snowfield { - pointer-events: none; - user-select: none; - z-index: 100000; - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; -} -.snowflake { - position: absolute; - color: #fff; - line-height: 1; - -webkit-font-smoothing: antialiased; -} -.snowflake-solid { - border-radius: 50%; - background: #ffffff; -} -.snowflake-image { - filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.5)); -} -`; - -// SVG snowflake images as data URIs -const SNOWFLAKE_SVGS = [ - // 6-pointed snowflake - "data:image/svg+xml," + encodeURIComponent(` - - - - - `), - // 8-pointed star snowflake - "data:image/svg+xml," + encodeURIComponent(` - - - - - - - - - - `), - // Discord snowflake - "data:image/svg+xml," + encodeURIComponent(` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - `), -]; - -const settings = definePluginSettings({ - typeOfSnow: { - description: "Change the type of snow displayed (Affects performance).", - type: OptionType.SELECT, - options: [ - { label: "Solid (Highest Performance)", value: "solid" }, - { label: "Text (Medium Performance)", value: "text", default: true }, - { label: "Image (Lowest Performance)", value: "image" } - ], - }, - maxSize: { - description: "Maximum snowflake size", - type: OptionType.SLIDER, - default: 30, - markers: [10, 20, 30, 40, 50] - }, - speed: { - description: "Snowfall speed (higher = faster fall)", - type: OptionType.SLIDER, - default: 50, - markers: [50, 100, 200, 300, 400, 500] - }, - flakesPerSecond: { - description: "Snowflakes per second (higher = denser snowfall)", - type: OptionType.SLIDER, - default: 5, - markers: [1, 5, 10, 20, 40, 60], - min: 1, - max: 60, - } -}); - -class CopleSnow { - private static winWidth = window.innerWidth; - private static winHeight = window.innerHeight; - - static readonly defaultOptions = { - minSize: 10, - maxSize: 30, - type: "text" as "text" | "solid" | "image", - content: "❄" as string | string[], - fadeOut: true, - autoplay: true, - interval: 200 - }; - - private static cssPrefix(propertyName: string): string | null { - const capitalize = propertyName.charAt(0).toUpperCase() + propertyName.slice(1); - const tempDiv = document.createElement("div"); - const { style } = tempDiv; - const prefixes = ["Webkit", "Moz", "ms", "O"]; - - if (propertyName in style) return propertyName; - for (const prefix of prefixes) { - const name = prefix + capitalize; - if (name in style) return name; - } - return null; - } - - private static readonly cssPrefixedNames = { - transform: this.cssPrefix("transform"), - transition: this.cssPrefix("transition") - }; - - private static readonly transitionEndEvent = - { WebkitTransition: "webkitTransitionEnd", OTransition: "oTransitionEnd", Moztransition: "transitionend", transition: "transitionend" }[ - this.cssPrefixedNames.transition ?? "transition" - ] ?? "transitionend"; - - private static random(min: number, max: number, deviation?: number): number { - if (deviation !== undefined) { - deviation *= max; - max += deviation; - min = max - deviation; - } else { - min = min || 0; - } - return Math.floor(Math.random() * (max - min + 1) + min); - } - - private static setStyle(element: HTMLElement, rules: Record) { - for (const [name, value] of Object.entries(rules)) { - const cssName = CopleSnow.cssPrefixedNames[name as keyof typeof CopleSnow.cssPrefixedNames] || name; - (element.style as any)[cssName] = value; - } - } - - private options = { ...CopleSnow.defaultOptions }; - private queue: HTMLElement[] = []; - private $snowfield: HTMLDivElement; - private timer: number | null = null; - public playing = false; - - constructor(newOptions: Partial = {}) { - Object.assign(this.options, newOptions); - - this.$snowfield = document.createElement("div"); - this.$snowfield.id = "snowfield"; - document.body.appendChild(this.$snowfield); - - const updateSize = () => { - CopleSnow.winHeight = window.innerHeight; - CopleSnow.winWidth = window.innerWidth; - }; - window.addEventListener("resize", updateSize); - (this as any)._resizeHandler = updateSize; - - this.$snowfield.addEventListener(CopleSnow.transitionEndEvent, e => { - const snowflake = e.target as HTMLElement; - if (snowflake.classList.contains("snowflake")) { - this.$snowfield.removeChild(snowflake); - this.queue.push(snowflake); - } - }); - - const handleVisibilityChange = () => { - if (document.hidden) this.stop(); - else this.play(); - }; - document.addEventListener("visibilitychange", handleVisibilityChange); - (this as any)._visibilityHandler = handleVisibilityChange; - - if (this.options.autoplay) this.play(); - } - - private createSnowflake(): HTMLElement { - const { type, content } = this.options as { type: "text" | "image" | "solid"; content: string | string[]; }; - const cntLength = Array.isArray(content) ? content.length : 1; - - let snowflake: HTMLElement; - - if (type === "image") { - snowflake = document.createElement("img"); - const src = typeof content === "string" - ? content - : content[Math.floor(Math.random() * cntLength)]; - (snowflake as HTMLImageElement).src = src; - snowflake.setAttribute("draggable", "false"); - } else { - snowflake = document.createElement("div"); - if (type === "text") { - const textContent = typeof content === "string" - ? content - : content[Math.floor(Math.random() * cntLength)]; - snowflake.textContent = textContent; - } - // if type is solid we don't need to set content - } - - snowflake.className = `snowflake snowflake-${type}`; - (snowflake as any).dataset.type = type; - - return snowflake; - } - - private animateSnowflake() { - const { winWidth, winHeight } = CopleSnow; - const size = CopleSnow.random(this.options.minSize, this.options.maxSize); - const top = -2 * size; - const left = CopleSnow.random(0, winWidth - size); - const opacity = CopleSnow.random(5, 10) / 10; - const angle = CopleSnow.random(0, winHeight * 0.8, 1); - const translateX = CopleSnow.random(-100, 100); - const translateY = winHeight + size * 2; - const baseSpeed = settings.store.speed; - const duration = (winHeight * 10) / (baseSpeed / 50); - let snowflake: HTMLElement; - if (this.queue.length > 0) { - const reused = this.queue.shift()!; - if ((reused.dataset.type as string) !== this.options.type) { - snowflake = this.createSnowflake(); - } else { - snowflake = reused; - if (this.options.type === "text") { - const { content } = this.options; - if (Array.isArray(content)) { - snowflake.textContent = content[Math.floor(Math.random() * content.length)]; - } - } else if (this.options.type === "image") { - const { content } = this.options; - if (Array.isArray(content)) { - (snowflake as HTMLImageElement).src = content[Math.floor(Math.random() * content.length)]; - } - } - } - } else { - snowflake = this.createSnowflake(); - } - - const styleRules: Record = { - top: `${top}px`, - left: `${left}px`, - opacity: opacity, - transform: "none", - transition: `${duration}ms linear` - }; - - switch (this.options.type) { - case "solid": - styleRules.width = styleRules.height = `${size}px`; - break; - case "text": - styleRules.fontSize = `${size}px`; - break; - case "image": - styleRules.width = `${size}px`; - styleRules.height = "auto"; - break; - } - - CopleSnow.setStyle(snowflake, styleRules); - this.$snowfield.appendChild(snowflake); - - setTimeout(() => { - CopleSnow.setStyle(snowflake, { - transform: `translate(${translateX}px, ${translateY}px) rotate(${angle}deg)`, - opacity: this.options.fadeOut ? 0 : opacity - }); - }, 100); - } - - play() { - if (this.playing) return; - this.timer = window.setInterval(() => this.animateSnowflake(), this.options.interval); - this.playing = true; - } - - stop() { - if (!this.playing) return; - if (this.timer !== null) { - clearInterval(this.timer); - this.timer = null; - } - this.playing = false; - } - - destroy() { - this.stop(); - if (this.$snowfield.parentNode) { - this.$snowfield.remove(); - } - window.removeEventListener("resize", (this as any)._resizeHandler); - document.removeEventListener("visibilitychange", (this as any)._visibilityHandler); - } - - updateOptions(newOptions: Partial) { - Object.assign(this.options, newOptions); - } -} - -const SnowfallManager: React.FC = () => { - const snowRef = React.useRef(null); - const styleRef = React.useRef(null); - - React.useEffect(() => { - // Inject CSS - const styleEl = document.createElement("style"); - styleEl.id = "snowfall-styles"; - styleEl.textContent = SnowfallCSS; - document.head.appendChild(styleEl); - styleRef.current = styleEl; - - // Get initial snow type from settings - const snowType = settings.store.typeOfSnow as "text" | "solid" | "image"; - const snowOptions: Partial = { - autoplay: false, - type: snowType, - maxSize: settings.store.maxSize, - interval: 1000 / settings.store.flakesPerSecond - }; - - snowOptions.interval = settings.store.speed; - - // Set content based on type - if (snowType === "image") { - snowOptions.content = SNOWFLAKE_SVGS; - } - - // Create snow instance with settings - const snow = new CopleSnow(snowOptions); - snowRef.current = snow; - - const blurHandler = () => snow.stop(); - const focusHandler = () => { - if (document.hasFocus()) snow.play(); - }; - - window.addEventListener("blur", blurHandler); - window.addEventListener("focus", focusHandler); - - if (document.hasFocus()) { - snow.play(); - } - - // Custom listener for settings changes (we don't have a built-in one) - let lastSettings = { - type: settings.store.typeOfSnow as "text" | "solid" | "image", - maxSize: settings.store.maxSize, - speed: settings.store.speed, - flakesPerSecond: settings.store.flakesPerSecond - }; - - const settingsInterval = setInterval(() => { - const newSettings = { - type: settings.store.typeOfSnow as "text" | "solid" | "image", - maxSize: settings.store.maxSize, - speed: settings.store.speed, - flakesPerSecond: settings.store.flakesPerSecond - }; - - if (Object.keys(newSettings).some(k => newSettings[k as keyof typeof newSettings] !== lastSettings[k as keyof typeof lastSettings])) { - lastSettings = newSettings; - - const updateOptions: Partial = { - type: newSettings.type, - maxSize: newSettings.maxSize, - interval: 1000 / newSettings.flakesPerSecond - }; - - if (newSettings.type === "image") updateOptions.content = SNOWFLAKE_SVGS; - else if (newSettings.type === "text") updateOptions.content = "❄"; - - snow.updateOptions(updateOptions); - } - }, 5000); - - return () => { - snow.destroy(); - snowRef.current = null; - - window.removeEventListener("blur", blurHandler); - window.removeEventListener("focus", focusHandler); - - clearInterval(settingsInterval); - - if (styleEl.parentNode) { - styleEl.remove(); - } - styleRef.current = null; - - const snowfield = document.getElementById("snowfield"); - if (snowfield) snowfield.remove(); - }; - }, []); - - return null; -}; - -let snowRoot: Root | null = null; -let container: HTMLDivElement | null = null; - -export default definePlugin({ - name: "Snowfall", - description: "Let it snow on Discord! Ported from the BetterDiscord plugin by square.", - authors: [EquicordDevs.ZcraftElite, EquicordDevs.square], - - settingsAboutComponent: () => ( - <> - Information - - This plugin adds a christmas-y snowfall effect on top of Discord's interface. - You can change the type of snow in the settings below. -

- NOTE: While on most computers this plugin will not impact performance any more than your average Equicord extension, - it may cause some lag on lower end systems. -
- - ), - - settings, - - start() { - container = document.createElement("div"); - container.id = "snowfall-plugin-container"; - document.body.appendChild(container); - - snowRoot = createRoot(container); - snowRoot.render(); - }, - - stop() { - if (snowRoot) { - snowRoot.unmount(); - snowRoot = null; - } - if (container) { - if (container.parentNode) container.remove(); - container = null; - } - } -}); diff --git a/src/equicordplugins/soggy/index.tsx b/src/equicordplugins/soggy/index.tsx deleted file mode 100644 index a622197d9..000000000 --- a/src/equicordplugins/soggy/index.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { AudioPlayerInterface, createAudioPlayer } from "@api/AudioPlayer"; -import { HeaderBarButton } from "@api/HeaderBar"; -import { definePluginSettings } from "@api/Settings"; -import { EquicordDevs } from "@utils/constants"; -import { ModalProps, ModalRoot, openModal } from "@utils/modal"; -import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; -let boopSound: AudioPlayerInterface; -let song: AudioPlayerInterface; - -function assignSong(url: string, volume: number) { - song?.delete(); - song = createAudioPlayer(url, { volume, preload: true, persistent: true }); - song.load(); -} - -function assignBoop(url: string, volume: number) { - boopSound?.delete(); - boopSound = createAudioPlayer(url, { volume, preload: true, persistent: true }); - boopSound.load(); -} - -function SoggyModal(props: ModalProps) { - if (settings.store.songVolume !== 0) { - React.useEffect(() => { - song?.loop(); - - return () => { - song?.stop(); - }; - }, []); - } - - const boop = (e: React.MouseEvent) => { - const { offsetX, offsetY } = e.nativeEvent; - - const region = { x: 155, y: 220, width: 70, height: 70 }; - - if ( - settings.store.boopVolume !== 0 && - offsetX >= region.x && - offsetX <= region.x + region.width && - offsetY >= region.y && - offsetY <= region.y + region.height - ) { - boopSound?.play(); - } - }; - - return ( - - - - ); -} - -function buildSoggyModal(): any { - openModal(props => ); -} - -function SoggyButton() { - return ( - ( - - )} - onClick={() => buildSoggyModal()} - selected={false} - /> - ); -} - -const settings = definePluginSettings({ - songVolume: { - description: "Volume of the song. 0 to disable", - type: OptionType.SLIDER, - default: 0.25, - markers: [0, 0.25, 0.5, 0.75, 1], - stickToMarkers: false, - - }, - boopVolume: { - description: "Volume of the boop sound", - type: OptionType.SLIDER, - default: 0.2, - markers: [0, 0.25, 0.5, 0.75, 1], - stickToMarkers: false, - }, - tooltipText: { - description: "The text shown when hovering over the button", - type: OptionType.STRING, - default: "the soggy", - }, - imageLink: { - description: "URL for the image (button and modal)", - type: OptionType.STRING, - default: "https://equicord.org/assets/plugins/soggy/cat.png", - }, - songLink: { - description: "URL for the song to play", - type: OptionType.STRING, - default: "https://github.com/Equicord/Equibored/raw/main/sounds/soggy/song.mp3?raw=true", - onChange(newValue) { - assignSong(newValue, settings.store.songVolume * 100); - }, - }, - boopLink: { - description: "URL for the boop sound", - type: OptionType.STRING, - default: "https://github.com/Equicord/Equibored/raw/main/sounds/soggy/honk.wav?raw=true", - onChange(newValue) { - assignBoop(newValue, settings.store.boopVolume * 100); - } - } -}); - -export default definePlugin({ - name: "Soggy", - description: "Adds a soggy button to the toolbox", - authors: [EquicordDevs.sliwka], - settings, - dependencies: ["AudioPlayerAPI"], - - headerBarButton: { - icon: () => ( - - ), - render: SoggyButton - }, - - start() { - assignBoop(settings.store.boopLink, settings.store.boopVolume * 100); - assignSong(settings.store.songLink, settings.store.songVolume * 100); - }, - - stop() { - boopSound?.delete(); - song?.delete(); - }, -}); diff --git a/src/equicordplugins/talkInReverse/index.tsx b/src/equicordplugins/talkInReverse/index.tsx deleted file mode 100644 index c469c9b31..000000000 --- a/src/equicordplugins/talkInReverse/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons"; -import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { React, useEffect, useState } from "@webpack/common"; - -let lastState = false; - -const ReverseMessageToggle: ChatBarButtonFactory = ({ isMainChat }) => { - const [enabled, setEnabled] = useState(lastState); - - function setEnabledValue(value: boolean) { - lastState = value; - - setEnabled(value); - } - - useEffect(() => { - const listener: MessageSendListener = async (_, message) => { - if (enabled && message.content) message.content = message.content.split("").reverse().join(""); - }; - - addMessagePreSendListener(listener); - return () => void removeMessagePreSendListener(listener); - }, [enabled]); - - if (!isMainChat) return null; - - return ( - setEnabledValue(!enabled)} - > - - - ); -}; - -function ReverseMessageIcon() { - return ( - - - - ); -} - -export default definePlugin({ - name: "TalkInReverse", - authors: [EquicordDevs.Tolgchu], - description: "Reverses the message content before sending it.", - dependencies: ["MessageEventsAPI"], - chatBarButton: { - icon: ReverseMessageIcon, - render: ReverseMessageToggle - } -}); diff --git a/src/equicordplugins/toggleVideoBind/index.ts b/src/equicordplugins/toggleVideoBind/index.ts deleted file mode 100644 index 3a56c4e8f..000000000 --- a/src/equicordplugins/toggleVideoBind/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 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 { findByPropsLazy } from "@webpack"; -import { FluxDispatcher } from "@webpack/common"; - -const validKeycodes = [ - "Backspace", "Tab", "Enter", "ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight", "Pause", "CapsLock", - "Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "PrintScreen", "Insert", - "Delete", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "KeyA", "KeyB", "KeyC", - "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", - "KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ", "MetaLeft", "MetaRight", "ContextMenu", "Numpad0", "Numpad1", "Numpad2", "Numpad3", - "Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "NumpadMultiply", "NumpadAdd", "NumpadSubtract", "NumpadDecimal", - "NumpadDivide", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "NumLock", "ScrollLock", -]; - -const settings = definePluginSettings({ - keyBind: { - description: "The key to toggle webcam when pressed.", - type: OptionType.STRING, - default: "KeyX", - isValid: (value: string) => validKeycodes.includes(value), - }, - reqCtrl: { - description: "Require control to be held.", - type: OptionType.BOOLEAN, - default: true, - }, - reqShift: { - description: "Require shift to be held.", - type: OptionType.BOOLEAN, - default: true, - }, - reqAlt: { - description: "Require alt to be held.", - type: OptionType.BOOLEAN, - default: false, - }, -}); - -const { isVideoEnabled } = findByPropsLazy("isVideoEnabled"); - -function handleKeydown({ code, ctrlKey, shiftKey, altKey }: KeyboardEvent) { - const { keyBind, reqCtrl, reqShift, reqAlt } = settings.store; - if (keyBind !== code || ctrlKey !== reqCtrl || shiftKey !== reqShift || altKey !== reqAlt) { return; } // please don't think about it - - FluxDispatcher.dispatch({ - type: "MEDIA_ENGINE_SET_VIDEO_ENABLED", - enabled: !isVideoEnabled(), - }); -} - -export default definePlugin({ - name: "ToggleVideoBind", - description: "Adds a customizable bind to toggle webcam.", - authors: [EquicordDevs.mochienya], - settings, - start() { - document.addEventListener("keydown", handleKeydown); - }, - stop() { - document.removeEventListener("keydown", handleKeydown); - }, -}); diff --git a/src/equicordplugins/toneIndicators/ToneIndicator.tsx b/src/equicordplugins/toneIndicators/ToneIndicator.tsx deleted file mode 100644 index 631b5736e..000000000 --- a/src/equicordplugins/toneIndicators/ToneIndicator.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Tooltip } from "@webpack/common"; -import type { ReactElement } from "react"; - -export interface ToneIndicatorProps { - prefix: string; - indicator: string; - desc: string; -} - -export default function ToneIndicator({ - prefix, - indicator, - desc, -}: ToneIndicatorProps): ReactElement { - return ( - - {tooltipProps => ( - - {prefix}{indicator} - - )} - - ); -} diff --git a/src/equicordplugins/toneIndicators/index.tsx b/src/equicordplugins/toneIndicators/index.tsx deleted file mode 100644 index 6dc0ba1a0..000000000 --- a/src/equicordplugins/toneIndicators/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 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 { React } from "@webpack/common"; -import { type ReactNode } from "react"; - -import indicatorsDefault from "./indicators"; -import ToneIndicator from "./ToneIndicator"; - -const settings = definePluginSettings({ - prefix: { - type: OptionType.STRING, - description: "Prefix character(s) for tone indicators.", - default: "/", - }, - customIndicators: { - type: OptionType.STRING, - description: "Custom tone indicators (format: jk=Joking; srs=Serious)", - default: "", - }, -}); - -function getCustomIndicators(): Record { - const raw = settings.store.customIndicators || ""; - const result: Record = {}; - - raw.split("; ").forEach(entry => { - const [key, ...rest] = entry.split("="); - if (key && rest.length > 0) { - result[key.trim().toLowerCase()] = rest.join("=").trim(); - } - }); - - return result; -} - -function getIndicator(text: string): string | null { - text = text.toLowerCase(); - const customIndicators = getCustomIndicators(); - - return ( - customIndicators[text] || - customIndicators[`_${text}`] || - indicatorsDefault.get(text) || - indicatorsDefault.get(`_${text}`) || - null - ); -} - -function buildIndicatorRegex(): RegExp { - const customIndicators = getCustomIndicators(); - const allIndicators = new Set(); - - indicatorsDefault.forEach((_, key) => { - allIndicators.add(key.replace(/^_/, "")); // remove underscore prefix for aliases - }); - Object.keys(customIndicators).forEach(key => { - allIndicators.add(key.replace(/^_/, "")); // remove underscore prefix for aliases - }); - - // escape special regex characters and sort by length (longest first) - const escaped = Array.from(allIndicators) - .map(ind => ind.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) - .sort((a, b) => b.length - a.length); // longest first to avoid partial matches (should fix some edge cases) - - const prefix = settings.store.prefix || "/"; - let escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - - // if prefix is a markdown character, also match escaped version - const isMarkdown = /[*_~`|]/.test(prefix); - if (isMarkdown) { - escapedPrefix = `(?:\\\\${escapedPrefix}|${escapedPrefix})`; - } - - // exclude forward slash from punctuation to prevent sed syntax conflicts (s/find/replace) - const pattern = `(?:^|\\s)${escapedPrefix}(${escaped.join("|")})(?=\\s|$|[^\\s\\w/])`; - return new RegExp(pattern, "giu"); // 'i' = case-insensitive, 'u' = unicode -} - -function splitTextWithIndicators(text: string): ReactNode[] { - const nodes: ReactNode[] = []; - let lastIndex = 0; - const regex = buildIndicatorRegex(); - const prefix = settings.store.prefix || "/"; - let match: RegExpExecArray | null; - - while ((match = regex.exec(text))) { - const indicator = match[1]; - const desc = getIndicator(indicator); - - const fullMatch = match[0]; - const leadingWhitespace = fullMatch.match(/^(\s*)/)?.[1] ?? ""; - - const matchStart = match.index; - const matchEnd = regex.lastIndex; - - if (matchStart > lastIndex) { - nodes.push(text.slice(lastIndex, matchStart)); - } - - if (desc) { - if (leadingWhitespace) nodes.push(leadingWhitespace); - nodes.push( - , - ); - } - - lastIndex = matchEnd; - } - - if (lastIndex < text.length) nodes.push(text.slice(lastIndex)); - - return nodes; -} - -function patchChildrenTree(children: any): any { - const transform = (node: any): any => { - if (node == null) return node; - - if (typeof node === "string") { - const prefix = settings.store.prefix || "/"; - let escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - - // if prefix is markdown character, also check for escaped version - const isMarkdown = /[*_~`|]/.test(prefix); - if (isMarkdown) { - escapedPrefix = `(?:\\\\${escapedPrefix}|${escapedPrefix})`; - } - - if (!new RegExp(`${escapedPrefix}[\\p{L}_]+`, "iu").test(node)) return node; - const parts = splitTextWithIndicators(node); - return parts.length === 1 ? parts[0] : parts; - } - - if (node?.props?.children != null) { - const c = node.props.children; - if (Array.isArray(c)) { - node.props.children = c.map(transform).flat(); - } else { - node.props.children = transform(c); - } - return node; - } - - return node; - }; - - if (Array.isArray(children)) return children.map(transform).flat(); - return transform(children); -} - -export default definePlugin({ - name: "ToneIndicators", - description: "Show tooltips for tone indicators like /srs, /gen, etc. in sent messages.", - authors: [EquicordDevs.justjxke], - settings, - - patches: [ - { - find: '["strong","em","u","text","inlineCode","s","spoiler"]', - replacement: [ - { - match: /(?=return\{hasSpoilerEmbeds:\i,.{0,15}content:(\i))/, - replace: "$1=$self.patchToneIndicators($1);", - }, - ], - }, - ], - - patchToneIndicators(content: any): any { - try { - return patchChildrenTree(content); - } catch { - return content; - } - }, -}); diff --git a/src/equicordplugins/toneIndicators/indicators.ts b/src/equicordplugins/toneIndicators/indicators.ts deleted file mode 100644 index e03a45015..000000000 --- a/src/equicordplugins/toneIndicators/indicators.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -const defaultIndicators = new Map( - Object.entries({ - j: "Joking", - hj: "Half Joking", - s: "Sarcastic", - _sarc: "Sarcastic", // Alias for s - gen: "Genuine", - _g: "Genuine", // Alias for gen, - genq: "Genuine Question", - _gq: "Genuine Question", // Alias for genq - srs: "Serious", - nsrs: "Non-Serious", - pos: "Positive Connotation", - _pc: "Positive Connotation", // Alias for pos - neu: "Neutral Connotation", - neg: "Negative Connotation", - _nc: "Negative Connotation", // Alias for neg - p: "Platonic", - r: "Romantic", - a: "Alterous", - c: "Copypasta", - l: "Lyrics", - _ly: "Lyrics", // Alias for l - lh: "Light-Hearted", - nm: "Not Mad", - lu: "A Little Upset", - nbh: "Not Directed At Anybody Here", - nay: "Not Directed At You", - ay: "Directed At You", - nsb: "Not Subtweeting", - x: "Sexual Intent", - _sx: "Sexual Intent", // Alias for x - nx: "Non-Sexual Intent", - _nsx: "Non-Sexual Intent", // Alias for nx - rh: "Rhetorical Question", - _rt: "Rhetorical Question", // Alias for rh - t: "Teasing", - ij: "Inside Joke", - m: "Metaphorically", - li: "Literally", - hyp: "Hyperbole", - ex: "Exaggeration", - f: "Fake", - q: "Quote", - th: "Threat", - cb: "Clickbait", - ref: "Reference", - nf: "Not Forced", - nbr: "Not Being Rude", - ot: "Off-Topic", - nj: "Not Joking", - naj: "Not A Joke", - }), -); - -export default defaultIndicators; diff --git a/src/equicordplugins/translatePlus/index.tsx b/src/equicordplugins/translatePlus/index.tsx deleted file mode 100644 index 78e48b120..000000000 --- a/src/equicordplugins/translatePlus/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 "./style.css"; - -import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { ChannelStore, Menu } from "@webpack/common"; - -import { settings } from "./settings"; -import { Accessory, handleTranslate } from "./utils/accessory"; -import { Icon } from "./utils/icon"; - -const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) => { - if (!message.content) return; - - const group = findGroupChildrenByChildId("copy-text", children); - if (!group) return; - - group.splice(group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, ( - handleTranslate(message)} - /> - )); -}; - -export default definePlugin({ - name: "Translate+", - description: "Vencord's translate plugin but with support for artistic languages!", - authors: [Devs.Ven, EquicordDevs.Prince527], - settings, - contextMenus: { - "message": messageCtxPatch - }, - renderMessageAccessory: props => , - messagePopoverButton: { - icon: Icon, - render(message) { - if (!message.content) return null; - return { - label: "Translate", - icon: Icon, - message: message, - channel: ChannelStore.getChannel(message.channel_id), - onClick: () => handleTranslate(message), - }; - } - } -}); diff --git a/src/equicordplugins/translatePlus/misc/languages.ts b/src/equicordplugins/translatePlus/misc/languages.ts deleted file mode 100644 index 20b0448dc..000000000 --- a/src/equicordplugins/translatePlus/misc/languages.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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 . -*/ - -/* -To generate: -- Visit https://translate.google.com/?sl=auto&tl=en&op=translate -- Open Language dropdown -- Open Devtools and use the element picker to pick the root of the language picker -- Right click on the element in devtools and click "Store as global variable" - -copy(Object.fromEntries( - Array.from( - temp1.querySelectorAll("[data-language-code]"), - e => [e.dataset.languageCode, e.children[1].textContent] - ).sort((a, b) => a[1] === "Detect language" ? -1 : b[1] === "Detect language" ? 1 : a[1].localeCompare(b[1])) -)) -*/ - -export type languages = keyof typeof languages; - -export const languages = { - "auto": "Detect language", - "af": "Afrikaans", - "sq": "Albanian", - "am": "Amharic", - "ar": "Arabic", - "hy": "Armenian", - "as": "Assamese", - "ay": "Aymara", - "az": "Azerbaijani", - "bm": "Bambara", - "eu": "Basque", - "be": "Belarusian", - "bn": "Bengali", - "bho": "Bhojpuri", - "bs": "Bosnian", - "bg": "Bulgarian", - "ca": "Catalan", - "ceb": "Cebuano", - "ny": "Chichewa", - "zh-CN": "Chinese (Simplified)", - "zh-TW": "Chinese (Traditional)", - "co": "Corsican", - "hr": "Croatian", - "cs": "Czech", - "da": "Danish", - "dv": "Dhivehi", - "doi": "Dogri", - "nl": "Dutch", - "en": "English", - "eo": "Esperanto", - "et": "Estonian", - "ee": "Ewe", - "tl": "Filipino", - "tp": "Toki Pona", - "sh": "Shavian", - "fi": "Finnish", - "fr": "French", - "fy": "Frisian", - "gl": "Galician", - "ka": "Georgian", - "de": "German", - "el": "Greek", - "gn": "Guarani", - "gu": "Gujarati", - "ht": "Haitian Creole", - "ha": "Hausa", - "haw": "Hawaiian", - "iw": "Hebrew", - "hi": "Hindi", - "hmn": "Hmong", - "hu": "Hungarian", - "is": "Icelandic", - "ig": "Igbo", - "ilo": "Ilocano", - "id": "Indonesian", - "ga": "Irish", - "it": "Italian", - "ja": "Japanese", - "jw": "Javanese", - "kn": "Kannada", - "kk": "Kazakh", - "km": "Khmer", - "rw": "Kinyarwanda", - "gom": "Konkani", - "ko": "Korean", - "kri": "Krio", - "ku": "Kurdish (Kurmanji)", - "ckb": "Kurdish (Sorani)", - "ky": "Kyrgyz", - "lo": "Lao", - "la": "Latin", - "lv": "Latvian", - "ln": "Lingala", - "lt": "Lithuanian", - "lg": "Luganda", - "lb": "Luxembourgish", - "mk": "Macedonian", - "mai": "Maithili", - "mg": "Malagasy", - "ms": "Malay", - "ml": "Malayalam", - "mt": "Maltese", - "mi": "Maori", - "mr": "Marathi", - "mni-Mtei": "Meiteilon (Manipuri)", - "lus": "Mizo", - "mn": "Mongolian", - "my": "Myanmar (Burmese)", - "ne": "Nepali", - "no": "Norwegian", - "or": "Odia (Oriya)", - "om": "Oromo", - "ps": "Pashto", - "fa": "Persian", - "pl": "Polish", - "pt": "Portuguese", - "pa": "Punjabi", - "qu": "Quechua", - "ro": "Romanian", - "ru": "Russian", - "sm": "Samoan", - "sa": "Sanskrit", - "gd": "Scots Gaelic", - "nso": "Sepedi", - "sr": "Serbian", - "st": "Sesotho", - "sn": "Shona", - "sd": "Sindhi", - "si": "Sinhala", - "sk": "Slovak", - "sl": "Slovenian", - "so": "Somali", - "es": "Spanish", - "su": "Sundanese", - "sw": "Swahili", - "sv": "Swedish", - "tg": "Tajik", - "ta": "Tamil", - "tt": "Tatar", - "te": "Telugu", - "th": "Thai", - "ti": "Tigrinya", - "ts": "Tsonga", - "tr": "Turkish", - "tk": "Turkmen", - "ak": "Twi", - "uk": "Ukrainian", - "ur": "Urdu", - "ug": "Uyghur", - "uz": "Uzbek", - "vi": "Vietnamese", - "cy": "Welsh", - "xh": "Xhosa", - "yi": "Yiddish", - "yo": "Yoruba", - "zu": "Zulu" -} as const; diff --git a/src/equicordplugins/translatePlus/misc/types.ts b/src/equicordplugins/translatePlus/misc/types.ts deleted file mode 100644 index 291be4576..000000000 --- a/src/equicordplugins/translatePlus/misc/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { classNameFactory } from "@utils/css"; - -export const cl = classNameFactory("eq-trans-"); - -export interface Translation { - text: string; - src: string; -} - -export type IconProps = { - width?: number; - height?: number; -}; diff --git a/src/equicordplugins/translatePlus/settings.ts b/src/equicordplugins/translatePlus/settings.ts deleted file mode 100644 index 3fd2f030b..000000000 --- a/src/equicordplugins/translatePlus/settings.ts +++ /dev/null @@ -1,35 +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 { OptionType } from "@utils/types"; - -export const settings = definePluginSettings({ - target: { - type: OptionType.STRING, - description: "Target language", - default: "en", - restartNeeded: true - }, - toki: { - type: OptionType.BOOLEAN, - description: "Enable Toki Pona", - default: true, - restartNeeded: true - }, - sitelen: { - type: OptionType.BOOLEAN, - description: "Enable Sitelen Pona", - default: true, - restartNeeded: true - }, - shavian: { - type: OptionType.BOOLEAN, - description: "Enable Shavian", - default: true, - restartNeeded: true - } -}); diff --git a/src/equicordplugins/translatePlus/style.css b/src/equicordplugins/translatePlus/style.css deleted file mode 100644 index 85bdfc2b2..000000000 --- a/src/equicordplugins/translatePlus/style.css +++ /dev/null @@ -1,22 +0,0 @@ -.eq-trans-accessory { - color: var(--text-muted); - margin-top: 0.5em; - font-style: italic; - font-weight: 400; - display: block; - white-space: pre-wrap; -} - -.eq-trans-accessory svg { - margin-right: 0.25em; -} - -.eq-trans-dismiss { - all: unset; - cursor: pointer; - color: var(--text-link); -} - -.eq-trans-dismiss:is(:hover, :focus) { - text-decoration: underline; -} \ No newline at end of file diff --git a/src/equicordplugins/translatePlus/utils/accessory.tsx b/src/equicordplugins/translatePlus/utils/accessory.tsx deleted file mode 100644 index 7ad167d73..000000000 --- a/src/equicordplugins/translatePlus/utils/accessory.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { languages } from "@equicordplugins/translatePlus/misc/languages"; -import { cl, Translation } from "@equicordplugins/translatePlus/misc/types"; -import { Message } from "@vencord/discord-types"; -import { Parser, useEffect, useState } from "@webpack/common"; - -import { SmallIcon } from "./icon"; -import { translate } from "./translator"; - -const setters = new Map(); - -export function Accessory({ message }: { message: Message; }) { - const [translation, setTranslation] = useState(undefined); - - useEffect(() => { - if ((message as any).vencordEmbeddedBy) return; - - setters.set(message.id, setTranslation); - - return () => void setters.delete(message.id); - }, []); - - if (!translation) return null; - - return ( -
- - {Parser.parse(translation.text)} - {" "} - (translated from {languages[translation.src] ?? translation.src} - ) -
- ); -} - -export async function handleTranslate(message: Message) { - setters.get(message.id)!(await translate(message.content)); -} diff --git a/src/equicordplugins/translatePlus/utils/icon.tsx b/src/equicordplugins/translatePlus/utils/icon.tsx deleted file mode 100644 index 8a08675e4..000000000 --- a/src/equicordplugins/translatePlus/utils/icon.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { cl } from "@equicordplugins/translatePlus/misc/types"; - -export function Icon() { - return ( - - - - - - ); -} - -export function SmallIcon() { - return ( - - - - - - ); -} diff --git a/src/equicordplugins/translatePlus/utils/translator.ts b/src/equicordplugins/translatePlus/utils/translator.ts deleted file mode 100644 index 6b002d769..000000000 --- a/src/equicordplugins/translatePlus/utils/translator.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { settings } from "@equicordplugins/translatePlus/settings"; - -function isTokiPona(text: string) { - const dictionary = /\b(?:leko|weka|pan|lete|linja|lipu|suli|nimi|akesi|misikeke|selo|ike|sijelo|sona|lili|pimeja|ante|jo|loje|telo|walo|kijetesantakalu|kasi|waso|wile|utala|lukin|sina|lape|ma|pilin|jasima|la|olin|pipi|meso|lawa|pi|pakala|oko|tan|ken|jaki|unpa|esun|seme|sitelen|len|kule|soko|open|ala|tenpo|lon|sinpin|pini|kokosila|mama|musi|monsi|mewika|taso|ona|mun|kiwen|tomo|mute|mi|nena|palisa|meli|laso|wawa|ale|kipisi|kulupu|ilo|lupa|nanpa|en|mu|jelo|kili|tonsi|moku|ni|kama|pu|poki|monsuta|sin|lasina|poka|soweli|sewi|elena|epiku|moli|pona|lanpan|alasa|anu|kute|uta|luka|suno|sama|awen|namako|suwi|noka|seli|mije|sike|jan|pali|tawa|inli|nasa|mani|wan|insa|nijon|nasin|kalama|ijo|toki|anpa|kala|kepeken|ko|kon|pana|tu|supa|kin|usawi|yupekosi)\b/gm; - - return (text.match(dictionary) || []).length >= text.split(/\s+/).length * 0.5; -} - -function isSitelen(text: string) { - const dictionary = /(?:󱤀|󱤁|󱤂|󱤃|󱤄|󱤅|󱤆|󱤇|󱤈|󱤉|󱤊|󱤋|󱤌|󱤍|󱤎|󱤏|󱤐|󱤑|󱤒|󱤓|󱤔|󱤕|󱤖|󱤗|󱤘|󱤙|󱤚|󱤛|󱤜|󱤝|󱤞|󱤟|󱤠|󱤡|󱤢|󱤣|󱤤|󱤥|󱤦|󱤧|󱤨|󱤩|󱤪|󱤫|󱤬|󱤭|󱤮|󱤯|󱤰|󱤱|󱤲|󱤳|󱤴|󱤵|󱤶|󱤷|󱤸|󱤹|󱤺|󱤻|󱤼|󱤽|󱤾|󱤿|󱥀|󱥁|󱥂|󱥃|󱥄|󱥅|󱥆|󱥇|󱥈|󱥉|󱥊|󱥋|󱥌|󱥍|󱥎|󱥏|󱥐|󱥑|󱥒|󱥓|󱥔|󱥕|󱥖|󱥗|󱥘|󱥙|󱥚|󱥛|󱥜|󱥝|󱥞|󱥟|󱥠|󱥡|󱥢|󱥣|󱥤|󱥥|󱥦|󱥧|󱥨|󱥩|󱥪|󱥫|󱥬|󱥭|󱥮|󱥯|󱥰|󱥱|󱥲|󱥳|󱥴|󱥵|󱥶|󱥷|󱦠|󱦡|󱦢|󱦣|󱥸|󱥹|󱥺|󱥻|󱥼|󱥽|󱥾|󱥿|󱦀|󱦁|󱦂|󱦃|󱦄|󱦅|󱦆|󱦇|󱦈|󱦐|󱦑|󱦒|󱦓|󱦔|󱦕|󱦖|󱦗|󱦘|󱦙|󱦚|󱦛|󱦜|󱦝)/gm; - - return dictionary.test(text); -} - -function isShavian(text: string) { - const shavianRegex = /[\u{10450}-\u{1047F}]+/u; - - return shavianRegex.test(text); -} - -async function translateShavian(message: string) { - const dictionary = await (await fetch("https://raw.githubusercontent.com/ForkPrince/TranslatePlus/322199d5fdb1a9506591c9f4a2826338b5d67e38/shavian.json")).json(); - - const punctuationMap = { - '"': "\"", - "«": "\"", - "»": "\"", - ",": ",", - "!": "!", - "?": "?", - ".": ".", - "(": "(", - ")": ")", - "/": "/", - ";": ";", - ":": ":" - }; - - let translated = ""; - const words = message.split(/\s+/); - - for (let word of words) { - let punctuationBefore = "", punctuationAfter = ""; - - if (word[0] in punctuationMap) { - punctuationBefore = punctuationMap[word[0]]; - word = word.slice(1); - } - - if (word[word.length - 1] in punctuationMap) { - punctuationAfter = punctuationMap[word[word.length - 1]]; - word = word.slice(0, -1); - } - - translated += punctuationBefore; - - if (word in dictionary) translated += dictionary[word]; - else translated += word; - - translated += punctuationAfter + " "; - } - - return translated.trim(); -} - -async function translateSitelen(message: string) { - message = Array.from(message).join(" "); - - const dictionary = await (await fetch("https://raw.githubusercontent.com/ForkPrince/TranslatePlus/5ca152b134ea11433971f21b2ef8d556d4306717/sitelen-pona.json")).json(); - - const sorted = Object.keys(dictionary).sort((a, b) => b.length - a.length); - - const pattern = new RegExp(`(${sorted.join("|")})`, "g"); - - const translate = message.replace(pattern, match => dictionary[match]); - - return translate; -} - -async function google(target: string, text: string) { - if (!text) return { src: "", text: "" }; - try { - const res = await fetch(`https://translate.googleapis.com/translate_a/single?${new URLSearchParams({ client: "gtx", sl: "auto", tl: target, dt: "t", dj: "1", source: "input", q: text })}`); - if (!res.ok) throw new Error(`Request failed with status ${res.status}`); - const translate = await res.json(); - - return { - src: translate.src, - text: translate.sentences?.map(s => s.trans).filter(Boolean).join("\n") - }; - } catch (error) { - console.error("[TranslatePlus] Google Translate request failed:", error); - return { src: "en", text: "Translation failed due to an error." }; - } -} - -export async function translate(text: string): Promise { - const { target, toki, sitelen, shavian } = settings.store; - - const output = { src: "", text: "" }; - - if ((isTokiPona(text) || isSitelen(text)) && (toki || sitelen)) { - if (isSitelen(text) && sitelen) text = await translateSitelen(text); - - console.log(text); - - const translate = await (await fetch("https://aiapi.serversmp.xyz/toki", { - method: "POST", - headers: { - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({ - text: text, - src: "tl", - target: "en" - }) - })).json(); - - console.log(translate); - - output.src = "tp"; - output.text = target === "en" ? translate.translation[0] : (await google(target, translate.translation[0])).text; - } else if (isShavian(text) && shavian) { - const translate = await translateShavian(text); - - output.src = "sh"; - output.text = target === "en" ? translate : (await google(target, translate)).text; - } else { - const translate = await google(target, text); - - output.src = translate.src; - output.text = translate.text; - } - - return output; -} diff --git a/src/equicordplugins/triviaAI/index.tsx b/src/equicordplugins/triviaAI/index.tsx deleted file mode 100644 index f4c860347..000000000 --- a/src/equicordplugins/triviaAI/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { IconComponent } from "@utils/types"; -import { Message } from "@vencord/discord-types"; -import { findExportedComponentLazy } from "@webpack"; -import { ChannelStore, Menu } from "@webpack/common"; - -import { settings } from "./settings"; -import { getResponse, handleResponse, parseMessageContent } from "./utils"; - -const RobotIconLazy = findExportedComponentLazy("RobotIcon"); -const RobotIcon: IconComponent = props => ; - -const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => { - const payload = parseMessageContent(message); - if (!payload) return; - - const group = findGroupChildrenByChildId("copy-text", children); - if (!group) return; - - group.splice(group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, ( - { - const ans = await getResponse(payload); - handleResponse(message, ans); - }} - /> - )); -}; - -export default definePlugin({ - name: "TriviaAI", - description: "A plugin that helps you answer trivia questions using AI.", - authors: [EquicordDevs.yash], - settings, - contextMenus: { - "message": messageCtxPatch - }, - messagePopoverButton: { - icon: RobotIcon, - render(message: Message) { - const payload = parseMessageContent(message); - if (!payload) return null; - - return { - label: "Answer With AI", - icon: RobotIcon, - message, - channel: ChannelStore.getChannel(message.channel_id), - onClick: async () => { - const ans = await getResponse(payload); - handleResponse(message, ans); - } - }; - } - } -}); diff --git a/src/equicordplugins/triviaAI/settings.ts b/src/equicordplugins/triviaAI/settings.ts deleted file mode 100644 index b4a97e823..000000000 --- a/src/equicordplugins/triviaAI/settings.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { definePluginSettings } from "@api/Settings"; -import { OptionType } from "@utils/types"; - -export const settings = definePluginSettings({ - apiKey: { - type: OptionType.STRING, - description: "API Key.", - default: "", - placeholder: "Enter API Key here for your AI endpoint.", - componentProps: { - type: "password" - } - }, - model: { - type: OptionType.STRING, - description: "AI Model to use.", - default: "google/gemini-3-flash-preview", - placeholder: "e.g. google/gemini-3-flash-preview, inception/mercury, openai/gpt-5.2-chat, etc." - }, - systemPrompt: { - type: OptionType.STRING, - description: "System Prompt for the AI.", - default: "You are a helpful assistant who answers questions for the user in a concise and short way while using the least amount of words and punctuation.", - placeholder: "Enter system prompt." - }, - maxTokens: { - type: OptionType.NUMBER, - description: "Maximum number of tokens in the response.", - default: 500 - }, - endpoint: { - type: OptionType.STRING, - description: "OpenAI Compatible AI Endpoint.", - default: "https://openrouter.ai/api/v1/chat/completions", - placeholder: "Enter your OpenAI compatible AI endpoint here." - }, - autoRespond: { - type: OptionType.BOOLEAN, - description: "Automatically respond to messages on receiving a response.", - default: false - }, - supportImages: { - type: OptionType.BOOLEAN, - description: "Pass images to the AI for context (if any). This is not supported by all models.", - default: true - } -}); diff --git a/src/equicordplugins/triviaAI/utils.ts b/src/equicordplugins/triviaAI/utils.ts deleted file mode 100644 index 501a14ffb..000000000 --- a/src/equicordplugins/triviaAI/utils.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2026 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { insertTextIntoChatInputBox, sendMessage } from "@utils/discord"; -import { Logger } from "@utils/Logger"; -import { Message } from "@vencord/discord-types"; -import { showToast, Toasts } from "@webpack/common"; - -import { settings } from "./settings"; - -const logger = new Logger("TriviaAI"); - -type TextPart = { - type: "text"; - text: string; -}; - -type ImagePart = { - type: "image_url"; - image_url: { - url: string; - detail?: "auto" | "high" | "low"; - }; -}; - -export type ContentPayload = string | (TextPart | ImagePart)[]; - -export function parseMessageContent(message: Message): ContentPayload | null { - const textParts: string[] = []; - - if (message.content && message.content.trim().length > 0) { - textParts.push(message.content); - } - - message.embeds.forEach(embed => { - const embedBuffer: string[] = []; - - const parts = [ - embed.provider?.name ? `> ${embed.provider.name}` : null, - embed.author?.name ? `**${embed.author.name}**` : null, - embed.rawTitle ? `## ${embed.rawTitle}` : null, - embed.rawDescription ?? null, - ...(embed.fields?.map(f => (f.rawName && f.rawValue) ? `**${f.rawName}**: ${f.rawValue}` : null) ?? []), - embed.footer?.text ? `_${embed.footer.text}_` : null, - ]; - - parts.forEach(p => { - if (p) embedBuffer.push(p); - }); - - if (embedBuffer.length > 0) textParts.push(embedBuffer.join("\n")); - }); - - const combinedText = textParts.join("\n\n"); - - if (!settings.store.supportImages) { - return combinedText || null; - } - - const imageUrls = new Set(); - - message.attachments - .filter(att => att.content_type?.startsWith("image/")) - .forEach(att => imageUrls.add(att.url)); - - message.embeds.forEach(embed => { - const potentialUrls = [ - embed.image?.url, - embed.thumbnail?.url, - ...(embed.images?.map(img => img.url) ?? []) - ]; - - potentialUrls.forEach(url => { - if (url) imageUrls.add(url); - }); - }); - - if (imageUrls.size === 0) { - return combinedText || null; - } - - const payload: (TextPart | ImagePart)[] = []; - - if (combinedText.length > 0) { - payload.push({ - type: "text", - text: combinedText - }); - } - - imageUrls.forEach(url => { - payload.push({ - type: "image_url", - image_url: { url } - }); - }); - - return payload; -} - -export async function handleResponse(message: Message, response: string): Promise { - if (settings.store.autoRespond) { - sendMessage( - message.channel_id, - { content: response }, - true, - { messageReference: { channel_id: message.channel_id, message_id: message.id } } - ); - return response; - } else { - insertTextIntoChatInputBox(response); - return response; - } -} - -export async function getResponse(payload: ContentPayload): Promise { - if (!settings.store.apiKey) { - showToast("TriviaAI: API Key is not set.", Toasts.Type.FAILURE); - return ""; - } - - if (!settings.store.endpoint) { - showToast("TriviaAI: Endpoint is not set.", Toasts.Type.FAILURE); - return ""; - } - - if (!settings.store.model) { - showToast("TriviaAI: Model is not set.", Toasts.Type.FAILURE); - return ""; - } - - try { - const req = await fetch(settings.store.endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${settings.store.apiKey}` - }, - body: JSON.stringify({ - model: settings.store.model, - messages: [ - { - role: "system", - content: settings.store.systemPrompt - }, - { - role: "user", - content: payload - } - ], - max_tokens: settings.store.maxTokens, - }) - }); - - if (!req.ok) { - const errorBody = await req.text(); - const errorMessage = `API request failed with status ${req.status}: ${errorBody}`; - logger.error(errorMessage); - showToast(errorMessage, Toasts.Type.FAILURE); - return ""; - } - - const data = await req.json(); - - if (data.error) { - const errorMessage = data.error.message ?? "An unknown error occurred"; - logger.error(`API Error: ${errorMessage}`); - showToast(errorMessage, Toasts.Type.FAILURE); - return ""; - } - - const response = data.choices[0].message.content; - - if (!response?.trim()) { - logger.warn("no response from AI model"); - return ""; - } - - return response; - } catch (e) { - logger.error("Error getting response from AI model", e); - showToast("Error getting response from AI model", Toasts.Type.FAILURE); - return ""; - } -} diff --git a/src/equicordplugins/unreadBadgeCount/index.tsx b/src/equicordplugins/unreadBadgeCount/index.tsx deleted file mode 100644 index 0f08d06cd..000000000 --- a/src/equicordplugins/unreadBadgeCount/index.tsx +++ /dev/null @@ -1,80 +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 ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { Channel } from "@vencord/discord-types"; -import { findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { ReadStateStore, useStateFromStores } from "@webpack/common"; - -const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore"); -const JoinedThreadsStore = findStoreLazy("JoinedThreadsStore"); -const NumberBadge = findComponentByCodeLazy("BADGE_NOTIFICATION_BACKGROUND", "let{count:"); - -const settings = definePluginSettings({ - showOnMutedChannels: { - description: "Show unread count on muted channels", - type: OptionType.BOOLEAN, - default: false, - }, - notificationCountLimit: { - description: "Show +99 instead of true amount", - type: OptionType.BOOLEAN, - default: false, - }, -}); - -export default definePlugin({ - name: "UnreadCountBadge", - authors: [Devs.Joona], - description: "Shows unread message count badges on channels in the channel list", - settings, - - patches: [ - // Kanged from typingindicators - { - find: "UNREAD_IMPORTANT:", - replacement: [ - { - match: /\.Children\.count.+?:null(?<=,channel:(\i).+?)/, - replace: "$&,$self.CountBadge({channel: arguments[0].channel})", - }, - ] - }, - // Threads - { - // This is the thread "spine" that shows in the left - find: "M0 15H2c0 1.6569", - replacement: [ - { - match: /mentionsCount:\i.{0,50}?null/, - replace: "$&,$self.CountBadge({channel: arguments[0].thread})", - }, - ] - }, - ], - - CountBadge: ErrorBoundary.wrap(({ channel }: { channel: Channel; }) => { - const unreadCount = useStateFromStores([ReadStateStore], () => ReadStateStore.getUnreadCount(channel.id)); - if (!unreadCount) return null; - - if (!settings.store.showOnMutedChannels && (UserGuildSettingsStore.isChannelMuted(channel.guild_id, channel.id) || JoinedThreadsStore.isMuted(channel.id))) - return null; - - return ( - 99 && settings.store.notificationCountLimit - ? "+99" - : unreadCount - } - /> - ); - }, { noop: true }), -}); diff --git a/src/equicordplugins/unreadBadgeCount/styles.css b/src/equicordplugins/unreadBadgeCount/styles.css deleted file mode 100644 index 878ef5162..000000000 --- a/src/equicordplugins/unreadBadgeCount/styles.css +++ /dev/null @@ -1,39 +0,0 @@ -.vc-unreadCountBadge { - box-sizing: border-box; - height: 16px; - min-height: 16px; - min-width: 16px; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - background-color: var(--brand-500); - color: var(--white); - font-size: 12px; - font-family: var(--font-display); - border-radius: 8px; - margin-left: 4px; - line-height: 1.33; - font-weight: 700; - letter-spacing: 0.02em; -} - -.vc-unreadCountBadge-dot { - position: absolute; - top: 25%; - margin-left: -3%; - scale: 0.9; - z-index: 1; -} - -.vc-unreadCountBadge-dot-thread { - position: absolute; - top: 20%; - margin-left: -18%; - scale: 0.9; - z-index: 1; -} - -[class*="modeMuted"] .unreadCountBadge { - display: none; -} \ No newline at end of file diff --git a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.css b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.css deleted file mode 100644 index 4a08e56f9..000000000 --- a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.css +++ /dev/null @@ -1,42 +0,0 @@ -.vc-voice-channel-log { - list-style-type: none; - height: 50px; - display: grid; - grid-template-columns: 2.25rem 24px 40px max-content; - gap: 4px; - width: 4px; - background-color: var(--border-subtle); -} - -.vc-voice-channel-log-date-separator, -.vc-voice-channel-log-timestamp { - margin-left: 4px; - height: 48px; - line-height: 48px; - font-size: .75rem; - text-align: center; - display: inline-block; - vertical-align: middle; - color: var(--text-default, white); -} - -.vc-voice-channel-log-avatar { - height: 40px; - width: 100%; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - font-weight: 600; - overflow: hidden; - text-align: center; - margin-top: 10px; - padding-left: 10px; - padding-right: 10px; - cursor: pointer; -} - -.vc-voice-channel-log-icon { - margin: auto 3px; - margin-left: 15px; -} \ No newline at end of file diff --git a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.tsx b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.tsx deleted file mode 100644 index 0ce2611ef..000000000 --- a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import "./VoiceChannelLogEntryComponent.css"; - -import { VoiceChannelLogEntry } from "@equicordplugins/voiceChannelLog.dev/logs"; -import { classes } from "@utils/misc"; -import { Channel } from "@vencord/discord-types"; -import { React, Timestamp, Tooltip, UserStore } from "@webpack/common"; -import { Util } from "Vencord"; - -import { cl } from ".."; -import Icon from "./VoiceChannelLogEntryIcons"; - -export function VoiceChannelLogEntryComponent({ logEntry, channel }: { logEntry: VoiceChannelLogEntry; channel: Channel; }) { - const user = UserStore.getUser(logEntry.userId); - return
  • - - - - {(tooltipProps: any) => ( - Util.openUserProfile(logEntry.userId)} - src={user.getAvatarURL(channel.getGuildId())} - /> - )} - -
    - { } -
    -
  • ; -} diff --git a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryIcons.tsx b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryIcons.tsx deleted file mode 100644 index 0054323f4..000000000 --- a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryIcons.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { VoiceChannelLogEntry } from "@equicordplugins/voiceChannelLog.dev/logs"; -import { classes } from "@utils/misc"; -import { Channel } from "@vencord/discord-types"; -import { React } from "@webpack/common"; - -import { cl } from ".."; - -export default function Icon({ logEntry, channel, className }: { logEntry: VoiceChannelLogEntry; channel: Channel; className: string; }) { - // Taken from /assets/7378a83d74ce97d83380.svg - const Join = ; - // Taken from /assets/192510ade1abc3149b46.svg - const Leave = ; - // For other contributors, please DO make specific designs for these instead of how I just copied the join/leave icons and making them orange - const MovedTo = ; - const MovedFrom = ; - - if (logEntry.newChannel && !logEntry.oldChannel) return React.cloneElement(Join, { className: classes(className, cl("join")) }); - if (!logEntry.newChannel && logEntry.oldChannel) return React.cloneElement(Leave, { className: classes(className, cl("leave")) }); - if (logEntry.newChannel === channel.id && logEntry.oldChannel) return React.cloneElement(MovedFrom, { className: classes(className, cl("moved-from")) }); - if (logEntry.newChannel && logEntry.oldChannel === channel.id) return React.cloneElement(MovedTo, { className: classes(className, cl("moved-to")) }); - // we should never get here, this is just here to shut up the type checker - return ; -} diff --git a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogModal.tsx b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogModal.tsx deleted file mode 100644 index 6315e8b06..000000000 --- a/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogModal.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { BaseText } from "@components/BaseText"; -import { getVcLogs, vcLogSubscribe } from "@equicordplugins/voiceChannelLog.dev/logs"; -import { classes } from "@utils/misc"; -import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { Channel } from "@vencord/discord-types"; -import { findStoreLazy } from "@webpack"; -import { React, ScrollerThin } from "@webpack/common"; - -import { cl } from ".."; -import { VoiceChannelLogEntryComponent } from "./VoiceChannelLogEntryComponent"; - -const AccessibilityStore = findStoreLazy("AccessibilityStore"); - -export function openVoiceChannelLog(channel: Channel) { - return openModal(props => ( - - )); -} - -export function VoiceChannelLogModal({ channel, props }: { channel: Channel; props: ModalProps; }) { - React.useSyncExternalStore(vcLogSubscribe, () => getVcLogs(channel.id)); - const vcLogs = getVcLogs(channel.id); - const logElements: (React.ReactNode)[] = []; - - if (vcLogs.length > 0) { - for (let i = 0; i < vcLogs.length; i++) { - const logEntry = vcLogs[i]; - if (i === 0 || logEntry.timestamp.toDateString() !== vcLogs[i - 1].timestamp.toDateString()) { - logElements.push(
    - - {logEntry.timestamp.toDateString()} - -
    ); - } else { - logElements.push(); - } - } - } else { - logElements.push(
    No logs to display.
    ); - } - - return ( - - - {channel.name} logs - - - - - - {logElements} - - - - ); -} diff --git a/src/equicordplugins/voiceChannelLog.dev/index.tsx b/src/equicordplugins/voiceChannelLog.dev/index.tsx deleted file mode 100644 index 94941f6e1..000000000 --- a/src/equicordplugins/voiceChannelLog.dev/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { definePluginSettings } from "@api/Settings"; -import { Devs, EquicordDevs } from "@utils/constants"; -import { classNameFactory } from "@utils/css"; -import definePlugin, { OptionType } from "@utils/types"; -import { Message, User } from "@vencord/discord-types"; -import { findByCodeLazy } from "@webpack"; -import { FluxDispatcher, Menu, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; - -import { openVoiceChannelLog } from "./components/VoiceChannelLogModal"; -import { addLogEntry } from "./logs"; - -export const cl = classNameFactory("vc-voice-channel-log-"); -const createBotMessage = findByCodeLazy('username:"Clyde"'); - -const settings = definePluginSettings({ - mode: { - type: OptionType.SELECT, - description: "How to show the voice channel log", - options: [ - { label: "Log menu", value: 1, default: true }, - { label: "Log to associated chat directly", value: 2 }, - { label: "Log to chat and log menu", value: 3 }, - ] - }, - voiceChannelChatSelf: { - type: OptionType.BOOLEAN, - description: "Log your own voice channel events in the voice channels", - default: true - }, - voiceChannelChatSilent: { - type: OptionType.BOOLEAN, - description: "Join/leave/move messages in voice channel chats will be silent", - default: true - }, - voiceChannelChatSilentSelf: { - type: OptionType.BOOLEAN, - description: "Join/leave/move messages in voice channel chats will be silent if you are in the voice channel", - default: false - }, - ignoreBlockedUsers: { - type: OptionType.BOOLEAN, - description: "Do not log blocked users", - default: false - }, -}); - -interface VoiceState { - guildId?: string; - channelId?: string; - oldChannelId?: string; - user: User; - userId: string; -} - -function getMessageFlags(selfInChannel: boolean) { - let flags = 1 << 6; - if (selfInChannel ? settings.store.voiceChannelChatSilentSelf : settings.store.voiceChannelChatSilent) flags += 1 << 12; - return flags; -} - -function sendVoiceStatusMessage(channelId: string, content: string, userId: string, selfInChannel: boolean): Message | null { - if (!channelId) return null; - - const message: Message = createBotMessage({ channelId, content, embeds: [] }); - message.flags = getMessageFlags(selfInChannel); - message.author = UserStore.getUser(userId); - // If we try to send a message into an unloaded channel, the client-sided messages get overwritten when the channel gets loaded - // This might be messy but It Works:tm: - const messagesLoaded: Promise = MessageStore.hasPresent(channelId) ? new Promise(r => r()) : MessageActions.fetchMessages({ channelId }); - messagesLoaded.then(() => { - FluxDispatcher.dispatch({ - type: "MESSAGE_CREATE", - channelId, - message, - optimistic: true, - sendMessageOptions: {}, - isPushNotification: false - }); - }); - return message; -} - -const patchChannelContextMenu: NavContextMenuPatchCallback = (children, { channel }) => { - const group = findGroupChildrenByChildId("mark-channel-read", children) ?? children; - group.push( - { openVoiceChannelLog(channel); }} - /> - ); -}; - -// Blatantly stolen from VcNarrator plugin - -// For every user, channelId and oldChannelId will differ when moving channel. -// Only for the local user, channelId and oldChannelId will be the same when moving channel, -// for some ungodly reason -let clientOldChannelId: string | undefined; - -export default definePlugin({ - name: "VoiceChannelLog", - description: "Logs who joins and leaves voice channels", - authors: [Devs.Sqaaakoi, Devs.thororen, EquicordDevs.nyx], - contextMenus: { - "channel-context": patchChannelContextMenu - }, - settings, - flux: { - VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - const clientUserId = UserStore.getCurrentUser().id; - voiceStates.forEach(state => { - // mmmm hacky workaround - const { userId, channelId } = state; - const user = UserStore.getUser(userId) as User & { globalName: string; }; - const username = user.globalName || user.username; - let { oldChannelId } = state; - if (userId === clientUserId && channelId !== clientOldChannelId) { - oldChannelId = clientOldChannelId; - clientOldChannelId = channelId; - } - if (settings.store.ignoreBlockedUsers && RelationshipStore.isBlocked(userId)) return; - // Ignore events from same channel - if (oldChannelId === channelId) return; - - const logEntry = { - userId, - username, - oldChannel: oldChannelId || null, - newChannel: channelId || null, - timestamp: new Date() - }; - - if (settings.store.mode !== 2) { - addLogEntry(logEntry, oldChannelId); - addLogEntry(logEntry, channelId); - } - - if (!settings.store.voiceChannelChatSelf && userId === clientUserId) return; - if (settings.store.mode === 1) return; - // Join / Leave - if ((!oldChannelId && channelId) || (oldChannelId && !channelId)) { - // empty string is to make type checker shut up - const targetChannelId = oldChannelId || channelId || ""; - const selfInChannel = SelectedChannelStore.getVoiceChannelId() === targetChannelId; - sendVoiceStatusMessage(targetChannelId, `${(channelId ? "Joined" : "Left")} <#${targetChannelId}>`, userId, selfInChannel); - } - // Move between channels - if (oldChannelId && channelId) { - sendVoiceStatusMessage(oldChannelId, `Moved to <#${channelId}>`, userId, SelectedChannelStore.getVoiceChannelId() === oldChannelId); - sendVoiceStatusMessage(channelId, `Moved from <#${oldChannelId}>`, userId, SelectedChannelStore.getVoiceChannelId() === channelId); - } - - }); - }, - } -}); diff --git a/src/equicordplugins/voiceChannelLog.dev/logs.ts b/src/equicordplugins/voiceChannelLog.dev/logs.ts deleted file mode 100644 index 6fefe4ba7..000000000 --- a/src/equicordplugins/voiceChannelLog.dev/logs.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -export interface VoiceChannelLogEntry { - userId: string; - username: string; - oldChannel: string | null; - newChannel: string | null; - timestamp: Date; -} - -export const vcLogs = new Map(); -let vcLogSubscriptions: (() => void)[] = []; - -export function getVcLogs(channel?: string): VoiceChannelLogEntry[] { - if (!channel) return []; - if (!vcLogs.has(channel)) vcLogs.set(channel, []); - return vcLogs.get(channel) || []; -} - -export function addLogEntry(logEntry: VoiceChannelLogEntry, channel?: string) { - if (!channel) return; - vcLogs.set(channel, [...getVcLogs(channel), logEntry]); - vcLogSubscriptions.forEach(u => u()); -} - -export function clearLogs(channel?: string) { - if (!channel) return; - vcLogs.set(channel, []); - vcLogSubscriptions.forEach(u => u()); -} - -export function vcLogSubscribe(listener: () => void) { - vcLogSubscriptions = [...vcLogSubscriptions, listener]; - return () => { - vcLogSubscriptions = vcLogSubscriptions.filter(l => l !== listener); - }; -} diff --git a/src/equicordplugins/voiceRejoin/index.tsx b/src/equicordplugins/voiceRejoin.dev/index.tsx similarity index 100% rename from src/equicordplugins/voiceRejoin/index.tsx rename to src/equicordplugins/voiceRejoin.dev/index.tsx diff --git a/src/equicordplugins/webpackTarball/index.tsx b/src/equicordplugins/webpackTarball.dev/index.tsx similarity index 100% rename from src/equicordplugins/webpackTarball/index.tsx rename to src/equicordplugins/webpackTarball.dev/index.tsx diff --git a/src/equicordplugins/webpackTarball/tar.ts b/src/equicordplugins/webpackTarball.dev/tar.ts similarity index 100% rename from src/equicordplugins/webpackTarball/tar.ts rename to src/equicordplugins/webpackTarball.dev/tar.ts diff --git a/src/equicordplugins/webpackTarball/webpack.ts b/src/equicordplugins/webpackTarball.dev/webpack.ts similarity index 100% rename from src/equicordplugins/webpackTarball/webpack.ts rename to src/equicordplugins/webpackTarball.dev/webpack.ts diff --git a/src/equicordplugins/wigglyText/index.tsx b/src/equicordplugins/wigglyText/index.tsx deleted file mode 100644 index 1f683e9e6..000000000 --- a/src/equicordplugins/wigglyText/index.tsx +++ /dev/null @@ -1,199 +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 { BaseText } from "@components/BaseText"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin, { makeRange, OptionType } from "@utils/types"; -import { ReactNode } from "react"; - -import ExampleWiggle from "./ui/components/ExampleWiggle"; - -const settings = definePluginSettings({ - intensity: { - type: OptionType.SLIDER, - description: "Animation intensity in px", - markers: makeRange(1, 10, 1), - default: 4, - stickToMarkers: true, - onChange: () => updateStyles() - } -}); - -const dirMap = { - x: "0.6s wiggle-wavy-x alternate ease-in-out infinite", - y: "1.2s wiggle-wavy-y linear infinite" -}; - -const classMap = [ - { - chars: ["<", ">"], - className: "wiggle-inner-x", - }, - { - chars: ["^", "^"], - className: "wiggle-inner-y", - }, - { - chars: [")", "("], - className: "wiggle-inner-xy" - } -]; - -let styles: HTMLStyleElement; -const updateStyles = () => { - const inten = settings.store.intensity + "px"; - styles.textContent = ` -.wiggle-example { - list-style-type: disc; - list-style-position: outside; - margin: 4px 0 0 16px; -} - -.wiggle-example li { - white-space: break-spaces; - margin-bottom: 4px; -} - -.wiggle-inner { - position: relative; - top: 0; - left: 0; - - &.wiggle-inner-x { - animation: ${dirMap.x}; - } - - &.wiggle-inner-y { - animation: ${dirMap.y}; - } - - &.wiggle-inner-xy { - animation: ${dirMap.x}, ${dirMap.y}; - } -} - -@keyframes wiggle-wavy-x { - from { - left: -${inten}; - } - - to { - left: ${inten}; - } -} - -@keyframes wiggle-wavy-y { - 0% { - top: 0; - animation-timing-function: ease-out; - } - - 25% { - top: -${inten}; - animation-timing-function: ease-in; - } - - 50% { - top: 0; - animation-timing-function: ease-out; - } - - 75% { - top: ${inten}; - animation-timing-function: ease-in; - } -}`; -}; - -export default definePlugin({ - name: "WigglyText", - description: "Adds a new markdown formatting that makes text wiggly.", - authors: [EquicordDevs.nexpid], - settings, - settingsAboutComponent: () => ( - - You can make text wiggle with the following:
    -
      -
    • left and right by typing <~text~>
    • -
    • up and down by typing ^~text~^
    • -
    • in a circle by typing )~text~(
    • -
    -
    - ), - - patches: [ - { - find: "parseToAST:", - replacement: { - match: /(parse[\w]*):(.*?)\((\i)\),/g, - replace: "$1:$2({...$3,wiggly:$self.wigglyRule}),", - }, - }, - ], - - wigglyRule: { - order: 24, - match: (source: string) => classMap.map(({ chars }) => source.match(new RegExp(`^(\\${chars[0]})~([\\s\\S]+?)~(\\${chars[1]})(?!_)`))).find(x => x !== null), - parse: ( - capture: RegExpMatchArray, - transform: (...args: any[]) => any, - state: any - ) => { - const className = classMap.find(({ chars }) => chars[0] === capture[1] && chars[1] === capture[3])?.className ?? ""; - - return { - content: transform(capture[2], state), - className - }; - }, - react: ( - data: { content: any[]; className: string; }, - output: (...args: any[]) => ReactNode[] - ) => { - let offset = 0; - const traverse = (raw: any) => { - const children = !Array.isArray(raw) ? [raw] : raw; - let modified = false; - - let j = -1; - for (const child of children) { - j++; - if (typeof child === "string") { - modified = true; - children[j] = child.split("").map((x, i) => ( - - - {x} - - - )); - } else if (child?.props?.children) - child.props.children = traverse(child.props.children); - } - - return modified ? children : raw; - }; - - return traverse(output(data.content)); - }, - }, - - start: () => { - styles = document.createElement("style"); - styles.id = "WigglyText"; - document.head.appendChild(styles); - - updateStyles(); - }, - - stop: () => styles.remove() -}); diff --git a/src/equicordplugins/wigglyText/ui/components/ExampleWiggle.tsx b/src/equicordplugins/wigglyText/ui/components/ExampleWiggle.tsx deleted file mode 100644 index 1f6e709ab..000000000 --- a/src/equicordplugins/wigglyText/ui/components/ExampleWiggle.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -export default function ExampleWiggle({ wiggle, children }: { wiggle: "x" | "y" | "xy", children: string; }) { - return children.split("").map((x, i) => ( - - - {x} - - - )); -} diff --git a/src/equicordplugins/writeUpperCase/index.ts b/src/equicordplugins/writeUpperCase/index.ts deleted file mode 100644 index 263b1a231..000000000 --- a/src/equicordplugins/writeUpperCase/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { addMessagePreSendListener, MessageSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; -import { definePluginSettings } from "@api/Settings"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; - -const settings = definePluginSettings( - { - blockedWords: { - type: OptionType.STRING, - description: "Strings not to capitilise (seperate with a comma)", - default: "http, https, ok" - } - } -); - -const presendObject: MessageSendListener = (_, msg) => { - const blockedWordsArray: string[] = settings.store.blockedWords.split(", "); - const sentences = msg.content.split(/(?<=[.!?]+['")\]]*)(\s+)/); - - msg.content = sentences.map((element, i) => { - if (i % 2 === 1) return element; - - if (!blockedWordsArray.some(word => element.toLowerCase().startsWith(word.toLocaleLowerCase()))) { - return element.charAt(0).toUpperCase() + element.slice(1); - } else { - return element; - } - }).join(""); -}; - -export default definePlugin({ - name: "WriteUpperCase", - description: "Changes the first Letter of each Sentence in Message Inputs to Uppercase", - authors: [Devs.Samwich, EquicordDevs.KrystalSkull], - settings, - - start() { - addMessagePreSendListener(presendObject); - }, - stop() { - removeMessagePreSendListener(presendObject); - } -}); diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index a2e1d8172..0c390eeae 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -156,9 +156,8 @@ async function generateDebugInfoMessage() { } const potentiallyProblematicPlugins = ([ - "NoRPC", "NoProfileThemes", "NoMosaic", "NoRoleHeaders", "NoSystemBadge", - "AlwaysAnimate", "ClientTheme", "SoundTroll", "Ingtoninator", "NeverPausePreviews", - "IdleAutoRestart", + "NoRPC", "NoProfileThemes", "NoMosaic", "NoSystemBadge", + "AlwaysAnimate", "ClientTheme", "SoundTroll", "NeverPausePreviews", ].filter(isPluginEnabled) ?? []).sort(); if (isPluginEnabled(customIdle.name) && customIdle.settings.store.idleTimeout === 0) {