mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-07 05:42:55 -04:00
Merge branch 'dev' into dev2
This commit is contained in:
commit
ae0163d366
72 changed files with 1264 additions and 620 deletions
10
README.md
10
README.md
|
@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
### Extra included plugins
|
||||
|
||||
<details>
|
||||
<summary>175 additional plugins</summary>
|
||||
<summary>184 additional plugins</summary>
|
||||
|
||||
### All Platforms
|
||||
|
||||
|
@ -41,6 +41,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
- CommandPalette by Ethan
|
||||
- CopyStickerLinks by Byeoon
|
||||
- CopyUserMention by Cortex & castdrian
|
||||
- CustomFolderIcons by sadan
|
||||
- CustomSounds by TheKodeToad & SpikeHD
|
||||
- CustomTimestamps by Rini & nvhrr
|
||||
- CustomUserColors by mochienya
|
||||
|
@ -105,13 +106,17 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
- Meow by Samwich
|
||||
- MessageBurst by port
|
||||
- MessageColors by Hen
|
||||
- MessageFetchTimer by GroupXyz
|
||||
- MessageLinkTooltip by Kyuuhachi
|
||||
- MessageLoggerEnhanced by Aria
|
||||
- MessageTranslate by Samwich
|
||||
- ModalFade by Kyuuhachi
|
||||
- MoreCommands by Arjix, echo, Samu
|
||||
- MoreKaomoji by JacobTm & voidbbg
|
||||
- MoreStickers by Leko & Arjix
|
||||
- MoreUserTags by Cyn, TheSun, RyanCaoDev, LordElias, AutumnVN, hen
|
||||
- Morse by zyqunix
|
||||
- Moyai by Megu & Nuckyz
|
||||
- NeverPausePreviews by vappstar
|
||||
- NewPluginsManager by Sqaaakoi
|
||||
- NoAppsAllowed by kvba
|
||||
|
@ -123,6 +128,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
- NoOnboarding by omaw & Glitch
|
||||
- NoRoleHeaders by Samwich
|
||||
- NotificationTitle by Kyuuhachi
|
||||
- PartyMode by UwUDev
|
||||
- PingNotifications by smuki
|
||||
- PinIcon by iamme
|
||||
- PlatformSpoofer by Drag
|
||||
|
@ -154,6 +160,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
- StatusPresets by iamme
|
||||
- SteamStatusSync by niko
|
||||
- StickerBlocker by Samwich
|
||||
- StreamingCodecDisabler by davidkra230
|
||||
- TalkInReverse by Tolgchu
|
||||
- TeX by Kyuuhachi
|
||||
- TextToSpeech by Samwich
|
||||
|
@ -198,6 +205,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch
|
|||
|
||||
- ClipsEnhancements by niko
|
||||
- MediaDownloader by Colorman
|
||||
- NoRPC by Cyn
|
||||
- StatusWhilePlaying by thororen
|
||||
|
||||
### Equicord Devbuilds Only
|
||||
|
|
|
@ -48,7 +48,7 @@ export function _modifyAccessories(
|
|||
) {
|
||||
for (const [key, accessory] of accessories.entries()) {
|
||||
const res = (
|
||||
<ErrorBoundary message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
|
||||
<accessory.render {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -236,7 +236,7 @@ export function migratePluginSettings(name: string, ...oldNames: string[]) {
|
|||
}
|
||||
}
|
||||
|
||||
export function migratePluginSetting(pluginName: string, oldSetting: string, newSetting: string) {
|
||||
export function migratePluginSetting(pluginName: string, newSetting: string, oldSetting: string) {
|
||||
const settings = SettingsStore.plain.plugins[pluginName];
|
||||
if (!settings) return;
|
||||
|
||||
|
|
|
@ -75,10 +75,15 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
logger.error(`${this.props.message || "A component threw an Error"}\n`, error, errorInfo.componentStack);
|
||||
}
|
||||
|
||||
get isNoop() {
|
||||
if (IS_DEV) return false;
|
||||
return this.props.noop;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error === NO_ERROR) return this.props.children;
|
||||
|
||||
if (this.props.noop) return null;
|
||||
if (this.isNoop) return null;
|
||||
|
||||
if (this.props.fallback)
|
||||
return (
|
||||
|
|
|
@ -4,4 +4,8 @@
|
|||
border: 1px solid #e78284;
|
||||
border-radius: 5px;
|
||||
color: var(--text-normal, white);
|
||||
|
||||
& a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,9 @@ export function Link(props: React.PropsWithChildren<Props>) {
|
|||
props.style.pointerEvents = "none";
|
||||
props["aria-disabled"] = true;
|
||||
}
|
||||
|
||||
props.rel ??= "noreferrer";
|
||||
|
||||
return (
|
||||
<a role="link" target="_blank" {...props}>
|
||||
{props.children}
|
||||
|
|
|
@ -270,7 +270,7 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti
|
|||
{!!plugin.settingsAboutComponent && (
|
||||
<div className={classes(Margins.bottom8, "vc-text-selectable")}>
|
||||
<Forms.FormSection>
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom InfoComponent">
|
||||
<ErrorBoundary message="An error occurred while rendering this plugin's custom Info Component">
|
||||
<plugin.settingsAboutComponent tempSettings={tempSettings} />
|
||||
</ErrorBoundary>
|
||||
</Forms.FormSection>
|
||||
|
|
|
@ -244,7 +244,7 @@ export default function PluginSettings() {
|
|||
}));
|
||||
}, []);
|
||||
|
||||
const depMap = React.useMemo(() => {
|
||||
const depMap = useMemo(() => {
|
||||
const o = {} as Record<string, string[]>;
|
||||
for (const plugin in Plugins) {
|
||||
const deps = Plugins[plugin].dependencies;
|
||||
|
|
|
@ -16,8 +16,8 @@ import { settings } from "../settings";
|
|||
import { ActivityListIcon, ActivityListProps, ApplicationIcon, IconCSSProperties } from "../types";
|
||||
import { cl, getApplicationIcons } from "../utils";
|
||||
|
||||
// if discord one day decides to change their icon this needs to be updated
|
||||
const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
|
||||
// Discord no longer shows an icon here by default but we use the one from the popout now here
|
||||
const DefaultActivityIcon = findComponentByCodeLazy("M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V5a3 3 0 0 0-3-3H5Zm6.81 7c-.54 0-1 .26-1.23.61A1 1 0 0 1 8.92 8.5 3.49 3.49 0 0 1 11.82 7c1.81 0 3.43 1.38 3.43 3.25 0 1.45-.98 2.61-2.27 3.06a1 1 0 0 1-1.96.37l-.19-1a1 1 0 0 1 .98-1.18c.87 0 1.44-.63 1.44-1.25S12.68 9 11.81 9ZM13 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7-10.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM18.5 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM7 18.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM5.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z");
|
||||
|
||||
export function patchActivityList({ activities, user, hideTooltip }: ActivityListProps): JSX.Element | null {
|
||||
const icons: ActivityListIcon[] = [];
|
||||
|
@ -86,7 +86,7 @@ export function patchActivityList({ activities, user, hideTooltip }: ActivityLis
|
|||
// We need to filter out custom statuses
|
||||
const shouldShow = activities.filter(a => a.type !== 4).length !== icons.length;
|
||||
if (shouldShow) {
|
||||
return <DefaultActivityIcon />;
|
||||
return <DefaultActivityIcon size="xs" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
124
src/equicordplugins/customFolderIcons/components.tsx
Normal file
124
src/equicordplugins/customFolderIcons/components.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 sadan
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { closeModal, ModalContent, ModalHeader, ModalRoot, openModalLazy } from "@utils/modal";
|
||||
import { Button, Menu, Slider, TextInput, useState } from "@webpack/common";
|
||||
|
||||
import { folderIconsData, settings } from "./settings";
|
||||
import { folderProp, int2rgba, setFolderData } from "./util";
|
||||
|
||||
export function ImageModal(folderProps: folderProp) {
|
||||
const [data, setData] = useState(((settings.store.folderIcons ?? {}) as folderIconsData)[folderProps.folderId]?.url ?? "");
|
||||
const [size, setSize] = useState(100);
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
// this looks like a horrorshow
|
||||
defaultValue={data}
|
||||
onChange={(val, _n) => {
|
||||
setData(val);
|
||||
}}
|
||||
placeholder="https://example.com/image.png"
|
||||
>
|
||||
</TextInput>
|
||||
<RenderPreview folderProps={folderProps} url={data} size={size} />
|
||||
{data && <>
|
||||
<div style={{
|
||||
color: "#FFF"
|
||||
}}>Change the size of the folder icon</div>
|
||||
<Slider
|
||||
initialValue={100}
|
||||
onValueChange={(v: number) => {
|
||||
setSize(v);
|
||||
}}
|
||||
maxValue={200}
|
||||
minValue={25}
|
||||
// [25, 200]
|
||||
markers={Array.apply(0, Array(176)).map((_, i) => i + 25)}
|
||||
stickToMarkers={true}
|
||||
keyboardStep={1}
|
||||
renderMarker={() => null} />
|
||||
</>}
|
||||
<Button onClick={() => {
|
||||
setFolderData(folderProps, {
|
||||
url: data,
|
||||
size: size
|
||||
});
|
||||
closeModal("custom-folder-icon");
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<hr />
|
||||
<Button onClick={() => {
|
||||
// INFO: unset button
|
||||
const folderSettings = settings.store.folderIcons as folderIconsData;
|
||||
if (folderSettings[folderProps.folderId]) {
|
||||
folderSettings[folderProps.folderId] = null;
|
||||
}
|
||||
closeModal("custom-folder-icon");
|
||||
}}>
|
||||
Unset
|
||||
</Button>
|
||||
<hr />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export function RenderPreview({ folderProps, url, size }: { folderProps: folderProp; url: string; size: number; }) {
|
||||
if (!url) return null;
|
||||
return (
|
||||
<div className="test1234" style={{
|
||||
width: "20vh",
|
||||
height: "20vh",
|
||||
overflow: "hidden",
|
||||
// 16/48
|
||||
borderRadius: "33%",
|
||||
backgroundColor: int2rgba(folderProps.folderColor, 0.4),
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
<img alt="" src={url} width={`${size}%`} height={`${size}%`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function makeContextItem(a: folderProp) {
|
||||
return (
|
||||
<Menu.MenuItem
|
||||
id="custom-folder-icons"
|
||||
key="custom-folder-icons"
|
||||
label="Change Icon"
|
||||
action={() => {
|
||||
openModalLazy(async () => {
|
||||
return props => (
|
||||
<ModalRoot {...props}>
|
||||
<ModalHeader >
|
||||
<div style={{
|
||||
color: "white"
|
||||
}}>
|
||||
Set a New Icon.
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<ImageModal folderId={a.folderId} folderColor={a.folderColor} />
|
||||
</ModalContent>
|
||||
<div style={{
|
||||
color: "white",
|
||||
margin: "2.5%",
|
||||
marginTop: "1%"
|
||||
}}>
|
||||
You might have to hover the folder after setting in order for it to refresh.
|
||||
</div>
|
||||
</ModalRoot>
|
||||
);
|
||||
},
|
||||
{
|
||||
modalKey: "custom-folder-icon"
|
||||
});
|
||||
}} />
|
||||
);
|
||||
}
|
58
src/equicordplugins/customFolderIcons/index.tsx
Normal file
58
src/equicordplugins/customFolderIcons/index.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 sadan
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
import { makeContextItem } from "./components";
|
||||
import { folderIconsData, settings } from "./settings";
|
||||
import { folderProp, int2rgba } from "./util";
|
||||
|
||||
export default definePlugin({
|
||||
name: "CustomFolderIcons",
|
||||
description: "Customize folder icons with any png",
|
||||
authors: [EquicordDevs.sadan],
|
||||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: ".folderButtonInner",
|
||||
replacement: {
|
||||
match: /(\(0,\i\.jsx\)\(\i,\{folderNode:(\i),hovered:\i,sorting:\i\}\))/,
|
||||
replace: "($self.shouldReplace({folderNode:$2})?$self.replace({folderNode:$2}):$1)"
|
||||
}
|
||||
},
|
||||
],
|
||||
contextMenus: {
|
||||
"guild-context": (menuItems, props: folderProp) => {
|
||||
if (!("folderId" in props)) return;
|
||||
menuItems.push(makeContextItem(props));
|
||||
}
|
||||
},
|
||||
shouldReplace(props: any): boolean {
|
||||
return !!((settings.store.folderIcons as folderIconsData)?.[props.folderNode.id]?.url);
|
||||
},
|
||||
replace(props: any) {
|
||||
const folderSettings = (settings.store.folderIcons as folderIconsData);
|
||||
if (folderSettings && folderSettings[props.folderNode.id]) {
|
||||
const data = folderSettings[props.folderNode.id];
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: int2rgba(props.folderNode.color, +settings.store.solidIcon || .4),
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<img alt="" src={data!.url} width={`${data!.size ?? 100}%`} height={`${data!.size ?? 100}%`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
28
src/equicordplugins/customFolderIcons/settings.tsx
Normal file
28
src/equicordplugins/customFolderIcons/settings.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 sadan
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
export interface folderIcon {
|
||||
url: string,
|
||||
size: number,
|
||||
}
|
||||
export type folderIconsData = Record<string, folderIcon | null>;
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
solidIcon: {
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false,
|
||||
description: "Use a solid background on the background of the image"
|
||||
},
|
||||
folderIcons: {
|
||||
type: OptionType.COMPONENT,
|
||||
hidden: true,
|
||||
description: "folder icon settings",
|
||||
component: () => <></>
|
||||
}
|
||||
});
|
31
src/equicordplugins/customFolderIcons/util.tsx
Normal file
31
src/equicordplugins/customFolderIcons/util.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 sadan
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { folderIcon, folderIconsData, settings } from "./settings";
|
||||
|
||||
export async function setFolderData(props: folderProp, newData: folderIcon) {
|
||||
if (!settings.store.folderIcons) {
|
||||
settings.store.folderIcons = {};
|
||||
}
|
||||
const folderSettings = (settings.store.folderIcons as folderIconsData);
|
||||
folderSettings[props.folderId] = newData;
|
||||
}
|
||||
|
||||
export interface folderProp {
|
||||
folderId: string;
|
||||
folderColor: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rgbVal RGB value
|
||||
* @param alpha alpha bewteen zero and 1
|
||||
*/
|
||||
export function int2rgba(rgbVal: number, alpha: number = 1) {
|
||||
const b = rgbVal & 0xFF,
|
||||
g = (rgbVal & 0xFF00) >>> 8,
|
||||
r = (rgbVal & 0xFF0000) >>> 16;
|
||||
return `rgba(${[r, g, b].join(",")},${alpha})`;
|
||||
}
|
|
@ -147,7 +147,7 @@ function VencordPopoutButton() {
|
|||
function ToolboxFragmentWrapper({ children }: { children: ReactNode[]; }) {
|
||||
children.splice(
|
||||
children.length - 1, 0,
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<VencordPopoutButton />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -9,9 +9,14 @@ import { GuildStore } from "@webpack/common";
|
|||
import { RC } from "@webpack/types";
|
||||
import { Channel, Guild, Message, User } from "discord-types/general";
|
||||
|
||||
import { settings } from "./settings";
|
||||
import type { ITag } from "./types";
|
||||
|
||||
export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot();
|
||||
export const isWebhook = (message: Message, user: User) => {
|
||||
const isFollowed = message?.type === 0 && !!message?.messageReference && !settings.store.showWebhookTagFully;
|
||||
return !!message?.webhookId && user.isNonUserBot() && !isFollowed;
|
||||
};
|
||||
|
||||
export const tags = [
|
||||
{
|
||||
name: "WEBHOOK",
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import "./styles.css";
|
||||
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { getCurrentChannel, getIntlMessage } from "@utils/discord";
|
||||
|
@ -31,11 +32,13 @@ const genTagTypes = () => {
|
|||
return obj;
|
||||
};
|
||||
|
||||
migratePluginSettings("ExpandedUserTags", "MoreUserTags");
|
||||
export default definePlugin({
|
||||
name: "MoreUserTags",
|
||||
name: "ExpandedUserTags",
|
||||
description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)",
|
||||
authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, EquicordDevs.Hen],
|
||||
dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"],
|
||||
tags: ["MoreUserTags"],
|
||||
settings,
|
||||
patches: [
|
||||
// Make discord actually use our tags
|
||||
|
@ -73,7 +76,7 @@ export default definePlugin({
|
|||
renderNicknameIcon(props) {
|
||||
const tagId = this.getTag({
|
||||
user: UserStore.getUser(props.userId),
|
||||
channel: ChannelStore.getChannel(this.getChannelId()),
|
||||
channel: getCurrentChannel(),
|
||||
channelId: this.getChannelId(),
|
||||
isChat: false
|
||||
});
|
||||
|
@ -89,7 +92,7 @@ export default definePlugin({
|
|||
message: props.message,
|
||||
user: UserStore.getUser(props.message.author.id),
|
||||
channelId: props.message.channel_id,
|
||||
isChat: false
|
||||
isChat: true
|
||||
});
|
||||
|
||||
return tagId && <Tag
|
||||
|
@ -143,9 +146,9 @@ export default definePlugin({
|
|||
const perms = this.getPermissions(user, channel);
|
||||
|
||||
for (const tag of tags) {
|
||||
if (isChat && !settings.tagSettings[tag.name].showInChat)
|
||||
if (isChat && !settings.tagSettings[tag.name]?.showInChat)
|
||||
continue;
|
||||
if (!isChat && !settings.tagSettings[tag.name].showInNotChat)
|
||||
if (!isChat && !settings.tagSettings[tag.name]?.showInNotChat)
|
||||
continue;
|
||||
|
||||
// If the owner tag is disabled, and the user is the owner of the guild,
|
||||
|
@ -156,7 +159,9 @@ export default definePlugin({
|
|||
user.id &&
|
||||
isChat &&
|
||||
!settings.tagSettings.OWNER.showInChat) ||
|
||||
(!isChat &&
|
||||
(GuildStore.getGuild(channel?.guild_id)?.ownerId ===
|
||||
user.id &&
|
||||
!isChat &&
|
||||
!settings.tagSettings.OWNER.showInNotChat)
|
||||
)
|
||||
continue;
|
|
@ -14,7 +14,7 @@ import { TagSettings } from "./types";
|
|||
|
||||
function SettingsComponent() {
|
||||
const tagSettings = settings.store.tagSettings as TagSettings;
|
||||
const { localTags } = Vencord.Plugins.plugins.MoreUserTags as any;
|
||||
const { localTags } = Vencord.Plugins.plugins.ExpandedUserTags as any;
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
|
@ -96,9 +96,14 @@ export const settings = definePluginSettings({
|
|||
default: false,
|
||||
restartNeeded: true
|
||||
},
|
||||
showWebhookTagFully: {
|
||||
description: "Show Webhook tag in followed channels like announcements",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
tagSettings: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: SettingsComponent,
|
||||
description: "fill me"
|
||||
}
|
||||
},
|
||||
});
|
|
@ -24,8 +24,6 @@ export const reverseExtensionMap = Object.entries(extensionMap).reduce((acc, [ta
|
|||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
type ExtUpload = Upload & { fixExtension?: boolean; };
|
||||
|
||||
export default definePlugin({
|
||||
name: "FixFileExtensions",
|
||||
authors: [EquicordDevs.thororen],
|
||||
|
@ -34,24 +32,21 @@ export default definePlugin({
|
|||
patches: [
|
||||
// Taken from AnonymiseFileNames
|
||||
{
|
||||
find: 'type:"UPLOAD_START"',
|
||||
replacement: {
|
||||
match: /await \i\.uploadFiles\((\i),/,
|
||||
replace: "$1.forEach($self.fixExt),$&"
|
||||
},
|
||||
find: "async uploadFiles(",
|
||||
replacement: [
|
||||
{
|
||||
match: /async uploadFiles\((\i),\i\){/,
|
||||
replace: "$&$1.forEach($self.fixExt);"
|
||||
},
|
||||
{
|
||||
match: /async uploadFilesSimple\((\i)\){/,
|
||||
replace: "$&$1.forEach($self.fixExt);"
|
||||
}
|
||||
],
|
||||
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
|
||||
},
|
||||
// Also taken from AnonymiseFileNames
|
||||
{
|
||||
find: 'addFilesTo:"message.attachments"',
|
||||
replacement: {
|
||||
match: /(\i.uploadFiles\((\i),)/,
|
||||
replace: "$2.forEach(f=>f.filename=$self.fixExt(f)),$1",
|
||||
},
|
||||
predicate: () => !Settings.plugins.AnonymiseFileNames.enabled,
|
||||
}
|
||||
],
|
||||
fixExt(upload: ExtUpload) {
|
||||
fixExt(upload: Upload) {
|
||||
const file = upload.filename;
|
||||
const tarMatch = tarExtMatcher.exec(file);
|
||||
const extIdx = tarMatch?.index ?? file.lastIndexOf(".");
|
||||
|
|
|
@ -24,7 +24,7 @@ export default async (
|
|||
}: FurudoSettings,
|
||||
repliedMessage?: Message
|
||||
): Promise<string> => {
|
||||
const completion = await fetch("http://localhost:11434/api/chat", {
|
||||
const completion = await fetch("http://127.0.0.1:11434/api/chat", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
@ -30,7 +30,7 @@ export const settings = definePluginSettings({
|
|||
},
|
||||
showInMiniProfile: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Only show a button in the mini profile",
|
||||
description: "Show full ui in the mini profile instead of just a button",
|
||||
default: true
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,60 +4,31 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByCode, findByProps } from "@webpack";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, UserStore } from "@webpack/common";
|
||||
import { VoiceState } from "@webpack/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
streamType: {
|
||||
description: "Stream screen or window",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Screen", value: "screen" },
|
||||
{ label: "Window", value: "window" }
|
||||
],
|
||||
default: "screen"
|
||||
},
|
||||
streamWindowKeyword: {
|
||||
description: "Keyword to search for in window title",
|
||||
type: OptionType.STRING,
|
||||
default: "",
|
||||
placeholder: "Enter keyword"
|
||||
}
|
||||
});
|
||||
import { getCurrentMedia, settings } from "./utils";
|
||||
|
||||
const startStream = findByCodeLazy('type:"STREAM_START"');
|
||||
|
||||
let hasStreamed;
|
||||
let sources;
|
||||
let source;
|
||||
|
||||
async function startStream() {
|
||||
const startStream = findByCode('type:"STREAM_START"');
|
||||
const mediaEngine = findByProps("getMediaEngine").getMediaEngine();
|
||||
const getDesktopSources = findByCode("desktop sources");
|
||||
async function autoStartStream() {
|
||||
const selected = SelectedChannelStore.getVoiceChannelId();
|
||||
if (!selected) return;
|
||||
const channel = ChannelStore.getChannel(selected);
|
||||
|
||||
if (channel.type === 13 || !PermissionStore.can(PermissionsBits.STREAM, channel)) return;
|
||||
|
||||
if (settings.store.streamType === "screen") {
|
||||
sources = await getDesktopSources(mediaEngine, ["screen"], null);
|
||||
source = sources[0];
|
||||
} else if (settings.store.streamType === "window") {
|
||||
const keyword = settings.store.streamWindowKeyword?.toLowerCase();
|
||||
sources = await getDesktopSources(mediaEngine, ["window", "application"], null);
|
||||
source = sources.find(s => s.name?.toLowerCase().includes(keyword));
|
||||
}
|
||||
|
||||
if (!source) return;
|
||||
const streamMedia = await getCurrentMedia();
|
||||
|
||||
startStream(channel.guild_id, selected, {
|
||||
"pid": null,
|
||||
"sourceId": source.id,
|
||||
"sourceName": source.name,
|
||||
"sourceId": streamMedia.id,
|
||||
"sourceName": streamMedia.name,
|
||||
"audioSourceId": null,
|
||||
"sound": true,
|
||||
"previewDisabled": false
|
||||
|
@ -78,7 +49,7 @@ export default definePlugin({
|
|||
|
||||
if (channelId && !hasStreamed) {
|
||||
hasStreamed = true;
|
||||
await startStream();
|
||||
await autoStartStream();
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
|
|
102
src/equicordplugins/instantScreenshare/utils.tsx
Normal file
102
src/equicordplugins/instantScreenshare/utils.tsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { OptionType } from "@utils/types";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { Forms, SearchableSelect, useEffect, useState } from "@webpack/common";
|
||||
|
||||
interface PickerProps {
|
||||
streamMediaSelection: any[];
|
||||
streamMedia: any[];
|
||||
}
|
||||
|
||||
const mediaEngine = findByPropsLazy("getMediaEngine");
|
||||
const getDesktopSources = findByCodeLazy("desktop sources");
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
streamMedia: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: SettingSection,
|
||||
},
|
||||
});
|
||||
|
||||
export async function getCurrentMedia() {
|
||||
const media = mediaEngine.getMediaEngine();
|
||||
const sources = [
|
||||
...(await getDesktopSources(media, ["screen"], null) ?? []),
|
||||
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
|
||||
];
|
||||
const streamMedia = sources.find(screen => screen.id === settings.store.streamMedia);
|
||||
console.log(sources);
|
||||
|
||||
if (streamMedia) return streamMedia;
|
||||
|
||||
new Logger("InstantScreenShare").error(`Stream Media "${settings.store.streamMedia}" not found. Resetting to default.`);
|
||||
|
||||
settings.store.streamMedia = sources[0];
|
||||
return sources[0];
|
||||
}
|
||||
|
||||
function StreamSimplePicker({ streamMediaSelection, streamMedia }: PickerProps) {
|
||||
const options = streamMediaSelection.map(screen => ({
|
||||
label: screen.name,
|
||||
value: screen.id,
|
||||
default: streamMediaSelection[0],
|
||||
}));
|
||||
|
||||
return (
|
||||
<SearchableSelect
|
||||
placeholder="Select a media source to stream "
|
||||
maxVisibleItems={5}
|
||||
options={options}
|
||||
value={options.find(o => o.value === streamMedia)}
|
||||
onChange={v => settings.store.streamMedia = v}
|
||||
closeOnSelect
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ScreenSetting() {
|
||||
const { streamMedia } = settings.use(["streamMedia"]);
|
||||
const media = mediaEngine.getMediaEngine();
|
||||
const [streamMediaSelection, setStreamMediaSelection] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
async function fetchMedia() {
|
||||
setLoading(true);
|
||||
const sources = [
|
||||
...(await getDesktopSources(media, ["screen"], null) ?? []),
|
||||
...(await getDesktopSources(media, ["window", "application"], null) ?? [])
|
||||
];
|
||||
|
||||
if (active) {
|
||||
setStreamMediaSelection(sources);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchMedia();
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
if (loading) return <Forms.FormText>Loading media sources...</Forms.FormText>;
|
||||
if (!streamMediaSelection.length) return <Forms.FormText>No Media found.</Forms.FormText>;
|
||||
|
||||
return <StreamSimplePicker streamMediaSelection={streamMediaSelection} streamMedia={streamMedia} />;
|
||||
}
|
||||
|
||||
function SettingSection() {
|
||||
return (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle>Media source to stream</Forms.FormTitle>
|
||||
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Resets to main screen if not found</Forms.FormText>
|
||||
<ScreenSetting />
|
||||
</Forms.FormSection>
|
||||
);
|
||||
}
|
|
@ -103,12 +103,21 @@ export async function stop(_: IpcMainInvokeEvent) {
|
|||
}
|
||||
|
||||
async function metadata(options: DownloadOptions) {
|
||||
stdout_global = "";
|
||||
const metadata = JSON.parse(await ytdlp(["-J", options.url, "--no-warnings"]));
|
||||
if (metadata.is_live) throw "Live streams are not supported.";
|
||||
stdout_global = "";
|
||||
return { videoTitle: `${metadata.title || "video"} (${metadata.id})` };
|
||||
try {
|
||||
stdout_global = "";
|
||||
const output = await ytdlp(["-J", options.url, "--no-warnings"]);
|
||||
const metadata = JSON.parse(output);
|
||||
|
||||
if (metadata.is_live) throw new Error("Live streams are not supported.");
|
||||
|
||||
stdout_global = "";
|
||||
return { videoTitle: `${metadata.title || "video"} (${metadata.id})` };
|
||||
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function genFormat({ videoTitle }: { videoTitle: string; }, { maxFileSize, format }: DownloadOptions) {
|
||||
const HAS_LIMIT = !!maxFileSize;
|
||||
const MAX_VIDEO_SIZE = HAS_LIMIT ? maxFileSize * 0.8 : 0;
|
||||
|
@ -161,8 +170,11 @@ async function download({ format, videoTitle }: { format: string; videoTitle: st
|
|||
: []
|
||||
: [];
|
||||
const customArgs = ytdlpArgs?.filter(Boolean) || [];
|
||||
|
||||
await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]);
|
||||
try {
|
||||
await ytdlp([url, ...baseArgs, ...remuxArgs, ...customArgs]);
|
||||
} catch (err) {
|
||||
console.error("Error during yt-dlp execution:", err);
|
||||
}
|
||||
const file = fs.readdirSync(getdir()).find(f => f.startsWith("download."));
|
||||
if (!file) throw "No video file was found!";
|
||||
return { file, videoTitle };
|
||||
|
|
170
src/equicordplugins/messageFetchTimer/index.tsx
Normal file
170
src/equicordplugins/messageFetchTimer/index.tsx
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ChatBarButton, ChatBarButtonFactory } from "@api/ChatButtons";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { getCurrentChannel } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { FluxDispatcher, React } from "@webpack/common";
|
||||
|
||||
interface FetchTiming {
|
||||
channelId: string;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
duration?: number;
|
||||
timestamp?: Date;
|
||||
}
|
||||
|
||||
let currentFetch: FetchTiming | null = null;
|
||||
let currentChannelId: string | null = null;
|
||||
const channelTimings: Map<string, { time: number; timestamp: Date; }> = new Map();
|
||||
|
||||
const settings = definePluginSettings({
|
||||
showIcon: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show fetch time icon in message bar",
|
||||
default: true,
|
||||
},
|
||||
showMs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show milliseconds in timing",
|
||||
default: true,
|
||||
},
|
||||
iconColor: {
|
||||
type: OptionType.STRING,
|
||||
description: "Icon color (CSS color value)",
|
||||
default: "#00d166",
|
||||
}
|
||||
});
|
||||
|
||||
const FetchTimeButton: ChatBarButtonFactory = ({ isMainChat }) => {
|
||||
const { showMs, iconColor } = settings.use(["showMs", "iconColor"]);
|
||||
|
||||
if (!isMainChat || !settings.store.showIcon || !currentChannelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channelData = channelTimings.get(currentChannelId);
|
||||
if (!channelData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { time, timestamp } = channelData;
|
||||
const displayTime = showMs ? `${Math.round(time)}ms` : `${Math.round(time / 1000)}s`;
|
||||
|
||||
if (!showMs && Math.round(time / 1000) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeAgo = formatTimeAgo(timestamp);
|
||||
|
||||
return (
|
||||
<ChatBarButton
|
||||
tooltip={`Messages loaded in ${Math.round(time)}ms (${timeAgo})`}
|
||||
onClick={() => { }}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px"
|
||||
}}>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" />
|
||||
</svg>
|
||||
<span style={{
|
||||
fontSize: "12px",
|
||||
color: iconColor,
|
||||
fontWeight: "500"
|
||||
}}>
|
||||
{displayTime}
|
||||
</span>
|
||||
</div>
|
||||
</ChatBarButton>
|
||||
);
|
||||
};
|
||||
|
||||
function formatTimeAgo(timestamp: Date): string {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - timestamp.getTime();
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days} day${days > 1 ? "s" : ""} ago`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
|
||||
} else {
|
||||
return "just now";
|
||||
}
|
||||
}
|
||||
|
||||
function handleChannelSelect(data: any) {
|
||||
if (data.channelId && data.channelId !== currentChannelId) {
|
||||
currentChannelId = data.channelId;
|
||||
currentFetch = {
|
||||
channelId: data.channelId,
|
||||
startTime: performance.now()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessageLoad(data: any) {
|
||||
if (!currentFetch || data.channelId !== currentFetch.channelId) return;
|
||||
|
||||
const existing = channelTimings.get(currentFetch.channelId);
|
||||
if (existing) return;
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - currentFetch.startTime;
|
||||
|
||||
channelTimings.set(currentFetch.channelId, {
|
||||
time: duration,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
currentFetch = null;
|
||||
}
|
||||
|
||||
|
||||
export default definePlugin({
|
||||
name: "MessageFetchTimer",
|
||||
description: "Shows how long it took to fetch messages for the current channel",
|
||||
authors: [EquicordDevs.GroupXyz],
|
||||
settings,
|
||||
|
||||
start() {
|
||||
FluxDispatcher.subscribe("CHANNEL_SELECT", handleChannelSelect);
|
||||
FluxDispatcher.subscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad);
|
||||
FluxDispatcher.subscribe("MESSAGE_CREATE", handleMessageLoad);
|
||||
|
||||
const currentChannel = getCurrentChannel();
|
||||
if (currentChannel) {
|
||||
currentChannelId = currentChannel.id;
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
FluxDispatcher.unsubscribe("CHANNEL_SELECT", handleChannelSelect);
|
||||
FluxDispatcher.unsubscribe("LOAD_MESSAGES_SUCCESS", handleMessageLoad);
|
||||
FluxDispatcher.unsubscribe("MESSAGE_CREATE", handleMessageLoad);
|
||||
|
||||
currentFetch = null;
|
||||
channelTimings.clear();
|
||||
currentChannelId = null;
|
||||
},
|
||||
|
||||
renderChatBarButton: FetchTimeButton,
|
||||
});
|
|
@ -78,7 +78,8 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo
|
|||
bot: message?.bot || message?.author?.bot,
|
||||
flags: message?.flags,
|
||||
ghostPinged,
|
||||
isCachedByUs: (message as LoggedMessageJSON).ourCache
|
||||
isCachedByUs: (message as LoggedMessageJSON).ourCache,
|
||||
webhookId: message?.webhookId
|
||||
})
|
||||
) {
|
||||
// Flogger.log("IGNORING", message, payload);
|
||||
|
|
|
@ -52,6 +52,12 @@ export const settings = definePluginSettings({
|
|||
}
|
||||
},
|
||||
|
||||
ignoreWebhooks: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to ignore messages by webhooks",
|
||||
default: false,
|
||||
},
|
||||
|
||||
ignoreSelf: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether to ignore messages by yourself",
|
||||
|
|
|
@ -74,6 +74,7 @@ interface ShouldIgnoreArguments {
|
|||
bot?: boolean;
|
||||
ghostPinged?: boolean;
|
||||
isCachedByUs?: boolean;
|
||||
webhookId?: string;
|
||||
}
|
||||
|
||||
const EPHEMERAL = 64;
|
||||
|
@ -87,7 +88,7 @@ const UserGuildSettingsStore = findStoreLazy("UserGuildSettingsStore");
|
|||
* @param {ShouldIgnoreArguments} args - An object containing the message details.
|
||||
* @returns {boolean} - True if the message should be ignored, false if it should be kept.
|
||||
*/
|
||||
export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs }: ShouldIgnoreArguments): boolean {
|
||||
export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPinged, isCachedByUs, webhookId }: ShouldIgnoreArguments): boolean {
|
||||
const isEphemeral = ((flags ?? 0) & EPHEMERAL) === EPHEMERAL;
|
||||
if (isEphemeral) return true; // ignore
|
||||
|
||||
|
@ -96,7 +97,7 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi
|
|||
|
||||
const myId = UserStore.getCurrentUser().id;
|
||||
const { ignoreUsers, ignoreChannels, ignoreGuilds } = Settings.plugins.MessageLogger;
|
||||
const { ignoreBots, ignoreSelf } = settings.store;
|
||||
const { ignoreBots, ignoreSelf, ignoreWebhooks } = settings.store;
|
||||
|
||||
if (ignoreSelf && authorId === myId)
|
||||
return true; // ignore
|
||||
|
@ -132,6 +133,8 @@ export function shouldIgnore({ channelId, authorId, guildId, flags, bot, ghostPi
|
|||
|
||||
if ((ignoreBots && bot) && !isAuthorWhitelisted) return true; // ignore
|
||||
|
||||
if ((ignoreWebhooks && webhookId) && !isAuthorWhitelisted) return true;
|
||||
|
||||
if (ghostPinged) return false; // keep
|
||||
|
||||
// author has highest priority
|
||||
|
|
65
src/equicordplugins/moreCommands/index.ts
Normal file
65
src/equicordplugins/moreCommands/index.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
|
||||
function mock(input: string): string {
|
||||
let output = "";
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MoreCommands",
|
||||
description: "echo, lenny, mock",
|
||||
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
|
||||
commands: [
|
||||
{
|
||||
name: "echo",
|
||||
description: "Sends a message as Clyde (locally)",
|
||||
options: [OptionalMessageOption],
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
execute: (opts, ctx) => {
|
||||
const content = findOption(opts, "message", "");
|
||||
|
||||
sendBotMessage(ctx.channel.id, { content });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lenny",
|
||||
description: "Sends a lenny face",
|
||||
options: [OptionalMessageOption],
|
||||
execute: opts => ({
|
||||
content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)"
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "mock",
|
||||
description: "mOcK PeOpLe",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: mock(findOption(opts, "message", ""))
|
||||
}),
|
||||
},
|
||||
]
|
||||
});
|
|
@ -52,10 +52,8 @@ interface IVoiceChannelEffectSendEvent {
|
|||
}
|
||||
|
||||
const MOYAI = "🗿";
|
||||
const MOYAI_URL =
|
||||
"https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3";
|
||||
const MOYAI_URL_HD =
|
||||
"https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav";
|
||||
const MOYAI_URL = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.mp3";
|
||||
const MOYAI_URL_HD = "https://github.com/Equicord/Equibored/raw/main/sounds/moyai/moyai.wav";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
volume: {
|
23
src/equicordplugins/noRPC.discordDesktop/index.ts
Normal file
23
src/equicordplugins/noRPC.discordDesktop/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 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: "NoRPC",
|
||||
description: "Disables Discord's RPC server.",
|
||||
authors: [Devs.Cyn],
|
||||
patches: [
|
||||
{
|
||||
find: '.ensureModule("discord_rpc")',
|
||||
replacement: {
|
||||
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
|
||||
replace: '.ensureModule("discord_rpc")}',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -51,26 +51,27 @@ async function openCompleteQuestUI() {
|
|||
const applicationId = quest.config.application.id;
|
||||
const applicationName = quest.config.application.name;
|
||||
const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null);
|
||||
const icon = `https://cdn.discordapp.com/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
|
||||
// @ts-ignore
|
||||
const secondsNeeded = quest.config.taskConfig.tasks[taskName].target;
|
||||
// @ts-ignore
|
||||
const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
|
||||
const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`;
|
||||
let secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0;
|
||||
if (taskName === "WATCH_VIDEO") {
|
||||
const tolerance = 2, speed = 10;
|
||||
const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000);
|
||||
const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded);
|
||||
const maxFuture = 10, speed = 7, interval = 1;
|
||||
const enrolledAt = new Date(quest.userStatus.enrolledAt).getTime();
|
||||
const fn = async () => {
|
||||
for (let i = startingPoint; i <= secondsNeeded; i += speed) {
|
||||
try {
|
||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } });
|
||||
} catch (ex) {
|
||||
console.log("Failed to send increment of", i, ex);
|
||||
while (true) {
|
||||
const maxAllowed = Math.floor((Date.now() - enrolledAt) / 1000) + maxFuture;
|
||||
const diff = maxAllowed - secondsDone;
|
||||
const timestamp = secondsDone + speed;
|
||||
if (diff >= speed) {
|
||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, timestamp + Math.random()) } });
|
||||
secondsDone = Math.min(secondsNeeded, timestamp);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, tolerance * 1000));
|
||||
}
|
||||
if ((secondsNeeded - secondsDone) % speed !== 0) {
|
||||
await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } });
|
||||
if (timestamp >= secondsNeeded) {
|
||||
break;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
||||
showNotification({
|
||||
title: `${applicationName} - Quest Completer`,
|
||||
body: "Quest Completed.",
|
||||
|
@ -81,10 +82,9 @@ async function openCompleteQuestUI() {
|
|||
fn();
|
||||
showNotification({
|
||||
title: `${applicationName} - Quest Completer`,
|
||||
body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`,
|
||||
body: `Spoofing video for ${applicationName}.`,
|
||||
icon: icon,
|
||||
});
|
||||
console.log(`Spoofing video for ${applicationName}.`);
|
||||
} else if (taskName === "PLAY_ON_DESKTOP") {
|
||||
RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => {
|
||||
const appData = res.body[0];
|
||||
|
|
|
@ -61,8 +61,7 @@ const listener: MessageSendListener = async (channelId, msg) => {
|
|||
if (hardSplit || splitIndex === -1) {
|
||||
chunks.push(msg.content.slice(0, maxLength));
|
||||
msg.content = msg.content.slice(maxLength);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
chunks.push(msg.content.slice(0, splitIndex));
|
||||
msg.content = msg.content.slice(splitIndex);
|
||||
}
|
||||
|
|
81
src/equicordplugins/streamingCodecDisabler/index.ts
Normal file
81
src/equicordplugins/streamingCodecDisabler/index.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
|
||||
let mediaEngine = findStoreLazy("MediaEngineStore");
|
||||
|
||||
const originalCodecStatuses: {
|
||||
AV1: boolean,
|
||||
H265: boolean,
|
||||
H264: boolean;
|
||||
} = {
|
||||
AV1: true,
|
||||
H265: true,
|
||||
H264: true
|
||||
};
|
||||
|
||||
const settings = definePluginSettings({
|
||||
disableAv1Codec: {
|
||||
description: "Make Discord not consider using AV1 for streaming.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
disableH265Codec: {
|
||||
description: "Make Discord not consider using H265 for streaming.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
disableH264Codec: {
|
||||
description: "Make Discord not consider using H264 for streaming.",
|
||||
type: OptionType.BOOLEAN,
|
||||
default: false
|
||||
},
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "StreamingCodecDisabler",
|
||||
description: "Disable codecs for streaming of your choice",
|
||||
authors: [EquicordDevs.davidkra230],
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: "setVideoBroadcast(this.shouldConnectionBroadcastVideo",
|
||||
replacement: {
|
||||
match: /setGoLiveSource\(.,.\)\{/,
|
||||
replace: "$&$self.updateDisabledCodecs();"
|
||||
},
|
||||
}
|
||||
],
|
||||
|
||||
async updateDisabledCodecs() {
|
||||
mediaEngine.setAv1Enabled(originalCodecStatuses.AV1 && !Settings.plugins.StreamingCodecDisabler.disableAv1Codec);
|
||||
mediaEngine.setH265Enabled(originalCodecStatuses.H265 && !Settings.plugins.StreamingCodecDisabler.disableH265Codec);
|
||||
mediaEngine.setH264Enabled(originalCodecStatuses.H264 && !Settings.plugins.StreamingCodecDisabler.disableH264Codec);
|
||||
},
|
||||
|
||||
async start() {
|
||||
mediaEngine = mediaEngine.getMediaEngine();
|
||||
const options = Object.keys(originalCodecStatuses);
|
||||
// [{"codec":"","decode":false,"encode":false}]
|
||||
const CodecCapabilities = JSON.parse(await new Promise(res => mediaEngine.getCodecCapabilities(res)));
|
||||
CodecCapabilities.forEach((codec: { codec: string; encode: boolean; }) => {
|
||||
if (options.includes(codec.codec)) {
|
||||
originalCodecStatuses[codec.codec] = codec.encode;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async stop() {
|
||||
mediaEngine.setAv1Enabled(originalCodecStatuses.AV1);
|
||||
mediaEngine.setH265Enabled(originalCodecStatuses.H265);
|
||||
mediaEngine.setH264Enabled(originalCodecStatuses.H264);
|
||||
}
|
||||
});
|
|
@ -8,9 +8,10 @@ import * as DataStore from "@api/DataStore";
|
|||
import { classNameFactory } from "@api/Styles";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
|
||||
import { Button, Forms, SearchableSelect, useMemo, useState } from "@webpack/common";
|
||||
import { Button, Forms, SearchableSelect, useEffect, useMemo, useState } from "@webpack/common";
|
||||
|
||||
import { DATASTORE_KEY, timezones } from ".";
|
||||
import { DATASTORE_KEY, settings, timezones } from ".";
|
||||
import { getTimezone, setTimezone, setUserDatabaseTimezone } from "./database";
|
||||
|
||||
export async function setUserTimezone(userId: string, timezone: string | null) {
|
||||
timezones[userId] = timezone;
|
||||
|
@ -19,9 +20,22 @@ export async function setUserTimezone(userId: string, timezone: string | null) {
|
|||
|
||||
const cl = classNameFactory("vc-timezone-");
|
||||
|
||||
export function SetTimezoneModal({ userId, modalProps }: { userId: string, modalProps: ModalProps; }) {
|
||||
export function SetTimezoneModal({ userId, modalProps, database }: { userId: string, modalProps: ModalProps; database?: boolean; }) {
|
||||
const [currentValue, setCurrentValue] = useState<string | null>(timezones[userId] ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
const localTimezone = timezones[userId];
|
||||
const shouldUseDatabase =
|
||||
settings.store.useDatabase &&
|
||||
(settings.store.preferDatabaseOverLocal || !localTimezone);
|
||||
|
||||
const value = shouldUseDatabase
|
||||
? getTimezone(userId) ?? localTimezone
|
||||
: localTimezone;
|
||||
|
||||
setCurrentValue(value ?? Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
}, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return Intl.supportedValuesOf("timeZone").map(timezone => {
|
||||
const offset = new Intl.DateTimeFormat(undefined, { timeZone: timezone, timeZoneName: "short" })
|
||||
|
@ -59,20 +73,28 @@ export function SetTimezoneModal({ userId, modalProps }: { userId: string, modal
|
|||
</ModalContent>
|
||||
|
||||
<ModalFooter className={cl("modal-footer")}>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={async () => {
|
||||
await setUserTimezone(userId, null);
|
||||
modalProps.onClose();
|
||||
}}
|
||||
>
|
||||
Delete Timezone
|
||||
</Button>
|
||||
{!database && (
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={async () => {
|
||||
await setUserTimezone(userId, null);
|
||||
modalProps.onClose();
|
||||
}}
|
||||
>
|
||||
Delete Timezone
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color={Button.Colors.BRAND}
|
||||
disabled={currentValue === null}
|
||||
onClick={async () => {
|
||||
await setUserTimezone(userId, currentValue!);
|
||||
if (database) {
|
||||
await setUserDatabaseTimezone(userId, currentValue);
|
||||
await setTimezone(currentValue!);
|
||||
} else {
|
||||
await setUserTimezone(userId, currentValue);
|
||||
}
|
||||
|
||||
modalProps.onClose();
|
||||
}}
|
||||
>
|
||||
|
|
110
src/equicordplugins/timezones/database.tsx
Normal file
110
src/equicordplugins/timezones/database.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { openModal } from "@utils/index";
|
||||
import { OAuth2AuthorizeModal, showToast, Toasts } from "@webpack/common";
|
||||
|
||||
const databaseTimezones: Record<string, { value: string | null; }> = {};
|
||||
|
||||
const DOMAIN = "https://timezone.creations.works";
|
||||
const REDIRECT_URI = `${DOMAIN}/auth/discord/callback`;
|
||||
const CLIENT_ID = "1377021506810417173";
|
||||
|
||||
export async function setUserDatabaseTimezone(userId: string, timezone: string | null) {
|
||||
databaseTimezones[userId] = { value: timezone };
|
||||
}
|
||||
|
||||
export function getTimezone(userId: string): string | null {
|
||||
return databaseTimezones[userId]?.value ?? null;
|
||||
}
|
||||
|
||||
export async function loadDatabaseTimezones(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${DOMAIN}/list`, {
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
for (const id in json) {
|
||||
databaseTimezones[id] = {
|
||||
value: json[id]?.timezone ?? null
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch timezones list:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setTimezone(timezone: string): Promise<boolean> {
|
||||
const res = await fetch(`${DOMAIN}/set?timezone=${encodeURIComponent(timezone)}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
export async function deleteTimezone(): Promise<boolean> {
|
||||
const res = await fetch(`${DOMAIN}/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
export function authModal(callback?: () => void) {
|
||||
openModal(modalProps => (
|
||||
<OAuth2AuthorizeModal
|
||||
{...modalProps}
|
||||
clientId={CLIENT_ID}
|
||||
redirectUri={REDIRECT_URI}
|
||||
responseType="code"
|
||||
scopes={["identify"]}
|
||||
permissions={0n}
|
||||
cancelCompletesFlow={false}
|
||||
callback={async (res: any) => {
|
||||
if (!res || !res.location) return;
|
||||
|
||||
try {
|
||||
const url = new URL(res.location);
|
||||
|
||||
const r = await fetch(url, {
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json" }
|
||||
});
|
||||
|
||||
const json = await r.json();
|
||||
if (!r.ok) {
|
||||
showToast(json.message ?? "Authorization failed", Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
showToast("Authorization successful!", Toasts.Type.SUCCESS);
|
||||
callback?.();
|
||||
} catch (e) {
|
||||
console.error("Error during authorization:", e);
|
||||
showToast("Unexpected error during authorization", Toasts.Type.FAILURE);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2024 Vendicated and contributors
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
@ -10,26 +10,29 @@ import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
|||
import * as DataStore from "@api/DataStore";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Devs, EquicordDevs } from "@utils/constants";
|
||||
import { openModal } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Menu, Tooltip, useEffect, useState } from "@webpack/common";
|
||||
import { Button, Menu, showToast, Toasts, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
|
||||
import { Message, User } from "discord-types/general";
|
||||
|
||||
import { authModal, deleteTimezone, getTimezone, loadDatabaseTimezones, setUserDatabaseTimezone } from "./database";
|
||||
import { SetTimezoneModal } from "./TimezoneModal";
|
||||
|
||||
export const DATASTORE_KEY = "vencord-timezones";
|
||||
|
||||
export let timezones: Record<string, string | null> = {};
|
||||
(async () => {
|
||||
timezones = await DataStore.get<Record<string, string>>(DATASTORE_KEY) || {};
|
||||
})();
|
||||
export const DATASTORE_KEY = "vencord-timezones";
|
||||
|
||||
const classes = findByPropsLazy("timestamp", "compact", "contentOnly");
|
||||
const locale = findByPropsLazy("getLocale");
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
"Show Own Timezone": {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show your own timezone in profiles and message headers",
|
||||
default: true
|
||||
},
|
||||
|
||||
"24h Time": {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Show time in 24h format",
|
||||
|
@ -46,6 +49,57 @@ export const settings = definePluginSettings({
|
|||
type: OptionType.BOOLEAN,
|
||||
description: "Show time in profiles",
|
||||
default: true
|
||||
},
|
||||
|
||||
useDatabase: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable database for getting user timezones",
|
||||
default: true
|
||||
},
|
||||
|
||||
preferDatabaseOverLocal: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Prefer database over local storage for timezones",
|
||||
default: true
|
||||
},
|
||||
|
||||
setDatabaseTimezone: {
|
||||
description: "Set your timezone on the database",
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<Button onClick={() => {
|
||||
authModal(async () => {
|
||||
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||
});
|
||||
}}>
|
||||
Set Timezone on Database
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
|
||||
resetDatabaseTimezone: {
|
||||
description: "Reset your timezone on the database",
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
onClick={() => {
|
||||
authModal(async () => {
|
||||
await setUserDatabaseTimezone(UserStore.getCurrentUser().id, null);
|
||||
await deleteTimezone();
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset Database Timezones
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
|
||||
askedTimezone: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Whether the user has been asked to set their timezone",
|
||||
hidden: true,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -64,25 +118,36 @@ interface Props {
|
|||
timestamp?: string;
|
||||
type: "message" | "profile";
|
||||
}
|
||||
|
||||
const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Props) => {
|
||||
const [currentTime, setCurrentTime] = useState(timestamp || Date.now());
|
||||
const timezone = timezones[userId];
|
||||
const [timezone, setTimezone] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
const localTimezone = timezones[userId];
|
||||
const shouldUseDatabase =
|
||||
settings.store.useDatabase &&
|
||||
(settings.store.preferDatabaseOverLocal || !localTimezone);
|
||||
|
||||
if (type === "profile") {
|
||||
setCurrentTime(Date.now());
|
||||
|
||||
const now = new Date();
|
||||
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
|
||||
|
||||
timer = setTimeout(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, delay);
|
||||
if (shouldUseDatabase) {
|
||||
setTimezone(getTimezone(userId) ?? localTimezone);
|
||||
} else {
|
||||
setTimezone(localTimezone);
|
||||
}
|
||||
}, [userId, settings.store.useDatabase, settings.store.preferDatabaseOverLocal]);
|
||||
|
||||
return () => timer && clearTimeout(timer);
|
||||
useEffect(() => {
|
||||
if (type !== "profile") return;
|
||||
|
||||
setCurrentTime(Date.now());
|
||||
|
||||
const now = new Date();
|
||||
const delay = (60 - now.getSeconds()) * 1000 + 1000 - now.getMilliseconds();
|
||||
const timer = setTimeout(() => {
|
||||
setCurrentTime(Date.now());
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [type, currentTime]);
|
||||
|
||||
if (!timezone) return null;
|
||||
|
@ -94,8 +159,9 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop
|
|||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
minute: "numeric"
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position="top"
|
||||
|
@ -107,18 +173,14 @@ const TimestampComponent = ErrorBoundary.wrap(({ userId, timestamp, type }: Prop
|
|||
tooltipClassName="timezone-tooltip"
|
||||
text={longTime}
|
||||
>
|
||||
{toolTipProps => {
|
||||
return (
|
||||
<span
|
||||
{...toolTipProps}
|
||||
className={type === "message" ? `timezone-message-item ${classes.timestamp}` : "timezone-profile-item"}
|
||||
>
|
||||
{
|
||||
type === "message" ? `(${shortTime})` : shortTime
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
{toolTipProps => (
|
||||
<span
|
||||
{...toolTipProps}
|
||||
className={type === "message" ? `timezone-message-item ${classes.timestamp}` : "timezone-profile-item"}
|
||||
>
|
||||
{type === "message" ? `(${shortTime})` : shortTime}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}, { noop: true });
|
||||
|
@ -128,19 +190,18 @@ const userContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: {
|
|||
|
||||
const setTimezoneItem = (
|
||||
<Menu.MenuItem
|
||||
label="Set Timezone"
|
||||
label="Set Local Timezone"
|
||||
id="set-timezone"
|
||||
action={() => openModal(modalProps => <SetTimezoneModal userId={user.id} modalProps={modalProps} />)}
|
||||
/>
|
||||
);
|
||||
|
||||
children.push(<Menu.MenuSeparator />, setTimezoneItem);
|
||||
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "Timezones",
|
||||
authors: [Devs.Aria],
|
||||
authors: [Devs.Aria, EquicordDevs.creations],
|
||||
description: "Shows the local time of users in profiles and message headers",
|
||||
contextMenus: {
|
||||
"user-context": userContextMenuPatch
|
||||
|
@ -164,26 +225,76 @@ export default definePlugin({
|
|||
}
|
||||
}
|
||||
],
|
||||
|
||||
toolboxActions: {
|
||||
"Set Database Timezone": () => {
|
||||
authModal(async () => {
|
||||
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||
});
|
||||
},
|
||||
"Refresh Database Timezones": async () => {
|
||||
try {
|
||||
const good = await loadDatabaseTimezones();
|
||||
|
||||
if (good) {
|
||||
showToast("Timezones refreshed successfully!", Toasts.Type.SUCCESS);
|
||||
} else {
|
||||
showToast("Timezones Failed to refresh!", Toasts.Type.FAILURE);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to refresh timezone:", error);
|
||||
showToast("Failed to refresh timezones.", Toasts.Type.FAILURE);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async start() {
|
||||
timezones = await DataStore.get<Record<string, string>>(DATASTORE_KEY) || {};
|
||||
|
||||
if (settings.store.useDatabase) {
|
||||
await loadDatabaseTimezones();
|
||||
|
||||
if (!settings.store.askedTimezone) {
|
||||
showToast(
|
||||
"",
|
||||
Toasts.Type.MESSAGE,
|
||||
{
|
||||
duration: 10000,
|
||||
component: (
|
||||
<Button
|
||||
color={Button.Colors.GREEN}
|
||||
onClick={() => {
|
||||
authModal(async () => {
|
||||
openModal(modalProps => <SetTimezoneModal userId={UserStore.getCurrentUser().id} modalProps={modalProps} database={true} />);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Want to save your timezone to the database? Click here to set it.
|
||||
</Button>
|
||||
),
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
);
|
||||
settings.store.askedTimezone = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
settings,
|
||||
getTime,
|
||||
|
||||
|
||||
renderProfileTimezone: (props?: { user?: User; }) => {
|
||||
if (!settings.store.showProfileTime || !props?.user?.id) return null;
|
||||
if (props.user.id === UserStore.getCurrentUser().id && !settings.store["Show Own Timezone"]) return null;
|
||||
|
||||
return <TimestampComponent
|
||||
userId={props.user.id}
|
||||
type="profile"
|
||||
/>;
|
||||
return <TimestampComponent userId={props.user.id} type="profile" />;
|
||||
},
|
||||
|
||||
renderMessageTimezone: (props?: { message?: Message; }) => {
|
||||
if (!settings.store.showMessageHeaderTime || !props?.message) return null;
|
||||
if (props.message.author.id === UserStore.getCurrentUser().id && !settings.store["Show Own Timezone"]) return null;
|
||||
|
||||
return <TimestampComponent
|
||||
userId={props.message.author.id}
|
||||
timestamp={props.message.timestamp.toISOString()}
|
||||
type="message"
|
||||
/>;
|
||||
return <TimestampComponent userId={props.message.author.id} timestamp={props.message.timestamp.toISOString()} type="message" />;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ export default definePlugin({
|
|||
authors: [Devs.AutumnVN],
|
||||
start() {
|
||||
(function connect() {
|
||||
ws = new WebSocket("ws://localhost:24050/websocket/v2");
|
||||
ws = new WebSocket("ws://127.0.0.1:24050/websocket/v2");
|
||||
ws.addEventListener("error", () => ws.close());
|
||||
ws.addEventListener("close", () => wsReconnect = setTimeout(connect, 5000));
|
||||
ws.addEventListener("message", ({ data }) => throttledOnMessage(data));
|
||||
|
|
|
@ -8,30 +8,30 @@ import { session } from "electron";
|
|||
|
||||
type PolicyMap = Record<string, string[]>;
|
||||
|
||||
const ConnectSrc = ["connect-src"];
|
||||
const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
|
||||
const CssSrc = ["style-src", "font-src"];
|
||||
const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
|
||||
export const ConnectSrc = ["connect-src"];
|
||||
export const MediaSrc = [...ConnectSrc, "img-src", "media-src"];
|
||||
export const CssSrc = ["style-src", "font-src"];
|
||||
export const MediaAndCssSrc = [...MediaSrc, ...CssSrc];
|
||||
export const MediaScriptsAndCssSrc = [...MediaAndCssSrc, "script-src", "worker-src"];
|
||||
|
||||
// Plugins can whitelist their own domains by importing this object in their native.ts
|
||||
// script and just adding to it. But generally, you should just edit this file instead
|
||||
|
||||
export const CspPolicies: PolicyMap = {
|
||||
"*.github.io": MediaAndCssSrc, // github pages, used by most themes
|
||||
"raw.githubusercontent.com": MediaAndCssSrc, // github raw, used by some themes
|
||||
"*.gitlab.io": MediaAndCssSrc, // gitlab pages, used by some themes
|
||||
"gitlab.com": MediaAndCssSrc, // gitlab raw, used by some themes
|
||||
"*.codeberg.page": MediaAndCssSrc, // codeberg pages, used by some themes
|
||||
"codeberg.org": MediaAndCssSrc, // codeberg raw, used by some themes
|
||||
"*.github.io": MediaAndCssSrc, // GitHub pages, used by most themes
|
||||
"raw.githubusercontent.com": MediaAndCssSrc, // GitHub raw, used by some themes
|
||||
"*.gitlab.io": MediaAndCssSrc, // GitLab pages, used by some themes
|
||||
"gitlab.com": MediaAndCssSrc, // GitLab raw, used by some themes
|
||||
"*.codeberg.page": MediaAndCssSrc, // Codeberg pages, used by some themes
|
||||
"codeberg.org": MediaAndCssSrc, // Codeberg raw, used by some themes
|
||||
|
||||
"*.githack.com": MediaAndCssSrc, // githack (namely raw.githack.com), used by some themes
|
||||
"jsdelivr.net": MediaAndCssSrc, // jsdeliver, used by very few themes
|
||||
"jsdelivr.net": MediaAndCssSrc, // jsDelivr, used by very few themes
|
||||
|
||||
"fonts.googleapis.com": CssSrc, // google fonts, used by many themes
|
||||
"fonts.googleapis.com": CssSrc, // Google Fonts, used by many themes
|
||||
|
||||
"i.imgur.com": MediaSrc, // imgur, used by some themes
|
||||
"i.ibb.co": MediaSrc, // imgbb, used by some themes
|
||||
"i.imgur.com": MediaSrc, // Imgur, used by some themes
|
||||
"i.ibb.co": MediaSrc, // ImgBB, used by some themes
|
||||
|
||||
"cdn.discordapp.com": MediaAndCssSrc, // Discord CDN, used by Vencord and some themes to load media
|
||||
"media.discordapp.net": MediaSrc, // Discord media CDN, possible alternative to Discord CDN
|
||||
|
@ -43,7 +43,7 @@ export const CspPolicies: PolicyMap = {
|
|||
|
||||
// Function Specific
|
||||
"api.github.com": ConnectSrc, // used for updating Vencord itself
|
||||
"ws.audioscrobbler.com": ConnectSrc, // last.fm API
|
||||
"ws.audioscrobbler.com": ConnectSrc, // Last.fm API
|
||||
"translate-pa.googleapis.com": ConnectSrc, // Google Translate API
|
||||
"*.vencord.dev": MediaSrc, // VenCloud (api.vencord.dev) and Badges (badges.vencord.dev)
|
||||
"manti.vendicated.dev": MediaSrc, // ReviewDB API
|
||||
|
@ -78,7 +78,7 @@ const stringifyPolicy = (policy: PolicyMap): string =>
|
|||
.join("; ");
|
||||
|
||||
|
||||
const patchCsp = (headers: Record<string, string[]>) => {
|
||||
const patchCsp = (headers: PolicyMap) => {
|
||||
const reportOnlyHeader = findHeader(headers, "content-security-policy-report-only");
|
||||
if (reportOnlyHeader)
|
||||
delete headers[reportOnlyHeader];
|
||||
|
|
|
@ -25,7 +25,7 @@ import { RendererSettings } from "./settings";
|
|||
import { IS_VANILLA, THEMES_DIR } from "./utils/constants";
|
||||
import { installExt } from "./utils/extensions";
|
||||
|
||||
if (IS_VESKTOP || IS_EQUIBOP || !IS_VANILLA) {
|
||||
if (!IS_VANILLA && !IS_EXTENSION) {
|
||||
app.whenReady().then(() => {
|
||||
// Source Maps! Maybe there's a better way but since the renderer is executed
|
||||
// from a string I don't think any other form of sourcemaps would work
|
||||
|
|
|
@ -24,14 +24,14 @@ import { openContributorModal } from "@components/PluginSettings/ContributorModa
|
|||
import { isEquicordDonor } from "@components/VencordSettings/VencordTab";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { isEquicordPluginDev, isPluginDev } from "@utils/misc";
|
||||
import { shouldShowContributorBadge, shouldShowEquicordContributorBadge } from "@utils/misc";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Toasts, UserStore } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
import { EquicordDonorModal, VencordDonorModal } from "./modals";
|
||||
|
||||
const CONTRIBUTOR_BADGE = "https://vencord.dev/assets/favicon.png";
|
||||
const CONTRIBUTOR_BADGE = "https://cdn.discordapp.com/emojis/1092089799109775453.png?size=64";
|
||||
const EQUICORD_CONTRIBUTOR_BADGE = "https://i.imgur.com/57ATLZu.png";
|
||||
const EQUICORD_DONOR_BADGE = "https://cdn.nest.rip/uploads/78cb1e77-b7a6-4242-9089-e91f866159bf.png";
|
||||
|
||||
|
@ -39,7 +39,7 @@ const ContributorBadge: ProfileBadge = {
|
|||
description: "Vencord Contributor",
|
||||
image: CONTRIBUTOR_BADGE,
|
||||
position: BadgePosition.START,
|
||||
shouldShow: ({ userId }) => isPluginDev(userId),
|
||||
shouldShow: ({ userId }) => shouldShowContributorBadge(userId),
|
||||
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
|
||||
};
|
||||
|
||||
|
@ -47,7 +47,7 @@ const EquicordContributorBadge: ProfileBadge = {
|
|||
description: "Equicord Contributor",
|
||||
image: EQUICORD_CONTRIBUTOR_BADGE,
|
||||
position: BadgePosition.START,
|
||||
shouldShow: ({ userId }) => isEquicordPluginDev(userId),
|
||||
shouldShow: ({ userId }) => shouldShowEquicordContributorBadge(userId),
|
||||
onClick: (_, { userId }) => openContributorModal(UserStore.getUser(userId))
|
||||
};
|
||||
|
||||
|
@ -77,7 +77,7 @@ async function loadBadges(url: string, noCache = false) {
|
|||
|
||||
async function loadAllBadges(noCache = false) {
|
||||
const vencordBadges = await loadBadges("https://badges.vencord.dev/badges.json", noCache);
|
||||
const equicordBadges = await loadBadges("https://equicord.org/badges", noCache);
|
||||
const equicordBadges = await loadBadges("https://equicord.org/badges.json", noCache);
|
||||
|
||||
DonorBadges = vencordBadges;
|
||||
EquicordDonorBadges = equicordBadges;
|
||||
|
|
|
@ -30,8 +30,8 @@ export default definePlugin({
|
|||
replacement: [
|
||||
// Main setting definition
|
||||
{
|
||||
match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
|
||||
replace: "userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1],$&"
|
||||
match: /\.updateAsync\(.+?(?=,useSetting:)/,
|
||||
replace: "$&,userSettingsAPIGroup:arguments[0],userSettingsAPIName:arguments[1]"
|
||||
},
|
||||
// Selective wrapper
|
||||
{
|
||||
|
|
|
@ -79,18 +79,17 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: 'type:"UPLOAD_START"',
|
||||
replacement: {
|
||||
match: /await \i\.uploadFiles\((\i),/,
|
||||
replace: "$1.forEach($self.anonymise),$&"
|
||||
},
|
||||
},
|
||||
{
|
||||
find: 'addFilesTo:"message.attachments"',
|
||||
replacement: {
|
||||
match: /\i.uploadFiles\((\i),/,
|
||||
replace: "$1.forEach($self.anonymise),$&"
|
||||
}
|
||||
find: "async uploadFiles(",
|
||||
replacement: [
|
||||
{
|
||||
match: /async uploadFiles\((\i),\i\){/,
|
||||
replace: "$&$1.forEach($self.anonymise);"
|
||||
},
|
||||
{
|
||||
match: /async uploadFilesSimple\((\i)\){/,
|
||||
replace: "$&$1.forEach($self.anonymise);"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
find: "#{intl::ATTACHMENT_UTILITIES_SPOILER}",
|
||||
|
|
|
@ -36,12 +36,21 @@ async function lookupApp(applicationId: string): Promise<string> {
|
|||
return socket.application;
|
||||
}
|
||||
|
||||
let hideSetting = false;
|
||||
|
||||
if (IS_VESKTOP || IS_EQUIBOP || "legcord" in window) {
|
||||
hideSetting = true;
|
||||
} else if ("goofcord" in window) {
|
||||
hideSetting = false;
|
||||
}
|
||||
|
||||
let ws: WebSocket;
|
||||
export default definePlugin({
|
||||
name: "WebRichPresence (arRPC)",
|
||||
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
|
||||
authors: [Devs.Ducko],
|
||||
reporterTestable: ReporterTestable.None,
|
||||
hidden: hideSetting,
|
||||
|
||||
settingsAboutComponent: () => (
|
||||
<>
|
||||
|
@ -73,9 +82,6 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
async start() {
|
||||
// Legcord comes with its own arRPC implementation, so this plugin just confuses users
|
||||
if ("legcord" in window) return;
|
||||
|
||||
if (ws) ws.close();
|
||||
ws = new WebSocket("ws://127.0.0.1:1337"); // try to open WebSocket
|
||||
|
||||
|
|
|
@ -139,5 +139,5 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
DecorSection: ErrorBoundary.wrap(DecorSection)
|
||||
DecorSection: ErrorBoundary.wrap(DecorSection, { noop: true })
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { migratePluginSettings } from "@api/Settings";
|
||||
import { CheckedTextInput } from "@components/CheckedTextInput";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
|
@ -165,7 +166,7 @@ async function doClone(guildId: string, data: Sticker | Emoji) {
|
|||
message = JSON.parse(e.text).message;
|
||||
} catch { }
|
||||
|
||||
new Logger("EmoteCloner").error("Failed to clone", data.name, "to", guildId, e);
|
||||
new Logger("ExpressionCloner").error("Failed to clone", data.name, "to", guildId, e);
|
||||
Toasts.show({
|
||||
message: "Failed to clone: " + message,
|
||||
type: Toasts.Type.FAILURE,
|
||||
|
@ -364,10 +365,11 @@ const expressionPickerPatch: NavContextMenuPatchCallback = (children, props: { t
|
|||
}
|
||||
};
|
||||
|
||||
migratePluginSettings("ExpressionCloner", "EmoteCloner");
|
||||
export default definePlugin({
|
||||
name: "EmoteCloner",
|
||||
name: "ExpressionCloner",
|
||||
description: "Allows you to clone Emotes & Stickers to your own server (right click them)",
|
||||
tags: ["StickerCloner"],
|
||||
tags: ["StickerCloner", "EmoteCloner", "EmojiCloner"],
|
||||
authors: [Devs.Ven, Devs.Nuckyz],
|
||||
contextMenus: {
|
||||
"message": messageContextMenuPatch,
|
|
@ -118,7 +118,7 @@ export default definePlugin({
|
|||
renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) {
|
||||
this.instance = instance;
|
||||
return (
|
||||
<ErrorBoundary noop={true}>
|
||||
<ErrorBoundary noop>
|
||||
<SearchBar instance={instance} SearchBarComponent={SearchBarComponent} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -98,7 +98,7 @@ export default definePlugin({
|
|||
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
}, { noop: true }),
|
||||
});
|
||||
|
||||
function getUsernameString(username: string) {
|
||||
|
|
|
@ -20,7 +20,6 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso
|
|||
import { updateMessage } from "@api/MessageUpdater";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classes } from "@utils/misc";
|
||||
import { Queue } from "@utils/Queue";
|
||||
|
@ -373,7 +372,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
start() {
|
||||
addMessageAccessory("messageLinkEmbed", props => {
|
||||
addMessageAccessory("MessageLinkEmbeds", props => {
|
||||
if (!messageLinkRegex.test(props.message.content))
|
||||
return null;
|
||||
|
||||
|
@ -381,15 +380,13 @@ export default definePlugin({
|
|||
messageLinkRegex.lastIndex = 0;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<MessageEmbedAccessory
|
||||
message={props.message}
|
||||
/>
|
||||
);
|
||||
}, 4 /* just above rich embeds */);
|
||||
},
|
||||
stop() {
|
||||
removeMessageAccessory("messageLinkEmbed");
|
||||
removeMessageAccessory("MessageLinkEmbeds");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,332 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated, Samu 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 { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, OptionalMessageOption, RequiredMessageOption, sendBotMessage } from "@api/Commands";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
|
||||
function mock(input: string): string {
|
||||
let output = "";
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
output += i % 2 ? input[i].toUpperCase() : input[i].toLowerCase();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "MoreCommands",
|
||||
description: "Echo, Lenny, Mock, and More",
|
||||
authors: [Devs.Arjix, Devs.echo, Devs.Samu],
|
||||
commands: [
|
||||
{
|
||||
name: "echo",
|
||||
description: "Sends a message as Clyde (locally)",
|
||||
options: [OptionalMessageOption],
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
execute: (opts, ctx) => {
|
||||
const content = findOption(opts, "message", "");
|
||||
|
||||
sendBotMessage(ctx.channel.id, { content });
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lenny",
|
||||
description: "Sends a lenny face",
|
||||
options: [OptionalMessageOption],
|
||||
execute: opts => ({
|
||||
content: findOption(opts, "message", "") + " ( ͡° ͜ʖ ͡°)"
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "mock",
|
||||
description: "mOcK PeOpLe",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: mock(findOption(opts, "message", ""))
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "reverse",
|
||||
description: "Reverses the input message",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: findOption(opts, "message", "").split("").reverse().join("")
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "uppercase",
|
||||
description: "Converts the message to uppercase",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: findOption(opts, "message", "").toUpperCase()
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "lowercase",
|
||||
description: "Converts the message to lowercase",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => ({
|
||||
content: findOption(opts, "message", "").toLowerCase()
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "wordcount",
|
||||
description: "Counts the number of words in a message",
|
||||
options: [RequiredMessageOption],
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
execute: (opts, ctx) => {
|
||||
const message = findOption(opts, "message", "");
|
||||
const wordCount = message.trim().split(/\s+/).length;
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `The message contains ${wordCount} words.`
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ping",
|
||||
description: "Pings the bot to check if it's responding",
|
||||
options: [],
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
execute: (opts, ctx) => {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: "Pong!"
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rolldice",
|
||||
description: "Roll a die with the specified number of sides",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => {
|
||||
const sides = parseInt(findOption(opts, "message", "6"));
|
||||
const roll = Math.floor(Math.random() * sides) + 1;
|
||||
return {
|
||||
content: `You rolled a ${roll}!`
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "flipcoin",
|
||||
description: "Flips a coin and returns heads or tails",
|
||||
options: [],
|
||||
execute: (opts, ctx) => {
|
||||
const flip = Math.random() < 0.5 ? "Heads" : "Tails";
|
||||
return {
|
||||
content: `The coin landed on: ${flip}`
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ask",
|
||||
description: "Ask a yes/no question and get an answer",
|
||||
options: [RequiredMessageOption],
|
||||
execute: opts => {
|
||||
const question = findOption(opts, "message", "");
|
||||
const responses = [
|
||||
"Yes", "No", "Maybe", "Ask again later", "Definitely not", "It is certain"
|
||||
];
|
||||
const response = responses[Math.floor(Math.random() * responses.length)];
|
||||
return {
|
||||
content: `${question} - ${response}`
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "randomcat",
|
||||
description: "Get a random cat picture",
|
||||
options: [],
|
||||
execute: (opts, ctx) => {
|
||||
return (async () => {
|
||||
try {
|
||||
const response = await fetch("https://api.thecatapi.com/v1/images/search");
|
||||
if (!response.ok) throw new Error("Failed to fetch cat image");
|
||||
const data = await response.json();
|
||||
return {
|
||||
content: data[0].url
|
||||
};
|
||||
} catch (err) {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: "Sorry, couldn't fetch a cat picture right now 😿"
|
||||
});
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "randomdog",
|
||||
description: "Get a random ddog picture",
|
||||
options: [],
|
||||
execute: (opts, ctx) => {
|
||||
return (async () => {
|
||||
try {
|
||||
const response = await fetch("https://api.thedogapi.com/v1/images/search");
|
||||
if (!response.ok) throw new Error("Failed to fetch dog image");
|
||||
const data = await response.json();
|
||||
return {
|
||||
content: data[0].url
|
||||
};
|
||||
} catch (err) {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: "Sorry, couldn't fetch a cat picture right now 🐶"
|
||||
});
|
||||
}
|
||||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "randomnumber",
|
||||
description: "Generates a random number between two values",
|
||||
options: [
|
||||
{
|
||||
name: "min",
|
||||
description: "Minimum value",
|
||||
type: ApplicationCommandOptionType.INTEGER,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: "max",
|
||||
description: "Maximum value",
|
||||
type: ApplicationCommandOptionType.INTEGER,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
execute: opts => {
|
||||
const min = parseInt(findOption(opts, "min", "0"));
|
||||
const max = parseInt(findOption(opts, "max", "100"));
|
||||
const number = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return {
|
||||
content: `Random number between ${min} and ${max}: ${number}`
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "countdown",
|
||||
description: "Starts a countdown from a specified number",
|
||||
options: [
|
||||
{
|
||||
name: "number",
|
||||
description: "Number to countdown from (max 10)",
|
||||
type: ApplicationCommandOptionType.INTEGER,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
inputType: ApplicationCommandInputType.BOT,
|
||||
execute: async (opts, ctx) => {
|
||||
const number = Math.min(parseInt(findOption(opts, "number", "5")), 10);
|
||||
if (isNaN(number) || number < 1) {
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: "Please provide a valid number between 1 and 10!"
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: `Starting countdown from ${number}...`
|
||||
});
|
||||
for (let i = number; i >= 0; i--) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
sendBotMessage(ctx.channel.id, {
|
||||
content: i === 0 ? "🎉 Go! 🎉" : `${i}...`
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "choose",
|
||||
description: "Randomly chooses from provided options",
|
||||
options: [
|
||||
{
|
||||
name: "choices",
|
||||
description: "Comma-separated list of choices",
|
||||
type: ApplicationCommandOptionType.STRING,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
execute: opts => {
|
||||
const choices = findOption(opts, "choices", "").split(",").map(c => c.trim());
|
||||
const choice = choices[Math.floor(Math.random() * choices.length)];
|
||||
return {
|
||||
content: `I choose: ${choice}`
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "systeminfo",
|
||||
description: "Shows system information",
|
||||
options: [],
|
||||
execute: async (opts, ctx) => {
|
||||
try {
|
||||
const { userAgent, hardwareConcurrency, onLine, languages } = navigator;
|
||||
const { width, height, colorDepth } = window.screen;
|
||||
const { deviceMemory, connection }: { deviceMemory: any, connection: any; } = navigator as any;
|
||||
const platform = userAgent.includes("Windows") ? "Windows" :
|
||||
userAgent.includes("Mac") ? "MacOS" :
|
||||
userAgent.includes("Linux") ? "Linux" : "Unknown";
|
||||
const isMobile = /Mobile|Android|iPhone/i.test(userAgent);
|
||||
const deviceType = isMobile ? "Mobile" : "Desktop";
|
||||
const browserInfo = userAgent.match(/(?:chrome|firefox|safari|edge|opr)\/?\s*(\d+)/i)?.[0] || "Unknown";
|
||||
const networkInfo = connection ? `${connection.effectiveType || "Unknown"}` : "Unknown";
|
||||
const info = [
|
||||
`> **Platform**: ${platform}`,
|
||||
`> **Device Type**: ${deviceType}`,
|
||||
`> **Browser**: ${browserInfo}`,
|
||||
`> **CPU Cores**: ${hardwareConcurrency || "N/A"}`,
|
||||
`> **Memory**: ${deviceMemory ? `${deviceMemory}GB` : "N/A"}`,
|
||||
`> **Screen**: ${width}x${height} (${colorDepth}bit)`,
|
||||
`> **Languages**: ${languages?.join(", ")}`,
|
||||
`> **Network**: ${networkInfo} (${onLine ? "Online" : "Offline"})`
|
||||
].join("\n");
|
||||
return { content: info };
|
||||
} catch (err) {
|
||||
sendBotMessage(ctx.channel.id, { content: "Failed to fetch system information" });
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "getUptime",
|
||||
description: "Returns the system uptime",
|
||||
execute: async (opts, ctx) => {
|
||||
const uptime = performance.now() / 1000;
|
||||
const uptimeInfo = `> **System Uptime**: ${Math.floor(uptime / 60)} minutes`;
|
||||
return { content: uptimeInfo };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "getTime",
|
||||
description: "Returns the current server time",
|
||||
execute: async (opts, ctx) => {
|
||||
const currentTime = new Date().toLocaleString();
|
||||
return { content: `> **Current Time**: ${currentTime}` };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "getLocation",
|
||||
description: "Returns the user's approximate location based on IP",
|
||||
execute: async (opts, ctx) => {
|
||||
try {
|
||||
const response = await fetch("https://ipapi.co/json/");
|
||||
const data = await response.json();
|
||||
const locationInfo = `> **Country**: ${data.country_name}\n> **Region**: ${data.region}\n> **City**: ${data.city}`;
|
||||
return { content: locationInfo };
|
||||
} catch (err) {
|
||||
sendBotMessage(ctx.channel.id, { content: "Failed to fetch location information" });
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
});
|
|
@ -121,14 +121,9 @@ export default definePlugin({
|
|||
},
|
||||
// Make the gap between each item smaller so our tab can fit.
|
||||
{
|
||||
match: /className:\i\.tabBar/,
|
||||
replace: '$& + " vc-mutual-gdms-modal-v2-tab-bar"'
|
||||
match: /type:"top",/,
|
||||
replace: '$&className:"vc-mutual-gdms-modal-v2-tab-bar",'
|
||||
},
|
||||
// Make the tab bar item text smaller so our tab can fit.
|
||||
{
|
||||
match: /(\.tabBarItem.+?variant:)"heading-md\/normal"/,
|
||||
replace: '$1"heading-sm/normal"'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -209,5 +204,5 @@ export default definePlugin({
|
|||
/>
|
||||
</>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
}
|
||||
|
||||
.vc-mutual-gdms-modal-v2-tab-bar {
|
||||
gap: 12px;
|
||||
--space-xl: 16px;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ interface MessageDeleteProps {
|
|||
}
|
||||
|
||||
// Remove this migration once enough time has passed
|
||||
migratePluginSetting("NoBlockedMessages", "ignoreBlockedMessages", "ignoreMessages");
|
||||
migratePluginSetting("NoBlockedMessages", "ignoreMessages", "ignoreBlockedMessages");
|
||||
const settings = definePluginSettings({
|
||||
ignoreMessages: {
|
||||
description: "Completely ignores incoming messages from blocked and ignored (if enabled) users",
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2022 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoRPC",
|
||||
description: "Disables Discord's RPC server.",
|
||||
authors: [Devs.Cyn],
|
||||
patches: [
|
||||
{
|
||||
find: '.ensureModule("discord_rpc")',
|
||||
replacement: {
|
||||
match: /\.ensureModule\("discord_rpc"\)\.then\(\(.+?\)}\)}/,
|
||||
replace: '.ensureModule("discord_rpc")}',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
|
@ -21,7 +21,7 @@ import definePlugin from "@utils/types";
|
|||
|
||||
export default definePlugin({
|
||||
name: "NoUnblockToJump",
|
||||
description: "Allows you to jump to messages of blocked users without unblocking them",
|
||||
description: "Allows you to jump to messages of blocked or ignored users and likely spammers without unblocking them",
|
||||
authors: [Devs.dzshn],
|
||||
patches: [
|
||||
{
|
||||
|
|
|
@ -75,5 +75,5 @@ export default definePlugin({
|
|||
}}> Pause Indefinitely.</a>}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -201,7 +201,7 @@ function toggleMessageDecorators(enabled: boolean) {
|
|||
}
|
||||
}
|
||||
|
||||
migratePluginSetting("PlatformIndicators", "badges", "profiles");
|
||||
migratePluginSetting("PlatformIndicators", "profiles", "badges");
|
||||
const settings = definePluginSettings({
|
||||
list: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
|
|
@ -151,7 +151,13 @@ function CompactConnectionComponent({ connection, theme }: { connection: Connect
|
|||
: <button
|
||||
{...tooltipProps}
|
||||
className="vc-user-connection"
|
||||
onClick={() => copyWithToast(connection.name)}
|
||||
onClick={() => {
|
||||
if (connection.type === "xbox") {
|
||||
VencordNative.native.openExternal(`https://www.xbox.com/en-US/play/user/${encodeURIComponent(connection.name)}`);
|
||||
} else {
|
||||
copyWithToast(connection.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{img}
|
||||
</button>
|
||||
|
|
|
@ -299,7 +299,7 @@ export default definePlugin({
|
|||
{
|
||||
find: '"MessageManager"',
|
||||
replacement: {
|
||||
match: /"Skipping fetch because channelId is a static route"\);return}(?=.+?getChannel\((\i)\))/,
|
||||
match: /forceFetch:\i,isPreload:.+?}=\i;(?=.+?getChannel\((\i)\))/,
|
||||
replace: (m, channelId) => `${m}if($self.isHiddenChannel({channelId:${channelId}}))return;`
|
||||
}
|
||||
},
|
||||
|
@ -526,10 +526,11 @@ export default definePlugin({
|
|||
|
||||
isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {
|
||||
try {
|
||||
if (!channel) return false;
|
||||
if (channel == null || Object.hasOwn(channel, "channelId") && channel.channelId == null) return false;
|
||||
|
||||
if (channel.channelId) channel = ChannelStore.getChannel(channel.channelId);
|
||||
if (!channel || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
|
||||
if (channel.channelId != null) channel = ChannelStore.getChannel(channel.channelId);
|
||||
if (channel == null || channel.isDM() || channel.isGroupDM() || channel.isMultiUserDM()) return false;
|
||||
if (["browse", "customize", "guide"].includes(channel.id)) return false;
|
||||
|
||||
return !PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel) || checkConnect && !PermissionStore.can(PermissionsBits.CONNECT, channel);
|
||||
} catch (e) {
|
||||
|
|
|
@ -86,5 +86,5 @@ export default definePlugin({
|
|||
</TooltipContainer>
|
||||
)}
|
||||
</div>;
|
||||
})
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -132,10 +132,16 @@ export default definePlugin({
|
|||
|
||||
{
|
||||
find: "Copy image not supported",
|
||||
replacement: {
|
||||
match: /(?<=(?:canSaveImage|canCopyImage)\(\i?\)\{.{0,50})!\i\.isPlatformEmbedded/g,
|
||||
replace: "false"
|
||||
}
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=(?:canSaveImage|canCopyImage)\(.{0,120}?)!\i\.isPlatformEmbedded/g,
|
||||
replace: "false"
|
||||
},
|
||||
{
|
||||
match: /canCopyImage\(.+?(?=return"function"==typeof \i\.clipboard\.copyImage)/,
|
||||
replace: "$&return true;"
|
||||
}
|
||||
]
|
||||
},
|
||||
// Add back Copy & Save Image
|
||||
{
|
||||
|
@ -147,7 +153,7 @@ export default definePlugin({
|
|||
replace: "false"
|
||||
},
|
||||
{
|
||||
match: /return\s*?\[.{0,50}?(?=\?.{0,100}?id:"copy-image")/,
|
||||
match: /return\s*?\[.{0,50}?(?=\?\(0,\i\.jsxs?.{0,100}?id:"copy-image")/,
|
||||
replace: "return [true"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -67,7 +67,7 @@ function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
|
|||
|
||||
function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
|
||||
const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`;
|
||||
const cache = reactions[key] ??= { fetched: false, users: {} };
|
||||
const cache = reactions[key] ??= { fetched: false, users: new Map() };
|
||||
if (!cache.fetched) {
|
||||
queue.unshift(() => fetchReactions(msg, e, type));
|
||||
cache.fetched = true;
|
||||
|
@ -169,7 +169,7 @@ export default definePlugin({
|
|||
}, [message.id, forceUpdate]);
|
||||
|
||||
const reactions = getReactionsWithQueue(message, emoji, type);
|
||||
const users = Object.values(reactions).filter(Boolean) as User[];
|
||||
const users = [...reactions.values()].filter(Boolean);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -201,7 +201,7 @@ export default definePlugin({
|
|||
|
||||
interface ReactionCacheEntry {
|
||||
fetched: boolean;
|
||||
users: Record<string, User>;
|
||||
users: Map<string, User>;
|
||||
}
|
||||
|
||||
interface RootObject {
|
||||
|
|
|
@ -57,7 +57,7 @@ export interface Dev {
|
|||
*/
|
||||
export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
Ven: {
|
||||
name: "Vee",
|
||||
name: "V",
|
||||
id: 343383572805058560n
|
||||
},
|
||||
Arjix: {
|
||||
|
@ -211,7 +211,7 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
axyie: {
|
||||
name: "'ax",
|
||||
id: 273562710745284628n
|
||||
id: 929877747151548487n,
|
||||
},
|
||||
pointy: {
|
||||
name: "pointy",
|
||||
|
@ -604,7 +604,11 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
|||
},
|
||||
samsam: {
|
||||
name: "samsam",
|
||||
id: 836452332387565589n,
|
||||
id: 400482410279469056n,
|
||||
},
|
||||
Cootshk: {
|
||||
name: "Cootshk",
|
||||
id: 921605971577548820n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
|
@ -1074,6 +1078,18 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "sliwka",
|
||||
id: 1165286199628419129n,
|
||||
},
|
||||
bbgaming25k: {
|
||||
name: "bbgaming25k",
|
||||
id: 851222385528274964n,
|
||||
},
|
||||
davidkra230: {
|
||||
name: "davidkra230",
|
||||
id: 652699312631054356n,
|
||||
},
|
||||
GroupXyz: {
|
||||
name: "GroupXyz",
|
||||
id: 950033410229944331n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
34
src/utils/cspViolations.ts
Normal file
34
src/utils/cspViolations.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { useLayoutEffect } from "@webpack/common";
|
||||
|
||||
import { useForceUpdater } from "./react";
|
||||
|
||||
const cssRelevantDirectives = ["style-src", "img-src", "font-src"] as const;
|
||||
|
||||
export const CspBlockedUrls = new Set<string>();
|
||||
const CspErrorListeners = new Set<() => void>();
|
||||
|
||||
document.addEventListener("securitypolicyviolation", ({ effectiveDirective, blockedURI }) => {
|
||||
if (!blockedURI || !cssRelevantDirectives.includes(effectiveDirective as any)) return;
|
||||
|
||||
CspBlockedUrls.add(blockedURI);
|
||||
|
||||
CspErrorListeners.forEach(listener => listener());
|
||||
});
|
||||
|
||||
export function useCspErrors() {
|
||||
const forceUpdate = useForceUpdater();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
CspErrorListeners.add(forceUpdate);
|
||||
|
||||
return () => void CspErrorListeners.delete(forceUpdate);
|
||||
}, [forceUpdate]);
|
||||
|
||||
return [...CspBlockedUrls] as const;
|
||||
}
|
|
@ -21,6 +21,7 @@ export * from "../shared/onceDefined";
|
|||
export * from "./ChangeList";
|
||||
export * from "./clipboard";
|
||||
export * from "./constants";
|
||||
export * from "./cspViolations";
|
||||
export * from "./discord";
|
||||
export * from "./guards";
|
||||
export * from "./intlHash";
|
||||
|
|
|
@ -92,7 +92,10 @@ export function identity<T>(value: T): T {
|
|||
export const isMobile = navigator.userAgent.includes("Mobi");
|
||||
|
||||
export const isPluginDev = (id: string) => Object.hasOwn(VencordDevsById, id);
|
||||
export const shouldShowContributorBadge = (id: string) => isPluginDev(id) && VencordDevsById[id].badge !== false;
|
||||
|
||||
export const isEquicordPluginDev = (id: string) => Object.hasOwn(EquicordDevsById, id);
|
||||
export const shouldShowEquicordContributorBadge = (id: string) => isEquicordPluginDev(id) && EquicordDevsById[id].badge !== false;
|
||||
|
||||
export function pluralise(amount: number, singular: string, plural = singular + "s") {
|
||||
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
|
||||
|
|
|
@ -141,7 +141,7 @@ export const UserUtils = {
|
|||
|
||||
export const UploadManager = findByPropsLazy("clearAll", "addFile");
|
||||
export const UploadHandler = {
|
||||
promptToUpload: findByCodeLazy("#{intl::ATTACHMENT_TOO_MANY_ERROR_TITLE}") as (files: File[], channel: Channel, draftType: Number) => void
|
||||
promptToUpload: findByCodeLazy("=!0,showLargeMessageDialog:") as (files: File[], channel: Channel, draftType: Number) => void
|
||||
};
|
||||
|
||||
export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size must === [", {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue