More Plugins

This commit is contained in:
thororen1234 2024-07-18 17:48:50 -04:00
parent 5eefc88ec2
commit 0d9457e1bc
39 changed files with 3648 additions and 2 deletions

View file

@ -47,6 +47,7 @@
"monaco-editor": "^0.50.0",
"nanoid": "^4.0.2",
"usercss-meta": "^0.12.0",
"openai": "^4.30.0",
"virtual-merge": "^1.0.1"
},
"devDependencies": {
@ -114,4 +115,4 @@
"node": ">=18",
"pnpm": ">=9"
}
}
}

89
pnpm-lock.yaml generated
View file

@ -49,6 +49,9 @@ importers:
nanoid:
specifier: ^4.0.2
version: 4.0.2
openai:
specifier: ^4.30.0
version: 4.52.7
usercss-meta:
specifier: ^0.12.0
version: 0.12.0
@ -523,6 +526,9 @@ packages:
'@types/minimist@1.2.2':
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
'@types/node-fetch@2.6.11':
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
'@types/node@18.16.3':
resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==}
@ -623,6 +629,10 @@ packages:
'@vap/shiki@0.10.5':
resolution: {integrity: sha512-5BHVGvQT8qonbLSASon5aQFQ18OZU4FxSl9tLSj6oJ0sap3KdMbYcfGq25M9zFZR1g1dJN7fgjmZXBIS5omIdw==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -637,6 +647,10 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agentkeepalive@4.5.0:
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
engines: {node: '>= 8.0.0'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -1201,6 +1215,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
@ -1270,10 +1288,17 @@ packages:
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@ -1432,6 +1457,9 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@ -1719,6 +1747,10 @@ packages:
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
node-fetch@2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
engines: {node: 4.x || >=6.0.0}
@ -1765,6 +1797,10 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
openai@4.52.7:
resolution: {integrity: sha512-dgxA6UZHary6NXUHEDj5TWt8ogv0+ibH+b4pT5RrWMjiRZVylNwLcw/2ubDrX5n0oUmHX/ZgudMJeemxzOvz7A==}
hasBin: true
optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@ -2276,6 +2312,14 @@ packages:
vscode-textmate@5.2.0:
resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@ -2628,6 +2672,11 @@ snapshots:
'@types/minimist@1.2.2': {}
'@types/node-fetch@2.6.11':
dependencies:
'@types/node': 18.16.3
form-data: 4.0.0
'@types/node@18.16.3': {}
'@types/normalize-package-data@2.4.1': {}
@ -2762,6 +2811,10 @@ snapshots:
vscode-oniguruma: 1.7.0
vscode-textmate: 5.2.0
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
acorn-jsx@5.3.2(acorn@8.10.0):
dependencies:
acorn: 8.10.0
@ -2774,6 +2827,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
agentkeepalive@4.5.0:
dependencies:
humanize-ms: 1.2.1
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@ -3423,6 +3480,8 @@ snapshots:
esutils@2.0.3: {}
event-target-shim@5.0.1: {}
eventemitter3@4.0.7: {}
extract-zip@2.0.1:
@ -3492,12 +3551,19 @@ snapshots:
dependencies:
is-callable: 1.2.7
form-data-encoder@1.7.2: {}
form-data@4.0.0:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
formdata-node@4.4.1:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
fs-constants@1.0.0: {}
fs-extra@11.2.0:
@ -3660,6 +3726,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
humanize-ms@1.2.1:
dependencies:
ms: 2.1.2
ieee754@1.2.1: {}
ignore@5.2.4: {}
@ -3913,6 +3983,8 @@ snapshots:
lower-case: 2.0.2
tslib: 2.6.2
node-domexception@1.0.0: {}
node-fetch@2.6.7:
dependencies:
whatwg-url: 5.0.0
@ -3967,6 +4039,19 @@ snapshots:
dependencies:
wrappy: 1.0.2
openai@4.52.7:
dependencies:
'@types/node': 18.16.3
'@types/node-fetch': 2.6.11
abort-controller: 3.0.0
agentkeepalive: 4.5.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.6.7
web-streams-polyfill: 3.3.3
transitivePeerDependencies:
- encoding
optionator@0.9.3:
dependencies:
'@aashutoshrathi/word-wrap': 1.2.6
@ -4552,6 +4637,10 @@ snapshots:
vscode-textmate@5.2.0: {}
web-streams-polyfill@3.3.3: {}
web-streams-polyfill@4.0.0-beta.3: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:

View file

@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import style from "./style.css?managed";
export default definePlugin({
name: "BetterUserArea",
description: "Reworks the user area styling to fit more buttons and overall look nicer",
authors: [Devs.Samwich],
patches: [{
find: ".Messages.ACCOUNT_SPEAKING_WHILE_MUTED",
replacement:
[
// add a custom class to make things easier
{
match: /className:(\i.container),/,
replace: "className: `${$1} vc-userAreaStyles`,"
},
]
}],
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View file

@ -0,0 +1,11 @@
/* stylelint-disable rule-empty-line-before */
.vc-userAreaStyles
{
display: grid;
height: 75px !important;
max-height: 9239080123px !important;
[class*="flex_"]
{
justify-content: space-between !important;
}
}

View file

@ -0,0 +1,224 @@
/*
* 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 { ChannelStore, GuildMemberStore, GuildStore, RelationshipStore, Text, UserStore } from "@webpack/common";
import { GuildMember } from "discord-types/general";
const settings = definePluginSettings(
{
usersToBlock: {
type: OptionType.STRING,
description: "User IDs seperated by a comma and a space",
restartNeeded: true,
default: ""
},
hideBlockedUsers: {
type: OptionType.BOOLEAN,
description: "Should blocked users should also be hidden everywhere",
default: true,
restartNeeded: true
},
hideBlockedMessages: {
type: OptionType.BOOLEAN,
description: "Should messages from blocked users should be hidden fully (same as the old noblockedmessages plugin)",
default: true,
restartNeeded: true
},
hideEmptyRoles: {
type: OptionType.BOOLEAN,
description: "Should role headers be hidden if all of their members are blocked",
restartNeeded: true,
default: true
},
/*
hideNewUsers: {
type: OptionType.BOOLEAN,
description: "Should content from users with the \"I'm new here, say hi!\" badge be blocked\"",
restartNeeded: true,
default: true
},
*/
blockedReplyDisplay: {
type: OptionType.SELECT,
description: "What should display instead of the message when someone replies to someone you have hidden",
restartNeeded: true,
options: [{ value: "displayText", label: "Display text saying a hidden message was replied to", default: true }, { value: "hideReply", label: "Literally nothing" }]
},
guildBlackList: {
type: OptionType.STRING,
description: "Guild ids to disable functionality in",
restartNeeded: true,
default: ""
},
guildWhiteList: {
type: OptionType.STRING,
description: "Guild ids to enable functionality in",
restartNeeded: true,
default: ""
}
});
function isChannelBlocked(channelID) {
const guildID = ChannelStore.getChannel(channelID)?.guild_id;
if (settings.store.guildBlackList.split(", ").includes(guildID) || (!settings.store.guildWhiteList.split(", ").includes(guildID) && settings.store.guildWhiteList.length > 0)) {
return true;
}
return false;
}
function shouldHideUser(userId: string, channelId?: string) {
if (channelId) {
if (isChannelBlocked(channelId)) {
return false;
}
const guildID = ChannelStore.getChannel(channelId)?.guild_id;
// add new user hiding logic here at some point
}
// hide the user if the user is blocked and the hide blocked users setting is enabled
if (RelationshipStore.isBlocked(userId) && settings.store.hideBlockedUsers) {
return true;
}
// failsafe that is needed for some reason
if (settings.store.usersToBlock.length === 0) {
return false;
}
// hide the user if the id is in the users to block setting
return settings.store.usersToBlock.split(", ").includes(userId);
}
// This is really horror
function isRoleAllBlockedMembers(roleId, guildId) {
const role = GuildStore.getRole(guildId, roleId);
if (!role) return false;
const membersWithRole: GuildMember[] = GuildMemberStore.getMembers(guildId).filter(member => member.roles.includes(roleId));
if (membersWithRole.length === 0) return false;
if (isChannelBlocked(guildId)) {
return false;
}
// need to add an online check at some point but this sorta works for now
return membersWithRole.every(member => shouldHideUser(member.userId) && !(UserStore.getUser(member.userId).desktop || UserStore.getUser(member.userId).mobile));
}
function hiddenReplyComponent() {
switch (settings.store.blockedReplyDisplay) {
case "displayText":
return <Text tag="p" selectable={false} variant="text-sm/normal" style={{ marginTop: "0px", marginBottom: "0px" }}><i> Replying to blocked message</i></Text>;
case "hideReply":
return null;
}
}
export default definePlugin({
name: "ClientSideBlock",
description: "Allows you to locally hide almost all content from any user",
tags: ["blocked", "block", "hide", "hidden", "noblockedmessages"],
authors: [Devs.Samwich],
settings,
shouldHideUser: shouldHideUser,
hiddenReplyComponent: hiddenReplyComponent,
isRoleAllBlockedMembers: isRoleAllBlockedMembers,
patches: [
// message
{
find: ".messageListItem",
replacement: {
match: /renderContentOnly:\i}=\i;/,
replace: "$&if($self.shouldHideUser(arguments[0].message.author.id, arguments[0].message.channel_id)) return null; "
}
},
// friends list (should work with all tabs)
{
find: "peopleListItemRef.current.componentWillLeave",
replacement: {
match: /\i}=this.state;/,
replace: "$&if($self.shouldHideUser(this.props.user.id)) return null; "
}
},
// member list
{
find: "._areActivitiesExperimentallyHidden=(",
replacement: {
match: /new Date\(\i\):null;/,
replace: "$&if($self.shouldHideUser(this.props.user.id, this.props.channel.id)) return null; "
}
},
// stop the role header from displaying if all users with that role are hidden (wip sorta)
{
find: "._areActivitiesExperimentallyHidden=(",
replacement: {
match: /\i.memo\(function\(\i\){/,
replace: "$&if($self.isRoleAllBlockedMembers(arguments[0].id, arguments[0].guildId)) return null;"
},
predicate: () => settings.store.hideEmptyRoles
},
// "1 blocked message"
{
find: "Messages.BLOCKED_MESSAGES_HIDE.format",
replacement: {
match: /\i.memo\(function\(\i\){/,
replace: "$&return null;"
},
predicate: () => settings.store.hideBlockedMessages
},
// replies
{
find: ".GUILD_APPLICATION_PREMIUM_SUBSCRIPTION||",
replacement: [
{
match: /let \i;let\{repliedAuthor:/,
replace: "if(arguments[0] != null && arguments[0].referencedMessage.message != null) { if($self.shouldHideUser(arguments[0].referencedMessage.message.author.id, arguments[0].baseMessage.messageReference.channel_id)) { return $self.hiddenReplyComponent(); } }$&"
}
]
},
// dm list
{
find: "PrivateChannel.renderAvatar",
replacement: {
// horror but it works
match: /function\(\i,(\i),\i\){.*,\[\i,\i,\i\]\);/,
replace: "$&if($1.rawRecipients[0] != null){if($1.rawRecipients[0].id != null){if($self.shouldHideUser($1.rawRecipients[0].id)) return null;}}"
}
},
// thank nick (644298972420374528) for these patches :3
// filter relationships
{
find: "getFriendIDs(){",
replacement: {
match: /\i.FRIEND\)/,
replace: "$&.filter(id => !$self.shouldHideUser(id))"
}
},
// active now list
{
find: "getUserAffinitiesUserIds(){",
replacement: {
match: /return (\i.affinityUserIds)/,
replace: "return new Set(Array.from($1).filter(id => !$self.shouldHideUser(id)))"
}
},
// mutual friends list in user profile
{
find: "}getMutualFriends(",
replacement: {
match: /(getMutualFriends\(\i\){)return (\i\[\i\])/,
replace: "$1if($2 != undefined) return $2.filter(u => !$self.shouldHideUser(u.key))"
}
}
]
});

View file

@ -0,0 +1,97 @@
/*
* 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 { Text } from "@webpack/common";
// definitely not stolen from glide :P
async function injectCSS() {
var elementToRemove = document.getElementById("DemonstrationStyle");
if (elementToRemove) {
elementToRemove.remove();
}
const styleElement = document.createElement("style");
styleElement.id = "DemonstrationStyle";
const content = await fetch("https://minidiscordthemes.github.io/Demonstration/Demonstration.theme.css").then(e => e.text());
styleElement.textContent = content;
document.documentElement.appendChild(styleElement);
}
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 the theme when pressed",
type: OptionType.STRING,
default: "F6",
isValid: (value: string) => {
if (validKeycodes.includes(value)) {
return true;
}
return false;
}
},
soundVolume: {
description: "How loud the toggle sound is (0 to disable)",
type: OptionType.SLIDER,
default: 0.5,
markers: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
},
});
function handleKeydown(event) {
if (event.code !== settings.store.keyBind) { return; }
const style = document.getElementById("DemonstrationStyle");
if (style != null) {
style.remove();
playSound("https://files.catbox.moe/wp5rpz.wav");
}
else {
injectCSS();
playSound("https://files.catbox.moe/ckz46t.wav");
}
}
async function playSound(url) {
const audio = new Audio(url);
audio.volume = settings.store.soundVolume;
await audio.play().catch(error => {
console.error("Error playing sound:", error);
});
audio.remove();
}
export default definePlugin({
name: "Demonstration",
description: "Plugin for taking theme screenshots - censors identifying images and text.",
authors: [Devs.Samwich],
settingsAboutComponent: () => {
return (
<>
<Text>To change your keycode, check out <a href="https://www.toptal.com/developers/keycode" target="_blank">this tool</a>!</Text>
</>
);
},
start() {
document.addEventListener("keydown", handleKeydown);
},
stop() {
document.removeEventListener("keydown", handleKeydown);
},
settings
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener } from "@api/MessageEvents";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Alerts, ChannelStore, Forms } from "@webpack/common";
import filterList from "./constants";
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function warningEmbedNotice(trigger) {
return new Promise<boolean>(resolve => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
Your message contains a term on the automod preset list. (Term "{trigger}")
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
There is a high chance your message will be blocked and potentially moderated by a server moderator.
</Forms.FormText>
</div>,
confirmText: "Send Anyway",
cancelText: "Cancel",
onConfirm: () => resolve(true),
onCloseCallback: () => setImmediate(() => resolve(false)),
});
});
}
export default definePlugin({
name: "DontFilterMe",
description: "Warns you if your message contains a term in the automod preset list",
authors: [Devs.Samwich],
dependencies: ["MessageEventsAPI"],
start() {
this.preSend = addPreSendListener(async (channelId, messageObj, extra) => {
if (ChannelStore.getChannel(channelId.toString()).isDM()) return { cancel: false };
const escapedStrings = filterList.map(escapeRegex);
const regexString = escapedStrings.join("|");
const regex = new RegExp(`(${regexString})`, "i");
console.log(channelId);
const matches = regex.exec(messageObj.content);
if (matches) {
if (!await warningEmbedNotice(matches[0])) {
return { cancel: true };
}
}
return { cancel: false };
});
}
});

View file

@ -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: ".QUICKSWITCHER_PLACEHOLDER",
replacement: {
match: /let{selectedIndex:\i,results:\i}/,
replace: "this.props.results = $self.generateSearchResults(this.state.query);$&"
},
}
]
});

View file

@ -0,0 +1,229 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { DataStore } from "@api/index";
import { definePluginSettings } from "@api/Settings";
import { ExpandableHeader } from "@components/ExpandableHeader";
import { Devs } from "@utils/constants";
import { useForceUpdater } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Button, ChannelStore, Forms, Menu, RelationshipStore, Text, TextInput, useEffect, UserStore, useState } from "@webpack/common";
const tagStoreName = "vc-friendtags-tags";
function parseUsertags(text: string): string[] {
const regex = /&(\w+)/g;
const matches = text.match(regex);
if (matches) {
const tags = matches.map(match => match.substring(1));
return tags.filter(tag => tag !== "");
} else {
return [];
}
}
function queryFriendTags(query) {
GetData();
const tags = parseUsertags(query).map(e => e.toLowerCase());
const filteredTagObjects = SavedData.filter(data => data.tagName.length && data.userIds.length).filter(data => tags.some(tag => tag === data.tagName));
if (filteredTagObjects.length === 0) return [];
const users = Array.from(new Set([...ChannelStore.getDMUserIds(), ...RelationshipStore.getFriendIDs()])).filter(user => filteredTagObjects.every(tag => tag.userIds.includes(user)));
const response = 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
}
);
});
return response;
}
let SavedData: UserTagData[] = [];
interface UserTagData {
tagName: string;
userIds: string[];
}
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 (
<>
<Text variant={"heading-md/normal"}>Name</Text>
<TextInput value={tagName} onChange={setTagName}></TextInput>
<Text variant={"heading-md/normal"}>Users (Seperated by comma)</Text>
<TextInput value={userIds} onChange={setUserIDs}></TextInput>
<ExpandableHeader headerText="User List (Click A User To Remove)" defaultState={true}>
{
userIds.split(", ").map(user => {
const userData: any = UserStore.getUser(user);
if (!userData) return null;
return (
<div style={{ display: "flex" }}>
<img src={userData.getAvatarURL()} style={{ height: "20px", borderRadius: "50%", marginRight: "5px" }}></img>
<Text style={{ cursor: "pointer" }} variant={"text-md/normal"} onClick={() => setUserIDs(userIds.replace(`, ${user}`, "").replace(user, ""))}>{userData.globalName || userData.username}</Text>
</div>
);
})
}
</ExpandableHeader>
<Button onClick={async () => {
SavedData = SavedData.filter(data => (data.tagName !== tagName));
await SetData();
update();
}} color={Button.Colors.RED}>Remove</Button>
</>
);
}
function TagConfigurationComponent() {
const update = useForceUpdater();
return (
<>
<Forms.FormDivider />
{
SavedData?.map(e => (
<>
<TagConfigCard tag={e} />
<Forms.FormDivider />
</>
))
}
<Button onClick={() => {
SavedData.push(
{
tagName: "",
userIds: []
});
SetData();
update();
}}>Add</Button>
</>
);
}
const settings = definePluginSettings(
{
tagConfiguration: {
type: OptionType.COMPONENT,
description: "The tag configuration component",
component: () => {
return (
<TagConfigurationComponent />
);
}
}
});
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 =
<Menu.MenuItem
id="vc-tag-group"
label="Tag"
>
{SavedData.map(tag => {
const isTagged = SavedData.filter(e => e.tagName === tag.tagName)[0].userIds.includes(user.id);
return (
<Menu.MenuItem label={`${isTagged ? "Remove from" : "Add to"} ${tag.tagName}`} key={`vc-tag-${tag.tagName}`} id={`vc-tag-${tag.tagName}`} action={() => { UserToTagID(user.id, tag.tagName, isTagged); }} />
);
})}
</Menu.MenuItem>;
children.push({ ...buttonElement });
};
export default definePlugin({
name: "FriendTags",
description: "Allows you to filter by custom tags in the quick switcher",
authors: [Devs.Samwich],
settings,
queryFriendTags: queryFriendTags,
patches: [
{
find: ".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();
},
contextMenus:
{
"user-context": userPatch
}
});

View file

@ -0,0 +1,54 @@
/*
* 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],
dependencies: ["CommandsAPI"],
settings,
commands: [
{
name: "gifroulette",
description: "Time to tempt your fate",
execute: (opts, other) => ({
content: getMessage(opts, other)
}),
}
]
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

View file

@ -0,0 +1,30 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function generateRandomColorHex(): string {
const r = Math.floor(Math.random() * 90);
const g = Math.floor(Math.random() * 90);
const b = Math.floor(Math.random() * 90);
return `${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
}
export function darkenColorHex(color: string): string {
const hex = color.replace(/^#/, "");
const bigint = parseInt(hex, 16);
let r = (bigint >> 16) & 255;
let g = (bigint >> 8) & 255;
let b = bigint & 255;
r = Math.max(r - 5, 0);
g = Math.max(g - 5, 0);
b = Math.max(b - 5, 0);
return `${((r << 16) + (g << 8) + b).toString(16).padStart(6, "0")}`;
}
export function saturateColorHex(color: string): string {
// i should really do something with this at some point :P
return color;
}

View file

@ -0,0 +1,789 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
import { Button, Clipboard, Forms, TextInput, Toasts, useState } from "@webpack/common";
import { darkenColorHex, generateRandomColorHex, saturateColorHex } from "./generateTheme";
import { themes } from "./themeDefinitions";
export interface ThemePreset {
bgcol: string;
accentcol: string;
textcol: string;
brand: string;
name: string;
}
let setPreset;
function LoadPreset(preset?: ThemePreset) {
if (setPreset === settings.store.ColorPreset) { return; }
const theme: ThemePreset = preset == null ? themes[settings.store.ColorPreset] : preset;
setPreset = settings.store.ColorPreset;
settings.store.Primary = theme.bgcol;
settings.store.Accent = theme.accentcol;
settings.store.Text = theme.textcol;
settings.store.Brand = theme.brand;
injectCSS();
}
function mute(hex, amount) {
hex = hex.replace(/^#/, "");
const bigint = parseInt(hex, 16);
let r = (bigint >> 16) & 255;
let g = (bigint >> 8) & 255;
let b = bigint & 255;
r = Math.max(r - amount, 0);
g = Math.max(g - amount, 0);
b = Math.max(b - amount, 0);
return "#" + ((r << 16) + (g << 8) + b).toString(16).padStart(6, "0");
}
function copyPreset(name: string) {
const template =
`
{
bgcol: "${settings.store.Primary}",
accentcol: "${settings.store.Accent}",
textcol: "${settings.store.Text}",
brand: "${settings.store.Brand}",
name: "${name}"
}
`;
if (Clipboard.SUPPORTS_COPY) {
Clipboard.copy(template);
}
}
function CopyPresetComponent() {
const [inputtedName, setInputtedName] = useState("");
return (
<>
<Forms.FormSection>
<Forms.FormTitle>{"Preset name"}</Forms.FormTitle>
<TextInput
type="text"
value={inputtedName}
onChange={setInputtedName}
placeholder={"Enter a name"}
/>
</Forms.FormSection>
<Button onClick={() => {
copyPreset(inputtedName);
}}>Copy preset</Button>
<Button onClick={() => {
generateAndApplyProceduralTheme();
}}>Generate Random</Button>
</>
);
}
const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)");
export function generateAndApplyProceduralTheme() {
const randomBackgroundColor = generateRandomColorHex();
const accentColor = darkenColorHex(randomBackgroundColor);
const textColor = "ddd0d0";
const brandColor = saturateColorHex(randomBackgroundColor);
settings.store.Primary = randomBackgroundColor;
settings.store.Accent = accentColor;
settings.store.Text = textColor;
settings.store.Brand = brandColor;
injectCSS();
}
const settings = definePluginSettings({
serverListAnim: {
type: OptionType.BOOLEAN,
description: "Toggles if the server list hides when not hovered",
default: false,
onChange: () => injectCSS()
},
memberListAnim: {
type: OptionType.BOOLEAN,
description: "Toggles if the member list hides when not hovered",
default: true,
onChange: () => injectCSS()
},
privacyBlur: {
type: OptionType.BOOLEAN,
description: "Blurs potentially sensitive information when not tabbed in",
default: false,
onChange: () => injectCSS()
},
tooltips: {
type: OptionType.BOOLEAN,
description: "If tooltips are displayed in the client",
default: false,
onChange: () => injectCSS()
},
customFont: {
type: OptionType.STRING,
description: "The google fonts @import for a custom font (blank to disable)",
default: "@import url('https://fonts.googleapis.com/css2?family=Poppins&wght@500&display=swap');",
onChange: injectCSS
},
animationSpeed: {
type: OptionType.STRING,
description: "The speed of animations",
default: "0.2",
onChange: injectCSS
},
ColorPreset: {
type: OptionType.SELECT,
description: "Some pre-made color presets (more soon hopefully)",
options: themes.map(theme => ({ label: theme.name, value: themes.indexOf(theme), default: themes.indexOf(theme) === 0 })),
onChange: () => { LoadPreset(); }
},
Primary: {
type: OptionType.COMPONENT,
description: "",
default: "000000",
component: () => <ColorPick propertyname="Primary" />
},
Accent: {
type: OptionType.COMPONENT,
description: "",
default: "313338",
component: () => <ColorPick propertyname="Accent" />
},
Text: {
type: OptionType.COMPONENT,
description: "",
default: "ffffff",
component: () => <ColorPick propertyname="Text" />
},
Brand: {
type: OptionType.COMPONENT,
description: "",
default: "ffffff",
component: () => <ColorPick propertyname="Brand" />
},
pastelStatuses: {
type: OptionType.BOOLEAN,
description: "Changes the status colors to be more pastel (fits with the catppuccin presets)",
default: true,
onChange: () => injectCSS()
},
DevTools:
{
type: OptionType.COMPONENT,
description: "meow",
default: "",
component: () => <CopyPresetComponent />
},
ExportTheme:
{
type: OptionType.COMPONENT,
description: "",
default: "",
component: () => <Button onClick={() => {
copyCSS();
Toasts.show({
id: Toasts.genId(),
message: "Successfully copied theme!",
type: Toasts.Type.SUCCESS
});
}} >Copy The CSS for your current configuration.</Button>
}
});
export function ColorPick({ propertyname }: { propertyname: string; }) {
return (
<div className="color-options-container">
<Forms.FormTitle tag="h3">{propertyname}</Forms.FormTitle>
<ColorPicker
color={parseInt(settings.store[propertyname], 16)}
onChange={color => {
const hexColor = color.toString(16).padStart(6, "0");
settings.store[propertyname] = hexColor;
injectCSS();
}
}
showEyeDropper={false}
/>
</div>
);
}
function copyCSS() {
if (Clipboard.SUPPORTS_COPY) {
Clipboard.copy(getCSS(parseFontContent()));
}
}
function parseFontContent() {
const fontRegex = /family=([^&;,:]+)/;
const customFontString: string = Settings.plugins.Glide.customFont;
if (customFontString == null) { return; }
const fontNameMatch: RegExpExecArray | null = fontRegex.exec(customFontString);
const fontName = fontNameMatch ? fontNameMatch[1].replace(/[^a-zA-Z0-9]+/g, " ") : "";
return fontName;
}
function injectCSS() {
const fontName = parseFontContent();
const theCSS = getCSS(fontName);
var elementToRemove = document.getElementById("GlideStyleInjection");
if (elementToRemove) {
elementToRemove.remove();
}
const styleElement = document.createElement("style");
styleElement.id = "GlideStyleInjection";
styleElement.textContent = theCSS;
document.documentElement.appendChild(styleElement);
}
function getCSS(fontName) {
return `
/* IMPORTS */
/* Fonts */
@import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=swap');
${Settings.plugins.Glide.customFont}
/*Settings things*/
/*Server list animation*/
${Settings.plugins.Glide.serverListAnim ? `
.guilds_a4d4d9 {
width: 10px;
transition: width var(--animspeed) ease 0.1s, opacity var(--animspeed) ease 0.1s;
opacity: 0;
}
.guilds_a4d4d9:hover {
width: 65px;
opacity: 100;
}
` : ""}
/*Member list anim toggle*/
${Settings.plugins.Glide.memberListAnim ? `
.container_cbd271
{
width: 60px;
opacity: 0.2;
transition: width var(--animspeed) ease 0.1s, opacity var(--animspeed) ease 0.1s;
}
.container_cbd271:hover
{
width: 250px;
opacity: 1;
}
` : ""}
/*Privacy blur*/
${Settings.plugins.Glide.privacyBlur ? `
.header_ec86aa,
.container_ee69e0,
.title_a7d72e,
.layout_f9647d,
[aria-label="Members"] {
filter: blur(0);
transition: filter 0.2s ease-in-out;
}
body:not(:hover) .header_ec86aa,
body:not(:hover) .container_ee69e0,
body:not(:hover) .title_a7d72e,
body:not(:hover) [aria-label="Members"],
body:not(:hover) .layout_f9647d {
filter: blur(5px);
}
` : ""}
/*Tooltips*/
[class*="tooltip"]
{
${!Settings.plugins.Glide.tooltips ? "display: none !important;" : ""}
}
/*Root configs*/
:root
{
--animspeed: ${Settings.plugins.Glide.animationSpeed + "s"};
--font-primary: ${(fontName.length > 0 ? fontName : "Nunito")};
--accent: #${Settings.plugins.Glide.Accent};
--bgcol: #${Settings.plugins.Glide.Primary};
--text: #${Settings.plugins.Glide.Text};
--brand: #${Settings.plugins.Glide.Brand};
--mutedtext: ${mute(Settings.plugins.Glide.Text, 20)};
--mutedbrand: ${mute(Settings.plugins.Glide.Brand, 10)};
--mutedaccent: ${mute(Settings.plugins.Glide.Accent, 10)};
}
:root
{
/*VARIABLES*/
/*editable variables. Feel free to mess around with these to your hearts content, i recommend not editing the logic variables unless you have an understanding of css*/
--glowcol: rgba(0, 0, 0, 0);
--mentioncol: rgb(0, 0, 0);
--mentionhighlightcol: rgb(0, 0, 0);
--linkcol: rgb(95, 231, 255);
--highlightcol: rgb(95, 231, 255);
/*COLOR ASSIGNING (most of these probably effect more than whats commented)*/
/*accent based*/
/*buttons*/
--button-secondary-background: var(--accent);
/*also buttons*/
--brand-experiment: var(--brand);
--brand-experiment-560: var(--brand);
--brand-500: var(--brand);
/*message bar*/
--channeltextarea-background: var(--accent);
/*selected dm background*/
--background-modifier-selected: var(--accent);
/*emoji autofill*/
--primary-630: var(--accent);
/*plugin grid square and nitro shop*/
--background-secondary-alt: var(--accent);
/*modal background, self explanatory*/
--modal-background: var(--accent);
/*color of the background of mention text*/
--mention-background: var(--accent);
--input-background: var(--accent);
/*the side profile thingy*/
--profile-body-background-color: var(--accent);
/*the weird hover thing idk*/
--background-modifier-hover: var(--mutedaccent) !important;
/*background based*/
/*primary color, self explanatory*/
--background-primary: var(--bgcol);
/*dm list*/
--background-secondary: var(--bgcol);
/*outer frame and search background*/
--background-tertiary: var(--bgcol);
/*friends header bar*/
--bg-overlay-2: var(--bgcol);
/*user panel*/
--bg-overlay-1: var(--bgcol);
/*call thingy*/
--bg-overlay-app-frame: var(--bgcol);
/*shop*/
--background-mentioned-hover: var(--bgcol) !important;
--background-mentioned: var(--bgcol) !important;
/*other*/
/*mention side line color color*/
--info-warning-foreground: var(--mentionhighlightcol);
/*text color of mention text*/
--mention-foreground: white;
/*Link color*/
--text-link: var(--linkcol);
--header-primary: var(--text);
--header-secondary: var(--text);
--font-display: var(--text);
--text-normal: var(--text);
--text-muted: var(--mutedtext);
--channels-default: var(--mutedtext);
--interactive-normal: var(--text) !important;
--white-500: var(--text);
}
/*EXTRA COLORS*/
[class*="tooltipPrimary__"]
{
background-color: var(--mutedaccent) !important;
}
[class*="tooltipPointer_"]
{
border-top-color: var(--mutedaccent) !important;
}
/*sorry, forgot to document what these are when i was adding them*/
.inspector_c3120f, .scroller_dcade6, .unicodeShortcut_dfa278
{
background-color: var(--bgcol);
}
.inner_effbe2
{
background-color: var(--accent);
}
/*recolor embeds*/
[class^="embedWrap"]
{
border-color: var(--accent) !important;
background: var(--accent);
}
/*emoji menu recolor*/
.contentWrapper_af5dbb, .header_a3bc57
{
background-color: var(--bgcol);
}
/*vc background recolor*/
.root_dd069c
{
background-color: var(--bgcol);
}
/*Fix the forum page*/
/*Set the bg color*/
.container_a6d69a
{
background-color: var(--bgcol);
}
/*Recolor the posts to the accent*/
.container_d331f1
{
background-color: var(--accent);
}
/*Recolor the background of stickers in the sticker picker that dont take up the full 1:1 ratio*/
[id^="sticker-picker-grid"]
{
background-color: var(--bgcol);
}
/* profile sidebar*/
[class="none_eed6a8 scrollerBase_eed6a8"]
{
background-color: var(--bgcol) !important;
}
/*Recolor the emoji, gif, and sticker picker selected button*/
.navButtonActive_af5dbb, .stickerCategoryGenericSelected_a7a485, .categoryItemDefaultCategorySelected_dfa278
{
background-color: var(--accent) !important;
}
/*side profile bar*/
[class="none_c49869 scrollerBase_c49869"]
{
background-color: var(--bgcol) !important;
}
.userPanelOverlayBackground_a2b6ae, .badgeList_ab525a
{
background-color: var(--accent) !important;
border-radius: 15px !important;
}
/*uhhhhhhhhhhhhhhh*/
.headerText_c47fa9
{
color: var(--text) !important;
}
/*message bar placeholder*/
.placeholder_a552a6
{
color: var(--mutedtext) !important
}
${settings.store.pastelStatuses ? `
/*Pastel statuses*/
rect[fill='#23a55a'], svg[fill='#23a55a'] {
fill: #80c968 !important;
}
rect[fill='#f0b232'], svg[fill='#f0b232'] {
fill: #e7ca45 !important;
}
rect[fill='#f23f43'], svg[fill='#f23f43'] {
fill: #e0526c !important;
}
rect[fill='#80848e'], svg[fill='#80848e'] {
fill: #696e88 !important;
}
rect[fill='#593695'], svg[fill='#593695'] {
fill: #ac7de6 important;
}
` : ""}
.name_d8bfb3
{
color: var(--text) !important;
}
.unread_d8bfb3
{
background-color: var(--text) !important;
}
/*ROUNDING (rounding)*/
/*round message bar, some buttons, dm list button, new messages notif bar, channel buttons, emoji menu search bar, context menus, account connections(in that order)*/
.scrollableContainer_d0696b, .button_dd4f85, .interactive_f5eb4b, .newMessagesBar_cf58b5, .link_d8bfb3, .searchBar_c6ee36, .menu_d90b3d, .connectedAccountContainer_f3eb60
{
border-radius: 25px;
}
/*round emojis seperately (and spotify activity icons)*/
[data-type="emoji"], [class*="Spotify"]
{
border-radius: 5px;
}
/*round gifs and stickers (and maybe images idk lmao), and embeds*/
[class^="imageWr"], [data-type="sticker"], [class^="embed"]
{
border-radius: 20px;
}
.item_d90b3d
{
border-radius: 15px;
}
/*slightly move messages right when hovered*/
.cozyMessage_d5deea
{
left: 0px;
transition-duration: 0.2s;
}
.cozyMessage_d5deea:hover
{
left: 3px;
}
/*CONTENT (Typically changing values or hiding elements)*/
/*remove status text in user thing*/
.panelSubtextContainer_b2ca13
{
display: none !important;
}
/*Hide most of the ugly useless scrollbars*/
::-webkit-scrollbar
{
display:none;
}
/*Hide user profile button, the dm favourite, dm close, support, gift buttons, the now playing column, and the channel + favourite icons*/
[aria-label="Hide User Profile"], .favoriteIcon_c91bad, .closeButton_c91bad, [href="https://support.discord.com"], .nowPlayingColumn_c2739c, button[aria-label="Send a gift"], .icon_d8bfb3, .iconContainer_d8bfb3
{
display :none;
}
/*yeet the shitty nitro and family link tabs that no one likes*/
.channel_c91bad[aria-posinset="2"],
.familyCenterLinkButton_f0963d
{
display: none;
}
/*Remove the buttons at the bottom of the user pop out (seriously, who wanted this?)*/
.addFriendSection__413d3
{
display: none;
}
/*No more useless spotify activity header*/
.headerContainer_d5089b
{
display: none;
}
/*hide sidebar connections*/
.profilePanelConnections_b433b4
{
display: none;
}
/*pad the message bar right slightly. Not sure what caused the buttons to flow out of it, might be something in the theme :shrug:*/
.inner_d0696b
{
padding-right: 10px;
}
/*Yeet hypesquad badges (who cares)*/
[aria-label*="HypeSquad"]
{
display: none !important;
}
/*Hide icon on file uploading status*/
.icon_a4623d
{
display: none;
}
/*hide the play button while a soundmoji is playing*/
.playing_bf9443 [viewBox="0 0 24 24"]
{
display:none;
}
/*hide the public servers button on member list*/
[aria-label="Explore Discoverable Servers"]
{
display: none;
}
/*fix context menu being not symmetrical*/
.scroller_d90b3d
{
padding: 6px 8px !important;
}
/*Hide the icon that displays what platform the user is listening with on spotify status*/
.platformIcon_d5089b
{
display: none !important;
}
/*hide the album name on spotify statuses (who cares)*/
[class="state_d5089b ellipsis_d5089b textRow_d5089b"]
{
display: none;
}
/*space the connections a bit better*/
.userInfoSection_a24910
{
margin-bottom: 0px;
padding-bottom: 0px;
}
/*Space channels*/
.containerDefault_f6f816
{
padding-top: 5px;
}
/*round banners in profile popout*/
.banner_c3e427:not(.panelBanner_c3e427)
{
border-radius: 20px;
}
/*round the user popout*/
.userPopoutOuter_c69a7b
{
border-radius: 25px;
}
/*round the inner profile popout*/
[class="userPopoutInner_c69a7b userProfileInner_c69a7b userProfileInnerThemedWithBanner_c69a7b"]::before
{
border-radius: 20px;
}
.footer_be6801
{
display: none !important;
}
/*STYLING (Modification of content to fit the theme)*/
/*Round and scale down the users banner*/
.panelBanner_c3e427
{
border-radius: 20px;
transform: scale(0.95);
}
/*add a soft glow to message bar contents, user panel, dms, channel names (in that order)*/
.inner_d0696b .layout_f9647d, .name_d8bfb3
{
filter: drop-shadow(0px 0px 3px var(--glowcol));
}
[type="button"]
{
transition: all 0.1s ease-in-out;
}
[type="button"]:hover
{
filter: drop-shadow(0px 0px 3px var(--glowcol));
}
/*Change the font*/
:root
{
--font-code: "Fira Code";
}
/*Round all status symbols. basically does what that one plugin does but easier (disabled because of a bug)
.pointerEvents_c51b4e
{
mask: url(#svg-mask-status-online);
}
*/
/*pfp uploader crosshair*/
.overlayAvatar_ba5b9e
{
background-image: url(https://raw.githubusercontent.com/Equicord/Equicord/main/src/equicordplugins/glide/crosshair.png);
background-repeat: no-repeat;
background-position-x: 50%;
background-position-y: 50%;
border-width: 2px;
}
/*change highlighted text color*/
::selection
{
color: inherit;
background-color: transparent;
text-shadow: 0px 0px 2px var(--highlightcol);
}
/*hide the line between connections and note*/
[class="connectedAccounts_f3eb60 userInfoSection_a24910"]
{
border-top: transparent !important;
}
.container_cebd1c:not(.checked_cebd1c)
{
background-color: var(--mutedbrand) !important;
}
.checked_cebd1c
{
background-color: var(--brand) !important;
}
`;
}
export default definePlugin({
name: "Glide",
description: "A sleek, rounded theme for discord.",
authors:
[
Devs.Samwich
],
settings,
start() {
injectCSS();
},
stop() {
const injectedStyle = document.getElementById("GlideStyleInjection");
if (injectedStyle) {
injectedStyle.remove();
}
},
startAt: StartAt.DOMContentLoaded,
// preview thing, kinda low effort but eh
settingsAboutComponent: () => <img src="https://files.catbox.moe/j8y2gt.webp" width="568px" border-radius="30px" ></img>
});

View file

@ -0,0 +1,164 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ThemePreset } from ".";
export const themes: ThemePreset[] = [
{
bgcol: "000000",
accentcol: "020202",
textcol: "c0d5e4",
brand: "070707",
name: "Amoled"
},
{
bgcol: "0e2936",
accentcol: "0c2430",
textcol: "99b0bd",
brand: "124057",
name: "Solar"
},
{
bgcol: "0e0e36",
accentcol: "0e0c30",
textcol: "bdbfd8",
brand: "171750",
name: "Indigo"
},
{
bgcol: "8a2b5f",
accentcol: "812658",
textcol: "ffedfb",
brand: "b23982",
name: "Grapefruit"
},
{
bgcol: "410b05",
accentcol: "360803",
textcol: "f8e6e6",
brand: "681109",
name: "Crimson"
},
{
bgcol: "184e66",
accentcol: "215a72",
textcol: "d0efff",
brand: "2d718f",
name: "Azure"
},
{
bgcol: "1d091a",
accentcol: "240d21",
textcol: "f3e1f0",
brand: "411837",
name: "Blackberry"
},
{
bgcol: "1f073b",
accentcol: "250b44",
textcol: "dfd7e9",
brand: "340d63",
name: "Porple"
},
{
bgcol: "0a0a0a",
accentcol: "0f0f0f",
textcol: "c9c9c9",
brand: "0a0a0a",
name: "Charcoal"
},
{
bgcol: "00345b",
accentcol: "002f53",
textcol: "e7d8df",
brand: "944068",
name: "Lofi Pop"
},
{
bgcol: "471b05",
accentcol: "4e2009",
textcol: "ffffff",
brand: "903e14",
name: "Oaken"
},
{
bgcol: "040b2b",
accentcol: "000626",
textcol: "ddd0d0",
brand: "040b2b",
name: "Deep Blue"
},
{
bgcol: "32464a",
accentcol: "2d4145",
textcol: "ddd0d0",
brand: "32464a",
name: "Steel Blue"
},
{
bgcol: "31031f",
accentcol: "2c001a",
textcol: "ddd0d0",
brand: "31031f",
name: "Velvet"
},
{
bgcol: "22111f",
accentcol: "1d0c1a",
textcol: "ddd0d0",
brand: "22111f",
name: "Really Oddly Depressed Purple"
},
{
bgcol: "2b3959",
accentcol: "263454",
textcol: "ddd0d0",
brand: "2b3959",
name: "Light Sky"
},
{
bgcol: "06403d",
accentcol: "013b38",
textcol: "ddd0d0",
brand: "06403d",
name: "Tealish"
},
{
bgcol: "273b0b",
accentcol: "223606",
textcol: "ddd0d0",
brand: "273b0b",
name: "Leaf (or a tree perhaps)"
},
{
bgcol: "1a2022",
accentcol: "151b1d",
textcol: "ddd0d0",
brand: "1a2022",
name: "Steel"
},
{
bgcol: "1e1e2e",
accentcol: "181825",
textcol: "cdd6f4",
brand: "45475a",
name: "Catppuccin Mocha"
},
{
bgcol: "303446",
accentcol: "292c3c",
textcol: "c6d0f5",
brand: "414559",
name: "Catppuccin Frappé"
},
{
bgcol: "6b422e",
accentcol: "754b36",
textcol: "ead9c9",
brand: "8b5032",
name: "Relax"
}
];

View file

@ -0,0 +1,78 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ApplicationCommandOptionType, findOption } from "@api/Commands";
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
function getMessage(opts) {
const inputOption = findOption(opts, "input", "");
const queryURL = "" + searchEngines[settings.store.defaultEngine] + encodeURIComponent(inputOption);
if (settings.store.hyperlink) {
return `[${inputOption}](${queryURL})`;
}
else {
return queryURL;
}
}
const searchEngines = {
"Google": "https://www.google.com/search?q=",
"Bing": "https://www.bing.com/search?q=",
"Yahoo": "https://search.yahoo.com/search?p=",
"DuckDuckGo": "https://duckduckgo.com/?q=",
"Baidu": "https://www.baidu.com/s?wd=",
"Yandex": "https://yandex.com/search/?text=",
"Ecosia": "https://www.ecosia.org/search?q=",
"Ask": "https://www.ask.com/web?q=",
"LetMeGoogleThatForYou": "https://letmegooglethat.com/?q="
};
const settings = definePluginSettings({
hyperlink: {
type: OptionType.BOOLEAN,
description: "If the sent link should hyperlink with the query as the label",
default: true
},
defaultEngine:
{
type: OptionType.SELECT,
description: "The search engine to use",
options: Object.keys(searchEngines).map((key, index) => ({
label: key,
value: key,
default: index === 0
}))
}
});
export default definePlugin({
name: "GoogleThat",
description: "Adds a command to send a google search link to a query",
authors: [Devs.Samwich],
tags: ["search", "google", "query", "duckduckgo", "command"],
dependencies: ["CommandsAPI"],
settings,
commands: [
{
name: "googlethat",
description: "send a search engine link to a query",
options: [
{
name: "input",
description: "The search query",
type: ApplicationCommandOptionType.STRING,
required: true,
}
],
execute: opts => ({
content: getMessage(opts)
}),
}
]
});

View file

@ -0,0 +1,109 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener, SendListener, } from "@api/MessageEvents";
import { definePluginSettings, Settings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const presendObject: SendListener = (channelId, msg) => {
msg.content = textProcessing(msg.content);
};
const settings = definePluginSettings(
{
blockedWords: {
type: OptionType.STRING,
description: "Words that will not be capitalised",
default: ""
}
});
export default definePlugin({
name: "Grammar",
description: "Tweaks your messages to make them look nicer and have better grammar",
authors: [Devs.Samwich],
dependencies: ["MessageEventsAPI"],
start() {
addPreSendListener(presendObject);
},
stop() {
removePreSendListener(presendObject);
},
settings
});
function textProcessing(input: string) {
let text = input;
text = cap(text);
text = apostrophe(text);
return text;
}
function apostrophe(textInput: string): string {
const corrected = "wasn't, can't, don't, won't, isn't, aren't, haven't, hasn't, hadn't, doesn't, didn't, shouldn't, wouldn't, couldn't, i'm, you're, he's, she's, it's, they're, that's, who's, what's, there's, here's, how's, where's, when's, why's, let's, you'll, I'll, they'll, it'll, I've, you've, we've, they've, you'd, he'd, she'd, it'd, we'd, they'd, y'all".toLowerCase();
const words: string[] = corrected.split(", ");
const wordsInputted = textInput.split(" ");
wordsInputted.forEach(element => {
words.forEach(wordelement => {
if (removeApostrophes(wordelement) === element.toLowerCase()) {
wordsInputted[wordsInputted.indexOf(element)] = restoreCap(wordelement, getCapData(element));
}
});
});
return wordsInputted.join(" ");
}
function getCapData(str: string) {
const booleanArray: boolean[] = [];
for (const char of str) {
booleanArray.push(char === char.toUpperCase());
}
return booleanArray;
}
function removeApostrophes(str: string): string {
return str.replace(/'/g, "");
}
function restoreCap(str: string, data: boolean[]): string {
let resultString = "";
let dataIndex = 0;
for (let i = 0; i < str.length; i++) {
const char = str[i];
if (!char.match(/[a-zA-Z]/)) {
resultString += char;
continue;
}
const isUppercase = data[dataIndex++];
resultString += isUppercase ? char.toUpperCase() : char.toLowerCase();
}
return resultString;
}
function cap(textInput: string): string {
const sentences = textInput.split(/(?<=\w\.)\s/);
const blockedWordsArray: string[] = Settings.plugins.Grammar.blockedWords.split(", ");
return sentences.map(element => {
if (!blockedWordsArray.some(word => element.toLowerCase().startsWith(word.toLowerCase()))) {
return element.charAt(0).toUpperCase() + element.slice(1);
}
else {
return element;
}
}).join(" ");
}

View file

@ -0,0 +1,45 @@
/*
* 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 { findExportedComponentLazy, findStoreLazy } from "@webpack";
import { useStateFromStores } from "@webpack/common";
const ThreeDots = findExportedComponentLazy("Dots", "AnimatedDots");
const TypingStore = findStoreLazy("TypingStore");
const PrivateChannelSortStore = findStoreLazy("PrivateChannelSortStore") as { getPrivateChannelIds: () => string[]; };
export default definePlugin({
name: "HomeTyping",
description: "Changes the home button to a typing indicator if someone in your dms is typing",
authors: [Devs.Samwich],
TypingIcon() {
return <ThreeDots dotRadius={3} themed={true} />;
},
isTyping() {
return useStateFromStores([TypingStore], () => PrivateChannelSortStore.getPrivateChannelIds().some(id => Object.keys(TypingStore.getTypingUsers(id)).length > 0));
},
patches: [
{
find: ".Messages.DISCODO_DISABLED",
replacement:
[
{
match: /(\(0,\i.jsx\)\(\i.\i,{}\))/,
replace: "arguments[0].user == null ? null : (vcIsTyping ? $self.TypingIcon() : $1)"
},
// define isTyping earlier in the function so i dont bReAk ThE rUlEs Of HoOkS
{
match: /(clearTimeout\(\i\)};)if\(null==\i\)return null;/,
replace: "$1 let vcIsTyping = $self.isTyping();"
}
],
group: true
}
]
});

View file

@ -0,0 +1,148 @@
/*
* 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 { Flex } from "@components/Flex";
import { Devs } from "@utils/constants";
import definePlugin, { PluginNative } from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { Alerts, Button, FluxDispatcher, Forms, Toasts, UserProfileStore, UserStore } from "@webpack/common";
const native = VencordNative.pluginHelpers.Identity as PluginNative<typeof import("./native")>;
const CustomizationSection = findByCodeLazy(".customizationSectionBackground");
async function SetNewData() {
const PersonData = JSON.parse(await native.RequestRandomUser());
console.log(PersonData);
const pfpBase64 = JSON.parse(await native.ToBase64ImageUrl({ imgUrl: PersonData.picture.large })).data;
// holy moly
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR", avatar: pfpBase64 });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", globalName: `${PersonData.name.first} ${PersonData.name.last}` });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS", pronouns: "" });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER", banner: null });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_ACCENT_COLOR", color: null });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS", themeColors: [null, null] });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BIO", bio: `Hello! I am ${PersonData.name.first} ${PersonData.name.last}` });
}
async function SaveData() {
const userData = UserProfileStore.getUserProfile(UserStore.getCurrentUser().id);
// the getUserProfile function doesn't return all the information we need, so we append the standard user object data to the end
const extraUserObject: any = { extraUserObject: UserStore.getCurrentUser() };
const pfp = JSON.parse(await native.ToBase64ImageUrl({ imgUrl: `https://cdn.discordapp.com/avatars/${userData.userId}/${extraUserObject.extraUserObject.avatar}.webp?size=4096` })).data;
const banner = JSON.parse(await native.ToBase64ImageUrl({ imgUrl: `https://cdn.discordapp.com/banners/${userData.userId}/${userData.banner}.webp?size=4096` })).data;
const fetchedBase64Data =
{
pfpBase64: pfp,
bannerBase64: banner
};
DataStore.set("identity-saved-base", JSON.stringify({ ...userData, ...extraUserObject, ...{ fetchedBase64Data: fetchedBase64Data } }));
}
async function LoadData() {
const userDataMaybeNull = await DataStore.get("identity-saved-base");
if (!userDataMaybeNull) {
Toasts.show({ message: "No saved base! Save one first.", id: Toasts.genId(), type: Toasts.Type.FAILURE });
return;
}
const userData = JSON.parse(userDataMaybeNull);
console.log(userData);
const { pfpBase64, bannerBase64 } = userData.fetchedBase64Data;
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_AVATAR", avatar: pfpBase64 });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_GLOBAL_NAME", globalName: userData.extraUserObject.globalName });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_PRONOUNS", pronouns: userData.pronouns });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BANNER", banner: bannerBase64 });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_ACCENT_COLOR", color: userData.accentColor });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_THEME_COLORS", themeColors: userData.themeColors });
FluxDispatcher.dispatch({ type: "USER_SETTINGS_ACCOUNT_SET_PENDING_BIO", bio: userData.bio });
}
function ResetCard() {
return (
<CustomizationSection
title={"Identity"}
hasBackground={true}
hideDivider={false}
>
<Flex>
<Button
onClick={() => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
Saving your base profile will allow you to have a backup of your actual profile
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
If you save, it will overwrite your previous data.
</Forms.FormText>
</div>,
confirmText: "Save Anyway",
cancelText: "Cancel",
onConfirm: SaveData
});
}}
size={Button.Sizes.MEDIUM}
>
Save Base
</Button>
<Button
onClick={() => {
Alerts.show({
title: "Hold on!",
body: <div>
<Forms.FormText>
Loading your base profile will restore your actual profile settings
</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>
If you load, it will overwrite your current profile configuration.
</Forms.FormText>
</div>,
confirmText: "Load Anyway",
cancelText: "Cancel",
onConfirm: LoadData
});
}}
size={Button.Sizes.MEDIUM}
>
Load Base
</Button>
<Button
onClick={SetNewData}
size={Button.Sizes.MEDIUM}
>
Randomise
</Button>
</Flex>
</CustomizationSection>
);
}
export default definePlugin({
name: "Identity",
description: "Allows you to edit your profile to a random fake person with the click of a button",
authors: [Devs.Samwich],
ResetCard: ResetCard,
patches: [
{
find: "DefaultCustomizationSections",
replacement: {
match: /(?<=USER_SETTINGS_AVATAR_DECORATION},"decoration"\),)/,
replace: "$self.ResetCard(),"
}
},
]
});

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export async function RequestRandomUser() {
const data = await fetch("https://randomuser.me/api").then(e => e.json());
return JSON.stringify(data.results[0]);
}
export async function ToBase64ImageUrl(_, data) {
const { imgUrl } = data;
try {
const fetchImageUrl = await fetch(imgUrl);
const responseArrBuffer = await fetchImageUrl.arrayBuffer();
const toBase64 =
`data:${fetchImageUrl.headers.get("Content-Type") || "image/png"};base64,${Buffer.from(responseArrBuffer).toString("base64")}`;
return JSON.stringify({ data: toBase64 });
} catch (error) {
console.error("Error converting image to Base64:", error);
return JSON.stringify({ error: "Failed to convert image to Base64" });
}
}

View file

@ -0,0 +1,41 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu, NavigationRouter } from "@webpack/common";
function jumpToFirstMessage(props) {
const guildid = props.guild_id !== null ? props.guild_id : "@me";
const channelid = props.id;
const url = `/channels/${guildid}/${channelid}/0`;
NavigationRouter.transitionTo(url);
}
const MenuPatch: NavContextMenuPatchCallback = (children, { channel }) => {
children.push(
<Menu.MenuItem
id="vc-jump-to-first"
label="Jump To First Message"
action={() => {
jumpToFirstMessage(channel);
}}
/>
);
};
export default definePlugin({
name: "JumpToStart",
description: "Adds a context menu option to jump to the start of a channel/DM",
authors: [Devs.Samwich],
contextMenus:
{
"channel-context": MenuPatch,
"user-context": MenuPatch,
"thread-context": MenuPatch
}
});

View file

@ -0,0 +1,67 @@
/*
* 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";
const settings = definePluginSettings(
{
targetLanguage: {
type: OptionType.STRING,
description: "The language messages should be translated to",
default: "en",
restartNeeded: true
},
confidenceRequirement: {
type: OptionType.STRING,
description: "The confidence required to translated the message. Best not to edit unless you know what you're doing",
default: "0.8",
restartNeeded: true
},
});
async function translateAPI(sourceLang: string, targetLang: string, text: string): Promise<any> {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dj=1&q=${encodeURIComponent(text)}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to translate "${text}" from ${sourceLang} to ${targetLang}: ${response.status} ${response.statusText}`);
}
return await response.json();
}
async function TranslateMessage(string) {
// there may be a better way to do this lmao
if (string.includes("(Translated)")) return string;
const response = await translateAPI("auto", settings.store.targetLanguage, string);
if (response.src === settings.store.targetLanguage || response.confidence < settings.store.confidenceRequirement) return string;
const { sentences }: { sentences: { trans?: string; }[]; } = await response;
const translatedText = sentences.map(s => s?.trans).filter(Boolean).join("");
return `${translatedText} *(Translated)*`;
}
export default definePlugin({
name: "MessageTranslate",
description: "Auto translate messages to your language",
authors: [Devs.Samwich],
settings,
TranslateMessage: TranslateMessage,
patches: [
{
find: ".messageListItem",
replacement: {
match: /renderContentOnly:\i}=\i;/,
replace: "$&$self.TranslateMessage(arguments[0].message.content).then(response => arguments[0].message.content = response);"
}
},
]
});

View file

@ -0,0 +1,44 @@
/*
* 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: "ModViewBypass",
description: "Open the mod view sidebar in guilds you don't have moderator permissions in, or where the experiment is disabled.",
authors: [Devs.Sqaaakoi],
patches: [
{
find: "canAccessGuildMemberModViewWithExperiment:",
replacement: {
match: /canAccessGuildMemberModViewWithExperiment:function\(\){return\s\i/,
replace: "canAccessGuildMemberModViewWithExperiment:function(){return ()=>true;",
},
},
{
find: "useCanAccessGuildMemberModView:",
replacement: {
match: /\i.default.hasAny\(/,
replace: "true; (",
},
},
{
find: "isInGuildMemberModViewExperiment:",
replacement: {
match: /isInGuildMemberModViewExperiment:function\(\){return\s\i/,
replace: "isInGuildMemberModViewExperiment:function(){return ()=>true;",
},
},
{
find: "useGuildMemberModViewExperiment:",
replacement: {
match: /useGuildMemberModViewExperiment:function\(\){return\s\i/,
replace: "useGuildMemberModViewExperiment:function(){return ()=>true;",
},
},
],
});

View file

@ -16,7 +16,7 @@ async function getcuteneko(): Promise<string> {
export default definePlugin({
name: "Cute nekos",
name: "CuteNekos",
authors: [Devs.echo],
description: "Neko Command",
dependencies: ["CommandsAPI"],

View file

@ -0,0 +1,29 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
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],
dependencies: ["MessageEventsAPI"],
start() {
this.preSend = addPreSendListener((channelId, msg) => {
msg.content = textProcessing(msg.content);
});
},
stop() {
this.preSend = removePreSendListener((channelId, msg) => {
msg.content = textProcessing(msg.content);
});
},
});
function textProcessing(text: string): string {
return text.replace(/- /g, "\\- ");
}

View file

@ -0,0 +1,109 @@
/*
* 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 { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { Menu } from "@webpack/common";
import style from "./style.css?managed";
const settings = definePluginSettings({
except: {
type: OptionType.STRING,
description: "",
default: ""
}
});
const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { target: HTMLElement; }) => () => {
// eslint-disable-next-line no-unsafe-optional-chaining
const { id, type, name } = props?.target?.dataset;
if (id) return;
if (type === "emoji") {
children.push(buttonThingy(name));
}
};
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, props) => () => {
const { favoriteableName } = props ?? {};
if (!favoriteableName) { return; }
// WHY DID I DO IT THIS WAY
const name = favoriteableName.split(":").join("");
if (name == null) { return; }
const group = findGroupChildrenByChildId("favorite", children) || findGroupChildrenByChildId("unfavorite", children);
if (!group) return;
group.splice(group.findIndex(c => c?.props?.id === "favorite" || c?.props?.id === "unfavorite") + 1, 0, buttonThingy(name));
};
function buttonThingy(name) {
return (
<Menu.MenuItem
id="add-emoji-autofill"
key="add-emoji-autofill"
label={`${(isEmojiExcepted(name)) ? "Remove From " : "Add To "} Autofill`}
action={() => addEmojiToAutofill(name)}
/>
);
}
function addEmojiToAutofill(name) {
const excepted = isEmojiExcepted(name);
if (excepted) {
// remove the emoji if its already in there
// split up the exceptions by the seperator, filter out the emoji, then re join it.
settings.store.except = settings.store.except.split(", ").filter(item => item !== name).join(", ");
}
else {
// add the emoji to the exceptions
settings.store.except = settings.store.except += (", " + name);
}
}
function isEmojiExcepted(name) {
return settings.store.except.split(", ").includes(name);
}
export default definePlugin({
name: "NoDefaultEmojis",
description: "Stops default emojis showing in the autocomplete. (You can add exceptions)",
authors: [Devs.Samwich],
settings,
patches: [
{
find: ".Messages.EMOJI_MATCHING",
replacement: {
match: /renderResults\(e\){/,
replace: "renderResults(e){ e.results.emojis = e.results.emojis.filter(emoji => !emoji.uniqueName || Vencord.Settings.plugins.NoDefaultEmojis.except.split(',\\ ').includes(emoji.uniqueName));"
}
}
],
contextMenus:
{
"expression-picker": expressionPickerPatch,
"message": messageContextMenuPatch
},
start() {
enableStyle(style);
},
stop() {
disableStyle(style);
}
});

View file

@ -0,0 +1,4 @@
/* stylelint-disable selector-class-pattern */
.scroller_cc9b9a.thin_b1c063.scrollerBase_dc3aa9:not(:has(.base__76a71)) {
display: none;
}

View file

@ -0,0 +1,52 @@
/*
* 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 { findByPropsLazy } from "@webpack";
import { Alerts, Button, GuildStore } from "@webpack/common";
const DeleteGuild = findByPropsLazy("deleteGuild", "sendTransferOwnershipPincode").deleteGuild;
function GetPropsAndDeleteGuild(id) {
const GotGuild = GuildStore.getGuild(id);
if (!GotGuild) return;
DeleteGuild(id, GotGuild.name);
}
const settings = definePluginSettings(
{
confirmModal: {
type: OptionType.BOOLEAN,
description: "Should a \"are you sure you want to delete\" modal be shown?",
default: true
},
});
export default definePlugin({
name: "NoDeleteSafety",
description: "Removes the \"enter server name\" requirement when deleting a server",
authors: [Devs.Samwich],
settings,
async HandleGuildDeleteModal(server) {
if (settings.store.confirmModal) {
Alerts.show({ title: "Delete server?", body: <p>It's permanent, if that wasn't obvious.</p>, confirmColor: Button.Colors.RED, confirmText: "Delete", onConfirm: () => GetPropsAndDeleteGuild(server.id), cancelText: "Cancel" });
}
else {
GetPropsAndDeleteGuild(server.id);
}
},
patches: [
{
find: ".DELETE,onClick(){let",
replacement: {
match: /let \i=(\i).toString\(\)/,
replace: "$self.HandleGuildDeleteModal($1);return;$&"
}
}
]
});

View file

@ -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!!",
authors: [Devs.Samwich],
patches: [
{
find: "._areActivitiesExperimentallyHidden=(",
replacement: {
match: /\i.memo\(function\(\i\){/,
replace: "$&return null;"
}
}
]
});

View file

@ -0,0 +1,15 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export function QuoteIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M21 3C21.5523 3 22 3.44772 22 4V18C22 18.5523 21.5523 19 21 19H6.455L2 22.5V4C2 3.44772 2.44772 3 3 3H21ZM20 5H4V18.385L5.76333 17H20V5ZM10.5153 7.4116L10.9616 8.1004C9.29402 9.0027 9.32317 10.4519 9.32317 10.7645C9.47827 10.7431 9.64107 10.7403 9.80236 10.7553C10.7045 10.8389 11.4156 11.5795 11.4156 12.5C11.4156 13.4665 10.6321 14.25 9.66558 14.25C9.12905 14.25 8.61598 14.0048 8.29171 13.6605C7.77658 13.1137 7.5 12.5 7.5 11.5052C7.5 9.75543 8.72825 8.18684 10.5153 7.4116ZM15.5153 7.4116L15.9616 8.1004C14.294 9.0027 14.3232 10.4519 14.3232 10.7645C14.4783 10.7431 14.6411 10.7403 14.8024 10.7553C15.7045 10.8389 16.4156 11.5795 16.4156 12.5C16.4156 13.4665 15.6321 14.25 14.6656 14.25C14.1291 14.25 13.616 14.0048 13.2917 13.6605C12.7766 13.1137 12.5 12.5 12.5 11.5052C12.5 9.75543 13.7283 8.18684 15.5153 7.4116Z"
></path>
</svg>
);
}

View file

@ -0,0 +1,296 @@
/*
* 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 } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import definePlugin, { OptionType } from "@utils/types";
import { Button, Menu, Select, Switch, Text, TextInput, UploadHandler, useEffect, UserStore, useState } from "@webpack/common";
import { Message } from "discord-types/general";
import { QuoteIcon } from "./components";
import { canvasToBlob, fetchImageAsBlob, FixUpQuote, wrapText } from "./utils";
enum ImageStyle {
inspirational
}
const messagePatch: NavContextMenuPatchCallback = (children, { message }) => {
recentmessage = message;
if (!message.content) return;
const buttonElement =
<Menu.MenuItem
id="vc-quote"
label="Quote"
icon={QuoteIcon}
action={async () => {
openModal(props => <QuoteModal {...props} />);
}}
/>;
const group = findGroupChildrenByChildId("copy-text", children);
if (!group) {
children.push(buttonElement);
return;
}
group.splice(
group.findIndex(c => c?.props?.id === "copy-text") + 1, 0, buttonElement
);
};
let recentmessage: Message;
let grayscale;
let setStyle: ImageStyle = ImageStyle.inspirational;
let customMessage: string = "";
let isUserCustomCapable = false;
enum userIDOptions {
displayName,
userName,
userId
}
const settings = definePluginSettings({
userIdentifier:
{
type: OptionType.SELECT,
description: "What the author's name should be displayed as",
options: [
{ label: "Display Name", value: userIDOptions.displayName, default: true },
{ label: "Username", value: userIDOptions.userName },
{ label: "User ID", value: userIDOptions.userId }
]
}
});
export default definePlugin({
name: "Quoter",
description: "Adds the ability to create an inspirational quote image from a message",
authors: [Devs.Samwich],
contextMenus: {
"message": messagePatch
},
settings
});
function sizeUpgrade(url) {
const u = new URL(url);
u.searchParams.set("size", "512");
return u.toString();
}
const preparingSentence: string[] = [];
const lines: string[] = [];
async function createQuoteImage(avatarUrl: string, quoteOld: string, grayScale: boolean): Promise<Blob> {
let quote;
if (isUserCustomCapable && customMessage.length > 0) {
quote = FixUpQuote(customMessage);
}
else {
quote = FixUpQuote(quoteOld);
}
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Cant get 2d rendering context :(");
}
let name: string = "";
switch (settings.store.userIdentifier) {
case userIDOptions.displayName:
// @ts-ignore
const meow = recentmessage.author.globalName;
if (meow) {
name = meow;
}
else {
name = recentmessage.author.username;
}
break;
case userIDOptions.userName:
name = recentmessage.author.username;
break;
case userIDOptions.userId:
name = recentmessage.author.id;
break;
default:
name = "MAN WTF HAPPENED";
break;
}
switch (setStyle) {
case ImageStyle.inspirational:
const cardWidth = 1200;
const cardHeight = 600;
canvas.width = cardWidth;
canvas.height = cardHeight;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const avatarBlob = await fetchImageAsBlob(avatarUrl);
const fadeBlob = await fetchImageAsBlob("https://files.catbox.moe/54e96l.png");
const avatar = new Image();
const fade = new Image();
const avatarPromise = new Promise<void>(resolve => {
avatar.onload = () => resolve();
avatar.src = URL.createObjectURL(avatarBlob);
});
const fadePromise = new Promise<void>(resolve => {
fade.onload = () => resolve();
fade.src = URL.createObjectURL(fadeBlob);
});
await Promise.all([avatarPromise, fadePromise]);
ctx.drawImage(avatar, 0, 0, cardHeight, cardHeight);
if (grayScale) {
ctx.globalCompositeOperation = "saturation";
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, cardWidth, cardHeight);
ctx.globalCompositeOperation = "source-over";
}
ctx.drawImage(fade, cardHeight - 400, 0, 400, cardHeight);
ctx.fillStyle = "#fff";
ctx.font = "italic 20px Georgia";
const quoteWidth = cardWidth / 2 - 50;
const quoteX = ((cardWidth - cardHeight));
const quoteY = cardHeight / 2 - 10;
wrapText(ctx, `"${quote}"`, quoteX, quoteY, quoteWidth, 20, preparingSentence, lines);
const wrappedTextHeight = lines.length * 25;
ctx.font = "bold 16px Georgia";
const authorNameX = (cardHeight * 1.5) - (ctx.measureText(`- ${name}`).width / 2) - 30;
const authorNameY = quoteY + wrappedTextHeight + 30;
ctx.fillText(`- ${name}`, authorNameX, authorNameY);
preparingSentence.length = 0;
lines.length = 0;
return await canvasToBlob(canvas);
}
}
function registerStyleChange(style) {
setStyle = style;
GeneratePreview();
}
async function setIsUserCustomCapable() {
const allowList: string[] = await fetch("https://raw.githubusercontent.com/Equicord/Ignore/main/quoterusers.json").then(e => e.json());
isUserCustomCapable = allowList.includes(UserStore.getCurrentUser().id);
}
function QuoteModal(props: ModalProps) {
setIsUserCustomCapable();
const [gray, setGray] = useState(true);
useEffect(() => {
grayscale = gray;
GeneratePreview();
}, [gray]);
const safeContent = recentmessage && recentmessage.content ? recentmessage.content : "";
const [custom, setCustom] = useState(safeContent);
useEffect(() => {
customMessage = custom;
GeneratePreview();
}, [custom]);
return (
<ModalRoot {...props} size={ModalSize.MEDIUM}>
<ModalHeader separator={false}>
<Text color="header-primary" variant="heading-lg/semibold" tag="h1" style={{ flexGrow: 1 }}>
Catch Them In 4K.
</Text>
<ModalCloseButton onClick={props.onClose} />
</ModalHeader>
<ModalContent scrollbarType="none">
<img src={""} id={"quoterPreview"} style={{ borderRadius: "20px", width: "100%" }}></img>
<br></br><br></br>
{isUserCustomCapable &&
(
<>
<TextInput onChange={setCustom} value={custom} placeholder="Custom Message"></TextInput>
<br />
</>
)}
<Switch value={gray} onChange={setGray}>Grayscale</Switch>
<Select look={1}
options={Object.keys(ImageStyle).filter(key => isNaN(parseInt(key, 10))).map(key => ({
label: key.charAt(0).toUpperCase() + key.slice(1),
value: ImageStyle[key as keyof typeof ImageStyle]
}))}
select={v => registerStyleChange(v)} isSelected={v => v === setStyle}
serialize={v => v}></Select>
<br />
<Button color={Button.Colors.BRAND_NEW} size={Button.Sizes.SMALL} onClick={() => Export()} style={{ display: "inline-block", marginRight: "5px" }}>Export</Button>
<Button color={Button.Colors.BRAND_NEW} size={Button.Sizes.SMALL} onClick={() => SendInChat(props.onClose)} style={{ display: "inline-block" }}>Send</Button>
</ModalContent>
<br></br>
</ModalRoot>
);
}
async function SendInChat(onClose) {
const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.content, grayscale);
const preview = generateFileNamePreview(recentmessage.content);
const imageName = `${preview} - ${recentmessage.author.username}`;
const file = new File([image], `${imageName}.png`, { type: "image/png" });
UploadHandler.promptToUpload([file], getCurrentChannel(), 0);
onClose();
}
async function Export() {
const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.content, grayscale);
const link = document.createElement("a");
link.href = URL.createObjectURL(image);
const preview = generateFileNamePreview(recentmessage.content);
const imageName = `${preview} - ${recentmessage.author.username}`;
link.download = `${imageName}.png`;
link.click();
link.remove();
}
async function GeneratePreview() {
const image = await createQuoteImage(sizeUpgrade(recentmessage.author.getAvatarURL()), recentmessage.content, grayscale);
document.getElementById("quoterPreview")?.setAttribute("src", URL.createObjectURL(image));
}
function generateFileNamePreview(message) {
let words;
if (isUserCustomCapable && customMessage.length) {
words = customMessage.split(" ");
}
else {
words = message.split(" ");
}
let preview;
if (words.length >= 6) {
preview = words.slice(0, 6).join(" ");
} else {
preview = words.join(" ");
}
return preview;
}

View file

@ -0,0 +1,65 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { UserStore } from "@webpack/common";
export function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
return new Promise<Blob>(resolve => {
canvas.toBlob(blob => {
if (blob) {
resolve(blob);
} else {
throw new Error("Failed to create Blob");
}
}, "image/png");
});
}
export function wrapText(context: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number, preparingSentence: string[], lines: string[]) {
const words = text.split(" ");
for (let i = 0; i < words.length; i++) {
const workSentence = preparingSentence.join(" ") + " " + words[i];
if (context.measureText(workSentence).width > maxWidth) {
lines.push(preparingSentence.join(" "));
preparingSentence = [words[i]];
} else {
preparingSentence.push(words[i]);
}
}
lines.push(preparingSentence.join(" "));
lines.forEach(element => {
const lineWidth = context.measureText(element).width;
const xOffset = (maxWidth - lineWidth) / 2;
y += lineHeight;
context.fillText(element, x + xOffset, y);
});
}
export async function fetchImageAsBlob(url: string): Promise<Blob> {
const response = await fetch(url);
const blob = await response.blob();
return blob;
}
export function FixUpQuote(quote) {
const emojiRegex = /<a?:(\w+):(\d+)>/g;
quote = quote.replace(emojiRegex, "");
const mentionRegex = /<@(.*)>/;
let result = quote;
mentionRegex.exec(quote)?.forEach(match => {
console.log(match);
result = result.replace(match, `@${UserStore.getUser(match.replace("<@", "").replace(">", "")).username}`);
});
return result;
}

View file

@ -0,0 +1,210 @@
/*
* 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 { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginNative } from "@utils/types";
import { ApplicationAssetUtils, FluxDispatcher, UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
export async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await ApplicationAssetUtils.fetchAssetIds("0", [key]))[0];
}
enum StatsDisplay {
messagesSentToday,
messagesSentAllTime,
mostListenedAlbum
}
const settings = definePluginSettings(
{
assetURL: {
type: OptionType.STRING,
description: "The image to use for your rpc. Your profile picture is used if left blank",
default: "",
restartNeeded: false,
onChange: () => { updateData(); }
},
RPCTitle: {
type: OptionType.STRING,
description: "The title for the rpc",
default: "RPCStats",
restartNeeded: false,
onChange: () => { updateData(); }
},
statDisplay: {
type: OptionType.SELECT,
description: "What should the rpc display? (you can only have one line i'm pretty sure)",
options: [
{ value: StatsDisplay.messagesSentToday, label: "The amount of messages sent today", default: true },
{ value: StatsDisplay.messagesSentAllTime, label: "The amount of messages sent all time" },
{ value: StatsDisplay.mostListenedAlbum, label: "Your most listened album for the week" }
],
restartNeeded: false,
onChange: () => { updateData(); }
},
lastFMApiKey: {
type: OptionType.STRING,
description: "Your last.fm API key",
default: "",
restartNeeded: false,
onChange: () => { updateData(); }
},
lastFMUsername: {
type: OptionType.STRING,
description: "Your last.fm username",
default: "",
restartNeeded: false,
onChange: () => { updateData(); }
},
albumCoverImage: {
type: OptionType.BOOLEAN,
description: "Should the album cover image be used as the rpc image? (if you have the last fm display chosen)",
default: true,
restartNeeded: false,
onChange: () => { updateData(); }
},
lastFMStatFormat: {
type: OptionType.STRING,
description: "How should the last fm stat be formatted? $album is replaced with the album name, and $artist is replaced with the artist name",
default: "Top album this week: \"$album - $artist\"",
restartNeeded: false,
onChange: () => { updateData(); }
}
});
async function setRpc(disable?: boolean, details?: string, imageURL?: string) {
if (!disable) {
if (!settings.store.lastFMApiKey.length && settings.store.statDisplay === StatsDisplay.mostListenedAlbum) {
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: null,
socketId: "RPCStats",
});
}
}
const activity = {
"application_id": "0",
"name": settings.store.RPCTitle,
"details": details || "No info right now :(",
"type": 0,
"flags": 1,
"assets": {
// i love insanely long statements
"large_image":
(imageURL == null || !settings.store.albumCoverImage) ?
await getApplicationAsset(settings.store.assetURL.length ? settings.store.assetURL : UserStore.getCurrentUser().getAvatarURL()) :
await getApplicationAsset(imageURL)
}
};
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: !disable ? activity : null,
socketId: "RPCStats",
});
}
function getCurrentDate(): string {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
interface IMessageCreate {
type: "MESSAGE_CREATE";
optimistic: boolean;
isPushNotification: boolean;
channelId: string;
message: Message;
}
const Native = VencordNative.pluginHelpers.RPCStats as PluginNative<typeof import("./native")>;
async function updateData() {
switch (settings.store.statDisplay) {
case StatsDisplay.messagesSentToday:
let messagesSent;
if (await DataStore.get("RPCStatsDate") === getCurrentDate()) {
messagesSent = await DataStore.get("RPCStatsMessages");
}
else {
await DataStore.set("RPCStatsDate", getCurrentDate());
await DataStore.set("RPCStatsMessages", 0);
messagesSent = 0;
}
setRpc(false, `Messages sent today: ${messagesSent}\n`);
break;
case StatsDisplay.messagesSentAllTime:
let messagesAllTime = await DataStore.get("RPCStatsAllTimeMessages");
if (!messagesAllTime) {
DataStore.set("RPCStatsAllTimeMessages", 0);
messagesAllTime = 0;
}
setRpc(false, `Messages sent all time: ${messagesAllTime}\n`);
break;
// slightly cursed
case StatsDisplay.mostListenedAlbum:
const lastFMDataJson = await Native.fetchTopAlbum(
{
apiKey: settings.store.lastFMApiKey,
user: settings.store.lastFMUsername,
period: "7day"
});
if (lastFMDataJson == null) return;
const lastFMData = JSON.parse(lastFMDataJson);
console.log(lastFMData);
setRpc(false, settings.store.lastFMStatFormat.replace("$album", lastFMData.albumName).replace("$artist", lastFMData.artistName), lastFMData?.albumCoverUrl);
break;
}
}
export default definePlugin({
name: "RPCStats",
description: "Displays stats about your activity as an rpc",
authors: [Devs.Samwich],
async start() {
updateData();
setInterval(() => {
checkForNewDay();
updateData();
}, 1000);
},
settings,
stop() {
setRpc(true);
},
flux:
{
async MESSAGE_CREATE({ optimistic, type, message }: IMessageCreate) {
if (optimistic || type !== "MESSAGE_CREATE") return;
if (message.state === "SENDING") return;
if (message.author.id !== UserStore.getCurrentUser().id) return;
await DataStore.set("RPCStatsMessages", await DataStore.get("RPCStatsMessages") + 1);
await DataStore.set("RPCStatsAllTimeMessages", await DataStore.get("RPCStatsAllTimeMessages") + 1);
updateData();
},
}
});
let lastCheckedDate: string = getCurrentDate();
function checkForNewDay(): void {
const currentDate = getCurrentDate();
if (currentDate !== lastCheckedDate) {
lastCheckedDate = currentDate;
}
}

View file

@ -0,0 +1,38 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
interface Album {
name: string;
artist: { name: string; };
image: { "#text": string; }[];
}
interface TopAlbumsResponse {
topalbums: {
album: Album[];
};
}
export async function fetchTopAlbum(_, args) {
const { apiKey, user, period } = args;
const url = `http://ws.audioscrobbler.com/2.0/?method=user.getTopAlbums&user=${user}&api_key=${apiKey}&period=${period}&format=json&limit=1`;
const response = await fetch(url);
const data: TopAlbumsResponse = await response.json();
if (data.topalbums && data.topalbums.album.length > 0) {
const topAlbum = data.topalbums.album[0];
const albumName = topAlbum.name;
const artistName = topAlbum.artist.name;
const albumCoverUrl = topAlbum.image[topAlbum.image.length - 1]["#text"];
return JSON.stringify({ albumName: albumName, artistName: artistName, albumCoverUrl });
}
else {
return null;
}
}

View file

@ -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 { Devs } from "@utils/constants";
import { getCurrentChannel } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import OpenAI from "openai";
let openai;
const settings = definePluginSettings({
apiKey: {
type: OptionType.STRING,
description: "Your OpenAI API Key",
default: "",
restartNeeded: true
},
voiceToUse:
{
type: OptionType.SELECT,
description: "The text to speech voice to use",
options: [
{ value: "alloy", label: "Alloy" },
{ value: "echo", label: "Echo" },
{ value: "fable", label: "Fable" },
{ value: "onyx", label: "Onyx" },
{ value: "nova", label: "Nova", default: true },
{ value: "shimmer", label: "Shimmer" }
]
}
});
async function readOutText(text) {
const mp3Response = await openai.audio.speech.create({
model: "tts-1",
voice: settings.store.voiceToUse,
input: text,
});
const mp3Data = await mp3Response.arrayBuffer();
const mp3Blob = new Blob([mp3Data], { type: "audio/mpeg" });
const audioElement = new Audio();
const audioURL = URL.createObjectURL(mp3Blob);
audioElement.src = audioURL;
audioElement.volume = 0.5;
document.body.appendChild(audioElement);
audioElement.play();
}
export default definePlugin({
name: "TextToSpeech",
description: "Reads out chat messages with openai tts",
authors: [Devs.Samwich],
flux:
{
async MESSAGE_CREATE({ optimistic, type, message, channelId }) {
if (optimistic || type !== "MESSAGE_CREATE") return;
if (message.state === "SENDING") return;
if (!message.content) return;
if (message.channel_id !== getCurrentChannel().id) return;
readOutText(message.content);
}
},
settings,
start() {
openai = new OpenAI({ apiKey: settings.store.apiKey, dangerouslyAllowBrowser: true });
}
});

View file

@ -0,0 +1,70 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
spoilerFilenames: {
description: "Strings in filenames that should be spoilered. Comma separated.",
type: OptionType.STRING,
default: "",
},
spoilerLinks: {
description: "Strings in link attachments that should be spoilered. Comma separated.",
type: OptionType.STRING,
default: ""
},
gifSpoilersOnly: {
description: "Should the links only be gifs?",
type: OptionType.BOOLEAN,
default: true
},
});
export default definePlugin({
name: "TriggerWarning",
authors: [EquicordDevs.Joona],
description: "Spoiler attachments based on filenames and links.",
patches: [
{
find: "SimpleMessageAccessories:",
replacement: [
{
match: /function \i\((\i),\i\){return/,
replace: "$& $self.shouldSpoiler($1.originalItem.filename) || "
},
{
match: /(\i)=\(0,\i\.getOb.{27,35}\);(?=if\((\i).type)/,
replace: "$&$1=$self.spoilerLink($1,$2.url,$2.type);"
}
]
}
],
settings,
shouldSpoiler(filename: string): string | null {
const { spoilerFilenames } = settings.store;
if (!filename || !spoilerFilenames) return null;
const strings = spoilerFilenames.split(",").map(s => s.trim());
return strings.some(s => filename.includes(s)) ? "spoiler" : null;
},
spoilerLink(alreadySpoilered: string, link: string, type: string): string | null {
if (alreadySpoilered) return alreadySpoilered;
const { spoilerLinks, gifSpoilersOnly } = settings.store;
if (!link || !spoilerLinks) return null;
const strings = spoilerLinks.split(",").map(s => s.trim());
const isLinkSpoiler = strings.some(s => link.includes(s));
if (gifSpoilersOnly) {
return type === "gifv" && isLinkSpoiler ? "spoiler" : null;
} else {
return isLinkSpoiler ? "spoiler" : null;
}
}
});

View file

@ -0,0 +1,147 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { Settings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findExportedComponentLazy } from "@webpack";
import { Menu, Popout, useState } from "@webpack/common";
import type { ReactNode } from "react";
function toggle(name: string, onClose: () => void) {
Settings.plugins.TextModifiers[name] = !Settings.plugins.TextModifiers[name];
onClose();
}
function isEnabled(name: string) {
return Settings.plugins.TextModifiers[name];
}
const HeaderBarIcon = findExportedComponentLazy("Icon", "Divider");
function utilityDockPopout(onClose: () => void) {
return (
<Menu.Menu
navId="utilityDock"
onClose={onClose}
>
<Menu.MenuCheckboxItem
id="utilityDock-quickcss-toggle"
checked={Settings.useQuickCss}
label={"QuickCSS"}
action={() => {
Settings.useQuickCss = !Settings.useQuickCss;
onClose();
}}
/>
<Menu.MenuItem
id="utilityDock-quickcss"
label="Edit QuickCSS"
action={() => VencordNative.quickCss.openEditor()}
/>
</Menu.Menu>
);
}
function utilityDockIcon(isShown: boolean) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={24} height={24}>
<path fill="currentColor" d={isShown ?
"M20 8H17V6C17 4.9 16.1 4 15 4H9C7.9 4 7 4.9 7 6V8H4C2.9 8 2 8.9 2 10V20H22V10C22 8.9 21.1 8 20 8M9 6H15V8H9V6M20 18H4V15H6V16H8V15H16V16H18V15H20V18M18 13V12H16V13H8V12H6V13H4V10H20V13H18Z"
:
"M18 16H16V15H8V16H6V15H2V20H22V15H18V16M20 8H17V6C17 4.9 16.1 4 15 4H9C7.9 4 7 4.9 7 6V8H4C2.9 8 2 8.9 2 10V14H6V12H8V14H16V12H18V14H22V10C22 8.9 21.1 8 20 8M15 8H9V6H15V8Z"} />
</svg>
);
}
function VencordPopoutButton() {
const [show, setShow] = useState(false);
return (
<Popout
position="bottom"
align="right"
animation={Popout.Animation.NONE}
shouldShow={show}
onRequestClose={() => setShow(false)}
renderPopout={() => utilityDockPopout(() => setShow(false))}
>
{(_, { isShown }) => (
<HeaderBarIcon
className="vc-toolbox-btn"
onClick={() => setShow(v => !v)}
tooltip={isShown ? null : "Utility Dock"}
icon={() => utilityDockIcon(isShown)}
selected={isShown}
/>
)}
</Popout>
);
}
function utilityDock({ children }: { children: ReactNode[]; }) {
children.splice(
children.length - 1, 0,
<ErrorBoundary noop={true}>
<VencordPopoutButton />
</ErrorBoundary>
);
return <>{children}</>;
}
export default definePlugin({
name: "utilityDock",
description: "Adds a button on your titlebar with multiple useful features",
authors: [Devs.Samwich],
patches: [
{
find: "toolbar:function",
replacement: {
match: /(?<=toolbar:function.{0,100}\()\i.Fragment,/,
replace: "$self.utilityDock,"
}
}
],
utilityDock: ErrorBoundary.wrap(utilityDock, {
fallback: () => <p style={{ color: "red" }}>Failed to render :(</p>
})
});
export function TextPlugin({ pluginName, onClose }) {
return (
<Menu.MenuCheckboxItem
id={`vc-toolbox-${pluginName}-toggle`}
checked={Settings.plugins[pluginName].enabled}
label={pluginName}
action={() => {
Settings.plugins[pluginName].isEnabled = !Settings.plugins[pluginName].isEnabled;
onClose();
}}
/>
);
}

View file

@ -0,0 +1,96 @@
/*
* 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 { Tooltip, useRef, useState } from "@webpack/common";
const settings = definePluginSettings(
{
preservePitch: {
type: OptionType.BOOLEAN,
description: "Should pitch be preserved when changing speed?",
default: false,
restartNeeded: true
},
});
export default definePlugin({
name: "VideoSpeed",
description: "Allows you to change the speed of videos",
authors: [Devs.Samwich],
settings,
patches: [
{
find: ".videoControls:",
replacement: {
match: /children:\[this\.renderPlayIcon\(\),.{0,200}\.setDurationRef}\),/,
replace: "$&$self.SpeedButton(),"
},
},
],
SpeedButton: ErrorBoundary.wrap(() => {
const elementRef = useRef<HTMLDivElement>(null);
const [speed, setSpeed] = useState(1);
const speedPaths =
{
1.5: "M320 48a48 48 0 1 1 96 0a48 48 0 1 1-96 0m-115.5 73.3c-5.4-2.5-11.7-1.9-16.4 1.7l-40.9 30.7c-14.1 10.6-34.2 7.7-44.8-6.4s-7.7-34.2 6.4-44.8l40.9-30.7c23.7-17.8 55.3-21 82.1-8.4l90.4 42.5c29.1 13.7 36.8 51.6 15.2 75.5L299.1 224h97.4c30.3 0 53 27.7 47.1 57.4l-28.2 140.9c-3.5 17.3-20.3 28.6-37.7 25.1s-28.6-20.3-25.1-37.7L377 288h-70.3c8.6 19.6 13.3 41.2 13.3 64c0 88.4-71.6 160-160 160S0 440.4 0 352s71.6-160 160-160c11.1 0 22 1.1 32.4 3.3l54.2-54.2zM160 448a96 96 0 1 0 0-192a96 96 0 1 0 0 192",
1.25: "M320 48a48 48 0 1 0-96 0a48 48 0 1 0 96 0M125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8.1 5.6.3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9l-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4l5.1 12.3c15 35.8 49.9 59.1 88.7 59.1H384c17.7 0 32-14.3 32-32s-14.3-32-32-32h-21.3c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15C186.6 97.8 175 96 163.3 96c-31 0-60.8 12.3-82.7 34.3l-23.2 23.1c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5l11.5-25.9l-9.5-6a95.394 95.394 0 0 1-37.9-44.9z",
1: "M112 48a48 48 0 1 1 96 0a48 48 0 1 1-96 0m40 304v128c0 17.7-14.3 32-32 32s-32-14.3-32-32V256.9l-28.6 47.6c-9.1 15.1-28.8 20-43.9 10.9s-20-28.8-10.9-43.9l58.3-97c17.4-28.9 48.6-46.6 82.3-46.6h29.7c33.7 0 64.9 17.7 82.3 46.6l58.3 97c9.1 15.1 4.2 34.8-10.9 43.9s-34.8 4.2-43.9-10.9L232 256.9V480c0 17.7-14.3 32-32 32s-32-14.3-32-32V352z",
0.5: "M368 32c41.7 0 75.9 31.8 79.7 72.5l85.6 26.3c25.4 7.8 42.8 31.3 42.8 57.9c0 21.8-11.7 41.9-30.7 52.7l-144.6 82.1l92.5 92.5H544c17.7 0 32 14.3 32 32s-14.3 32-32 32h-64c-8.5 0-16.6-3.4-22.6-9.4L346.9 360.2c11.7-36 3.2-77.1-25.4-105.7c-40.6-40.6-106.3-40.6-146.9-.1l-73.6 70c-6.4 6.1-6.7 16.2-.6 22.6s16.2 6.6 22.6.6l73.8-70.2l.1-.1l.1-.1c3.5-3.5 7.3-6.6 11.3-9.2c27.9-18.5 65.9-15.4 90.5 9.2c24.7 24.7 27.7 62.9 9 90.9c-2.6 3.8-5.6 7.5-9 10.9l-37 37H352c17.7 0 32 14.3 32 32s-14.3 32-32 32H64c-35.3 0-64-28.7-64-64C0 249.6 127 112.9 289.3 97.5C296.2 60.2 328.8 32 368 32m0 104a24 24 0 1 0 0-48a24 24 0 1 0 0 48"
};
return <div ref={elementRef}>
<Tooltip text={`Toggle Speed (${speed})`}>
{tooltipProps => (
<div
{...tooltipProps}
role="button"
style={{
cursor: "pointer",
paddingTop: "4px",
paddingLeft: "4px",
paddingRight: "4px",
}}
onClick={e => {
console.log(e);
let newSpeed;
switch (speed) {
case 1:
newSpeed = 1.25;
break;
case 1.25:
newSpeed = 1.5;
break;
case 1.5:
newSpeed = 0.5;
break;
case 0.5:
newSpeed = 1;
break;
}
const parent = e.currentTarget.parentNode!.parentNode!.parentNode!;
// this works with audio too as it is but it doesnt always select the right audio tag
const media = parent.querySelector("video")! /* ?? document.querySelector("source")?.parentElement*/;
console.log(media);
media.playbackRate = newSpeed;
media.preservesPitch = settings.store.preservePitch;
setSpeed(newSpeed);
}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="19px" height="19px" viewBox="0 0 448 512"><path fill="currentColor" d={speedPaths[speed]}></path></svg>
</div>
)
}
</Tooltip >
</div >;
}, { noop: true })
});

View file

@ -703,6 +703,10 @@ export const EquicordDevs = Object.freeze({
name: "Shady Goat",
id: 376079696489742338n,
},
Joona: {
name: "Joona",
id: 297410829589020673n
},
} satisfies Record<string, Dev>);
export const SuncordDevs = /* #__PURE__*/ Object.freeze({