mirror of
https://github.com/Equicord/Equicord.git
synced 2025-01-18 13:23:28 -05:00
Plugins
This commit is contained in:
parent
38bba2d900
commit
b93c0aa1d4
17 changed files with 1233 additions and 28 deletions
12
README.md
12
README.md
|
@ -27,6 +27,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- AltKrispSwitch by newwares
|
||||
- AmITyping by MrDiamond
|
||||
- Anammox by Kyuuhachi
|
||||
- atSomeone by Joona
|
||||
- DecodeBase64 by ThePirateStoner
|
||||
- BetterActivities by D3SOX, Arjix, AutumnVN
|
||||
- BetterBanReasons by Inbestigator
|
||||
|
@ -35,12 +36,12 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- BlockKeywords by catcraft
|
||||
- BlockKrsip by D3SOX
|
||||
- BypassDND by Inbestigator
|
||||
- ChannelTabs by TheSun, TheKodeToad, keifufu, Nickyux
|
||||
- CleanChannelName by AutumnVN
|
||||
- ClientSideBlock by Samwich
|
||||
- ColorMessage by Kyuuhachi
|
||||
- CommandPalette by Ethan
|
||||
- CopyUserMention by Cortex and castdrian
|
||||
- CustomAppIcons by Happy Enderman and SerStars
|
||||
- CustomSounds by ScattrdBlade
|
||||
- CuteAnimeBoys by ShadyGoat
|
||||
- CuteNekos by echo
|
||||
|
@ -65,6 +66,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- Glide by Samwich
|
||||
- GlobalBadges by HypedDomi and Hosted by Wolfie
|
||||
- GodMode by Tolgchu
|
||||
- GoodPerson by nin0dev
|
||||
- GoogleThat by Samwich
|
||||
- Grammar by Samwich
|
||||
- GrammarFix by unstream
|
||||
|
@ -72,19 +74,22 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- HolyNotes by Wolfie
|
||||
- HomeTyping by Samwich
|
||||
- HopOn by ImLvna
|
||||
- Husk by nin0dev
|
||||
- Identity by Samwich
|
||||
- IgnoreTerms by D3SOX
|
||||
- InRole by nin0dev
|
||||
- IrcColors by Grzesiek11
|
||||
- IRememberYou by zoodogood
|
||||
- JumpToStart by Samwich
|
||||
- KeyboardSounds by HypedDomi
|
||||
- KeywordNotify by camila314
|
||||
- LoginWithQR by nexpid
|
||||
- MediaDownloader by Colorman
|
||||
- Meow by Samwich
|
||||
- MessageLinkTooltip by Kyuuhachi
|
||||
- MessageLoggerEnhanced by Aria
|
||||
- MessageTranslate by Samwich
|
||||
- ModalFade by Kyuuhachi
|
||||
- MusicTitleRPC by Blackilykay
|
||||
- NewPluginsManager by Sqaaakoi
|
||||
- noAppsAllowed by kvba
|
||||
- NoBulletPoints by Samwich
|
||||
|
@ -112,6 +117,8 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- ShowBadgesInChat by Inbestigator and KrystalSkull
|
||||
- Slap by Korbo
|
||||
- SoundBoardLogger by Moxxie, fres, echo (maintained by thororen)
|
||||
- SteamStatusSync by niko
|
||||
- StickerBlocker by Samwich
|
||||
- TalkInReverse by Tolgchu
|
||||
- TeX by Kyuuhachi
|
||||
- TextToSpeech by Samwich
|
||||
|
@ -129,6 +136,7 @@ An enhanced version of [Vencord](https://github.com/Vendicated/Vencord) by [Vend
|
|||
- VoiceChatUtilities by Dams and D3SOX
|
||||
- WebpackTarball by Kyuuhachi
|
||||
- WhosWatching by fres
|
||||
- WigglyText by nexpid
|
||||
- Woof by Samwich
|
||||
- YoutubeDescription by arHSM
|
||||
|
||||
|
|
64
src/equicordplugins/goodPerson/index.ts
Normal file
64
src/equicordplugins/goodPerson/index.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2024 nin0dev
|
||||
*
|
||||
* 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 { addPreSendListener, removePreSendListener } from "@api/MessageEvents";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
const badVerbs = ["fuck", " cum", "kill", "destroy"];
|
||||
const badNouns = ["cunt", "shit", "bullshit", "ass", "bitch", "nigga", "hell", "whore", "dick", "piss", "pussy", "slut", "tit", "cum", "cock", "retard", "blowjob", "bastard", "kotlin", "die", "sex", "nigger", "brainless", "mant", "manti", "mantik", "mantika", "mantikaf", "mantikafa", "mantikafas", "mantikafasi", "boykisser", "mewing", "mew", "skibidi", "gyat", "gyatt", "rizzler", "avast"];
|
||||
const badVerbsReplacements = ["love", "eat", "deconstruct", "marry", "fart", "teach", "display", "plug", "explode", "undress", "finish", "freeze", "beat", "free", "brush", "allocate", "date", "melt", "breed", "educate", "injure", "change"];
|
||||
const badNounsReplacements = ["pasta", "kebab", "cake", "potato", "woman", "computer", "java", "hamburger", "monster truck", "osu!", "Ukrainian ball in search of gas game", "Anime", "Anime girl", "good", "keyboard", "NVIDIA RTX 3090 Graphics Card", "storm", "queen", "single", "umbrella", "mosque", "physics", "bath", "virus", "bathroom", "mom", "owner", "airport", "Avast Antivirus Free"];
|
||||
|
||||
function replaceBadNouns(content) {
|
||||
// eslint-disable-next-line quotes
|
||||
const regex = new RegExp('\\b(' + badNouns.join('|') + ')\\b', 'gi');
|
||||
|
||||
return content.replace(regex, function (match) {
|
||||
const randomIndex = Math.floor(Math.random() * badNounsReplacements.length);
|
||||
return badNounsReplacements[randomIndex];
|
||||
});
|
||||
}
|
||||
|
||||
function replaceBadVerbs(content) {
|
||||
// eslint-disable-next-line quotes
|
||||
const regex = new RegExp('\\b(' + badVerbs.join('|') + ')\\b', 'gi');
|
||||
|
||||
return content.replace(regex, function (match) {
|
||||
const randomIndex = Math.floor(Math.random() * badVerbsReplacements.length);
|
||||
return badVerbsReplacements[randomIndex];
|
||||
});
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "GoodPerson",
|
||||
description: "Makes you a good person",
|
||||
authors: [Devs.nin0dev],
|
||||
dependencies: ["MessageEventsAPI"],
|
||||
|
||||
async start() {
|
||||
this.preSend = addPreSendListener((channelId, msg) => {
|
||||
const newContent = replaceBadVerbs(replaceBadNouns(msg.content));
|
||||
msg.content = newContent;
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
removePreSendListener(this.preSend);
|
||||
}
|
||||
});
|
127
src/equicordplugins/husk/index.tsx
Normal file
127
src/equicordplugins/husk/index.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
/* eslint-disable simple-header/header */
|
||||
/* eslint-disable indent */
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 your mom lol
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addButton, removeButton } from "@api/MessagePopover";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore, EmojiStore, RestAPI } from "@webpack/common";
|
||||
import type { SVGProps } from "react";
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
|
||||
interface BaseIconProps extends IconProps {
|
||||
viewBox: string;
|
||||
}
|
||||
|
||||
interface IconProps extends SVGProps<SVGSVGElement> {
|
||||
className?: string;
|
||||
height?: string | number;
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
|
||||
return (
|
||||
<svg
|
||||
className={classes(className, "vc-icon")}
|
||||
role="img"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
{...svgProps}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function Husk(props: IconProps) {
|
||||
return (
|
||||
<Icon
|
||||
{...props}
|
||||
className={classes(props.className, "vc-husk")}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,2C6.47,2 2,6.47 2,12C2,17.53 6.47,22 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M16.18,7.76L15.12,8.82L14.06,7.76L13,8.82L14.06,9.88L13,10.94L14.06,12L15.12,10.94L16.18,12L17.24,10.94L16.18,9.88L17.24,8.82L16.18,7.76M7.82,12L8.88,10.94L9.94,12L11,10.94L9.94,9.88L11,8.82L9.94,7.76L8.88,8.82L7.82,7.76L6.76,8.82L7.82,9.88L6.76,10.94L7.82,12M12,14C9.67,14 7.69,15.46 6.89,17.5H17.11C16.31,15.46 14.33,14 12,14Z"
|
||||
/>
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "Husk",
|
||||
description: "Adds Husk button (check settings to change used emoji)",
|
||||
authors: [Devs.nin0dev],
|
||||
dependencies: ["MessagePopoverAPI"],
|
||||
settings: definePluginSettings({
|
||||
findInServer: {
|
||||
description: "Attempt to find emoji of same name in server before using ID in settings (useful if no nitro)",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true
|
||||
},
|
||||
emojiName: {
|
||||
description: "Emoji name (default (from Vencord Server): husk)",
|
||||
type: OptionType.STRING,
|
||||
default: "husk"
|
||||
},
|
||||
emojiID: {
|
||||
description: "Emoji ID (default (from Vencord Server): 1026532993923293184)",
|
||||
type: OptionType.BIGINT,
|
||||
default: 1026532993923293184n
|
||||
}
|
||||
}),
|
||||
getEmojiIdThatShouldBeUsed(guildId: string) {
|
||||
if (!this.settings.store.findInServer || guildId === "") return this.settings.store.emojiID;
|
||||
let id = "";
|
||||
EmojiStore.getGuildEmoji(guildId).forEach(emoji => {
|
||||
if (emoji.name === this.settings.store.emojiName) {
|
||||
id = emoji.id;
|
||||
}
|
||||
});
|
||||
return id !== "" ? id : this.settings.store.emojiID;
|
||||
},
|
||||
async start() {
|
||||
addButton("Husk", msg => {
|
||||
return {
|
||||
label: "Husk",
|
||||
icon: Husk,
|
||||
message: msg,
|
||||
channel: ChannelStore.getChannel(msg.channel_id),
|
||||
onClick: () => {
|
||||
const guildId = ChannelStore.getChannel(msg.channel_id).guild_id !== null ? ChannelStore.getChannel(msg.channel_id).guild_id : "";
|
||||
RestAPI.put({
|
||||
url: `/channels/${msg.channel_id}/messages/${msg.id}/reactions/${this.settings.store.emojiName}:${this.getEmojiIdThatShouldBeUsed(guildId)}/@me`
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeButton("Husk");
|
||||
},
|
||||
|
||||
});
|
43
src/equicordplugins/inRole/RoleMembersModal.tsx
Normal file
43
src/equicordplugins/inRole/RoleMembersModal.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { Forms, Parser } from "@webpack/common";
|
||||
import { GuildMember } from "discord-types/general";
|
||||
|
||||
const cl = classNameFactory("vc-inrole-");
|
||||
|
||||
export function showInRoleModal(members: GuildMember[], roleId: string, channelId: string) {
|
||||
openModal(props =>
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<ModalRoot {...props} size={ModalSize.DYNAMIC} fullscreenOnMobile={true} >
|
||||
<ModalHeader className={cl("header")}>
|
||||
<Forms.FormText style={{ fontSize: "1.2rem", fontWeight: "bold", marginRight: "7px" }}>Members of role {
|
||||
Parser.parse(`<@&${roleId}>`, true, { channelId, viewingChannelId: channelId })
|
||||
} ({members.length})</Forms.FormText>
|
||||
<ModalCloseButton onClick={props.onClose} className={cl("close")} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<div style={{ padding: "13px 20px" }} className={cl("member-list")}>
|
||||
{
|
||||
members.length !== 0 ? members.map(member =>
|
||||
<>
|
||||
<Forms.FormText className={cl("modal-member")}>
|
||||
{Parser.parse(`<@${member.userId}>`, true, { channelId, viewingChannelId: channelId })}
|
||||
</Forms.FormText>
|
||||
</>
|
||||
) : <Forms.FormText>Looks like no online cached members with that role were found. Try scrolling down on your member list to cache more users!</Forms.FormText>
|
||||
}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
);
|
||||
}
|
100
src/equicordplugins/inRole/index.tsx
Normal file
100
src/equicordplugins/inRole/index.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 nin0dev
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { ApplicationCommandInputType, ApplicationCommandOptionType, sendBotMessage } from "@api/Commands";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { InfoIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Forms, GuildMemberStore, GuildStore, Menu, Parser } from "@webpack/common";
|
||||
import { GuildMember } from "discord-types/general";
|
||||
|
||||
import { showInRoleModal } from "./RoleMembersModal";
|
||||
|
||||
const DeveloperMode = getUserSettingLazy("appearance", "developerMode")!;
|
||||
|
||||
function getMembersInRole(roleId: string, guildId: string) {
|
||||
const members = GuildMemberStore.getMembers(guildId);
|
||||
const membersInRole: GuildMember[] = [];
|
||||
members.forEach(member => {
|
||||
if (member.roles.includes(roleId)) {
|
||||
membersInRole.push(member);
|
||||
}
|
||||
});
|
||||
return membersInRole;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "InRole",
|
||||
description: "Know who is in a role with the role context menu or /inrole command (read plugin info!)",
|
||||
authors: [Devs.nin0dev],
|
||||
dependencies: ["UserSettingsAPI"],
|
||||
start() {
|
||||
// DeveloperMode needs to be enabled for the context menu to be shown
|
||||
DeveloperMode.updateSetting(true);
|
||||
},
|
||||
settingsAboutComponent: () => {
|
||||
return (
|
||||
<>
|
||||
<Forms.FormText style={{ fontSize: "1.2rem", marginTop: "15px", fontWeight: "bold" }}>{Parser.parse(":warning:")} Limitations</Forms.FormText>
|
||||
<Forms.FormText style={{ marginTop: "10px", fontWeight: "500" }} >If you don't have mod permissions on the server, and that server is large (over 100 members), the plugin may be limited in the following ways:</Forms.FormText>
|
||||
<Forms.FormText>• Offline members won't be listed</Forms.FormText>
|
||||
<Forms.FormText>• Up to 100 members will be listed by default. To get more, scroll down in the member list to load more members.</Forms.FormText>
|
||||
<Forms.FormText>• However, friends will always be shown regardless of their status.</Forms.FormText>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: "inrole",
|
||||
description: "Know who is in a role",
|
||||
inputType: ApplicationCommandInputType.BUILT_IN,
|
||||
options: [
|
||||
{
|
||||
name: "role",
|
||||
description: "The role",
|
||||
type: ApplicationCommandOptionType.ROLE,
|
||||
required: true
|
||||
},
|
||||
],
|
||||
execute: (args, ctx) => {
|
||||
// Guild check
|
||||
if (!ctx.guild) {
|
||||
return sendBotMessage(ctx.channel.id, { content: "Make sure that you are in a server." });
|
||||
}
|
||||
const role = args[0].value;
|
||||
showInRoleModal(getMembersInRole(role, ctx.guild.id), role, ctx.channel.id);
|
||||
}
|
||||
}
|
||||
],
|
||||
contextMenus: {
|
||||
"dev-context"(children, { id }: { id: string; }) {
|
||||
const guild = getCurrentGuild();
|
||||
if (!guild) return;
|
||||
|
||||
const channel = getCurrentChannel();
|
||||
if (!channel) return;
|
||||
|
||||
const role = GuildStore.getRole(guild.id, id);
|
||||
if (!role) return;
|
||||
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-view-inrole"
|
||||
label="View Members in Role"
|
||||
action={() => {
|
||||
showInRoleModal(getMembersInRole(role.id, guild.id), role.id, channel.id);
|
||||
}}
|
||||
icon={InfoIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
29
src/equicordplugins/inRole/style.css
Normal file
29
src/equicordplugins/inRole/style.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
.vc-inrole-member-list {
|
||||
max-height: 400px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 13px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.vc-inrole-member-list::-webkit-scrollbar {
|
||||
background-color: #fff1;
|
||||
border-radius: 100px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.vc-inrole-member-list::-webkit-scrollbar-thumb {
|
||||
background-color: #fff3;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.vc-inrole-modal-member {
|
||||
margin: 11px 0;
|
||||
}
|
||||
|
||||
.vc-inrole-header {
|
||||
padding-top: "15px";
|
||||
}
|
||||
|
||||
.vc-inrole-close {
|
||||
margin-left: auto;
|
||||
}
|
392
src/equicordplugins/musicTitleRPC/index.tsx
Normal file
392
src/equicordplugins/musicTitleRPC/index.tsx
Normal file
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* 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 { Link } from "@components/Link";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ApplicationAssetUtils, FluxDispatcher, Forms, React } from "@webpack/common";
|
||||
|
||||
interface ActivityAssets {
|
||||
large_image?: string;
|
||||
large_text?: string;
|
||||
small_image?: string;
|
||||
small_text?: string;
|
||||
}
|
||||
|
||||
interface Activity {
|
||||
state?: string;
|
||||
details?: string;
|
||||
timestamps?: {
|
||||
start?: number;
|
||||
end?: number;
|
||||
};
|
||||
assets?: ActivityAssets;
|
||||
buttons?: Array<string>;
|
||||
name: string;
|
||||
application_id: string;
|
||||
metadata?: {
|
||||
button_urls?: Array<string>;
|
||||
};
|
||||
type: ActivityType;
|
||||
url?: string;
|
||||
flags: number;
|
||||
}
|
||||
|
||||
interface Data {
|
||||
activity: Activity;
|
||||
pid?: number;
|
||||
socketId: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
|
||||
const enum ActivityType {
|
||||
PLAYING = 0,
|
||||
STREAMING = 1,
|
||||
LISTENING = 2,
|
||||
WATCHING = 3,
|
||||
COMPETING = 5
|
||||
}
|
||||
interface SpotifyImage {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface SpotifyElement {
|
||||
id: string;
|
||||
name: string;
|
||||
// track, album or artist
|
||||
type: string;
|
||||
// only on track
|
||||
duration?: number;
|
||||
album?: SpotifyElement;
|
||||
artists?: Array<SpotifyElement>;
|
||||
isLocal?: boolean;
|
||||
// only on album
|
||||
image?: SpotifyImage;
|
||||
// only on artist
|
||||
external_urls?: {
|
||||
spotify: string;
|
||||
};
|
||||
href?: string;
|
||||
uri?: string;
|
||||
}
|
||||
|
||||
interface SpotifyEvent {
|
||||
type: string;
|
||||
track: SpotifyElement;
|
||||
connectionId: string;
|
||||
}
|
||||
|
||||
interface SpotifyDevice {
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
is_private_session: boolean;
|
||||
is_restricted: boolean;
|
||||
name: string;
|
||||
supports_volume: boolean;
|
||||
type: string;
|
||||
volume_percent: number;
|
||||
}
|
||||
|
||||
enum NonSpotifyShowOptions {
|
||||
Details,
|
||||
State,
|
||||
LargeImageText,
|
||||
SmallImageText
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
applicationId: {
|
||||
type: OptionType.STRING,
|
||||
description: "Application ID (required)",
|
||||
isValid: (value: string) => {
|
||||
if (!value) return "Application ID is required.";
|
||||
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
||||
return true;
|
||||
}
|
||||
},
|
||||
musicPlayerNames: {
|
||||
type: OptionType.STRING,
|
||||
default: "Music",
|
||||
description: "List of activity names which will get replaced by the song's title (separated by ,)",
|
||||
},
|
||||
forceListeningType: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Force all music player activities to be \"Listening to\", instead of \"Playing\"",
|
||||
},
|
||||
spotifyActivityName: {
|
||||
type: OptionType.STRING,
|
||||
default: "{{Title}}",
|
||||
description: "The activity's name (spotify)"
|
||||
},
|
||||
spotifyActivityDetails: {
|
||||
type: OptionType.STRING,
|
||||
default: "{{Title}}",
|
||||
description: "The activity's details field (first line) (spotify)"
|
||||
},
|
||||
spotifyActivityState: {
|
||||
type: OptionType.STRING,
|
||||
default: "by {{AllArtistsGrammarSeparated}}",
|
||||
description: "The activity's state field (second line) (spotify)"
|
||||
},
|
||||
spotifyActivityLargeImageText: {
|
||||
type: OptionType.STRING,
|
||||
default: "on {{Album}}",
|
||||
description: "The activity's large image text (third line and image hover text) (spotify)"
|
||||
},
|
||||
spotifyButtonOneText: {
|
||||
type: OptionType.STRING,
|
||||
default: "Open in Spotify",
|
||||
description: "The activity's first button's text (spotify)"
|
||||
},
|
||||
spotifyButtonTwoText: {
|
||||
type: OptionType.STRING,
|
||||
default: "",
|
||||
description: "The activity's second button's text (spotify)"
|
||||
},
|
||||
spotifyButtonOneLink: {
|
||||
type: OptionType.STRING,
|
||||
default: "https://open.spotify.com/track/{{Id}}",
|
||||
description: "The activity's first button's link (spotify)"
|
||||
},
|
||||
spotifyButtonTwoLink: {
|
||||
type: OptionType.STRING,
|
||||
default: "",
|
||||
description: "The activity's second button's link (spotify)"
|
||||
},
|
||||
nonSpotifyWhatToShow: {
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Details", value: NonSpotifyShowOptions.Details, default: true },
|
||||
{ label: "State", value: NonSpotifyShowOptions.State },
|
||||
{ label: "Large image text", value: NonSpotifyShowOptions.LargeImageText },
|
||||
{ label: "Small image text", value: NonSpotifyShowOptions.SmallImageText },
|
||||
],
|
||||
description: "What to show (non-spotify)"
|
||||
},
|
||||
nonSpotifyRegex: {
|
||||
type: OptionType.STRING,
|
||||
default: "(.+)",
|
||||
description: "Regex pattern to determine what to show using the first captured group (non-spotify)"
|
||||
},
|
||||
cloneSpotifyActivity: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: true,
|
||||
description: "Clone Spotify's activity to give it the custom name. Does not remove the original one.",
|
||||
restartNeeded: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function handleUpdate(data: Data) {
|
||||
if (data.activity === null || data.activity.state === undefined) return;
|
||||
const { nonSpotifyWhatToShow: what_to_show, nonSpotifyRegex: regex_option } = settings.store;
|
||||
|
||||
const players = settings.store.musicPlayerNames.split(",").map(x => x.trim());
|
||||
if (!players.includes(data.activity.name)) return;
|
||||
|
||||
if (settings.store.forceListeningType) {
|
||||
data.activity.type = ActivityType.LISTENING;
|
||||
}
|
||||
|
||||
let name: string = "";
|
||||
if (what_to_show === NonSpotifyShowOptions.Details && data.activity.details !== undefined) {
|
||||
name = data.activity.details;
|
||||
} else if (what_to_show === NonSpotifyShowOptions.State && data.activity.state !== undefined) {
|
||||
name = data.activity.state;
|
||||
} else if (what_to_show === NonSpotifyShowOptions.LargeImageText && data.activity.assets?.large_text !== undefined) {
|
||||
name = data.activity.assets.large_text;
|
||||
} else if (what_to_show === NonSpotifyShowOptions.State && data.activity.assets?.small_text !== undefined) {
|
||||
name = data.activity.assets.small_text;
|
||||
}
|
||||
|
||||
const regex_output = new RegExp(regex_option).exec(name);
|
||||
if (regex_output === null || regex_output[1] === undefined) {
|
||||
// no match, use the full string
|
||||
data.activity.name = name;
|
||||
} else {
|
||||
data.activity.name = regex_output[1];
|
||||
}
|
||||
}
|
||||
|
||||
var playbackStoppedTimeout: number | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "MusicTitleRPC",
|
||||
description: "Makes the song's title appear as the activity name when listening to music.",
|
||||
authors: [EquicordDevs.Blackilykat],
|
||||
start: () => {
|
||||
FluxDispatcher.subscribe("LOCAL_ACTIVITY_UPDATE", handleUpdate);
|
||||
},
|
||||
stop: () => {
|
||||
FluxDispatcher.unsubscribe("LOCAL_ACTIVITY_UPDATE", handleUpdate);
|
||||
},
|
||||
patches: [
|
||||
{
|
||||
find: "let{connectionId:",
|
||||
predicate: () => settings.store.cloneSpotifyActivity,
|
||||
replacement: [
|
||||
{
|
||||
match: /(?=let{connectionId:\i,track:\i}=(\i);)/,
|
||||
replace: "$self.handleSpotifySongChange($1);"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "SPOTIFY_SET_DEVICES:function(",
|
||||
predicate: () => settings.store.cloneSpotifyActivity,
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=SPOTIFY_SET_DEVICES:function\((\i)\){)/,
|
||||
replace: "$self.handleSpotifyChangeDevices($1);"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
settings,
|
||||
|
||||
async handleSpotifyChangeDevices(e: { accountId: string, devices: SpotifyDevice[]; }) {
|
||||
const { devices } = e;
|
||||
let playing: boolean = false;
|
||||
devices.forEach(device => {
|
||||
if (device.is_active) playing = true;
|
||||
});
|
||||
if (!playing) {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: null,
|
||||
socketId: "MusicTitleRPC:Spotify"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async handleSpotifySongChange(e: SpotifyEvent) {
|
||||
const {
|
||||
applicationId: application_id,
|
||||
spotifyActivityName: activity_name,
|
||||
spotifyActivityState: activity_state,
|
||||
spotifyActivityDetails: activity_details,
|
||||
spotifyActivityLargeImageText: activity_large_image_text,
|
||||
spotifyButtonOneText: activity_button_one_text,
|
||||
spotifyButtonTwoText: activity_button_two_text,
|
||||
spotifyButtonOneLink: activity_button_one_link,
|
||||
spotifyButtonTwoLink: activity_button_two_link
|
||||
} = settings.store;
|
||||
|
||||
if (application_id === undefined) return;
|
||||
|
||||
let large_image: string | undefined = undefined;
|
||||
if (e.track.album?.image?.url !== undefined) {
|
||||
large_image = (await ApplicationAssetUtils.fetchAssetIds(application_id, [e.track.album.image.url]))[0];
|
||||
}
|
||||
const activity: Activity = {
|
||||
application_id,
|
||||
name: formatSpotifyString(activity_name, e.track),
|
||||
type: ActivityType.LISTENING,
|
||||
flags: 0,
|
||||
details: formatSpotifyString(activity_details, e.track),
|
||||
state: formatSpotifyString(activity_state, e.track),
|
||||
timestamps: {
|
||||
start: Date.now(),
|
||||
end: (Date.now() + (e.track.duration ?? 0))
|
||||
},
|
||||
assets: {
|
||||
large_image,
|
||||
large_text: formatSpotifyString(activity_large_image_text, e.track)
|
||||
},
|
||||
buttons: [],
|
||||
metadata: {
|
||||
button_urls: []
|
||||
}
|
||||
};
|
||||
if (activity_button_one_text !== "") {
|
||||
activity.buttons![0] = formatSpotifyString(activity_button_one_text, e.track);
|
||||
activity.metadata!.button_urls![0] = formatSpotifyString(activity_button_one_link, e.track);
|
||||
}
|
||||
if (activity_button_two_text !== "") {
|
||||
activity.buttons![1] = formatSpotifyString(activity_button_two_text, e.track);
|
||||
activity.metadata!.button_urls![1] = formatSpotifyString(activity_button_two_link, e.track);
|
||||
}
|
||||
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: activity,
|
||||
socketId: "MusicTitleRPC:Spotify"
|
||||
});
|
||||
|
||||
if (playbackStoppedTimeout) clearTimeout(playbackStoppedTimeout);
|
||||
playbackStoppedTimeout = window.setTimeout(() => {
|
||||
FluxDispatcher.dispatch({
|
||||
type: "LOCAL_ACTIVITY_UPDATE",
|
||||
activity: null,
|
||||
socketId: "MusicTitleRPC:Spotify"
|
||||
});
|
||||
}, (e.track.duration ?? 0) + 5000);
|
||||
},
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Forms.FormTitle tag="h3">Getting the Application ID</Forms.FormTitle>
|
||||
<Forms.FormText variant="text-md/normal">
|
||||
To get your application ID, go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> and create an application.
|
||||
</Forms.FormText>
|
||||
<Forms.FormText variant="text-md/normal">
|
||||
If you already created one for CustomRPC, you can use the same ID here.
|
||||
</Forms.FormText>
|
||||
<br />
|
||||
<Forms.FormTitle tag="h3">For spotify users</Forms.FormTitle>
|
||||
<Forms.FormText variant="text-md/normal">
|
||||
If you use spotify, make sure to disable <code>Settings</code> > <code>Connections</code> > <code>Display Spotify as your status</code>. Keeping it enabled will result in two activities showing up.
|
||||
</Forms.FormText>
|
||||
<br />
|
||||
<Forms.FormText variant="text-md/normal">
|
||||
The new activity this plugin creates will be missing some features (time bar, play on spotify and listen along). This is a compromise, not a bug.
|
||||
</Forms.FormText>
|
||||
<br />
|
||||
<Forms.FormText variant="text-md/normal">
|
||||
Text fields in settings for spotify users will have the following strings replaced:<br />
|
||||
<code>{"{{Title}}"}</code>: The song's title<br />
|
||||
<code>{"{{Album}}"}</code>: The song's album's name<br />
|
||||
<code>{"{{FirstArtist}}"}</code>: The first artist's name<br />
|
||||
<code>{"{{AllArtistsCommaSeparated}}"}</code>: Names of all artists, separated by commas (Artist1, Artist2, Artist3)<br />
|
||||
<code>{"{{AllArtistsGrammarSeparated}}"}</code>: Names of all artists, separated according to english grammar (Artist1, Artist2 and Artist3)<br />
|
||||
<code>{"{{Duration}}"}</code>: The song's duration (formatted mm:ss)<br />
|
||||
<code>{"{{Id}}"}</code>: The track id<br />
|
||||
</Forms.FormText>
|
||||
<br />
|
||||
<Forms.FormTitle tag="h3">If you don't see the activity</Forms.FormTitle>
|
||||
<Forms.FormText variant="text-md/normal">
|
||||
Make sure you enabled <code>Settings</code> > <code>Activity privacy</code> > <code>Share your detected activities with others</code>.
|
||||
</Forms.FormText>
|
||||
</React.Fragment>
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
function formatSpotifyString(input: string, song: SpotifyElement): string {
|
||||
if (input === undefined) return "";
|
||||
|
||||
const durationMinutes: number = Math.trunc(song.duration! / 60000);
|
||||
const durationSeconds: number = Math.trunc(song.duration! / 1000) - durationMinutes * 60;
|
||||
return input
|
||||
.replace("{{Title}}", song.name)
|
||||
.replace("{{Album}}", song.album!.name)
|
||||
.replace("{{FirstArtist}}", song.artists![0].name)
|
||||
.replace("{{AllArtistsCommaSeparated}}", song.artists!.map(artist => artist.name).join(", "))
|
||||
.replace("{{AllArtistsGrammarSeparated}}", song.artists!
|
||||
.slice(0, song.artists!.length > 1 ? -1 : undefined)
|
||||
.map(artist => artist.name)
|
||||
.join(", ")
|
||||
+ (song.artists!.length > 1 ? ` and ${song.artists![song.artists!.length - 1].name}` : ""))
|
||||
.replace("{{Duration}}", durationMinutes + ":" + ("00" + durationSeconds).slice(-2))
|
||||
.replace("{{Id}}", song.id);
|
||||
}
|
|
@ -38,7 +38,7 @@ export default definePlugin({
|
|||
dependencies: ["ChatInputButtonAPI"],
|
||||
settings,
|
||||
start: async () => {
|
||||
const fonts = [{ name: "YurukaStd", url: "https://raw.githubusercontent.com/TheOriginalAyaka/sekai-stickers/main/src/fonts/YurukaStd.woff2" }, { name: "SSFangTangTi", url: "https://raw.githubusercontent.com/TheOriginalAyaka/sekai-stickers/main/src/fonts/ShangShouFangTangTi.woff2" }];
|
||||
const fonts = [{ name: "YurukaStd", url: "https://raw.githubusercontent.com/TheOriginalAyaka/sekai-stickers/47a2ca33b8cb35f59800e8faad48980e4ce5ea71/src/fonts/YurukaStd.woff2" }, { name: "SSFangTangTi", url: "https://raw.githubusercontent.com/TheOriginalAyaka/sekai-stickers/main/src/fonts/ShangShouFangTangTi.woff2" }];
|
||||
if (!IS_FONTS_LOADED) {
|
||||
fonts.map(n => {
|
||||
new FontFace(n.name, `url(${n.url})`).load().then(
|
||||
|
|
111
src/equicordplugins/steamStatusSync/index.tsx
Normal file
111
src/equicordplugins/steamStatusSync/index.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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";
|
||||
|
||||
enum SteamStatus {
|
||||
Online = "online",
|
||||
Away = "away",
|
||||
Invisible = "invisible",
|
||||
Offline = "offline",
|
||||
None = "none"
|
||||
}
|
||||
|
||||
interface SettingsProto {
|
||||
settings: {
|
||||
proto: {
|
||||
status?: {
|
||||
status: {
|
||||
value: String;
|
||||
},
|
||||
showCurrentGame: {
|
||||
value: Boolean;
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
onlineStatus: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Steam status when on Online",
|
||||
options: [
|
||||
{ label: "Online", value: SteamStatus.Online, default: true },
|
||||
{ label: "Away", value: SteamStatus.Away },
|
||||
{ label: "Invisible", value: SteamStatus.Invisible },
|
||||
{ label: "Offline (Disconnect Steam Chat)", value: SteamStatus.Offline },
|
||||
{ label: "Disabled", value: SteamStatus.None }
|
||||
],
|
||||
},
|
||||
idleStatus: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Steam status when on Idle",
|
||||
options: [
|
||||
{ label: "Online", value: SteamStatus.Online },
|
||||
{ label: "Away", value: SteamStatus.Away, default: true },
|
||||
{ label: "Invisible", value: SteamStatus.Invisible },
|
||||
{ label: "Offline (Disconnect Steam Chat)", value: SteamStatus.Offline },
|
||||
{ label: "Disabled", value: SteamStatus.None }
|
||||
],
|
||||
},
|
||||
dndStatus: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Steam status when on Do Not Disturb",
|
||||
options: [
|
||||
{ label: "Online", value: SteamStatus.Online },
|
||||
{ label: "Away", value: SteamStatus.Away },
|
||||
{ label: "Invisible", value: SteamStatus.Invisible },
|
||||
{ label: "Offline (Disconnect Steam Chat)", value: SteamStatus.Offline },
|
||||
{ label: "Disabled", value: SteamStatus.None, default: true }
|
||||
],
|
||||
},
|
||||
invisibleStatus: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Steam status when on Invisible",
|
||||
options: [
|
||||
{ label: "Online", value: SteamStatus.Online },
|
||||
{ label: "Away", value: SteamStatus.Away },
|
||||
{ label: "Invisible", value: SteamStatus.Invisible, default: true },
|
||||
{ label: "Offline (Disconnect Steam Chat)", value: SteamStatus.Offline },
|
||||
{ label: "Disabled", value: SteamStatus.None }
|
||||
],
|
||||
},
|
||||
goInvisibleIfActivityIsHidden: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Always go invisible if hiding game activity on Discord"
|
||||
}
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "SteamStatusSync",
|
||||
description: "Sync your status to Steam!",
|
||||
authors: [EquicordDevs.niko],
|
||||
|
||||
settings,
|
||||
|
||||
flux: {
|
||||
USER_SETTINGS_PROTO_UPDATE(settingsUpdate: SettingsProto) {
|
||||
const protoStatus = settingsUpdate.settings.proto.status;
|
||||
|
||||
if (protoStatus !== undefined) {
|
||||
const steamStatus: SteamStatus = settings.store[`${protoStatus.status.value}Status`];
|
||||
|
||||
if (settings.store.goInvisibleIfActivityIsHidden && !protoStatus.showCurrentGame.value) {
|
||||
open(`steam://friends/status/${SteamStatus.Invisible}`);
|
||||
|
||||
return;
|
||||
}
|
||||
if (steamStatus === SteamStatus.None) { return; }
|
||||
|
||||
// Open steam protocol URI for status change
|
||||
open(`steam://friends/status/${steamStatus}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
66
src/equicordplugins/unitConverter/ConverterAccessory.tsx
Normal file
66
src/equicordplugins/unitConverter/ConverterAccessory.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { classNameFactory } from "@api/Styles";
|
||||
import { useState } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
export const conversions = new Map<string, (conv: string) => void>();
|
||||
const cl = classNameFactory("vc-converter-");
|
||||
function Dismiss({ onDismiss }: { onDismiss: () => void; }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className={cl("dismiss")}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
);
|
||||
}
|
||||
// thanks <@408047304864432139>
|
||||
export function ConvertIcon({ height = 24, width = 24, className }: {
|
||||
height?: number,
|
||||
width?: number,
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 98 98"
|
||||
height={height}
|
||||
width={width}
|
||||
className={[cl("icon"), className].join(" ")}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m50 16.668v-7.4609c0-1.875-2.25-2.7891-3.543-1.457l-11.664 11.625c-0.83594 0.83203-0.83594 2.125 0 2.957l11.625 11.625c1.332 1.293 3.582 0.375 3.582-1.5v-7.457c13.793 0 25 11.207 25 25 0 3.293-0.625 6.5-1.832 9.375-0.625 1.5-0.16797 3.207 0.95703 4.332 2.125 2.125 5.707 1.375 6.832-1.4141 1.543-3.793 2.375-7.9609 2.375-12.293 0-18.418-14.914-33.332-33.332-33.332zm0 58.332c-13.793 0-25-11.207-25-25 0-3.293 0.625-6.5 1.832-9.375 0.625-1.5 0.16797-3.207-0.95703-4.332-2.125-2.125-5.707-1.375-6.832 1.4141-1.543 3.793-2.375 7.9609-2.375 12.293 0 18.418 14.914 33.332 33.332 33.332v7.4609c0 1.875 2.25 2.7891 3.543 1.457l11.625-11.625c0.83203-0.83203 0.83203-2.125 0-2.957l-11.625-11.625c-1.293-1.293-3.543-0.375-3.543 1.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function ConverterAccessory({ message }: { message: Message; }) {
|
||||
const [conversion, setConversion] = useState<string>("");
|
||||
conversions.set(message.id, setConversion);
|
||||
if (!conversion) return null;
|
||||
return (
|
||||
<span className={cl("accessory")}>
|
||||
<ConvertIcon width={16} height={16} />
|
||||
{conversion}
|
||||
{" - "}
|
||||
<Dismiss onDismiss={() => setConversion("")} />
|
||||
</span>
|
||||
);
|
||||
}
|
177
src/equicordplugins/unitConverter/converter.ts
Normal file
177
src/equicordplugins/unitConverter/converter.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { settings } from ".";
|
||||
|
||||
interface regexes {
|
||||
imperial: {
|
||||
[key: string]: {
|
||||
regex: RegExp,
|
||||
convert: (...groups: string[]) => string;
|
||||
};
|
||||
};
|
||||
metric: {
|
||||
[key: string]: {
|
||||
regex: RegExp,
|
||||
convert: (...groups: string[]) => string;
|
||||
};
|
||||
};
|
||||
}
|
||||
// TODO: add grams, kilograms, ounces, and pounds
|
||||
const regexes: regexes = {
|
||||
// matches imperial units, converts them to metric
|
||||
imperial: {
|
||||
farenheight: {
|
||||
regex: /(-?\d+(?:\.\d+)?)°?(f)(?!\w)/ig,
|
||||
convert(...groups) {
|
||||
const c = ((parseFloat(groups[1]) - 32) * (5 / 9)).toFixed(2);
|
||||
return `${c}°C`;
|
||||
},
|
||||
},
|
||||
// feetMark: {
|
||||
// regex: /(\d+(?:\.\d+))(')(?!(\d+(?:\.\d+)?(''|")|'))/g,
|
||||
// convert(...groups) {
|
||||
// },
|
||||
// },
|
||||
// leaving this one in because it is commonly used for something like 5'9'' for a persons height
|
||||
feetInchesMark: {
|
||||
regex: /(\d+)(') ?(\d+(?:\.\d+)?)("|'')?/g,
|
||||
convert(...groups) {
|
||||
let ftin = parseFloat(groups[1]) / 3.281;
|
||||
ftin += parseFloat(groups[3]) / 39.37;
|
||||
return `${ftin.toFixed(2)}m`;
|
||||
},
|
||||
},
|
||||
// inchesMark: {
|
||||
// regex: /(?<!\d+')(\d+(?:\.\d+)?)("|'')/g,
|
||||
// convert(...groups) {
|
||||
// },
|
||||
// },
|
||||
feetWord: {
|
||||
regex: /(\d+(?:\.\d+)?) *(f(ee)?t)(?! *\d)/ig,
|
||||
convert(...groups) {
|
||||
const ft = (parseFloat(groups[1]) / 3.281).toFixed(2);
|
||||
return `${ft}m`;
|
||||
},
|
||||
},
|
||||
inchesWord: {
|
||||
regex: /(?<!\d+ *(?:f(?:ee|oo)?t) *)(\d+(?:\.\d+)?) *(in(?:ches?)?)/ig,
|
||||
convert(...groups) {
|
||||
const inches = (parseFloat(groups[1]) / 2.54).toFixed(2);
|
||||
return `${inches}cm`;
|
||||
},
|
||||
},
|
||||
feetInchesWord: {
|
||||
regex: /(\d+) *(f(?:ee|oo)?t) *(\d+(?:\.\d+)?) *(in(?:ches?)?)/ig,
|
||||
convert(...groups) {
|
||||
let ftin = parseFloat(groups[1]) / 3.281;
|
||||
ftin += parseFloat(groups[3]) / 39.37;
|
||||
return `${ftin.toFixed(2)}m`;
|
||||
},
|
||||
},
|
||||
poundWord: {
|
||||
regex: /(\d+(?:\.\d+)?) *(lbs?|pounds?)(?! ?\d)/ig,
|
||||
convert(...groups: string[]) {
|
||||
const lbs = (parseFloat(groups[1]) / 2.205).toFixed(2);
|
||||
return `${lbs}kg`;
|
||||
},
|
||||
},
|
||||
poundOunceWord: {
|
||||
regex: /(\d+(?:\.\d+)?) *(lbs?|pounds?) *(\d+(?:\.\d+)?) *(ozs?|ounces?)/ig,
|
||||
convert(...groups) {
|
||||
let lbs = (parseInt(groups[1]) / 2.205);
|
||||
lbs += (parseFloat(groups[2]) / 35.274);
|
||||
return `${lbs.toFixed(2)}kg`;
|
||||
}
|
||||
},
|
||||
ounceWord: {
|
||||
regex: /(\d+(?:\.\d+)?) ?(ounces?|oz)/gi,
|
||||
convert(...groups) {
|
||||
const ozs = (parseFloat(groups[1]) * 28.35).toFixed(2);
|
||||
return `${ozs}g`;
|
||||
},
|
||||
},
|
||||
milesPerHour: {
|
||||
regex: /(\d+(?:\.\d+)?) ?(m(?:p|\/)h)/gi,
|
||||
convert(...groups) {
|
||||
const mph = (parseFloat(groups[1]) * 1.609).toFixed(2);
|
||||
return `${mph}km/h`;
|
||||
},
|
||||
}
|
||||
},
|
||||
// matches metric untis, converts them into imperial
|
||||
metric: {
|
||||
// i dont think people ever write metric units as 1m3cm or something like that
|
||||
celcius: {
|
||||
regex: /(-?\d+(?:\.\d+)?) ?°?(c)(?!\w)/ig,
|
||||
convert(...groups) {
|
||||
const f = ((parseFloat(groups[1]) * (9 / 5)) + 32).toFixed(2);
|
||||
return `${f}°F`;
|
||||
}
|
||||
},
|
||||
// convert to inches
|
||||
centimeters: {
|
||||
regex: /(\d+(?:\.\d+)?) *(cm)(?!\w)/ig,
|
||||
convert(...groups) {
|
||||
const cm = (parseFloat(groups[1]) / 2.54).toFixed(2);
|
||||
return `${cm}in`;
|
||||
},
|
||||
},
|
||||
// convert to feet
|
||||
meters: {
|
||||
regex: /(\d+(?:\.\d+)?) *(m)(?!\w)/ig,
|
||||
convert(...groups) {
|
||||
const m = parseFloat((parseFloat(groups[1]) * 3.821).toFixed(2));
|
||||
if (Number.isInteger(m))
|
||||
return `${m}ft`;
|
||||
return `${m.toFixed(0)}ft${((m % 1) * 12).toFixed(2)}in`;
|
||||
},
|
||||
},
|
||||
// covnert to miles
|
||||
kilometers: {
|
||||
regex: /(\d+(?:\.\d+)?) *(km)(?!\w)/ig,
|
||||
convert(...groups) {
|
||||
const m = (parseFloat(groups[1]) / 1.609).toFixed(2);
|
||||
return `${m}mi`;
|
||||
},
|
||||
},
|
||||
grams: {
|
||||
regex: /(\d+(?:\.\d+)?) ?(grams?|g)/gi,
|
||||
convert(...groups) {
|
||||
const g = (parseFloat(groups[1]) / 28.35).toFixed(2);
|
||||
return `${g}oz(s)`;
|
||||
},
|
||||
},
|
||||
kilograms: {
|
||||
regex: /(\d+(?:\.\d+)?) ?(kg|kilos?)/gi,
|
||||
convert(...groups) {
|
||||
const kg = (parseFloat(groups[1]) * 2.205).toFixed(2);
|
||||
return `${kg}lb(s)`;
|
||||
},
|
||||
},
|
||||
kilometersPerHour: {
|
||||
regex: /(\d+(?:\.\d+)?) ?(km?p?\/?h)/gi,
|
||||
convert(...groups) {
|
||||
const kph = (parseFloat(groups[1]) / 1.609).toFixed(2);
|
||||
return `${kph}mph`;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
export function convert(message: string): string {
|
||||
let newMessage = message;
|
||||
if (settings.store.myUnits === "imperial") {
|
||||
for (const unit in regexes.metric) {
|
||||
newMessage = newMessage.replaceAll(regexes.metric[unit].regex, regexes.metric[unit].convert);
|
||||
}
|
||||
} else {
|
||||
for (const unit in regexes.imperial) {
|
||||
newMessage = newMessage.replaceAll(regexes.imperial[unit].regex, regexes.imperial[unit].convert);
|
||||
}
|
||||
}
|
||||
return newMessage;
|
||||
}
|
77
src/equicordplugins/unitConverter/index.tsx
Normal file
77
src/equicordplugins/unitConverter/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { addAccessory } from "@api/MessageAccessories";
|
||||
import { addButton } from "@api/MessagePopover";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { ChannelStore } from "@webpack/common";
|
||||
|
||||
import { convert } from "./converter";
|
||||
import { conversions, ConverterAccessory, ConvertIcon } from "./ConverterAccessory";
|
||||
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
myUnits: {
|
||||
type: OptionType.SELECT,
|
||||
description: "the units you use and want things converted to. defaults to imperial",
|
||||
options: [
|
||||
{
|
||||
default: true,
|
||||
label: "Imperial",
|
||||
value: "imperial",
|
||||
},
|
||||
{
|
||||
label: "Metric",
|
||||
value: "metric"
|
||||
}
|
||||
]
|
||||
},
|
||||
// invert: {
|
||||
// type: OptionType.BOOLEAN,
|
||||
// default: false,
|
||||
// // is there a better way to word this?
|
||||
// description: "If this option is set, ignore the units you set and invert every conversion."
|
||||
// }
|
||||
});
|
||||
export default definePlugin({
|
||||
name: "UnitConverter",
|
||||
description: "Converts metric units to Imperal units and vice versa",
|
||||
authors: [EquicordDevs.sadan],
|
||||
start() {
|
||||
addAccessory("vc-converter", props => <ConverterAccessory message={props.message} />);
|
||||
addButton("vc-converter", message => {
|
||||
if (!message.content) return null;
|
||||
return {
|
||||
label: "Convert Units",
|
||||
icon: ConvertIcon,
|
||||
message,
|
||||
channel: ChannelStore.getChannel(message.channel_id),
|
||||
onClick: async () => {
|
||||
const setConversion = conversions.get(message.id);
|
||||
if (!setConversion) return;
|
||||
setConversion(convert(message.content));
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
settings,
|
||||
});
|
24
src/equicordplugins/unitConverter/style.css
Normal file
24
src/equicordplugins/unitConverter/style.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.vc-converter-dismiss {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
color: var(--text-link);
|
||||
}
|
||||
|
||||
.vc-converter-dismiss:is(:hover, :focus) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.vc-converter-accessory {
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5em;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.vc-converter-accessory svg {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.vc-converter-chat-button {
|
||||
scale: 1.085;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
# OpenInApp
|
||||
|
||||
Open links in their respective apps instead of your browser
|
||||
|
||||
## Currently supports:
|
||||
|
||||
- Spotify
|
||||
- Steam
|
||||
- EpicGames
|
||||
- Tidal
|
||||
- Apple Music (iTunes)
|
|
@ -1,14 +0,0 @@
|
|||
# XSOverlay Notifier
|
||||
|
||||
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
|
||||
|
||||
## Preview
|
||||
|
||||
![Resulting notification inside XSOverlay](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
|
||||
|
||||
![Test notification inside XSOverlay](https://github.com/user-attachments/assets/d3b0c387-1d67-4697-a470-d4a927e228f4)
|
||||
|
||||
## Usage
|
||||
- Enable this plugin
|
||||
- Set port and plugin settings as desired (defaults should work fine)
|
||||
- Open SteamVR and XSOverlay
|
|
@ -723,6 +723,18 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "keifufu",
|
||||
id: 469588398110146590n
|
||||
},
|
||||
Blackilykat: {
|
||||
name: "Blackilykat",
|
||||
id: 442033332952498177n
|
||||
},
|
||||
niko: {
|
||||
name: "niko",
|
||||
id: 341377368075796483n
|
||||
},
|
||||
sadan: {
|
||||
name: "sadan",
|
||||
id: 521819891141967883n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
Loading…
Reference in a new issue