Compare commits

..

No commits in common. "52c63bf13ce8502eef1d223a91fa51f12c66c41a" and "41702851c7c55990f597f72004ec8714b3f62526" have entirely different histories.

40 changed files with 211 additions and 554 deletions

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.9.7",
"version": "1.9.5",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {

View file

@ -15,9 +15,9 @@ export async function loadLazyChunks() {
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<number>();
const invalidChunks = new Set<number>();
const deferredRequires = new Set<number>();
const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
@ -29,14 +29,14 @@ export async function loadLazyChunks() {
async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
// the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
if (chunkIds.length === 0) {
return;
@ -61,7 +61,7 @@ export async function loadLazyChunks() {
}
if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, Number(entryPoint)]);
validChunkGroups.add([chunkIds, entryPoint]);
}
}));
@ -131,14 +131,14 @@ export async function loadLazyChunks() {
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as number[];
const allChunks = [] as string[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)")|(?:([\deE]+?):)/g)) {
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
allChunks.push(Number(id));
allChunks.push(id);
}
if (allChunks.length === 0) throw new Error("Failed to get all chunks");

View file

@ -35,8 +35,7 @@ export const ALLOWED_PROTOCOLS = [
"steam:",
"spotify:",
"com.epicgames.launcher:",
"tidal:",
"itunes:",
"tidal:"
];
export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla");

View file

@ -27,8 +27,12 @@ export default definePlugin({
find: "Messages.MESSAGE_UTILITIES_A11Y_LABEL",
replacement: {
// foo && !bar ? createElement(reactionStuffs)... createElement(blah,...makeElement(reply-other))
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"(?<=message:(\i).+?)/,
replace: (m, makeElement, msg) => `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`
match: /\i&&!\i\?\(0,\i\.jsxs?\)\(.{0,200}renderEmojiPicker:.{0,500}\?(\i)\(\{key:"reply-other"/,
replace: (m, makeElement) => {
const msg = m.match(/message:(.{1,3}),/)?.[1];
if (!msg) throw new Error("Could not find message variable");
return `...Vencord.Api.MessagePopover._buildPopoverElements(${msg},${makeElement}),${m}`;
}
}
}],
});

View file

@ -249,10 +249,6 @@ export default definePlugin({
dispatchingFoldersClose = false;
});
}
},
LOGOUT() {
closeFolders();
}
},

View file

@ -25,9 +25,11 @@ export default definePlugin({
description: "Upload with a single click, open menu with right click",
patches: [
{
find: '"ChannelAttachButton"',
find: "Messages.CHAT_ATTACH_UPLOAD_OR_INVITE",
replacement: {
match: /\.attachButtonInner,"aria-label":.{0,50},onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
// Discord merges multiple props here with Object.assign()
// This patch passes a third object to it with which we override onClick and onContextMenu
match: /CHAT_ATTACH_UPLOAD_OR_INVITE,onDoubleClick:(.+?:void 0),\.\.\.(\i),/,
replace: "$&onClick:$1,onContextMenu:$2.onClick,",
},
},

View file

@ -30,7 +30,7 @@ function onPickColor(color: number) {
updateColorVars(hexColor);
}
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE",settings:{useSystemTheme:"system"===');
function setTheme(theme: string) {
saveClientTheme({ theme });

View file

@ -8,7 +8,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc";
import { classes } from "@utils/misc";
import { closeAllModals, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { findComponentByCodeLazy } from "@webpack";
import { Alerts, Button, FluxDispatcher, Forms, GuildStore, NavigationRouter, Parser, Text, Tooltip, useEffect, UserStore, UserUtils, useState } from "@webpack/common";
@ -45,11 +45,7 @@ interface Section {
authorIds?: string[];
}
interface SectionHeaderProps {
section: Section;
}
function SectionHeader({ section }: SectionHeaderProps) {
function SectionHeader({ section }: { section: Section; }) {
const hasSubtitle = typeof section.subtitle !== "undefined";
const hasAuthorIds = typeof section.authorIds !== "undefined";
@ -66,7 +62,6 @@ function SectionHeader({ section }: SectionHeaderProps) {
})();
}, [section.authorIds]);
return <div>
<Flex>
<Forms.FormTitle style={{ flexGrow: 1 }}>{section.title}</Forms.FormTitle>
@ -79,7 +74,8 @@ function SectionHeader({ section }: SectionHeaderProps) {
size={16}
showUserPopout
className={Margins.bottom8}
/>}
/>
}
</Flex>
{hasSubtitle &&
<Forms.FormText type="description" className={Margins.bottom8}>
@ -208,16 +204,7 @@ function ChangeDecorationModal(props: ModalProps) {
{activeSelectedDecoration?.alt}
</Text>
}
{activeDecorationHasAuthor && (
<Text key={`createdBy-${activeSelectedDecoration.authorId}`}>
Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}
</Text>
)}
{isActiveDecorationPreset && (
<Button onClick={() => copyWithToast(activeDecorationPreset.id)}>
Copy Preset ID
</Button>
)}
{activeDecorationHasAuthor && <Text key={`createdBy-${activeSelectedDecoration.authorId}`}>Created by {Parser.parse(`<@${activeSelectedDecoration.authorId}>`)}</Text>}
</div>
</ErrorBoundary>
</ModalContent>

View file

@ -57,7 +57,7 @@ function decode(bio: string): Array<number> | null {
if (bio == null) return null;
const colorString = bio.match(
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]{1,6})\u{e005d}/u,
/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u,
);
if (colorString != null) {
const parsed = [...colorString[0]]

View file

@ -171,7 +171,7 @@ export default definePlugin({
find: ".handleImageLoad)",
replacement: [
{
match: /placeholderVersion:\i,(?=.{0,50}children:)/,
match: /placeholderVersion:\i,/,
replace: "...$self.makeProps(this),$&"
},

View file

@ -6,21 +6,12 @@
import "./styles.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin from "@utils/types";
import { SelectedGuildStore, useState } from "@webpack/common";
import { User } from "discord-types/general";
const settings = definePluginSettings({
showAtSymbol: {
type: OptionType.BOOLEAN,
description: "Whether the the @ symbol should be displayed",
default: true
}
});
export default definePlugin({
name: "MentionAvatars",
description: "Shows user avatars inside mentions",
@ -34,13 +25,11 @@ export default definePlugin({
}
}],
settings,
renderUsername: ErrorBoundary.wrap((props: { user: User, username: string; }) => {
const { user, username } = props;
const [isHovering, setIsHovering] = useState(false);
if (!user) return <>{getUsernameString(username)}</>;
if (!user) return <>@{username}</>;
return (
<span
@ -48,15 +37,8 @@ export default definePlugin({
onMouseLeave={() => setIsHovering(false)}
>
<img src={user.getAvatarURL(SelectedGuildStore.getGuildId(), 16, isHovering)} className="vc-mentionAvatars-avatar" />
{getUsernameString(username)}
@{username}
</span>
);
}, { noop: true })
});
function getUsernameString(username: string) {
return settings.store.showAtSymbol
? `@${username}`
: username;
}

View file

@ -151,7 +151,6 @@ export default definePlugin({
contextMenus: {
"message": patchMessageContextMenu,
"channel-context": patchChannelContextMenu,
"thread-context": patchChannelContextMenu,
"user-context": patchChannelContextMenu,
"gdm-context": patchChannelContextMenu
},

View file

@ -49,7 +49,7 @@ export default definePlugin({
find: ".Messages.MUTUAL_GUILDS_WITH_END_COUNT", // Note: the module is lazy-loaded
replacement: {
match: /(?<=\.tabBarItem.{0,50}MUTUAL_GUILDS.+?}\),)(?=.+?(\(0,\i\.jsxs?\)\(.{0,100}id:))/,
replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:$self.getMutualGDMCountText(arguments[0].user)}),'
replace: '$self.isBotOrSelf(arguments[0].user)?null:$1"MUTUAL_GDMS",children:"Mutual Groups"}),'
}
},
{
@ -64,7 +64,7 @@ export default definePlugin({
replacement: [
{
match: /(?<=onItemSelect:\i,children:)(\i)\.map/,
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:$self.getMutualGDMCountText(arguments[0].user)}])].map"
replace: "[...$1, ...($self.isBotOrSelf(arguments[0].user) ? [] : [{section:'MUTUAL_GDMS',text:'Mutual Groups'}])].map"
},
{
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
@ -76,11 +76,6 @@ export default definePlugin({
isBotOrSelf: (user: User) => user.bot || user.id === UserStore.getCurrentUser().id,
getMutualGDMCountText: (user: User) => {
const count = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).length;
return `${count === 0 ? "No" : count} Mutual Group${count !== 1 ? "s" : ""}`;
},
renderMutualGDMs: ErrorBoundary.wrap(({ user, onClose }: { user: User, onClose: () => void; }) => {
const entries = ChannelStore.getSortedPrivateChannels().filter(c => c.isGroupDM() && c.recipients.includes(user.id)).map(c => (
<Clickable

View file

@ -1,11 +0,0 @@
# OpenInApp
Open links in their respective apps instead of your browser
## Currently supports:
- Spotify
- Steam
- EpicGames
- Tidal
- Apple Music (iTunes)

View file

@ -18,70 +18,46 @@
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, PluginNative, SettingsDefinition } from "@utils/types";
import definePlugin, { OptionType, PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import type { MouseEvent } from "react";
interface URLReplacementRule {
match: RegExp;
replace: (...matches: string[]) => string;
description: string;
shortlinkMatch?: RegExp;
accountViewReplace?: (userId: string) => string;
}
const ShortUrlMatcher = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
const SpotifyMatcher = /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/;
const SteamMatcher = /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/;
const EpicMatcher = /^https:\/\/store\.epicgames\.com\/(.+)$/;
const TidalMatcher = /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/;
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = {
const settings = definePluginSettings({
spotify: {
match: /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `spotify://${type}/${id}`,
type: OptionType.BOOLEAN,
description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,
accountViewReplace: userId => `spotify:user:${userId}`,
default: true,
},
steam: {
match: /^https:\/\/(steamcommunity\.com|(?:help|store)\.steampowered\.com)\/.+$/,
replace: match => `steam://openurl/${match}`,
type: OptionType.BOOLEAN,
description: "Open Steam links in the Steam app",
shortlinkMatch: /^https:\/\/s.team\/.+$/,
accountViewReplace: userId => `steam://openurl/https://steamcommunity.com/profiles/${userId}`,
default: true,
},
epic: {
match: /^https:\/\/store\.epicgames\.com\/(.+)$/,
replace: (_, id) => `com.epicgames.launcher://store/${id}`,
type: OptionType.BOOLEAN,
description: "Open Epic Games links in the Epic Games Launcher",
default: true,
},
tidal: {
match: /^https:\/\/tidal\.com\/browse\/(track|album|artist|playlist|user|video|mix)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `tidal://${type}/${id}`,
description: "Open Tidal links in the Tidal app",
},
itunes: {
match: /^https:\/\/music\.apple\.com\/([a-z]{2}\/)?(album|artist|playlist|song|curator)\/([^/?#]+)\/?([^/?#]+)?(?:\?.*)?(?:#.*)?$/,
replace: (_, lang, type, name, id) => id ? `itunes://music.apple.com/us/${type}/${name}/${id}` : `itunes://music.apple.com/us/${type}/${name}`,
description: "Open Apple Music links in the iTunes app"
},
};
const pluginSettings = definePluginSettings(
Object.entries(UrlReplacementRules).reduce((acc, [key, rule]) => {
acc[key] = {
type: OptionType.BOOLEAN,
description: rule.description,
description: "Open Tidal links in the Tidal app",
default: true,
};
return acc;
}, {} as SettingsDefinition)
);
}
});
const Native = VencordNative.pluginHelpers.OpenInApp as PluginNative<typeof import("./native")>;
export default definePlugin({
name: "OpenInApp",
description: "Open links in their respective apps instead of your browser",
authors: [Devs.Ven, Devs.surgedevs],
settings: pluginSettings,
description: "Open Spotify, Tidal, Steam and Epic Games URLs in their respective apps instead of your browser",
authors: [Devs.Ven],
settings,
patches: [
{
@ -94,7 +70,7 @@ export default definePlugin({
// Make Spotify profile activity links open in app on web
{
find: "WEB_OPEN(",
predicate: () => !IS_DISCORD_DESKTOP && pluginSettings.store.spotify,
predicate: () => !IS_DISCORD_DESKTOP && settings.store.spotify,
replacement: {
match: /\i\.\i\.isProtocolRegistered\(\)(.{0,100})window.open/g,
replace: "true$1VencordNative.native.openExternal"
@ -103,8 +79,8 @@ export default definePlugin({
{
find: ".CONNECTED_ACCOUNT_VIEWED,",
replacement: {
match: /(?<=href:\i,onClick:(\i)=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/,
replace: "if($self.handleAccountView($1,$2.type,$2.id)) return;"
match: /(?<=href:\i,onClick:\i=>\{)(?=.{0,10}\i=(\i)\.type,.{0,100}CONNECTED_ACCOUNT_VIEWED)/,
replace: "$self.handleAccountView(arguments[0],$1.type,$1.id);"
}
}
],
@ -113,25 +89,61 @@ export default definePlugin({
if (!data) return false;
let url = data.href;
if (!url) return false;
for (const [key, rule] of Object.entries(UrlReplacementRules)) {
if (!pluginSettings.store[key]) continue;
if (rule.shortlinkMatch?.test(url)) {
if (!IS_WEB && ShortUrlMatcher.test(url)) {
event?.preventDefault();
// CORS jumpscare
url = await Native.resolveRedirect(url);
}
if (rule.match.test(url)) {
showToast("Opened link in native app", Toasts.Type.SUCCESS);
spotify: {
if (!settings.store.spotify) break spotify;
const newUrl = url.replace(rule.match, rule.replace);
VencordNative.native.openExternal(newUrl);
const match = SpotifyMatcher.exec(url);
if (!match) break spotify;
const [, type, id] = match;
VencordNative.native.openExternal(`spotify:${type}:${id}`);
event?.preventDefault();
return true;
}
steam: {
if (!settings.store.steam) break steam;
if (!SteamMatcher.test(url)) break steam;
VencordNative.native.openExternal(`steam://openurl/${url}`);
event?.preventDefault();
// Steam does not focus itself so show a toast so it's slightly less confusing
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
return true;
}
epic: {
if (!settings.store.epic) break epic;
const match = EpicMatcher.exec(url);
if (!match) break epic;
VencordNative.native.openExternal(`com.epicgames.launcher://store/${match[1]}`);
event?.preventDefault();
return true;
}
tidal: {
if (!settings.store.tidal) break tidal;
const match = TidalMatcher.exec(url);
if (!match) break tidal;
const [, type, id] = match;
VencordNative.native.openExternal(`tidal://${type}/${id}`);
event?.preventDefault();
return true;
}
// in case short url didn't end up being something we can handle
@ -143,12 +155,14 @@ export default definePlugin({
return false;
},
handleAccountView(e: MouseEvent, platformType: string, userId: string) {
const rule = UrlReplacementRules[platformType];
if (rule?.accountViewReplace && pluginSettings.store[platformType]) {
VencordNative.native.openExternal(rule.accountViewReplace(userId));
e.preventDefault();
return true;
handleAccountView(event: { preventDefault(): void; }, platformType: string, userId: string) {
if (platformType === "spotify" && settings.store.spotify) {
VencordNative.native.openExternal(`spotify:user:${userId}`);
event.preventDefault();
} else if (platformType === "steam" && settings.store.steam) {
VencordNative.native.openExternal(`steam://openurl/https://steamcommunity.com/profiles/${userId}`);
showToast("Opened link in Steam", Toasts.Type.SUCCESS);
event.preventDefault();
}
}
});

View file

@ -30,7 +30,7 @@ export default definePlugin({
{
find: ".nonMediaMosaicItem]",
replacement: {
match: /\.nonMediaMosaicItem\]:!(\i).{0,50}?children:\[(\S)/,
match: /\.nonMediaMosaicItem\]:!(\i).{0,10}children:\[(\S)/,
replace: "$&,$1&&$2&&$self.renderPiPButton(),"
},
},

View file

@ -211,9 +211,9 @@ export default definePlugin({
}
},
{
find: '"BiteSizeProfileBody"',
find: /\.BITE_SIZE,onOpenProfile:\i,usernameIcon:/,
replacement: {
match: /currentUser:\i,guild:\i}\)(?<=user:(\i),bio:null==(\i)\?.+?)/,
match: /currentUser:\i,guild:\i,onOpenProfile:.+?}\)(?=])(?<=user:(\i),bio:null==(\i)\?.+?)/,
replace: "$&,$self.profilePopoutComponent({ user: $1, displayProfile: $2, simplified: true })"
}
}

View file

@ -307,7 +307,7 @@ export default definePlugin({
]
},
{
find: '})},"overflow"))',
find: '+1]})},"overflow"))',
replacement: [
{
// Create a variable for the channel prop

View file

@ -66,15 +66,6 @@ export default definePlugin({
replace: "return true",
}
},
// fixes a bug where Members page must be loaded to see highest role, why is Discord depending on MemberSafetyStore.getEnhancedMember for something that can be obtained here?
{
find: "Messages.GUILD_MEMBER_MOD_VIEW_PERMISSION_GRANTED_BY_ARIA_LABEL,allowOverflow",
predicate: () => settings.store.showModView,
replacement: {
match: /(role:)\i(?=,guildId.{0,100}role:(\i\[))/,
replace: "$1$2arguments[0].member.highestRoleId]",
}
},
{
find: "prod_discoverable_guilds",
predicate: () => settings.store.disableDiscoveryFilters,

View file

@ -48,7 +48,7 @@ export default definePlugin({
},
patches: [
{
find: "this.isCopiedStreakGodlike",
find: '"AccountConnected"',
replacement: {
// react.jsx)(AccountPanel, { ..., showTaglessAccountPanel: blah })
match: /(?<=\i\.jsxs?\)\()(\i),{(?=[^}]*?userTag:\i,hidePrivateData:)/,

View file

@ -17,9 +17,10 @@
*/
import { ChatBarButton } from "@api/ChatButtons";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { openModal } from "@utils/modal";
import { Alerts, Forms, Tooltip, useEffect, useState } from "@webpack/common";
import { Alerts, Forms } from "@webpack/common";
import { settings } from "./settings";
import { TranslateModal } from "./TranslateModal";
@ -38,17 +39,9 @@ export function TranslateIcon({ height = 24, width = 24, className }: { height?:
);
}
export let setShouldShowTranslateEnabledTooltip: undefined | ((show: boolean) => void);
export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
const { autoTranslate, showChatBarButton } = settings.use(["autoTranslate", "showChatBarButton"]);
const [shouldShowTranslateEnabledTooltip, setter] = useState(false);
useEffect(() => {
setShouldShowTranslateEnabledTooltip = setter;
return () => setShouldShowTranslateEnabledTooltip = undefined;
}, []);
if (!isMainChat || !showChatBarButton) return null;
const toggle = () => {
@ -59,20 +52,21 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
title: "Vencord Auto-Translate Enabled",
body: <>
<Forms.FormText>
You just enabled Auto Translate! Any message <b>will automatically be translated</b> before being sent.
You just enabled auto translate (by right clicking the Translate icon). Any message you send will automatically be translated before being sent.
</Forms.FormText>
<Forms.FormText className={Margins.top16}>
If this was an accident, disable it again, or it will change your message content before sending.
</Forms.FormText>
</>,
confirmText: "Disable Auto-Translate",
cancelText: "Got it",
cancelText: "Disable Auto-Translate",
confirmText: "Got it",
secondaryConfirmText: "Don't show again",
onConfirmSecondary: () => settings.store.showAutoTranslateAlert = false,
onConfirm: () => settings.store.autoTranslate = false,
// troll
confirmColor: "vc-notification-log-danger-btn",
onCancel: () => settings.store.autoTranslate = false
});
};
const button = (
return (
<ChatBarButton
tooltip="Open Translate Modal"
onClick={e => {
@ -82,7 +76,7 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
<TranslateModal rootProps={props} />
));
}}
onContextMenu={toggle}
onContextMenu={() => toggle()}
buttonProps={{
"aria-haspopup": "dialog"
}}
@ -90,13 +84,4 @@ export const TranslateChatBarIcon: ChatBarButton = ({ isMainChat }) => {
<TranslateIcon className={cl({ "auto-translate": autoTranslate, "chat-button": true })} />
</ChatBarButton>
);
if (shouldShowTranslateEnabledTooltip && settings.store.showAutoTranslateTooltip)
return (
<Tooltip text="Auto Translate Enabled" forceOpen>
{() => button}
</Tooltip>
);
return button;
};

View file

@ -20,8 +20,9 @@ import { Margins } from "@utils/margins";
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal";
import { Forms, SearchableSelect, Switch, useMemo } from "@webpack/common";
import { Languages } from "./languages";
import { settings } from "./settings";
import { cl, getLanguages } from "./utils";
import { cl } from "./utils";
const LanguageSettingKeys = ["receivedInput", "receivedOutput", "sentInput", "sentOutput"] as const;
@ -30,7 +31,7 @@ function LanguageSelect({ settingsKey, includeAuto }: { settingsKey: typeof Lang
const options = useMemo(
() => {
const options = Object.entries(getLanguages()).map(([value, label]) => ({ value, label }));
const options = Object.entries(Languages).map(([value, label]) => ({ value, label }));
if (!includeAuto)
options.shift();

View file

@ -19,6 +19,7 @@
import { Parser, useEffect, useState } from "@webpack/common";
import { Message } from "discord-types/general";
import { Languages } from "./languages";
import { TranslateIcon } from "./TranslateIcon";
import { cl, TranslationValue } from "./utils";
@ -58,7 +59,7 @@ export function TranslationAccessory({ message }: { message: Message; }) {
<TranslateIcon width={16} height={16} />
{Parser.parse(translation.text)}
{" "}
(translated from {translation.sourceLanguage} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
(translated from {Languages[translation.src] ?? translation.src} - <Dismiss onDismiss={() => setTranslation(undefined)} />)
</span>
);
}

View file

@ -28,7 +28,7 @@ import definePlugin from "@utils/types";
import { ChannelStore, Menu } from "@webpack/common";
import { settings } from "./settings";
import { setShouldShowTranslateEnabledTooltip, TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
import { TranslateChatBarIcon, TranslateIcon } from "./TranslateIcon";
import { handleTranslate, TranslationAccessory } from "./TranslationAccessory";
import { translate } from "./utils";
@ -53,8 +53,8 @@ const messageCtxPatch: NavContextMenuPatchCallback = (children, { message }) =>
export default definePlugin({
name: "Translate",
description: "Translate messages with Google Translate or DeepL",
authors: [Devs.Ven, Devs.AshtonMemer],
description: "Translate messages with Google Translate",
authors: [Devs.Ven],
dependencies: ["MessageAccessoriesAPI", "MessagePopoverAPI", "MessageEventsAPI", "ChatInputButtonAPI"],
settings,
contextMenus: {
@ -83,18 +83,11 @@ export default definePlugin({
};
});
let tooltipTimeout: any;
this.preSend = addPreSendListener(async (_, message) => {
if (!settings.store.autoTranslate) return;
if (!message.content) return;
setShouldShowTranslateEnabledTooltip?.(true);
clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => setShouldShowTranslateEnabledTooltip?.(false), 2000);
const trans = await translate("sent", message.content);
message.content = trans.text;
message.content = (await translate("sent", message.content)).text;
});
},

View file

@ -31,10 +31,9 @@ copy(Object.fromEntries(
))
*/
export type GoogleLanguage = keyof typeof GoogleLanguages;
export type DeeplLanguage = keyof typeof DeeplLanguages;
export type Language = keyof typeof Languages;
export const GoogleLanguages = {
export const Languages = {
"auto": "Detect language",
"af": "Afrikaans",
"sq": "Albanian",
@ -170,57 +169,3 @@ export const GoogleLanguages = {
"yo": "Yoruba",
"zu": "Zulu"
} as const;
export const DeeplLanguages = {
"": "Detect language",
"ar": "Arabic",
"bg": "Bulgarian",
"zh-hans": "Chinese (Simplified)",
"zh-hant": "Chinese (Traditional)",
"cs": "Czech",
"da": "Danish",
"nl": "Dutch",
"en-us": "English (American)",
"en-gb": "English (British)",
"et": "Estonian",
"fi": "Finnish",
"fr": "French",
"de": "German",
"el": "Greek",
"hu": "Hungarian",
"id": "Indonesian",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"lv": "Latvian",
"lt": "Lithuanian",
"nb": "Norwegian",
"pl": "Polish",
"pt-br": "Portuguese (Brazilian)",
"pt-pt": "Portuguese (European)",
"ro": "Romanian",
"ru": "Russian",
"sk": "Slovak",
"sl": "Slovenian",
"es": "Spanish",
"sv": "Swedish",
"tr": "Turkish",
"uk": "Ukrainian"
} as const;
export function deeplLanguageToGoogleLanguage(language: string) {
switch (language) {
case "": return "auto";
case "nb": return "no";
case "zh-hans": return "zh-CN";
case "zh-hant": return "zh-TW";
case "en-us":
case "en-gb":
return "en";
case "pt-br":
case "pt-pt":
return "pt";
default:
return language;
}
}

View file

@ -1,29 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { IpcMainInvokeEvent } from "electron";
export async function makeDeeplTranslateRequest(_: IpcMainInvokeEvent, pro: boolean, apiKey: string, payload: string) {
const url = pro
? "https://api.deepl.com/v2/translate"
: "https://api-free.deepl.com/v2/translate";
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `DeepL-Auth-Key ${apiKey}`
},
body: payload
});
const data = await res.text();
return { status: res.status, data };
} catch (e) {
return { status: -1, data: String(e) };
}
}

View file

@ -22,76 +22,38 @@ import { OptionType } from "@utils/types";
export const settings = definePluginSettings({
receivedInput: {
type: OptionType.STRING,
description: "Language that received messages should be translated from",
description: "Input language for received messages",
default: "auto",
hidden: true
},
receivedOutput: {
type: OptionType.STRING,
description: "Language that received messages should be translated to",
description: "Output language for received messages",
default: "en",
hidden: true
},
sentInput: {
type: OptionType.STRING,
description: "Language that your own messages should be translated from",
description: "Input language for sent messages",
default: "auto",
hidden: true
},
sentOutput: {
type: OptionType.STRING,
description: "Language that your own messages should be translated to",
description: "Output language for sent messages",
default: "en",
hidden: true
},
showChatBarButton: {
type: OptionType.BOOLEAN,
description: "Show translate button in chat bar",
default: true
},
service: {
type: OptionType.SELECT,
description: IS_WEB ? "Translation service (Not supported on Web!)" : "Translation service",
disabled: () => IS_WEB,
options: [
{ label: "Google Translate", value: "google", default: true },
{ label: "DeepL Free", value: "deepl" },
{ label: "DeepL Pro", value: "deepl-pro" }
] as const,
onChange: resetLanguageDefaults
},
deeplApiKey: {
type: OptionType.STRING,
description: "DeepL API key",
default: "",
placeholder: "Get your API key from https://deepl.com/your-account",
disabled: () => IS_WEB
},
autoTranslate: {
type: OptionType.BOOLEAN,
description: "Automatically translate your messages before sending. You can also shift/right click the translate button to toggle this",
default: false
},
showAutoTranslateTooltip: {
showChatBarButton: {
type: OptionType.BOOLEAN,
description: "Show a tooltip on the ChatBar button whenever a message is automatically translated",
description: "Show translate button in chat bar",
default: true
},
}
}).withPrivateSettings<{
showAutoTranslateAlert: boolean;
}>();
export function resetLanguageDefaults() {
if (IS_WEB || settings.store.service === "google") {
settings.store.receivedInput = "auto";
settings.store.receivedOutput = "en";
settings.store.sentInput = "auto";
settings.store.sentOutput = "en";
} else {
settings.store.receivedInput = "";
settings.store.receivedOutput = "en-us";
settings.store.sentInput = "";
settings.store.sentOutput = "en-us";
}
}

View file

@ -17,18 +17,12 @@
*/
import { classNameFactory } from "@api/Styles";
import { onlyOnce } from "@utils/onlyOnce";
import { PluginNative } from "@utils/types";
import { showToast, Toasts } from "@webpack/common";
import { DeeplLanguages, deeplLanguageToGoogleLanguage, GoogleLanguages } from "./languages";
import { resetLanguageDefaults, settings } from "./settings";
import { settings } from "./settings";
export const cl = classNameFactory("vc-trans-");
const Native = VencordNative.pluginHelpers.Translate as PluginNative<typeof import("./native")>;
interface GoogleData {
interface TranslationData {
src: string;
sentences: {
// 🏳️‍⚧️
@ -36,47 +30,15 @@ interface GoogleData {
}[];
}
interface DeeplData {
translations: {
detected_source_language: string;
text: string;
}[];
}
export interface TranslationValue {
sourceLanguage: string;
src: string;
text: string;
}
export const getLanguages = () => IS_WEB || settings.store.service === "google"
? GoogleLanguages
: DeeplLanguages;
export async function translate(kind: "received" | "sent", text: string): Promise<TranslationValue> {
const translate = IS_WEB || settings.store.service === "google"
? googleTranslate
: deeplTranslate;
const sourceLang = settings.store[kind + "Input"];
const targetLang = settings.store[kind + "Output"];
try {
return await translate(
text,
settings.store[`${kind}Input`],
settings.store[`${kind}Output`]
);
} catch (e) {
const userMessage = typeof e === "string"
? e
: "Something went wrong. If this issue persists, please check the console or ask for help in the support server.";
showToast(userMessage, Toasts.Type.FAILURE);
throw e instanceof Error
? e
: new Error(userMessage);
}
}
async function googleTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
const url = "https://translate.googleapis.com/translate_a/single?" + new URLSearchParams({
// see https://stackoverflow.com/a/29537590 for more params
// holy shidd nvidia
@ -101,69 +63,13 @@ async function googleTranslate(text: string, sourceLang: string, targetLang: str
+ `\n${res.status} ${res.statusText}`
);
const { src, sentences }: GoogleData = await res.json();
const { src, sentences }: TranslationData = await res.json();
return {
sourceLanguage: GoogleLanguages[src] ?? src,
src,
text: sentences.
map(s => s?.trans).
filter(Boolean).
join("")
};
}
function fallbackToGoogle(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
return googleTranslate(
text,
deeplLanguageToGoogleLanguage(sourceLang),
deeplLanguageToGoogleLanguage(targetLang)
);
}
const showDeeplApiQuotaToast = onlyOnce(
() => showToast("Deepl API quota exceeded. Falling back to Google Translate", Toasts.Type.FAILURE)
);
async function deeplTranslate(text: string, sourceLang: string, targetLang: string): Promise<TranslationValue> {
if (!settings.store.deeplApiKey) {
showToast("DeepL API key is not set. Resetting to Google", Toasts.Type.FAILURE);
settings.store.service = "google";
resetLanguageDefaults();
return fallbackToGoogle(text, sourceLang, targetLang);
}
// CORS jumpscare
const { status, data } = await Native.makeDeeplTranslateRequest(
settings.store.service === "deepl-pro",
settings.store.deeplApiKey,
JSON.stringify({
text: [text],
target_lang: targetLang,
source_lang: sourceLang.split("-")[0]
})
);
switch (status) {
case 200:
break;
case -1:
throw "Failed to connect to DeepL API: " + data;
case 403:
throw "Invalid DeepL API key or version";
case 456:
showDeeplApiQuotaToast();
return fallbackToGoogle(text, sourceLang, targetLang);
default:
throw new Error(`Failed to translate "${text}" (${sourceLang} -> ${targetLang})\n${status} ${data}`);
}
const { translations }: DeeplData = JSON.parse(data);
const src = translations[0].detected_source_language;
return {
sourceLanguage: DeeplLanguages[src] ?? src,
text: translations[0].text
};
}

View file

@ -189,8 +189,7 @@ export default definePlugin({
replacement: {
match: /avatarSrc:(\i),eventHandlers:(\i).+?"div",{...\2,/,
replace: "$&style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)},"
},
all: true
}
},
// Old Profiles Modal pfp
{

View file

@ -153,7 +153,6 @@ export default definePlugin({
contextMenus: {
"guild-context": MakeContextCallback("Guild"),
"channel-context": MakeContextCallback("Channel"),
"thread-context": MakeContextCallback("Channel"),
"user-context": MakeContextCallback("User")
},

View file

@ -1,7 +1,6 @@
# WatchTogetherAdblock
Block ads in YouTube embeds and the WatchTogether activity via AdGuard
Block ads in the YouTube WatchTogether activity via AdGuard
Note that this only works for yourself, other users in the activity will still see ads.
Powered by a modified version of [Adguard's BlockYoutubeAdsShortcut](https://github.com/AdguardTeam/BlockYouTubeAdsShortcut)

View file

@ -4,14 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
// The entire code of this plugin can be found in native.ts
migratePluginSettings("YoutubeAdblock", "WatchTogetherAdblock");
export default definePlugin({
name: "YoutubeAdblock",
description: "Block ads in YouTube embeds and the WatchTogether activity via AdGuard",
authors: [Devs.ImLvna, Devs.Ven],
name: "WatchTogetherAdblock",
description: "Block ads in the YouTube WatchTogether activity via AdGuard",
authors: [Devs.ImLvna],
});

View file

@ -11,9 +11,9 @@ import adguard from "file://adguard.js?minify";
app.on("browser-window-created", (_, win) => {
win.webContents.on("frame-created", (_, { frame }) => {
frame.once("dom-ready", () => {
if (!RendererSettings.store.plugins?.YoutubeAdblock?.enabled) return;
if (frame.url.includes("discordsays") && frame.url.includes("youtube.com")) {
if (!RendererSettings.store.plugins?.WatchTogetherAdblock?.enabled) return;
if (frame.url.includes("youtube.com/embed/") || (frame.url.includes("discordsays") && frame.url.includes("youtube.com"))) {
frame.executeJavaScript(adguard);
}
});

View file

@ -0,0 +1,15 @@
# XSOverlay Notifier
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
## Preview
![](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
![](https://github.com/Vendicated/Vencord/assets/24845294/f15eff61-2d52-4620-bcab-808ecb1606d2)
## Usage
- Enable this plugin
- Set plugin settings as desired
- Open XSOverlay
- get ping spammed

View file

@ -8,9 +8,9 @@ import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Button, ChannelStore, GuildStore, UserStore } from "@webpack/common";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
const ChannelTypes = findLazy(m => m.ANNOUNCEMENT_THREAD === 10);
@ -68,40 +68,10 @@ interface Call {
ringing: string[];
}
interface ApiObject {
sender: string,
target: string,
command: string,
jsonData: string,
rawData: string | null,
}
interface NotificationObject {
type: number;
timeout: number;
height: number;
opacity: number;
volume: number;
audioPath: string;
title: string;
content: string;
useBase64Icon: boolean;
icon: ArrayBuffer | string;
sourceApp: string;
}
const notificationsShouldNotify = findByCodeLazy(".SUPPRESS_NOTIFICATIONS))return!1");
const logger = new Logger("XSOverlay");
const XSLog = new Logger("XSOverlay");
const settings = definePluginSettings({
webSocketPort: {
type: OptionType.NUMBER,
description: "Websocket port",
default: 42070,
async onChange() {
await start();
}
},
botNotifications: {
type: OptionType.BOOLEAN,
description: "Allow bot notifications",
@ -166,17 +136,7 @@ const settings = definePluginSettings({
},
});
let socket: WebSocket;
async function start() {
if (socket) socket.close();
socket = new WebSocket(`ws://127.0.0.1:${settings.store.webSocketPort ?? 42070}/?client=Vencord`);
return new Promise((resolve, reject) => {
socket.onopen = resolve;
socket.onerror = reject;
setTimeout(reject, 3000);
});
}
const Native = VencordNative.pluginHelpers.XSOverlay as PluginNative<typeof import("./native")>;
export default definePlugin({
name: "XSOverlay",
@ -288,21 +248,7 @@ export default definePlugin({
if (shouldIgnoreForChannelType(channel)) return;
sendMsgNotif(titleString, finalMsg, message);
}
},
start,
stop() {
socket.close();
},
settingsAboutComponent: () => (
<>
<Button onClick={() => sendOtherNotif("This is a test notification! explode", "Hello from Vendor!")}>
Send test notification
</Button>
</>
)
}
});
function shouldIgnoreForChannelType(channel: Channel) {
@ -313,8 +259,9 @@ function shouldIgnoreForChannelType(channel: Channel) {
function sendMsgNotif(titleString: string, content: string, message: Message) {
fetch(`https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png?size=128`).then(response => response.arrayBuffer()).then(result => {
const msgData: NotificationObject = {
type: 1,
const msgData = {
messageType: 1,
index: 0,
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
height: calculateHeight(content),
opacity: settings.store.opacity,
@ -323,17 +270,17 @@ function sendMsgNotif(titleString: string, content: string, message: Message) {
title: titleString,
content: content,
useBase64Icon: true,
icon: new TextDecoder().decode(result),
icon: result,
sourceApp: "Vencord"
};
sendToOverlay(msgData);
Native.sendToOverlay(msgData);
});
}
function sendOtherNotif(content: string, titleString: string) {
const msgData: NotificationObject = {
type: 1,
const msgData = {
messageType: 1,
index: 0,
timeout: settings.store.lengthBasedTimeout ? calculateTimeout(content) : settings.store.timeout,
height: calculateHeight(content),
opacity: settings.store.opacity,
@ -342,22 +289,10 @@ function sendOtherNotif(content: string, titleString: string) {
title: titleString,
content: content,
useBase64Icon: false,
icon: "default",
icon: null,
sourceApp: "Vencord"
};
sendToOverlay(msgData);
}
async function sendToOverlay(notif: NotificationObject) {
const apiObject: ApiObject = {
sender: "Vencord",
target: "xsoverlay",
command: "SendNotification",
jsonData: JSON.stringify(notif),
rawData: null
};
if (socket.readyState !== socket.OPEN) await start();
socket.send(JSON.stringify(apiObject));
Native.sendToOverlay(msgData);
}
function shouldNotify(message: Message, channel: string) {

View file

@ -0,0 +1,16 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { createSocket, Socket } from "dgram";
let xsoSocket: Socket;
export function sendToOverlay(_, data: any) {
data.icon = Buffer.from(data.icon).toString("base64");
const json = JSON.stringify(data);
xsoSocket ??= createSocket("udp4");
xsoSocket.send(json, 42069, "127.0.0.1");
}

View file

@ -1,14 +0,0 @@
# XSOverlay Notifier
Sends Discord messages to [XSOverlay](https://store.steampowered.com/app/1173510/XSOverlay/) for easier viewing while using VR.
## Preview
![Resulting notification inside XSOverlay](https://github.com/Vendicated/Vencord/assets/24845294/205d2055-bb4a-44e4-b7e3-265391bccd40)
![Test notification inside XSOverlay](https://github.com/user-attachments/assets/d3b0c387-1d67-4697-a470-d4a927e228f4)
## Usage
- Enable this plugin
- Set port and plugin settings as desired (defaults should work fine)
- Open SteamVR and XSOverlay

View file

@ -533,18 +533,6 @@ export const Devs = /* #__PURE__*/ Object.freeze({
Antti: {
name: "Antti",
id: 312974985876471810n
},
Joona: {
name: "Joona",
id: 297410829589020673n
},
AshtonMemer: {
name: "AshtonMemer",
id: 373657230530052099n
},
surgedevs: {
name: "Chloe",
id: 1084592643784331324n
}
} satisfies Record<string, Dev>);

View file

@ -544,7 +544,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
}
if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => Number(m[1]));
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id)));
}
@ -559,7 +559,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false;
}
wreq(Number(entryPointId));
wreq(entryPointId);
return true;
}