From fe91af7693c9a1f321cd9bfc6eb1367619e74260 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:32:43 -0500 Subject: [PATCH 1/8] AlwaysAnimate: Use VC Patch --- src/plugins/alwaysAnimate/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts index 3c26219de..071596253 100644 --- a/src/plugins/alwaysAnimate/index.ts +++ b/src/plugins/alwaysAnimate/index.ts @@ -92,8 +92,8 @@ export default definePlugin({ find: ".MINI_PREVIEW,[", predicate: () => settings.store.nameplates, replacement: { - match: /animate:\i,loop:(?:!0===\i(?:&&)?){2}/, - replace: "animate:true,loop:true" + match: /animate:\i,loop:/, + replace: "animate:true,loop:true,_loop:" }, }, { From ace9f213af3a05cd89d8af5ecb24579b477c40d0 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 6 Mar 2026 23:20:21 -0500 Subject: [PATCH 2/8] UnlimitedAccounts: Update Patches --- src/equicordplugins/unlimitedAccounts/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/equicordplugins/unlimitedAccounts/index.ts b/src/equicordplugins/unlimitedAccounts/index.ts index 984d9d1d1..cbf77811d 100644 --- a/src/equicordplugins/unlimitedAccounts/index.ts +++ b/src/equicordplugins/unlimitedAccounts/index.ts @@ -36,7 +36,7 @@ export default definePlugin({ settings, patches: [ { - find: "pushSyncToken:null", + find: "pushSyncToken:null}),", replacement: [ { match: /(\).length>)5/, @@ -56,11 +56,7 @@ export default definePlugin({ replace: "$1$self.getMaxAccounts()", }, { - match: /(\i.length<)5/, - replace: "$1$self.getMaxAccounts()", - }, - { - match: /(\i.length>=)5/, + match: /(\i.length(<|>=))5/g, replace: "$1$self.getMaxAccounts()", }, ] From 666b1b2806e50be2beb92a5b1f040dce3e13fe07 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:00:51 -0500 Subject: [PATCH 3/8] merge stuff to more cmds --- src/equicordplugins/friendCloud/index.ts | 294 -------------- src/equicordplugins/gifRoulette/index.tsx | 52 --- src/equicordplugins/imgToGif/index.tsx | 157 -------- src/equicordplugins/moreCommands/index.ts | 431 ++++++++++++++++++++- src/equicordplugins/moreCommands/utils.ts | 179 +++++++++ src/equicordplugins/moreKaomoji/index.ts | 216 ----------- src/plugins/implicitRelationships/index.ts | 5 +- src/webpack/common/stores.ts | 2 + 8 files changed, 610 insertions(+), 726 deletions(-) delete mode 100644 src/equicordplugins/friendCloud/index.ts delete mode 100644 src/equicordplugins/gifRoulette/index.tsx delete mode 100644 src/equicordplugins/imgToGif/index.tsx delete mode 100644 src/equicordplugins/moreKaomoji/index.ts diff --git a/src/equicordplugins/friendCloud/index.ts b/src/equicordplugins/friendCloud/index.ts deleted file mode 100644 index 759b9f12f..000000000 --- a/src/equicordplugins/friendCloud/index.ts +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2025 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; -import { EquicordDevs } from "@utils/constants"; -import { Logger } from "@utils/Logger"; -import definePlugin from "@utils/types"; -import { User } from "@vencord/discord-types"; -import { findStore } from "@webpack"; -import { DraftType, UploadHandler, UserStore } from "@webpack/common"; - -const logger = new Logger("FriendCloud"); - -interface AffinitiesV2 { - otherUserId: User["id"]; - userSegment: "NON_MAU" | "NON_HFU_MAU" | "HFU_MAU"; - otherUserSegment: "NON_MAU" | "NON_HFU_MAU" | "HFU_MAU"; - isFriend: boolean; - dmProbability: number; - dmRank: number; - vcProbability: number; - vcRank: number; - serverMessageProbability: number; - serverMessageRank: number; - communicationProbability: number; - communicationRank: number; -} - -interface UserPosition { - member: User; - affinity: number; - x: number; - y: number; - size: number; -} - -export default definePlugin({ - name: "FriendCloud", - description: "Adds a /friendcloud command to visualize the users you most interact with", - authors: [EquicordDevs.Fafa], - commands: [ - { - inputType: ApplicationCommandInputType.BUILT_IN, - name: "friendcloud", - description: "Display user you most interact with in a cloud", - options: [ - { - name: "count", - description: "Number of users to display", - type: ApplicationCommandOptionType.NUMBER, - required: false - } - ], - execute: async (opts, cmdCtx) => { - const count = findOption(opts, "count", 25); - - if (!count) return sendBotMessage(cmdCtx.channel.id, { content: "The count must be 1 or higher!" }); - - try { - const affinities: AffinitiesV2[] = findStore("UserAffinitiesV2Store").getUserAffinities(); - - if (!affinities?.length) { - return sendBotMessage(cmdCtx.channel.id, { - content: "No affinities found. Check your [privacy settings]()." - }); - } - - const users = affinities - .map(e => ({ - member: UserStore.getUser(e.otherUserId), - affinity: calculateAffinityScore(e as AffinitiesV2) - })) - .filter(x => x.member?.id) - .sort((a, b) => b.affinity - a.affinity) - .slice(0, count); - - if (!users.length) { - return sendBotMessage(cmdCtx.channel.id, { - content: "No valid users found in affinities. Check your [privacy settings]()." - }); - } - - const minAffinity = Math.min(...users.map(u => u.affinity)); - const maxAffinity = Math.max(...users.map(u => u.affinity)); - const minSize = 120; - const maxSize = 240; - - const getSize = (affinity: number): number => { - if (maxAffinity === minAffinity) return (minSize + maxSize) / 2; - return minSize + ((affinity - minAffinity) / (maxAffinity - minAffinity)) * (maxSize - minSize); - }; - - const avgSize = (minSize + maxSize) / 2; - const { width: canvasWidth, height: canvasHeight } = calculateCanvasSize(users.length, avgSize); - - const canvas = document.createElement("canvas"); - canvas.width = canvasWidth; - canvas.height = canvasHeight; - const ctx = canvas.getContext("2d")!; - - const positions: Array<{ x: number, y: number, size: number; }> = []; - const userPositions = users.map(user => { - const size = getSize(user.affinity); - const pos = generatePoissonDiskPosition(positions, canvasWidth, canvasHeight, size); - positions.push({ x: pos.x, y: pos.y, size }); - return { ...user, x: pos.x, y: pos.y, size }; - }); - - let loadedImages = 0; - const totalImages = userPositions.length; - - const drawImage = async (user: UserPosition) => { - try { - const avatarUrl = user.member?.avatar - ? `https://cdn.discordapp.com/avatars/${user.member.id}/${user.member?.avatar}.webp?size=256` - : `https://cdn.discordapp.com/embed/avatars/${user.member.id as any as number % 5}.png`; - - const img = await loadImage(avatarUrl); - const centerX = user.x + user.size / 2; - const centerY = user.y + user.size / 2; - - ctx.save(); - ctx.beginPath(); - ctx.arc(centerX, centerY, user.size / 2, 0, Math.PI * 2); - ctx.clip(); - ctx.drawImage(img, user.x, user.y, user.size, user.size); - ctx.restore(); - - ctx.strokeStyle = "#808080"; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.arc(centerX, centerY, user.size / 2 + 1, 0, Math.PI * 2); - ctx.stroke(); - } catch { - // we ignore - } finally { - loadedImages++; - if (loadedImages === totalImages) { - canvas.toBlob(blob => { - if (!blob) { - sendBotMessage(cmdCtx.channel.id, { content: "Couldn't generate the image :c" }); - return; - } - const file = new File([blob], "affinities-cloud.png", { type: "image/png" }); - UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage); - }, "image/png"); - } - } - }; - - userPositions.forEach(drawImage); - } catch (e: unknown) { - if (e instanceof Error) - sendBotMessage(cmdCtx.channel.id, { content: e.message }); - else logger.error(e); - } - }, - }, - ] -}); - -function calculateAffinityScore(affinity: AffinitiesV2): number { - const weights = { - friend: 0.15, - dm: 0.30, - vc: 0.25, - serverMsg: 0.20, - communication: 0.10 - }; - - let score = 0; - if (affinity.isFriend) score += weights.friend * 100; - score += affinity.dmProbability * weights.dm * 100; - score += affinity.vcProbability * weights.vc * 100; - score += affinity.serverMessageProbability * weights.serverMsg * 100; - score += affinity.communicationProbability * weights.communication * 100; - - return Math.round(Math.min(100, Math.max(0, score)) * 100) / 100; -} - -// stolen from petpet thanks vee -function loadImage(source: File | string): Promise { - const isFile = source instanceof File; - const url = isFile ? URL.createObjectURL(source) : source; - - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - if (isFile) URL.revokeObjectURL(url); - resolve(img); - }; - img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); - img.crossOrigin = "anonymous"; - img.src = url; - }); -} - -function generatePoissonDiskPosition( - existingPositions: Array<{ x: number, y: number, size: number; }>, - canvasWidth: number, - canvasHeight: number, - size: number -): { x: number, y: number; } { - const edgePadding = 10; - const minDist = size * 1.5; - const textSpace = 60; - const k = 30; - - function isValid(x: number, y: number) { - if ( - x < edgePadding || - x + size > canvasWidth - edgePadding || - y < edgePadding || - y + size > canvasHeight - textSpace - edgePadding - ) return false; - - return !existingPositions.some(pos => { - const dx = pos.x - x; - const dy = pos.y - y; - const dist = Math.hypot(dx, dy); - const minAllowed = (pos.size + size) / 2 + (minDist - size); - return dist < minAllowed; - }); - } - - if (existingPositions.length === 0) { - return { - x: canvasWidth / 2 - size / 2, - y: canvasHeight / 2 - size / 2 - }; - } - - for (let tries = 0; tries < 100; tries++) { - const base = existingPositions[Math.floor(Math.random() * existingPositions.length)]; - for (let i = 0; i < k; i++) { - const angle = Math.random() * Math.PI * 2; - const radius = minDist + Math.random() * minDist; - const x = base.x + Math.cos(angle) * radius; - const y = base.y + Math.sin(angle) * radius; - if (isValid(x, y)) { - return { - x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)), - y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding)) - }; - } - } - } - - for (let tries = 0; tries < 100; tries++) { - const x = Math.random() * (canvasWidth - size - edgePadding * 2) + edgePadding; - const y = Math.random() * (canvasHeight - size - textSpace - edgePadding * 2) + edgePadding; - if (isValid(x, y)) { - return { - x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)), - y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding)) - }; - } - } - - return { - x: edgePadding, - y: edgePadding - }; -} - -function calculateCanvasSize(userCount: number, avatarSize: number): { width: number, height: number; } { - const padding = 50; - const textSpace = 60; - const itemWidth = avatarSize + padding; - const itemHeight = avatarSize + textSpace + padding; - const aspectRatio = 16 / 9; - const cols = Math.ceil(Math.sqrt(userCount * aspectRatio)); - const rows = Math.ceil(userCount / cols); - - return { - width: Math.max(1000, cols * itemWidth + padding), - height: Math.max(700, rows * itemHeight + padding) - }; -} diff --git a/src/equicordplugins/gifRoulette/index.tsx b/src/equicordplugins/gifRoulette/index.tsx deleted file mode 100644 index fdb527f04..000000000 --- a/src/equicordplugins/gifRoulette/index.tsx +++ /dev/null @@ -1,52 +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 { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { UserSettingsActionCreators } from "@webpack/common"; - -function getMessage(opts, other) { - const frecencyStore = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue(); - - const gifsArray = Object.keys(frecencyStore.favoriteGifs.gifs); - - const chosenGifUrl = gifsArray[Math.floor(Math.random() * gifsArray.length)]; - - let ownerPing = ""; - - if (other.guild != null) { - if (other.guild.ownerId != null && settings.store.pingOwnerChance && Math.random() <= 0.1) { - ownerPing = `<@${other.guild.ownerId}>`; - } - } - - return `${chosenGifUrl} ${ownerPing}`; -} - -const settings = definePluginSettings({ - pingOwnerChance: { - type: OptionType.BOOLEAN, - description: "If there should be a 1 in 10 change to ping the owner of the guild (oh no)", - default: true - } -}); - -export default definePlugin({ - name: "GifRoulette", - description: "Adds a command to send a random gif from your favourites, and a one in ten chance to ping the owner of the server", - authors: [Devs.Samwich], - settings, - commands: [ - { - name: "gifroulette", - description: "Tempt fate and send a gif", - execute: (opts, other) => ({ - content: getMessage(opts, other) - }), - } - ] -}); diff --git a/src/equicordplugins/imgToGif/index.tsx b/src/equicordplugins/imgToGif/index.tsx deleted file mode 100644 index fc4d81d52..000000000 --- a/src/equicordplugins/imgToGif/index.tsx +++ /dev/null @@ -1,157 +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 { ApplicationCommandInputType, ApplicationCommandOptionType, sendBotMessage } from "@api/Commands"; -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { CommandArgument, CommandContext } from "@vencord/discord-types"; -import { DraftType, UploadAttachmentStore, UploadHandler, UploadManager } from "@webpack/common"; -import { applyPalette, GIFEncoder, quantize } from "gifenc"; - -const FRAMES = 1; - -function loadImage(source: File | string) { - const isFile = source instanceof File; - const url = isFile ? URL.createObjectURL(source) : source; - - return new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - if (isFile) URL.revokeObjectURL(url); - resolve(img); - }; - img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); - img.crossOrigin = "Anonymous"; - img.src = url; - }); -} - -async function resolveImage(options: CommandArgument[], ctx: CommandContext): Promise<{ image: File | null; width: number | null; height: number | null; }> { - let image: File | null = null; - let width: number | null = null; - let height: number | null = null; - - for (const opt of options) { - switch (opt.name) { - case "image": - const upload = UploadAttachmentStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand); - if (upload) { - if (!upload.isImage) { - UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand); - throw "Upload is not an image"; - } - image = upload.item.file; - } - break; - case "width": - width = Number(opt.value); - break; - case "height": - height = Number(opt.value); - break; - } - } - - UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand); - return { image, width, height }; -} - -export default definePlugin({ - name: "ImgToGif", - description: "Adds a /imgtogif slash command to create a gif from any image", - authors: [EquicordDevs.zyqunix], - commands: [ - { - inputType: ApplicationCommandInputType.BUILT_IN, - name: "imgtogif", - description: "Allows you to turn an image to a gif", - options: [ - { - name: "image", - description: "Image attachment to use", - type: ApplicationCommandOptionType.ATTACHMENT - }, - { - name: "width", - description: "Width of the gif", - type: ApplicationCommandOptionType.INTEGER - }, - { - name: "height", - description: "Height of the gif", - type: ApplicationCommandOptionType.INTEGER - } - ], - execute: async (opts, cmdCtx) => { - try { - const { image, width, height } = await resolveImage(opts, cmdCtx); - if (!image) throw "No Image specified!"; - - const avatar = await loadImage(image); - - let gifWidth: number; - let gifHeight: number; - - if (width && height) { - gifWidth = width; - gifHeight = height; - } else if (width) { - gifWidth = width; - gifHeight = Math.round((avatar.height / avatar.width) * width); - } else if (height) { - gifHeight = height; - gifWidth = Math.round((avatar.width / avatar.height) * height); - } else { - gifWidth = avatar.width; - gifHeight = avatar.height; - } - - const gif = GIFEncoder(); - const canvas = document.createElement("canvas"); - canvas.width = gifWidth; - canvas.height = gifHeight; - const ctx = canvas.getContext("2d")!; - - UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); - - for (let i = 0; i < FRAMES; i++) { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(avatar, 0, 0, avatar.width, avatar.height, 0, 0, canvas.width, canvas.height); - - const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); - const palette = quantize(data, 256); - const index = applyPalette(data, palette); - - gif.writeFrame(index, canvas.width, canvas.height, { - transparent: true, - palette, - }); - } - - gif.finish(); - const originalName = image.name ? image.name.replace(/\.[^/.]+$/, "") : "converted"; - const file = new File([new Uint8Array(gif.bytesView())], `${originalName}.gif`, { type: "image/gif" }); - setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10); - } catch (err) { - UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); - sendBotMessage(cmdCtx.channel.id, { content: String(err) }); - } - }, - }, - ] -}); diff --git a/src/equicordplugins/moreCommands/index.ts b/src/equicordplugins/moreCommands/index.ts index 8e367cd0d..b174a4576 100644 --- a/src/equicordplugins/moreCommands/index.ts +++ b/src/equicordplugins/moreCommands/index.ts @@ -18,17 +18,55 @@ import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands"; import { addMessagePreEditListener, addMessagePreSendListener, MessageObject, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; +import { migratePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import definePlugin from "@utils/types"; -import { UserStore } from "@webpack/common"; +import { DraftType, UploadHandler, UploadManager, UserAffinitiesStore, UserStore } from "@webpack/common"; +import { applyPalette, GIFEncoder, quantize } from "gifenc"; -import { fromMorse, getCuteAnimeBoys, getCuteNeko, getCutePats, isMorse, makeFreaky, mock, settings, toMorse, uwuify, uwuifyArray } from "./utils"; +import { + calculateAffinityScore, + calculateCanvasSize, + FRAMES, + fromMorse, + generatePoissonDiskPosition, + getCuteAnimeBoys, + getCuteNeko, + getCutePats, + getMessage, + isMorse, + loadFriendImage, + loadImage, + makeFreaky, + mock, + resolveImage, + settings, + toMorse, + uwuify, + uwuifyArray +} from "./utils"; + +migratePluginSettings("MoreCommands", "FriendCloud", "GifRoulette", "ImgToGif", "MoreKaomoji"); export default definePlugin({ name: "MoreCommands", description: "Adds various fun and useful commands", - authors: [Devs.Arjix, Devs.amy, Devs.Samu, EquicordDevs.zyqunix, EquicordDevs.ShadyGoat, Devs.thororen, Devs.Korbo, Devs.nyx, Devs.amy], + authors: [ + Devs.Arjix, + Devs.amy, + Devs.Samu, + EquicordDevs.zyqunix, + EquicordDevs.ShadyGoat, + Devs.thororen, + Devs.Korbo, + Devs.nyx, + Devs.amy, + Devs.Samwich, + EquicordDevs.Fafa, + Devs.JacobTm, + EquicordDevs.voidbbg + ], settings, commands: [ { @@ -435,7 +473,394 @@ export default definePlugin({ content: uwuify(findOption(opts, "message", "")), }), }, + { + name: "gifroulette", + description: "Tempt fate and send a gif", + execute: (opts, other) => ({ + content: getMessage(opts, other) + }), + }, + { + inputType: ApplicationCommandInputType.BUILT_IN, + name: "friendcloud", + description: "Display user you most interact with in a cloud", + options: [ + { + name: "count", + description: "Number of users to display", + type: ApplicationCommandOptionType.NUMBER, + required: false + } + ], + execute: async (opts, cmdCtx) => { + const count = findOption(opts, "count", 25); + + if (!count) return sendBotMessage(cmdCtx.channel.id, { content: "The count must be 1 or higher!" }); + + try { + const affinities = UserAffinitiesStore.getUserAffinities(); + + if (!affinities?.length) { + return sendBotMessage(cmdCtx.channel.id, { + content: "No affinities found. Check your [privacy settings]()." + }); + } + + const users = affinities + .map(e => ({ + member: UserStore.getUser(e.otherUserId), + affinity: calculateAffinityScore(e) + })) + .filter(x => x.member?.id) + .sort((a, b) => b.affinity - a.affinity) + .slice(0, count); + + if (!users.length) { + return sendBotMessage(cmdCtx.channel.id, { + content: "No valid users found in affinities. Check your [privacy settings]()." + }); + } + + const minAffinity = Math.min(...users.map(u => u.affinity)); + const maxAffinity = Math.max(...users.map(u => u.affinity)); + const minSize = 120; + const maxSize = 240; + + const getSize = (affinity: number): number => { + if (maxAffinity === minAffinity) return (minSize + maxSize) / 2; + return minSize + ((affinity - minAffinity) / (maxAffinity - minAffinity)) * (maxSize - minSize); + }; + + const avgSize = (minSize + maxSize) / 2; + const { width: canvasWidth, height: canvasHeight } = calculateCanvasSize(users.length, avgSize); + + const canvas = document.createElement("canvas"); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + const ctx = canvas.getContext("2d")!; + + const positions: Array<{ x: number, y: number, size: number; }> = []; + const userPositions = users.map(user => { + const size = getSize(user.affinity); + const pos = generatePoissonDiskPosition(positions, canvasWidth, canvasHeight, size); + positions.push({ x: pos.x, y: pos.y, size }); + return { ...user, x: pos.x, y: pos.y, size }; + }); + + let loadedImages = 0; + const totalImages = userPositions.length; + + const drawImage = async user => { + try { + const avatarUrl = user.member?.avatar + ? `https://cdn.discordapp.com/avatars/${user.member.id}/${user.member?.avatar}.webp?size=256` + : `https://cdn.discordapp.com/embed/avatars/${user.member.id as any as number % 5}.png`; + + const img = await loadFriendImage(avatarUrl); + const centerX = user.x + user.size / 2; + const centerY = user.y + user.size / 2; + + ctx.save(); + ctx.beginPath(); + ctx.arc(centerX, centerY, user.size / 2, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(img, user.x, user.y, user.size, user.size); + ctx.restore(); + + ctx.strokeStyle = "#808080"; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(centerX, centerY, user.size / 2 + 1, 0, Math.PI * 2); + ctx.stroke(); + } catch { + // we ignore + } finally { + loadedImages++; + if (loadedImages === totalImages) { + canvas.toBlob(blob => { + if (!blob) { + sendBotMessage(cmdCtx.channel.id, { content: "Couldn't generate the image :c" }); + return; + } + const file = new File([blob], "affinities-cloud.png", { type: "image/png" }); + UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage); + }, "image/png"); + } + } + }; + + userPositions.forEach(drawImage); + } catch (e: unknown) { + if (e instanceof Error) sendBotMessage(cmdCtx.channel.id, { content: e.message }); + } + }, + }, + { + inputType: ApplicationCommandInputType.BUILT_IN, + name: "imgtogif", + description: "Allows you to turn an image to a gif", + options: [ + { + name: "image", + description: "Image attachment to use", + type: ApplicationCommandOptionType.ATTACHMENT + }, + { + name: "width", + description: "Width of the gif", + type: ApplicationCommandOptionType.INTEGER + }, + { + name: "height", + description: "Height of the gif", + type: ApplicationCommandOptionType.INTEGER + } + ], + execute: async (opts, cmdCtx) => { + try { + const { image, width, height } = await resolveImage(opts, cmdCtx); + if (!image) throw "No Image specified!"; + + const avatar = await loadImage(image); + + let gifWidth: number; + let gifHeight: number; + + if (width && height) { + gifWidth = width; + gifHeight = height; + } else if (width) { + gifWidth = width; + gifHeight = Math.round((avatar.height / avatar.width) * width); + } else if (height) { + gifHeight = height; + gifWidth = Math.round((avatar.width / avatar.height) * height); + } else { + gifWidth = avatar.width; + gifHeight = avatar.height; + } + + const gif = GIFEncoder(); + const canvas = document.createElement("canvas"); + canvas.width = gifWidth; + canvas.height = gifHeight; + const ctx = canvas.getContext("2d")!; + + UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); + + for (let i = 0; i < FRAMES; i++) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(avatar, 0, 0, avatar.width, avatar.height, 0, 0, canvas.width, canvas.height); + + const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); + const palette = quantize(data, 256); + const index = applyPalette(data, palette); + + gif.writeFrame(index, canvas.width, canvas.height, { + transparent: true, + palette, + }); + } + + gif.finish(); + const originalName = image.name ? image.name.replace(/\.[^/.]+$/, "") : "converted"; + const file = new File([new Uint8Array(gif.bytesView())], `${originalName}.gif`, { type: "image/gif" }); + setTimeout(() => UploadHandler.promptToUpload([file], cmdCtx.channel, DraftType.ChannelMessage), 10); + } catch (err) { + UploadManager.clearAll(cmdCtx.channel.id, DraftType.SlashCommand); + sendBotMessage(cmdCtx.channel.id, { content: String(err) }); + } + }, + }, + { + name: "dissatisfaction", + description: " >﹏<", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + " >﹏<", + }), + }, + { + name: "smug", + description: "ಠ_ಠ", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "ಠ_ಠ", + }), + }, + { + name: "happy", + description: "ヽ(´▽`)/", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "ヽ(´▽`)/", + }), + }, + { + name: "crying", + description: "ಥ_ಥ", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "ಥ_ಥ", + }), + }, + { + name: "angry", + description: "ヽ(`Д´)ノ", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "ヽ(`Д´)ノ", + }), + }, + { + name: "anger", + description: "ヽ(o`皿′o)ノ", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "ヽ(o`皿′o)ノ", + }), + }, + { + name: "joy", + description: "<( ̄︶ ̄)>", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "<( ̄︶ ̄)>", + }), + }, + { + name: "blush", + description: "૮ ˶ᵔ ᵕ ᵔ˶ ა", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "૮ ˶ᵔ ᵕ ᵔ˶ ა", + }), + }, + { + name: "confused", + description: "(•ิ_•ิ)?", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(•ิ_•ิ)?", + }), + }, + { + name: "sleeping", + description: "(ᴗ_ᴗ)", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(ᴗ_ᴗ)", + }), + }, + { + name: "laughing", + description: "o(≧▽≦)o", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "o(≧▽≦)o", + }), + }, + /* + even more kaomoji + */ + { + name: "giving", + description: "(ノ◕ヮ◕)ノ*:・゚✧", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(ノ◕ヮ◕)ノ*:・゚✧", + }), + }, + { + name: "peace", + description: "✌(◕‿-)✌", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "✌(◕‿-)✌", + }), + }, + { + name: "ending1", + description: "Ꮺ ָ࣪ ۰ ͙⊹", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "Ꮺ ָ࣪ ۰ ͙⊹", + }), + }, + { + name: "uwu", + description: "(>⩊<)", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(>⩊<)", + }), + }, + { + name: "comfy", + description: "(─‿‿─)♡", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(─‿‿─)♡", + }), + }, + { + name: "lovehappy", + description: "(*≧ω≦*)", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(*≧ω≦*)", + }), + }, + { + name: "loveee", + description: "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)", + }), + }, + { + name: "give", + description: "(ノ= ⩊ = )ノ", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "(ノ= ⩊ = )ノ", + }), + }, + { + name: "lovegive", + description: "ღゝ◡╹)ノ♡", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "ღゝ◡╹)ノ♡", + }), + }, + { + name: "music", + description: "( ̄▽ ̄)/♫•¨•.¸¸♪", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "( ̄▽ ̄)/♫•¨•.¸¸♪", + }), + }, + { + name: "stars", + description: ".𖥔 ݁ ˖๋ ࣭ ⭑", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + ".𖥔 ݁ ˖๋ ࣭ ⭑", + }), + }, + { + name: "lovegiving", + description: "⸜(。˃ ᵕ ˂ )⸝♡", + options: [OptionalMessageOption], + execute: opts => ({ + content: findOption(opts, "message", "") + " " + "⸜(。˃ ᵕ ˂ )⸝♡", + }), + } ], + patches: [ { find: ".isPureReactComponent=!0;", diff --git a/src/equicordplugins/moreCommands/utils.ts b/src/equicordplugins/moreCommands/utils.ts index a5a0f7815..0dd7ae3a1 100644 --- a/src/equicordplugins/moreCommands/utils.ts +++ b/src/equicordplugins/moreCommands/utils.ts @@ -6,6 +6,8 @@ import { definePluginSettings } from "@api/Settings"; import { OptionType } from "@utils/types"; +import { CommandArgument, CommandContext } from "@vencord/discord-types"; +import { DraftType, UploadAttachmentStore, UploadManager, UserSettingsActionCreators } from "@webpack/common"; export const settings = definePluginSettings({ addFreakyEnding: { @@ -207,3 +209,180 @@ export function uwuifyArray(arr) { return newArr; } + +export function getMessage(opts, other) { + const frecencyStore = UserSettingsActionCreators.FrecencyUserSettingsActionCreators.getCurrentValue(); + + const gifsArray = Object.keys(frecencyStore.favoriteGifs.gifs); + + const chosenGifUrl = gifsArray[Math.floor(Math.random() * gifsArray.length)]; + + return `${chosenGifUrl}`; +} + +export function calculateAffinityScore(affinity): number { + const weights = { + friend: 0.15, + dm: 0.30, + vc: 0.25, + serverMsg: 0.20, + communication: 0.10 + }; + + let score = 0; + if (affinity.isFriend) score += weights.friend * 100; + score += affinity.dmProbability * weights.dm * 100; + score += affinity.vcProbability * weights.vc * 100; + score += affinity.serverMessageProbability * weights.serverMsg * 100; + score += affinity.communicationProbability * weights.communication * 100; + + return Math.round(Math.min(100, Math.max(0, score)) * 100) / 100; +} + +// stolen from petpet thanks vee +export function loadFriendImage(source: File | string): Promise { + const isFile = source instanceof File; + const url = isFile ? URL.createObjectURL(source) : source; + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + if (isFile) URL.revokeObjectURL(url); + resolve(img); + }; + img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); + img.crossOrigin = "anonymous"; + img.src = url; + }); +} + +export function generatePoissonDiskPosition( + existingPositions: Array<{ x: number, y: number, size: number; }>, + canvasWidth: number, + canvasHeight: number, + size: number +): { x: number, y: number; } { + const edgePadding = 10; + const minDist = size * 1.5; + const textSpace = 60; + const k = 30; + + function isValid(x: number, y: number) { + if ( + x < edgePadding || + x + size > canvasWidth - edgePadding || + y < edgePadding || + y + size > canvasHeight - textSpace - edgePadding + ) return false; + + return !existingPositions.some(pos => { + const dx = pos.x - x; + const dy = pos.y - y; + const dist = Math.hypot(dx, dy); + const minAllowed = (pos.size + size) / 2 + (minDist - size); + return dist < minAllowed; + }); + } + + if (existingPositions.length === 0) { + return { + x: canvasWidth / 2 - size / 2, + y: canvasHeight / 2 - size / 2 + }; + } + + for (let tries = 0; tries < 100; tries++) { + const base = existingPositions[Math.floor(Math.random() * existingPositions.length)]; + for (let i = 0; i < k; i++) { + const angle = Math.random() * Math.PI * 2; + const radius = minDist + Math.random() * minDist; + const x = base.x + Math.cos(angle) * radius; + const y = base.y + Math.sin(angle) * radius; + if (isValid(x, y)) { + return { + x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)), + y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding)) + }; + } + } + } + + for (let tries = 0; tries < 100; tries++) { + const x = Math.random() * (canvasWidth - size - edgePadding * 2) + edgePadding; + const y = Math.random() * (canvasHeight - size - textSpace - edgePadding * 2) + edgePadding; + if (isValid(x, y)) { + return { + x: Math.max(edgePadding, Math.min(x, canvasWidth - size - edgePadding)), + y: Math.max(edgePadding, Math.min(y, canvasHeight - size - textSpace - edgePadding)) + }; + } + } + + return { + x: edgePadding, + y: edgePadding + }; +} + +export function calculateCanvasSize(userCount: number, avatarSize: number): { width: number, height: number; } { + const padding = 50; + const textSpace = 60; + const itemWidth = avatarSize + padding; + const itemHeight = avatarSize + textSpace + padding; + const aspectRatio = 16 / 9; + const cols = Math.ceil(Math.sqrt(userCount * aspectRatio)); + const rows = Math.ceil(userCount / cols); + + return { + width: Math.max(1000, cols * itemWidth + padding), + height: Math.max(700, rows * itemHeight + padding) + }; +} + +export const FRAMES = 1; + +export function loadImage(source: File | string) { + const isFile = source instanceof File; + const url = isFile ? URL.createObjectURL(source) : source; + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + if (isFile) URL.revokeObjectURL(url); + resolve(img); + }; + img.onerror = (event, _source, _lineno, _colno, err) => reject(err || event); + img.crossOrigin = "Anonymous"; + img.src = url; + }); +} + +export async function resolveImage(options: CommandArgument[], ctx: CommandContext): Promise<{ image: File | null; width: number | null; height: number | null; }> { + let image: File | null = null; + let width: number | null = null; + let height: number | null = null; + + for (const opt of options) { + switch (opt.name) { + case "image": + const upload = UploadAttachmentStore.getUpload(ctx.channel.id, opt.name, DraftType.SlashCommand); + if (upload) { + if (!upload.isImage) { + UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand); + throw "Upload is not an image"; + } + image = upload.item.file; + } + break; + case "width": + width = Number(opt.value); + break; + case "height": + height = Number(opt.value); + break; + } + } + + UploadManager.clearAll(ctx.channel.id, DraftType.SlashCommand); + return { image, width, height }; +} diff --git a/src/equicordplugins/moreKaomoji/index.ts b/src/equicordplugins/moreKaomoji/index.ts deleted file mode 100644 index 986b1f332..000000000 --- a/src/equicordplugins/moreKaomoji/index.ts +++ /dev/null @@ -1,216 +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 { findOption, OptionalMessageOption } from "@api/Commands"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "MoreKaomoji", - description: "Adds more Kaomoji to discord. ヽ(´▽`)/", - authors: [Devs.JacobTm, EquicordDevs.voidbbg], - commands: [ - { - name: "dissatisfaction", - description: " >﹏<", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + " >﹏<", - }), - }, - { - name: "smug", - description: "ಠ_ಠ", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "ಠ_ಠ", - }), - }, - { - name: "happy", - description: "ヽ(´▽`)/", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "ヽ(´▽`)/", - }), - }, - { - name: "crying", - description: "ಥ_ಥ", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "ಥ_ಥ", - }), - }, - { - name: "angry", - description: "ヽ(`Д´)ノ", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "ヽ(`Д´)ノ", - }), - }, - { - name: "anger", - description: "ヽ(o`皿′o)ノ", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "ヽ(o`皿′o)ノ", - }), - }, - { - name: "joy", - description: "<( ̄︶ ̄)>", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "<( ̄︶ ̄)>", - }), - }, - { - name: "blush", - description: "૮ ˶ᵔ ᵕ ᵔ˶ ა", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "૮ ˶ᵔ ᵕ ᵔ˶ ა", - }), - }, - { - name: "confused", - description: "(•ิ_•ิ)?", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(•ิ_•ิ)?", - }), - }, - { - name: "sleeping", - description: "(ᴗ_ᴗ)", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(ᴗ_ᴗ)", - }), - }, - { - name: "laughing", - description: "o(≧▽≦)o", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "o(≧▽≦)o", - }), - }, - /* - even more kaomoji - */ - { - name: "giving", - description: "(ノ◕ヮ◕)ノ*:・゚✧", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(ノ◕ヮ◕)ノ*:・゚✧", - }), - }, - { - name: "peace", - description: "✌(◕‿-)✌", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "✌(◕‿-)✌", - }), - }, - { - name: "ending1", - description: "Ꮺ ָ࣪ ۰ ͙⊹", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "Ꮺ ָ࣪ ۰ ͙⊹", - }), - }, - { - name: "uwu", - description: "(>⩊<)", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(>⩊<)", - }), - }, - { - name: "comfy", - description: "(─‿‿─)♡", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(─‿‿─)♡", - }), - }, - { - name: "lovehappy", - description: "(*≧ω≦*)", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(*≧ω≦*)", - }), - }, - { - name: "loveee", - description: "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(⁄ ⁄>⁄ ▽ ⁄<⁄ ⁄)", - }), - }, - { - name: "give", - description: "(ノ= ⩊ = )ノ", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "(ノ= ⩊ = )ノ", - }), - }, - { - name: "lovegive", - description: "ღゝ◡╹)ノ♡", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "ღゝ◡╹)ノ♡", - }), - }, - { - name: "music", - description: "( ̄▽ ̄)/♫•¨•.¸¸♪", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "( ̄▽ ̄)/♫•¨•.¸¸♪", - }), - }, - { - name: "stars", - description: ".𖥔 ݁ ˖๋ ࣭ ⭑", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + ".𖥔 ݁ ˖๋ ࣭ ⭑", - }), - }, - { - name: "lovegiving", - description: "⸜(。˃ ᵕ ˂ )⸝♡", - options: [OptionalMessageOption], - execute: opts => ({ - content: findOption(opts, "message", "") + " " + "⸜(。˃ ᵕ ˂ )⸝♡", - }), - } - ] -}); diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts index 9c681386c..e5a547dbc 100644 --- a/src/plugins/implicitRelationships/index.ts +++ b/src/plugins/implicitRelationships/index.ts @@ -20,12 +20,9 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; -import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserStore } from "@webpack/common"; +import { Constants, FluxDispatcher, GuildStore, RelationshipStore, SnowflakeUtils, UserAffinitiesStore, UserStore } from "@webpack/common"; import { Settings } from "Vencord"; -const UserAffinitiesStore = findStoreLazy("UserAffinitiesV2Store"); - export default definePlugin({ name: "ImplicitRelationships", description: "Shows your implicit relationships in the Friends tab.", diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 175ab75de..1217eedbd 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -84,6 +84,7 @@ export let EditMessageStore: t.EditMessageStore; export let QuestStore: t.QuestStore; export let PendingReplyStore: t.PendingReplyStore; export let ExperimentStore: GenericStore; +export let UserAffinitiesStore: GenericStore; /** * @see jsdoc of {@link t.useStateFromStores} @@ -140,6 +141,7 @@ waitForStore("EditMessageStore", m => EditMessageStore = m); waitForStore("PendingReplyStore", m => PendingReplyStore = m); waitForStore("ExperimentStore", m => ExperimentStore = m); waitForStore("QuestStore", m => QuestStore = m); +waitForStore("UserAffinitiesV2Store", m => UserAffinitiesStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies. From 6ca466b6a5283abfed4c1f634637a911f96a1ceb Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:17:52 -0500 Subject: [PATCH 4/8] switch neo id --- src/utils/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 34f87b66b..995257ae8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1218,7 +1218,7 @@ export const EquicordDevs = Object.freeze({ }, neoarz: { name: "neoarz", - id: 1015372540937502851n + id: 218675193592283137n }, KamiRu: { name: "KamiRu", From a62d41de64f35219d8a42555887af026044426db Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 7 Mar 2026 00:34:23 -0500 Subject: [PATCH 5/8] merge more plugins --- src/equicordplugins/equicordHelper/index.tsx | 89 ++++++++++++++++++- src/equicordplugins/moreUserTags/index.tsx | 13 ++- src/equicordplugins/moreUserTags/settings.tsx | 6 ++ src/equicordplugins/noAppsAllowed/index.ts | 23 ----- src/equicordplugins/noBulletPoints/index.tsx | 20 ----- src/equicordplugins/noModalAnimation/index.ts | 44 --------- src/plugins/fixYoutubeEmbeds.desktop/index.ts | 14 --- .../fixYoutubeEmbeds.desktop}/index.tsx | 24 +++-- 8 files changed, 122 insertions(+), 111 deletions(-) delete mode 100644 src/equicordplugins/noAppsAllowed/index.ts delete mode 100644 src/equicordplugins/noBulletPoints/index.tsx delete mode 100644 src/equicordplugins/noModalAnimation/index.ts delete mode 100644 src/plugins/fixYoutubeEmbeds.desktop/index.ts rename src/{equicordplugins/youtubeDescription => plugins/fixYoutubeEmbeds.desktop}/index.tsx (68%) diff --git a/src/equicordplugins/equicordHelper/index.tsx b/src/equicordplugins/equicordHelper/index.tsx index 3c6bb883f..0cf7b4841 100644 --- a/src/equicordplugins/equicordHelper/index.tsx +++ b/src/equicordplugins/equicordHelper/index.tsx @@ -6,8 +6,9 @@ import { ApplicationCommandInputType, sendBotMessage } from "@api/Commands"; import { HeaderBarButton } from "@api/HeaderBar"; +import { addMessagePreSendListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { isPluginEnabled } from "@api/PluginManager"; -import { definePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginToSettings } from "@api/Settings"; import customRPC from "@plugins/customRPC"; import { Devs, EquicordDevs, GUILD_ID, SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_IDS, VC_SUPPORT_CHANNEL_IDS } from "@utils/constants"; import { isAnyPluginDev } from "@utils/misc"; @@ -20,6 +21,9 @@ import { ComponentType } from "react"; import { PluginButtons } from "./pluginButtons"; import { PluginCards } from "./pluginCards"; +migratePluginToSettings(true, "EquicordHelper", "NoBulletPoints", "noBulletPoints"); +migratePluginToSettings(true, "EquicordHelper", "NoModalAnimation", "noModalAnimation"); + let clicked = false; const SafetyHubStore = findStoreLazy("SafetyHubStore"); @@ -58,6 +62,11 @@ function StandingButton() { ); } +const listener = async (channelId, msg) => { + if (!settings.store.noBulletPoints) return; + msg.content = textProcessing(msg.content); +}; + const settings = definePluginSettings({ noMirroredCamera: { type: OptionType.BOOLEAN, @@ -106,12 +115,34 @@ const settings = definePluginSettings({ restartNeeded: true, default: false }, + noBulletPoints: { + type: OptionType.BOOLEAN, + description: "Stops you from typing markdown bullet points (stinky)", + restartNeeded: true, + default: false + }, + noModalAnimation: { + type: OptionType.BOOLEAN, + description: "Remove the 300ms long animation when opening or closing modals", + restartNeeded: true, + default: false + }, }); export default definePlugin({ name: "EquicordHelper", description: "Used to provide support, fix discord caused crashes, and other misc features.", - authors: [Devs.thororen, EquicordDevs.nyx, EquicordDevs.Naibuu, EquicordDevs.keircn, EquicordDevs.SerStars, EquicordDevs.mart, EquicordDevs.omaw], + authors: [ + Devs.thororen, + EquicordDevs.nyx, + EquicordDevs.Naibuu, + EquicordDevs.keircn, + EquicordDevs.SerStars, + EquicordDevs.mart, + EquicordDevs.omaw, + Devs.Samwich, + Devs.AutumnVN + ], required: true, settings, headerBarButton: { @@ -199,13 +230,49 @@ export default definePlugin({ }, // Restore File Download Button { - predicate: () => settings.store.restoreFileDownloadButton, find: '"VISUAL_PLACEHOLDER":', + predicate: () => settings.store.restoreFileDownloadButton, replacement: { match: /(\.downloadUrl,showDownload:)\i/, replace: "$1!0" } }, + // Removes Modal Animation + { + find: "DURATION_IN:", + predicate: () => settings.store.noModalAnimation, + replacement: { + match: /300,/, + replace: "0,", + } + }, + // Removes Modal Animation + { + find: 'backdropFilter:"blur(0px)"', + predicate: () => settings.store.noModalAnimation, + replacement: { + match: /\?0:200/, + replace: "?0:0", + } + }, + // Removes Modal Animation + { + find: '="ABOVE"', + predicate: () => settings.store.noModalAnimation, + replacement: { + match: /\?\?300/, + replace: "??0", + } + }, + // Removes Modal Animation + { + find: "renderLurkerModeUpsellPopout,position:", + predicate: () => settings.store.noModalAnimation, + replacement: { + match: /200:300/g, + replace: "0:0", + }, + } ], renderMessageAccessory(props) { return ( @@ -255,5 +322,19 @@ export default definePlugin({ } } } - ] + ], + start() { + if (settings.store.noBulletPoints) { + addMessagePreSendListener(listener); + } + }, + stop() { + if (settings.store.noBulletPoints) { + removeMessagePreSendListener(listener); + } + } }); + +function textProcessing(text: string): string { + return text.replace(/(^|\n)(\s*)([*+-])\s+/g, "$1$2\\$3 "); +} diff --git a/src/equicordplugins/moreUserTags/index.tsx b/src/equicordplugins/moreUserTags/index.tsx index 4889178e0..c15bb5e39 100644 --- a/src/equicordplugins/moreUserTags/index.tsx +++ b/src/equicordplugins/moreUserTags/index.tsx @@ -6,6 +6,7 @@ import "./styles.css"; +import { migratePluginToSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import { classNameFactory } from "@utils/css"; import { getCurrentChannel, getIntlMessage } from "@utils/discord"; @@ -31,10 +32,12 @@ const genTagTypes = () => { return obj; }; +migratePluginToSettings(true, "MoreUserTags", "NoAppsAllowed", "noAppsAllowed"); + export default definePlugin({ name: "MoreUserTags", description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", - authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen], + authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen, EquicordDevs.meowabyte], settings, patches: [ // Make discord actually use our tags @@ -51,6 +54,14 @@ export default definePlugin({ predicate: () => settings.store.dontShowBotTag }, ], + }, + { + find: '"#{intl::APP_TAG::hash}":', + predicate: () => settings.store.noAppsAllowed, + replacement: { + match: /(#{intl::APP_TAG::hash}":\[").*?("\])/, + replace: "$1BOT$2" + } } ], start() { diff --git a/src/equicordplugins/moreUserTags/settings.tsx b/src/equicordplugins/moreUserTags/settings.tsx index a9be8ed83..23aa28cc1 100644 --- a/src/equicordplugins/moreUserTags/settings.tsx +++ b/src/equicordplugins/moreUserTags/settings.tsx @@ -115,4 +115,10 @@ export const settings = definePluginSettings({ component: SettingsComponent, description: "fill me" }, + noAppsAllowed: { + description: "Replaces app for bot", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + } }); diff --git a/src/equicordplugins/noAppsAllowed/index.ts b/src/equicordplugins/noAppsAllowed/index.ts deleted file mode 100644 index 91e03dd5c..000000000 --- a/src/equicordplugins/noAppsAllowed/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { EquicordDevs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoAppsAllowed", - description: "returns the bot's tag :skulk:", - authors: [EquicordDevs.meowabyte], - patches: [ - { - find: '"#{intl::APP_TAG::hash}":', - replacement: { - match: /(#{intl::APP_TAG::hash}":\[").*?("\])/, - replace: "$1BOT$2" - } - } - ] -}); diff --git a/src/equicordplugins/noBulletPoints/index.tsx b/src/equicordplugins/noBulletPoints/index.tsx deleted file mode 100644 index cedd4226c..000000000 --- a/src/equicordplugins/noBulletPoints/index.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 - */ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -export default definePlugin({ - name: "NoBulletPoints", - description: "Stops you from typing markdown bullet points (stinky)", - authors: [Devs.Samwich], - onBeforeMessageSend(channelId, msg) { - msg.content = textProcessing(msg.content); - }, -}); - -function textProcessing(text: string): string { - return text.replace(/(^|\n)(\s*)([*+-])\s+/g, "$1$2\\$3 "); -} diff --git a/src/equicordplugins/noModalAnimation/index.ts b/src/equicordplugins/noModalAnimation/index.ts deleted file mode 100644 index b86bb95b3..000000000 --- a/src/equicordplugins/noModalAnimation/index.ts +++ /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 { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; - -export default definePlugin({ - name: "NoModalAnimation", - description: "Remove the 300ms long animation when opening or closing modals", - authors: [Devs.AutumnVN], - patches: [ - { - find: "DURATION_IN:", - replacement: { - match: /300,/, - replace: "0,", - } - }, - { - find: 'backdropFilter:"blur(0px)"', - replacement: { - match: /\?0:200/, - replace: "?0:0", - } - }, - { - find: '="ABOVE"', - replacement: { - match: /\?\?300/, - replace: "??0", - } - }, - { - find: "renderLurkerModeUpsellPopout,position:", - replacement: { - match: /200:300/g, - replace: "0:0", - }, - } - ] -}); diff --git a/src/plugins/fixYoutubeEmbeds.desktop/index.ts b/src/plugins/fixYoutubeEmbeds.desktop/index.ts deleted file mode 100644 index 55486d763..000000000 --- a/src/plugins/fixYoutubeEmbeds.desktop/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2023 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: "FixYoutubeEmbeds", - description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)", - authors: [Devs.coolelectronics] -}); diff --git a/src/equicordplugins/youtubeDescription/index.tsx b/src/plugins/fixYoutubeEmbeds.desktop/index.tsx similarity index 68% rename from src/equicordplugins/youtubeDescription/index.tsx rename to src/plugins/fixYoutubeEmbeds.desktop/index.tsx index 4b2072893..6ea46fd7f 100644 --- a/src/equicordplugins/youtubeDescription/index.tsx +++ b/src/plugins/fixYoutubeEmbeds.desktop/index.tsx @@ -1,24 +1,38 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2023 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { OptionType } from "@utils/types"; import { Embed } from "@vencord/discord-types"; import { useState } from "@webpack/common"; interface ToggleableDescriptionProps { embed: Embed, original: () => any; } +const settings = definePluginSettings({ + youtubeDescription: { + description: "Adds descriptions to youtube video embeds", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + } +}); + +migratePluginSettings("FixYoutubeEmbeds", "YoutubeDescription"); + export default definePlugin({ - name: "YoutubeDescription", - description: "Adds descriptions to youtube video embeds", - authors: [Devs.arHSM], + name: "FixYoutubeEmbeds", + description: "Bypasses youtube videos being blocked from display on Discord (for example by UMG)", + authors: [Devs.coolelectronics, Devs.arHSM], + settings, patches: [ { find: "#{intl::SUPPRESS_ALL_EMBEDS}", + predicate: () => settings.store.youtubeDescription, replacement: { match: /case (\i\.\i\.VIDEO):(case \i\.\i\.\i:)*break;default:(\i)=(?:(this\.renderDescription)\(\))\}/, replace: "$2 break; case $1: $3 = $self.ToggleableDescriptionWrapper({ embed: this.props.embed, original: $4.bind(this) }); break; default: $3 = $4() }" From 7dd37b53bc37d3ac0a5e2fad670934846162cc94 Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 7 Mar 2026 00:39:25 -0500 Subject: [PATCH 6/8] goodbye --- .../copyProfileColors/index.tsx | 98 ---- src/equicordplugins/equissant/index.ts | 47 -- src/equicordplugins/exitSounds/index.tsx | 147 ------ src/equicordplugins/exitSounds/styles.css | 17 - .../fastDeleteChannels/index.tsx | 128 ----- src/equicordplugins/followVoiceUser/index.tsx | 115 ---- src/equicordplugins/fontLoader/index.tsx | 219 -------- src/equicordplugins/fontLoader/styles.css | 23 - .../frequentQuickSwitcher/index.tsx | 51 -- src/equicordplugins/friendTags/index.tsx | 230 -------- src/equicordplugins/friendTags/styles.css | 11 - src/equicordplugins/friendshipRanks/index.tsx | 154 ------ .../friendshipRanks/styles.css | 19 - src/equicordplugins/ghosted/index.tsx | 302 ++++++----- src/equicordplugins/ghosted/styles.css | 87 ---- src/equicordplugins/hopOn/index.tsx | 48 -- src/equicordplugins/husk/index.tsx | 122 ----- .../iRememberYou/components/data.tsx | 217 -------- .../iRememberYou/components/ui.tsx | 141 ----- src/equicordplugins/iRememberYou/index.tsx | 55 -- src/equicordplugins/iRememberYou/styles.css | 78 --- src/equicordplugins/idleAutoRestart/index.tsx | 85 --- .../inRole/RoleMembersModal.tsx | 44 -- src/equicordplugins/inRole/index.tsx | 127 ----- src/equicordplugins/inRole/style.css | 29 -- src/equicordplugins/ingtoninator/index.tsx | 161 ------ .../components/DecryptionModal.tsx | 77 --- .../components/EncryptionModal.tsx | 113 ---- .../invisibleChat.desktop/index.tsx | 234 --------- src/equicordplugins/jumpscare/index.tsx | 90 ---- src/equicordplugins/jumpscare/styles.css | 33 -- src/equicordplugins/lastActive/index.tsx | 144 ----- src/equicordplugins/messageBurst/index.ts | 96 ---- .../messageFetchTimer/index.tsx | 178 ------- src/equicordplugins/messageNotifier/index.ts | 63 --- src/equicordplugins/moyai/index.ts | 174 ------- src/equicordplugins/noRoleHeaders/index.ts | 23 - .../index.tsx | 67 --- src/equicordplugins/partyMode/index.ts | 109 ---- .../pingNotifications/index.tsx | 145 ------ .../profileSets/components/confirmModal.tsx | 81 --- .../profileSets/components/presetList.tsx | 220 -------- .../profileSets/components/presetManager.tsx | 290 ----------- src/equicordplugins/profileSets/index.tsx | 56 -- src/equicordplugins/profileSets/styles.css | 209 -------- .../profileSets/utils/actions.ts | 152 ------ .../profileSets/utils/profile.ts | 415 --------------- .../profileSets/utils/storage.ts | 120 ----- .../questionMarkReplacement/index.tsx | 46 -- .../screenRecorder.equibop/index.tsx | 66 --- src/equicordplugins/serverSearch/index.tsx | 64 --- src/equicordplugins/serverSearch/styles.css | 33 -- src/equicordplugins/signature/index.tsx | 155 ------ src/equicordplugins/snowfall/index.tsx | 490 ------------------ src/equicordplugins/soggy/index.tsx | 167 ------ src/equicordplugins/talkInReverse/index.tsx | 74 --- src/equicordplugins/toggleVideoBind/index.ts | 70 --- .../toneIndicators/ToneIndicator.tsx | 36 -- src/equicordplugins/toneIndicators/index.tsx | 188 ------- .../toneIndicators/indicators.ts | 62 --- src/equicordplugins/translatePlus/index.tsx | 68 --- .../translatePlus/misc/languages.ts | 173 ------- .../translatePlus/misc/types.ts | 19 - src/equicordplugins/translatePlus/settings.ts | 35 -- src/equicordplugins/translatePlus/style.css | 22 - .../translatePlus/utils/accessory.tsx | 42 -- .../translatePlus/utils/icon.tsx | 27 - .../translatePlus/utils/translator.ts | 143 ----- src/equicordplugins/triviaAI/index.tsx | 66 --- src/equicordplugins/triviaAI/settings.ts | 53 -- src/equicordplugins/triviaAI/utils.ts | 188 ------- .../unreadBadgeCount/index.tsx | 80 --- .../unreadBadgeCount/styles.css | 39 -- .../VoiceChannelLogEntryComponent.css | 42 -- .../VoiceChannelLogEntryComponent.tsx | 37 -- .../components/VoiceChannelLogEntryIcons.tsx | 29 -- .../components/VoiceChannelLogModal.tsx | 68 --- .../voiceChannelLog.dev/index.tsx | 164 ------ .../voiceChannelLog.dev/logs.ts | 41 -- .../index.tsx | 0 .../index.tsx | 0 .../tar.ts | 0 .../webpack.ts | 0 src/equicordplugins/wigglyText/index.tsx | 199 ------- .../ui/components/ExampleWiggle.tsx | 20 - src/equicordplugins/writeUpperCase/index.ts | 49 -- src/plugins/_core/supportHelper.tsx | 5 +- 87 files changed, 162 insertions(+), 8742 deletions(-) delete mode 100644 src/equicordplugins/copyProfileColors/index.tsx delete mode 100644 src/equicordplugins/equissant/index.ts delete mode 100644 src/equicordplugins/exitSounds/index.tsx delete mode 100644 src/equicordplugins/exitSounds/styles.css delete mode 100644 src/equicordplugins/fastDeleteChannels/index.tsx delete mode 100644 src/equicordplugins/followVoiceUser/index.tsx delete mode 100644 src/equicordplugins/fontLoader/index.tsx delete mode 100644 src/equicordplugins/fontLoader/styles.css delete mode 100644 src/equicordplugins/frequentQuickSwitcher/index.tsx delete mode 100644 src/equicordplugins/friendTags/index.tsx delete mode 100644 src/equicordplugins/friendTags/styles.css delete mode 100644 src/equicordplugins/friendshipRanks/index.tsx delete mode 100644 src/equicordplugins/friendshipRanks/styles.css delete mode 100644 src/equicordplugins/hopOn/index.tsx delete mode 100644 src/equicordplugins/husk/index.tsx delete mode 100644 src/equicordplugins/iRememberYou/components/data.tsx delete mode 100644 src/equicordplugins/iRememberYou/components/ui.tsx delete mode 100644 src/equicordplugins/iRememberYou/index.tsx delete mode 100644 src/equicordplugins/iRememberYou/styles.css delete mode 100644 src/equicordplugins/idleAutoRestart/index.tsx delete mode 100644 src/equicordplugins/inRole/RoleMembersModal.tsx delete mode 100644 src/equicordplugins/inRole/index.tsx delete mode 100644 src/equicordplugins/inRole/style.css delete mode 100644 src/equicordplugins/ingtoninator/index.tsx delete mode 100644 src/equicordplugins/invisibleChat.desktop/components/DecryptionModal.tsx delete mode 100644 src/equicordplugins/invisibleChat.desktop/components/EncryptionModal.tsx delete mode 100644 src/equicordplugins/invisibleChat.desktop/index.tsx delete mode 100644 src/equicordplugins/jumpscare/index.tsx delete mode 100644 src/equicordplugins/jumpscare/styles.css delete mode 100644 src/equicordplugins/lastActive/index.tsx delete mode 100644 src/equicordplugins/messageBurst/index.ts delete mode 100644 src/equicordplugins/messageFetchTimer/index.tsx delete mode 100644 src/equicordplugins/messageNotifier/index.ts delete mode 100644 src/equicordplugins/moyai/index.ts delete mode 100644 src/equicordplugins/noRoleHeaders/index.ts delete mode 100644 src/equicordplugins/notificationTitle.discordDesktop/index.tsx delete mode 100644 src/equicordplugins/partyMode/index.ts delete mode 100644 src/equicordplugins/pingNotifications/index.tsx delete mode 100644 src/equicordplugins/profileSets/components/confirmModal.tsx delete mode 100644 src/equicordplugins/profileSets/components/presetList.tsx delete mode 100644 src/equicordplugins/profileSets/components/presetManager.tsx delete mode 100644 src/equicordplugins/profileSets/index.tsx delete mode 100644 src/equicordplugins/profileSets/styles.css delete mode 100644 src/equicordplugins/profileSets/utils/actions.ts delete mode 100644 src/equicordplugins/profileSets/utils/profile.ts delete mode 100644 src/equicordplugins/profileSets/utils/storage.ts delete mode 100644 src/equicordplugins/questionMarkReplacement/index.tsx delete mode 100644 src/equicordplugins/screenRecorder.equibop/index.tsx delete mode 100644 src/equicordplugins/serverSearch/index.tsx delete mode 100644 src/equicordplugins/serverSearch/styles.css delete mode 100644 src/equicordplugins/signature/index.tsx delete mode 100644 src/equicordplugins/snowfall/index.tsx delete mode 100644 src/equicordplugins/soggy/index.tsx delete mode 100644 src/equicordplugins/talkInReverse/index.tsx delete mode 100644 src/equicordplugins/toggleVideoBind/index.ts delete mode 100644 src/equicordplugins/toneIndicators/ToneIndicator.tsx delete mode 100644 src/equicordplugins/toneIndicators/index.tsx delete mode 100644 src/equicordplugins/toneIndicators/indicators.ts delete mode 100644 src/equicordplugins/translatePlus/index.tsx delete mode 100644 src/equicordplugins/translatePlus/misc/languages.ts delete mode 100644 src/equicordplugins/translatePlus/misc/types.ts delete mode 100644 src/equicordplugins/translatePlus/settings.ts delete mode 100644 src/equicordplugins/translatePlus/style.css delete mode 100644 src/equicordplugins/translatePlus/utils/accessory.tsx delete mode 100644 src/equicordplugins/translatePlus/utils/icon.tsx delete mode 100644 src/equicordplugins/translatePlus/utils/translator.ts delete mode 100644 src/equicordplugins/triviaAI/index.tsx delete mode 100644 src/equicordplugins/triviaAI/settings.ts delete mode 100644 src/equicordplugins/triviaAI/utils.ts delete mode 100644 src/equicordplugins/unreadBadgeCount/index.tsx delete mode 100644 src/equicordplugins/unreadBadgeCount/styles.css delete mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.css delete mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.tsx delete mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryIcons.tsx delete mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogModal.tsx delete mode 100644 src/equicordplugins/voiceChannelLog.dev/index.tsx delete mode 100644 src/equicordplugins/voiceChannelLog.dev/logs.ts rename src/equicordplugins/{voiceRejoin => voiceRejoin.dev}/index.tsx (100%) rename src/equicordplugins/{webpackTarball => webpackTarball.dev}/index.tsx (100%) rename src/equicordplugins/{webpackTarball => webpackTarball.dev}/tar.ts (100%) rename src/equicordplugins/{webpackTarball => webpackTarball.dev}/webpack.ts (100%) delete mode 100644 src/equicordplugins/wigglyText/index.tsx delete mode 100644 src/equicordplugins/wigglyText/ui/components/ExampleWiggle.tsx delete mode 100644 src/equicordplugins/writeUpperCase/index.ts 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) { From a480ab051799f1e86ffdde46f228f479eb3c6fb4 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 7 Mar 2026 08:31:31 -0500 Subject: [PATCH 7/8] Revert "goodbye" This reverts commit 7dd37b53bc37d3ac0a5e2fad670934846162cc94. --- .../copyProfileColors/index.tsx | 98 ++++ src/equicordplugins/equissant/index.ts | 47 ++ src/equicordplugins/exitSounds/index.tsx | 147 ++++++ src/equicordplugins/exitSounds/styles.css | 17 + .../fastDeleteChannels/index.tsx | 128 +++++ src/equicordplugins/followVoiceUser/index.tsx | 115 ++++ src/equicordplugins/fontLoader/index.tsx | 219 ++++++++ src/equicordplugins/fontLoader/styles.css | 23 + .../frequentQuickSwitcher/index.tsx | 51 ++ src/equicordplugins/friendTags/index.tsx | 230 ++++++++ src/equicordplugins/friendTags/styles.css | 11 + src/equicordplugins/friendshipRanks/index.tsx | 154 ++++++ .../friendshipRanks/styles.css | 19 + src/equicordplugins/ghosted/index.tsx | 302 +++++------ src/equicordplugins/ghosted/styles.css | 87 ++++ src/equicordplugins/hopOn/index.tsx | 48 ++ src/equicordplugins/husk/index.tsx | 122 +++++ .../iRememberYou/components/data.tsx | 217 ++++++++ .../iRememberYou/components/ui.tsx | 141 +++++ src/equicordplugins/iRememberYou/index.tsx | 55 ++ src/equicordplugins/iRememberYou/styles.css | 78 +++ src/equicordplugins/idleAutoRestart/index.tsx | 85 +++ .../inRole/RoleMembersModal.tsx | 44 ++ src/equicordplugins/inRole/index.tsx | 127 +++++ src/equicordplugins/inRole/style.css | 29 ++ src/equicordplugins/ingtoninator/index.tsx | 161 ++++++ .../components/DecryptionModal.tsx | 77 +++ .../components/EncryptionModal.tsx | 113 ++++ .../invisibleChat.desktop/index.tsx | 234 +++++++++ src/equicordplugins/jumpscare/index.tsx | 90 ++++ src/equicordplugins/jumpscare/styles.css | 33 ++ src/equicordplugins/lastActive/index.tsx | 144 +++++ src/equicordplugins/messageBurst/index.ts | 96 ++++ .../messageFetchTimer/index.tsx | 178 +++++++ src/equicordplugins/messageNotifier/index.ts | 63 +++ src/equicordplugins/moyai/index.ts | 174 +++++++ src/equicordplugins/noRoleHeaders/index.ts | 23 + .../index.tsx | 67 +++ src/equicordplugins/partyMode/index.ts | 109 ++++ .../pingNotifications/index.tsx | 145 ++++++ .../profileSets/components/confirmModal.tsx | 81 +++ .../profileSets/components/presetList.tsx | 220 ++++++++ .../profileSets/components/presetManager.tsx | 290 +++++++++++ src/equicordplugins/profileSets/index.tsx | 56 ++ src/equicordplugins/profileSets/styles.css | 209 ++++++++ .../profileSets/utils/actions.ts | 152 ++++++ .../profileSets/utils/profile.ts | 415 +++++++++++++++ .../profileSets/utils/storage.ts | 120 +++++ .../questionMarkReplacement/index.tsx | 46 ++ .../screenRecorder.equibop/index.tsx | 66 +++ src/equicordplugins/serverSearch/index.tsx | 64 +++ src/equicordplugins/serverSearch/styles.css | 33 ++ src/equicordplugins/signature/index.tsx | 155 ++++++ src/equicordplugins/snowfall/index.tsx | 490 ++++++++++++++++++ src/equicordplugins/soggy/index.tsx | 167 ++++++ src/equicordplugins/talkInReverse/index.tsx | 74 +++ src/equicordplugins/toggleVideoBind/index.ts | 70 +++ .../toneIndicators/ToneIndicator.tsx | 36 ++ src/equicordplugins/toneIndicators/index.tsx | 188 +++++++ .../toneIndicators/indicators.ts | 62 +++ src/equicordplugins/translatePlus/index.tsx | 68 +++ .../translatePlus/misc/languages.ts | 173 +++++++ .../translatePlus/misc/types.ts | 19 + src/equicordplugins/translatePlus/settings.ts | 35 ++ src/equicordplugins/translatePlus/style.css | 22 + .../translatePlus/utils/accessory.tsx | 42 ++ .../translatePlus/utils/icon.tsx | 27 + .../translatePlus/utils/translator.ts | 143 +++++ src/equicordplugins/triviaAI/index.tsx | 66 +++ src/equicordplugins/triviaAI/settings.ts | 53 ++ src/equicordplugins/triviaAI/utils.ts | 188 +++++++ .../unreadBadgeCount/index.tsx | 80 +++ .../unreadBadgeCount/styles.css | 39 ++ .../VoiceChannelLogEntryComponent.css | 42 ++ .../VoiceChannelLogEntryComponent.tsx | 37 ++ .../components/VoiceChannelLogEntryIcons.tsx | 29 ++ .../components/VoiceChannelLogModal.tsx | 68 +++ .../voiceChannelLog.dev/index.tsx | 164 ++++++ .../voiceChannelLog.dev/logs.ts | 41 ++ .../index.tsx | 0 .../index.tsx | 0 .../tar.ts | 0 .../webpack.ts | 0 src/equicordplugins/wigglyText/index.tsx | 199 +++++++ .../ui/components/ExampleWiggle.tsx | 20 + src/equicordplugins/writeUpperCase/index.ts | 49 ++ src/plugins/_core/supportHelper.tsx | 5 +- 87 files changed, 8742 insertions(+), 162 deletions(-) create mode 100644 src/equicordplugins/copyProfileColors/index.tsx create mode 100644 src/equicordplugins/equissant/index.ts create mode 100644 src/equicordplugins/exitSounds/index.tsx create mode 100644 src/equicordplugins/exitSounds/styles.css create mode 100644 src/equicordplugins/fastDeleteChannels/index.tsx create mode 100644 src/equicordplugins/followVoiceUser/index.tsx create mode 100644 src/equicordplugins/fontLoader/index.tsx create mode 100644 src/equicordplugins/fontLoader/styles.css create mode 100644 src/equicordplugins/frequentQuickSwitcher/index.tsx create mode 100644 src/equicordplugins/friendTags/index.tsx create mode 100644 src/equicordplugins/friendTags/styles.css create mode 100644 src/equicordplugins/friendshipRanks/index.tsx create mode 100644 src/equicordplugins/friendshipRanks/styles.css create mode 100644 src/equicordplugins/hopOn/index.tsx create mode 100644 src/equicordplugins/husk/index.tsx create mode 100644 src/equicordplugins/iRememberYou/components/data.tsx create mode 100644 src/equicordplugins/iRememberYou/components/ui.tsx create mode 100644 src/equicordplugins/iRememberYou/index.tsx create mode 100644 src/equicordplugins/iRememberYou/styles.css create mode 100644 src/equicordplugins/idleAutoRestart/index.tsx create mode 100644 src/equicordplugins/inRole/RoleMembersModal.tsx create mode 100644 src/equicordplugins/inRole/index.tsx create mode 100644 src/equicordplugins/inRole/style.css create mode 100644 src/equicordplugins/ingtoninator/index.tsx create mode 100644 src/equicordplugins/invisibleChat.desktop/components/DecryptionModal.tsx create mode 100644 src/equicordplugins/invisibleChat.desktop/components/EncryptionModal.tsx create mode 100644 src/equicordplugins/invisibleChat.desktop/index.tsx create mode 100644 src/equicordplugins/jumpscare/index.tsx create mode 100644 src/equicordplugins/jumpscare/styles.css create mode 100644 src/equicordplugins/lastActive/index.tsx create mode 100644 src/equicordplugins/messageBurst/index.ts create mode 100644 src/equicordplugins/messageFetchTimer/index.tsx create mode 100644 src/equicordplugins/messageNotifier/index.ts create mode 100644 src/equicordplugins/moyai/index.ts create mode 100644 src/equicordplugins/noRoleHeaders/index.ts create mode 100644 src/equicordplugins/notificationTitle.discordDesktop/index.tsx create mode 100644 src/equicordplugins/partyMode/index.ts create mode 100644 src/equicordplugins/pingNotifications/index.tsx create mode 100644 src/equicordplugins/profileSets/components/confirmModal.tsx create mode 100644 src/equicordplugins/profileSets/components/presetList.tsx create mode 100644 src/equicordplugins/profileSets/components/presetManager.tsx create mode 100644 src/equicordplugins/profileSets/index.tsx create mode 100644 src/equicordplugins/profileSets/styles.css create mode 100644 src/equicordplugins/profileSets/utils/actions.ts create mode 100644 src/equicordplugins/profileSets/utils/profile.ts create mode 100644 src/equicordplugins/profileSets/utils/storage.ts create mode 100644 src/equicordplugins/questionMarkReplacement/index.tsx create mode 100644 src/equicordplugins/screenRecorder.equibop/index.tsx create mode 100644 src/equicordplugins/serverSearch/index.tsx create mode 100644 src/equicordplugins/serverSearch/styles.css create mode 100644 src/equicordplugins/signature/index.tsx create mode 100644 src/equicordplugins/snowfall/index.tsx create mode 100644 src/equicordplugins/soggy/index.tsx create mode 100644 src/equicordplugins/talkInReverse/index.tsx create mode 100644 src/equicordplugins/toggleVideoBind/index.ts create mode 100644 src/equicordplugins/toneIndicators/ToneIndicator.tsx create mode 100644 src/equicordplugins/toneIndicators/index.tsx create mode 100644 src/equicordplugins/toneIndicators/indicators.ts create mode 100644 src/equicordplugins/translatePlus/index.tsx create mode 100644 src/equicordplugins/translatePlus/misc/languages.ts create mode 100644 src/equicordplugins/translatePlus/misc/types.ts create mode 100644 src/equicordplugins/translatePlus/settings.ts create mode 100644 src/equicordplugins/translatePlus/style.css create mode 100644 src/equicordplugins/translatePlus/utils/accessory.tsx create mode 100644 src/equicordplugins/translatePlus/utils/icon.tsx create mode 100644 src/equicordplugins/translatePlus/utils/translator.ts create mode 100644 src/equicordplugins/triviaAI/index.tsx create mode 100644 src/equicordplugins/triviaAI/settings.ts create mode 100644 src/equicordplugins/triviaAI/utils.ts create mode 100644 src/equicordplugins/unreadBadgeCount/index.tsx create mode 100644 src/equicordplugins/unreadBadgeCount/styles.css create mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.css create mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.tsx create mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryIcons.tsx create mode 100644 src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogModal.tsx create mode 100644 src/equicordplugins/voiceChannelLog.dev/index.tsx create mode 100644 src/equicordplugins/voiceChannelLog.dev/logs.ts rename src/equicordplugins/{voiceRejoin.dev => voiceRejoin}/index.tsx (100%) rename src/equicordplugins/{webpackTarball.dev => webpackTarball}/index.tsx (100%) rename src/equicordplugins/{webpackTarball.dev => webpackTarball}/tar.ts (100%) rename src/equicordplugins/{webpackTarball.dev => webpackTarball}/webpack.ts (100%) create mode 100644 src/equicordplugins/wigglyText/index.tsx create mode 100644 src/equicordplugins/wigglyText/ui/components/ExampleWiggle.tsx create mode 100644 src/equicordplugins/writeUpperCase/index.ts diff --git a/src/equicordplugins/copyProfileColors/index.tsx b/src/equicordplugins/copyProfileColors/index.tsx new file mode 100644 index 000000000..256e86994 --- /dev/null +++ b/src/equicordplugins/copyProfileColors/index.tsx @@ -0,0 +1,98 @@ +/* + * 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 new file mode 100644 index 000000000..188316213 --- /dev/null +++ b/src/equicordplugins/equissant/index.ts @@ -0,0 +1,47 @@ +/* + * 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 new file mode 100644 index 000000000..db67fe6ed --- /dev/null +++ b/src/equicordplugins/exitSounds/index.tsx @@ -0,0 +1,147 @@ +/* + * 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 new file mode 100644 index 000000000..246817838 --- /dev/null +++ b/src/equicordplugins/exitSounds/styles.css @@ -0,0 +1,17 @@ +.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 new file mode 100644 index 000000000..e4b20c200 --- /dev/null +++ b/src/equicordplugins/fastDeleteChannels/index.tsx @@ -0,0 +1,128 @@ +/* + * 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 new file mode 100644 index 000000000..18ef15244 --- /dev/null +++ b/src/equicordplugins/followVoiceUser/index.tsx @@ -0,0 +1,115 @@ +/* + * 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 new file mode 100644 index 000000000..f81c1d9a5 --- /dev/null +++ b/src/equicordplugins/fontLoader/index.tsx @@ -0,0 +1,219 @@ +/* + * 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 new file mode 100644 index 000000000..7ca42e5ea --- /dev/null +++ b/src/equicordplugins/fontLoader/styles.css @@ -0,0 +1,23 @@ +.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 new file mode 100644 index 000000000..c2d61a11e --- /dev/null +++ b/src/equicordplugins/frequentQuickSwitcher/index.tsx @@ -0,0 +1,51 @@ +/* + * 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 new file mode 100644 index 000000000..0f893cdb8 --- /dev/null +++ b/src/equicordplugins/friendTags/index.tsx @@ -0,0 +1,230 @@ +/* + * 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 new file mode 100644 index 000000000..c214d5d9c --- /dev/null +++ b/src/equicordplugins/friendTags/styles.css @@ -0,0 +1,11 @@ +.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 new file mode 100644 index 000000000..9294c9f61 --- /dev/null +++ b/src/equicordplugins/friendshipRanks/index.tsx @@ -0,0 +1,154 @@ +/* + * 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 new file mode 100644 index 000000000..b331f601e --- /dev/null +++ b/src/equicordplugins/friendshipRanks/styles.css @@ -0,0 +1,19 @@ +.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 13c1b167d..845a34f6b 100644 --- a/src/equicordplugins/ghosted/index.tsx +++ b/src/equicordplugins/ghosted/index.tsx @@ -7,197 +7,179 @@ 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 type { Channel } from "@vencord/discord-types"; +import { 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 [count, setCount] = useState(getBooCount()); + const [showJumpscare, setShowJumpscare] = useState(false); - useEffect(() => { - const unsubscribe = onBooCountChange(newCount => { - setCount(newCount); - }); + useEffect(() => { + const unsubscribe = onBooCountChange(newCount => { + setCount(newCount); + }); - return () => { - unsubscribe(); - }; - }, []); + return () => { + unsubscribe(); + }; + }, []); - if (!settings.store.showIndicator) return null; + if (!settings.store.showIndicator && !showJumpscare) 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 a1db4c98f..bb5d05bbf 100644 --- a/src/equicordplugins/ghosted/styles.css +++ b/src/equicordplugins/ghosted/styles.css @@ -53,6 +53,93 @@ 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 new file mode 100644 index 000000000..cd0de30ed --- /dev/null +++ b/src/equicordplugins/hopOn/index.tsx @@ -0,0 +1,48 @@ +/* + * 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 new file mode 100644 index 000000000..25aad8e37 --- /dev/null +++ b/src/equicordplugins/husk/index.tsx @@ -0,0 +1,122 @@ +/* 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 new file mode 100644 index 000000000..4d75730bb --- /dev/null +++ b/src/equicordplugins/iRememberYou/components/data.tsx @@ -0,0 +1,217 @@ +/* + * 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 new file mode 100644 index 000000000..ba79e6da6 --- /dev/null +++ b/src/equicordplugins/iRememberYou/components/ui.tsx @@ -0,0 +1,141 @@ +/* + * 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 new file mode 100644 index 000000000..8b4614331 --- /dev/null +++ b/src/equicordplugins/iRememberYou/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 new file mode 100644 index 000000000..3a0b283ba --- /dev/null +++ b/src/equicordplugins/iRememberYou/styles.css @@ -0,0 +1,78 @@ +.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 new file mode 100644 index 000000000..2112f671f --- /dev/null +++ b/src/equicordplugins/idleAutoRestart/index.tsx @@ -0,0 +1,85 @@ +/* + * 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 new file mode 100644 index 000000000..6483b105b --- /dev/null +++ b/src/equicordplugins/inRole/RoleMembersModal.tsx @@ -0,0 +1,44 @@ +/* + * 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 new file mode 100644 index 000000000..755f9219c --- /dev/null +++ b/src/equicordplugins/inRole/index.tsx @@ -0,0 +1,127 @@ +/* + * 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 new file mode 100644 index 000000000..c7d398a0c --- /dev/null +++ b/src/equicordplugins/inRole/style.css @@ -0,0 +1,29 @@ +.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 new file mode 100644 index 000000000..43013fc4f --- /dev/null +++ b/src/equicordplugins/ingtoninator/index.tsx @@ -0,0 +1,161 @@ +/* + * 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 new file mode 100644 index 000000000..b4946e28f --- /dev/null +++ b/src/equicordplugins/invisibleChat.desktop/components/DecryptionModal.tsx @@ -0,0 +1,77 @@ +/* + * 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 new file mode 100644 index 000000000..04a225d5c --- /dev/null +++ b/src/equicordplugins/invisibleChat.desktop/components/EncryptionModal.tsx @@ -0,0 +1,113 @@ +/* + * 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 new file mode 100644 index 000000000..5589f8dec --- /dev/null +++ b/src/equicordplugins/invisibleChat.desktop/index.tsx @@ -0,0 +1,234 @@ +/* + * 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 new file mode 100644 index 000000000..5a8e4d531 --- /dev/null +++ b/src/equicordplugins/jumpscare/index.tsx @@ -0,0 +1,90 @@ +/* + * 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 new file mode 100644 index 000000000..3bfd50c3c --- /dev/null +++ b/src/equicordplugins/jumpscare/styles.css @@ -0,0 +1,33 @@ +.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 new file mode 100644 index 000000000..9e8333a5e --- /dev/null +++ b/src/equicordplugins/lastActive/index.tsx @@ -0,0 +1,144 @@ +/* + * 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 new file mode 100644 index 000000000..0d60216c8 --- /dev/null +++ b/src/equicordplugins/messageBurst/index.ts @@ -0,0 +1,96 @@ +/* + * 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 new file mode 100644 index 000000000..139a35209 --- /dev/null +++ b/src/equicordplugins/messageFetchTimer/index.tsx @@ -0,0 +1,178 @@ +/* + * 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 new file mode 100644 index 000000000..7a41158d0 --- /dev/null +++ b/src/equicordplugins/messageNotifier/index.ts @@ -0,0 +1,63 @@ +/* + * 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 new file mode 100644 index 000000000..4c6d79391 --- /dev/null +++ b/src/equicordplugins/moyai/index.ts @@ -0,0 +1,174 @@ +/* + * 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 new file mode 100644 index 000000000..424ed032f --- /dev/null +++ b/src/equicordplugins/noRoleHeaders/index.ts @@ -0,0 +1,23 @@ +/* + * 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 new file mode 100644 index 000000000..56d8b3fef --- /dev/null +++ b/src/equicordplugins/notificationTitle.discordDesktop/index.tsx @@ -0,0 +1,67 @@ +/* + * 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 new file mode 100644 index 000000000..f7cddbf97 --- /dev/null +++ b/src/equicordplugins/partyMode/index.ts @@ -0,0 +1,109 @@ +/* + * 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 new file mode 100644 index 000000000..e6b0e45e2 --- /dev/null +++ b/src/equicordplugins/pingNotifications/index.tsx @@ -0,0 +1,145 @@ +/* + * 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 new file mode 100644 index 000000000..76da537d0 --- /dev/null +++ b/src/equicordplugins/profileSets/components/confirmModal.tsx @@ -0,0 +1,81 @@ +/* + * 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 new file mode 100644 index 000000000..61bf18ce6 --- /dev/null +++ b/src/equicordplugins/profileSets/components/presetList.tsx @@ -0,0 +1,220 @@ +/* + * 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 new file mode 100644 index 000000000..74f2c5bc8 --- /dev/null +++ b/src/equicordplugins/profileSets/components/presetManager.tsx @@ -0,0 +1,290 @@ +/* + * 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 new file mode 100644 index 000000000..a2db97aa4 --- /dev/null +++ b/src/equicordplugins/profileSets/index.tsx @@ -0,0 +1,56 @@ +/* + * 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 new file mode 100644 index 000000000..390c827b9 --- /dev/null +++ b/src/equicordplugins/profileSets/styles.css @@ -0,0 +1,209 @@ +.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 new file mode 100644 index 000000000..a43aa3e24 --- /dev/null +++ b/src/equicordplugins/profileSets/utils/actions.ts @@ -0,0 +1,152 @@ +/* + * 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 new file mode 100644 index 000000000..148081443 --- /dev/null +++ b/src/equicordplugins/profileSets/utils/profile.ts @@ -0,0 +1,415 @@ +/* + * 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 new file mode 100644 index 000000000..ff5cc965f --- /dev/null +++ b/src/equicordplugins/profileSets/utils/storage.ts @@ -0,0 +1,120 @@ +/* + * 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 new file mode 100644 index 000000000..1c9acf14f --- /dev/null +++ b/src/equicordplugins/questionMarkReplacement/index.tsx @@ -0,0 +1,46 @@ +/* + * 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 new file mode 100644 index 000000000..a24ef052b --- /dev/null +++ b/src/equicordplugins/screenRecorder.equibop/index.tsx @@ -0,0 +1,66 @@ +/* + * 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 new file mode 100644 index 000000000..48cbe348f --- /dev/null +++ b/src/equicordplugins/serverSearch/index.tsx @@ -0,0 +1,64 @@ +/* + * 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 new file mode 100644 index 000000000..545036734 --- /dev/null +++ b/src/equicordplugins/serverSearch/styles.css @@ -0,0 +1,33 @@ +#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 new file mode 100644 index 000000000..1a7d28605 --- /dev/null +++ b/src/equicordplugins/signature/index.tsx @@ -0,0 +1,155 @@ +/* + * 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 new file mode 100644 index 000000000..7f103e9f4 --- /dev/null +++ b/src/equicordplugins/snowfall/index.tsx @@ -0,0 +1,490 @@ +/* + * 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 new file mode 100644 index 000000000..a622197d9 --- /dev/null +++ b/src/equicordplugins/soggy/index.tsx @@ -0,0 +1,167 @@ +/* + * 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 new file mode 100644 index 000000000..c469c9b31 --- /dev/null +++ b/src/equicordplugins/talkInReverse/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 new file mode 100644 index 000000000..3a56c4e8f --- /dev/null +++ b/src/equicordplugins/toggleVideoBind/index.ts @@ -0,0 +1,70 @@ +/* + * 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 new file mode 100644 index 000000000..631b5736e --- /dev/null +++ b/src/equicordplugins/toneIndicators/ToneIndicator.tsx @@ -0,0 +1,36 @@ +/* + * 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 new file mode 100644 index 000000000..6dc0ba1a0 --- /dev/null +++ b/src/equicordplugins/toneIndicators/index.tsx @@ -0,0 +1,188 @@ +/* + * 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 new file mode 100644 index 000000000..e03a45015 --- /dev/null +++ b/src/equicordplugins/toneIndicators/indicators.ts @@ -0,0 +1,62 @@ +/* + * 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 new file mode 100644 index 000000000..78e48b120 --- /dev/null +++ b/src/equicordplugins/translatePlus/index.tsx @@ -0,0 +1,68 @@ +/* + * 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 new file mode 100644 index 000000000..20b0448dc --- /dev/null +++ b/src/equicordplugins/translatePlus/misc/languages.ts @@ -0,0 +1,173 @@ +/* + * 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 new file mode 100644 index 000000000..291be4576 --- /dev/null +++ b/src/equicordplugins/translatePlus/misc/types.ts @@ -0,0 +1,19 @@ +/* + * 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 new file mode 100644 index 000000000..3fd2f030b --- /dev/null +++ b/src/equicordplugins/translatePlus/settings.ts @@ -0,0 +1,35 @@ +/* + * 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 new file mode 100644 index 000000000..85bdfc2b2 --- /dev/null +++ b/src/equicordplugins/translatePlus/style.css @@ -0,0 +1,22 @@ +.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 new file mode 100644 index 000000000..7ad167d73 --- /dev/null +++ b/src/equicordplugins/translatePlus/utils/accessory.tsx @@ -0,0 +1,42 @@ +/* + * 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 new file mode 100644 index 000000000..8a08675e4 --- /dev/null +++ b/src/equicordplugins/translatePlus/utils/icon.tsx @@ -0,0 +1,27 @@ +/* + * 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 new file mode 100644 index 000000000..6b002d769 --- /dev/null +++ b/src/equicordplugins/translatePlus/utils/translator.ts @@ -0,0 +1,143 @@ +/* + * 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 new file mode 100644 index 000000000..f4c860347 --- /dev/null +++ b/src/equicordplugins/triviaAI/index.tsx @@ -0,0 +1,66 @@ +/* + * 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 new file mode 100644 index 000000000..b4a97e823 --- /dev/null +++ b/src/equicordplugins/triviaAI/settings.ts @@ -0,0 +1,53 @@ +/* + * 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 new file mode 100644 index 000000000..501a14ffb --- /dev/null +++ b/src/equicordplugins/triviaAI/utils.ts @@ -0,0 +1,188 @@ +/* + * 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 new file mode 100644 index 000000000..0f08d06cd --- /dev/null +++ b/src/equicordplugins/unreadBadgeCount/index.tsx @@ -0,0 +1,80 @@ +/* + * 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 new file mode 100644 index 000000000..878ef5162 --- /dev/null +++ b/src/equicordplugins/unreadBadgeCount/styles.css @@ -0,0 +1,39 @@ +.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 new file mode 100644 index 000000000..4a08e56f9 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.css @@ -0,0 +1,42 @@ +.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 new file mode 100644 index 000000000..0ce2611ef --- /dev/null +++ b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryComponent.tsx @@ -0,0 +1,37 @@ +/* + * 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 new file mode 100644 index 000000000..0054323f4 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogEntryIcons.tsx @@ -0,0 +1,29 @@ +/* + * 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 new file mode 100644 index 000000000..6315e8b06 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog.dev/components/VoiceChannelLogModal.tsx @@ -0,0 +1,68 @@ +/* + * 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 new file mode 100644 index 000000000..94941f6e1 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog.dev/index.tsx @@ -0,0 +1,164 @@ +/* + * 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 new file mode 100644 index 000000000..6fefe4ba7 --- /dev/null +++ b/src/equicordplugins/voiceChannelLog.dev/logs.ts @@ -0,0 +1,41 @@ +/* + * 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.dev/index.tsx b/src/equicordplugins/voiceRejoin/index.tsx similarity index 100% rename from src/equicordplugins/voiceRejoin.dev/index.tsx rename to src/equicordplugins/voiceRejoin/index.tsx diff --git a/src/equicordplugins/webpackTarball.dev/index.tsx b/src/equicordplugins/webpackTarball/index.tsx similarity index 100% rename from src/equicordplugins/webpackTarball.dev/index.tsx rename to src/equicordplugins/webpackTarball/index.tsx diff --git a/src/equicordplugins/webpackTarball.dev/tar.ts b/src/equicordplugins/webpackTarball/tar.ts similarity index 100% rename from src/equicordplugins/webpackTarball.dev/tar.ts rename to src/equicordplugins/webpackTarball/tar.ts diff --git a/src/equicordplugins/webpackTarball.dev/webpack.ts b/src/equicordplugins/webpackTarball/webpack.ts similarity index 100% rename from src/equicordplugins/webpackTarball.dev/webpack.ts rename to src/equicordplugins/webpackTarball/webpack.ts diff --git a/src/equicordplugins/wigglyText/index.tsx b/src/equicordplugins/wigglyText/index.tsx new file mode 100644 index 000000000..1f683e9e6 --- /dev/null +++ b/src/equicordplugins/wigglyText/index.tsx @@ -0,0 +1,199 @@ +/* + * 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 new file mode 100644 index 000000000..1f6e709ab --- /dev/null +++ b/src/equicordplugins/wigglyText/ui/components/ExampleWiggle.tsx @@ -0,0 +1,20 @@ +/* + * 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 new file mode 100644 index 000000000..263b1a231 --- /dev/null +++ b/src/equicordplugins/writeUpperCase/index.ts @@ -0,0 +1,49 @@ +/* + * 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 0c390eeae..a2e1d8172 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -156,8 +156,9 @@ async function generateDebugInfoMessage() { } const potentiallyProblematicPlugins = ([ - "NoRPC", "NoProfileThemes", "NoMosaic", "NoSystemBadge", - "AlwaysAnimate", "ClientTheme", "SoundTroll", "NeverPausePreviews", + "NoRPC", "NoProfileThemes", "NoMosaic", "NoRoleHeaders", "NoSystemBadge", + "AlwaysAnimate", "ClientTheme", "SoundTroll", "Ingtoninator", "NeverPausePreviews", + "IdleAutoRestart", ].filter(isPluginEnabled) ?? []).sort(); if (isPluginEnabled(customIdle.name) && customIdle.settings.store.idleTimeout === 0) { From a8ae6fd5d50075845145cf68be58c87d23ca95d5 Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 7 Mar 2026 08:32:14 -0500 Subject: [PATCH 8/8] bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83ee10bcc..6967bb960 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "equicord", "private": "true", - "version": "1.14.4.0", + "version": "1.14.4.1", "description": "The other cutest Discord client mod", "homepage": "https://github.com/Equicord/Equicord#readme", "bugs": {