Merge branch 'Vendicated:main' into main

This commit is contained in:
catcraft 2024-09-01 13:23:40 +09:30 committed by GitHub
commit 49c3afe893
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 439 additions and 88 deletions

View file

@ -70,6 +70,7 @@
"stylelint": "^16.8.1",
"stylelint-config-standard": "^36.0.1",
"ts-patch": "^3.2.1",
"ts-pattern": "^5.3.1",
"tsx": "^4.16.5",
"type-fest": "^4.23.0",
"typescript": "^5.5.4",

8
pnpm-lock.yaml generated
View file

@ -116,6 +116,9 @@ importers:
ts-patch:
specifier: ^3.2.1
version: 3.2.1
ts-pattern:
specifier: ^5.3.1
version: 5.3.1
tsx:
specifier: ^4.16.5
version: 4.16.5
@ -2524,6 +2527,9 @@ packages:
resolution: {integrity: sha512-hlR43v+GUIUy8/ZGFP1DquEqPh7PFKQdDMTAmYt671kCCA6AkDQMoeFaFmZ7ObPLYOmpMgyKUqL1C+coFMf30w==}
hasBin: true
ts-pattern@5.3.1:
resolution: {integrity: sha512-1RUMKa8jYQdNfmnK4jyzBK3/PS/tnjcZ1CW0v1vWDeYe5RBklc/nquw03MEoB66hVBm4BnlCfmOqDVxHyT1DpA==}
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
@ -5158,6 +5164,8 @@ snapshots:
semver: 7.6.3
strip-ansi: 6.0.1
ts-pattern@5.3.1: {}
tsconfig-paths@3.15.0:
dependencies:
'@types/json5': 0.0.29

View file

@ -230,6 +230,10 @@ export function definePluginSettings<
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return Settings.plugins[definedSettings.pluginName] as any;
},
get plain() {
if (!definedSettings.pluginName) throw new Error("Cannot access settings before plugin is initialized");
return PlainSettings.plugins[definedSettings.pluginName] as any;
},
use: settings => useSettings(
settings?.map(name => `plugins.${definedSettings.pluginName}.${name}`) as UseSettings<Settings>[]
).plugins[definedSettings.pluginName] as any,

View file

@ -65,8 +65,7 @@ export function LinkIcon({ height = 24, width = 24, className }: IconProps) {
}
/**
* Discord's copy icon, as seen in the user popout right of the username when clicking
* your own username in the bottom left user panel
* Discord's copy icon, as seen in the user panel popout on the right of the username and in large code blocks
*/
export function CopyIcon(props: IconProps) {
return (
@ -76,8 +75,9 @@ export function CopyIcon(props: IconProps) {
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1z" />
<path d="M15 5H8c-1.1 0-1.99.9-1.99 2L6 21c0 1.1.89 2 1.99 2H19c1.1 0 2-.9 2-2V11l-6-6zM8 21V7h6v5h5v9H8z" />
<path d="M3 16a1 1 0 0 1-1-1v-5a8 8 0 0 1 8-8h5a1 1 0 0 1 1 1v.5a.5.5 0 0 1-.5.5H10a6 6 0 0 0-6 6v5.5a.5.5 0 0 1-.5.5H3Z" />
<path d="M6 18a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4v-4h-3a5 5 0 0 1-5-5V6h-4a4 4 0 0 0-4 4v8Z" />
<path d="M21.73 12a3 3 0 0 0-.6-.88l-4.25-4.24a3 3 0 0 0-.88-.61V9a3 3 0 0 0 3 3h2.73Z" />
</g>
</Icon>
);

View file

@ -382,6 +382,7 @@ function PatchHelper() {
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<CodeBlock lang="js" content={code} />
<Button onClick={() => Clipboard.copy(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => Clipboard.copy("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
</>
)}
</SettingsTab>

View file

@ -0,0 +1,5 @@
# CopyFileContents
Adds a button to text file attachments to copy their contents.
![](https://github.com/user-attachments/assets/b1a0f6f4-106f-4953-94d9-4c5ef5810bca)

View file

@ -0,0 +1,60 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./style.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { CopyIcon, NoEntrySignIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import { copyWithToast } from "@utils/misc";
import definePlugin from "@utils/types";
import { Tooltip, useState } from "@webpack/common";
const CheckMarkIcon = () => {
return <svg width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M21.7 5.3a1 1 0 0 1 0 1.4l-12 12a1 1 0 0 1-1.4 0l-6-6a1 1 0 1 1 1.4-1.4L9 16.58l11.3-11.3a1 1 0 0 1 1.4 0Z"></path>
</svg>;
};
export default definePlugin({
name: "CopyFileContents",
description: "Adds a button to text file attachments to copy their contents",
authors: [Devs.Obsidian, Devs.Nuckyz],
patches: [
{
find: ".Messages.PREVIEW_BYTES_LEFT.format(",
replacement: {
match: /\.footerGap.+?url:\i,fileName:\i,fileSize:\i}\),(?<=fileContents:(\i),bytesLeft:(\i).+?)/g,
replace: "$&$self.addCopyButton({fileContents:$1,bytesLeft:$2}),"
}
}
],
addCopyButton: ErrorBoundary.wrap(({ fileContents, bytesLeft }: { fileContents: string, bytesLeft: number; }) => {
const [recentlyCopied, setRecentlyCopied] = useState(false);
return (
<Tooltip text={recentlyCopied ? "Copied!" : bytesLeft > 0 ? "File too large to copy" : "Copy File Contents"}>
{tooltipProps => (
<div
{...tooltipProps}
className="vc-cfc-button"
role="button"
onClick={() => {
if (!recentlyCopied && bytesLeft <= 0) {
copyWithToast(fileContents);
setRecentlyCopied(true);
setTimeout(() => setRecentlyCopied(false), 2000);
}
}}
>
{recentlyCopied ? <CheckMarkIcon /> : bytesLeft > 0 ? <NoEntrySignIcon color="var(--channel-icon)" /> : <CopyIcon />}
</div>
)}
</Tooltip>
);
}, { noop: true }),
});

View file

@ -0,0 +1,8 @@
.vc-cfc-button {
color: var(--interactive-normal);
cursor: pointer;
}
.vc-cfc-button:hover {
color: var(--interactive-hover);
}

View file

@ -64,7 +64,7 @@ export default definePlugin({
replace: (_, sectionTypes, commaOrSemi, elements, element) => `${commaOrSemi} $self.addSettings(${elements}, ${element}, ${sectionTypes}) ${commaOrSemi}`
},
{
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,30}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
match: /({(?=.+?function (\i).{0,120}(\i)=\i\.useMemo.{0,60}return \i\.useMemo\(\(\)=>\i\(\3).+?function\(\){return )\2(?=})/,
replace: (_, rest, settingsHook) => `${rest}$self.wrapSettingsHook(${settingsHook})`
}
]

View file

@ -0,0 +1,3 @@
# Always Expand Roles
Always expands the role list in profile popouts

View file

@ -1,6 +1,6 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -16,20 +16,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
migratePluginSettings("AlwaysExpandRoles", "ShowAllRoles");
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.fawn],
name: "AlwaysExpandRoles",
description: "Always expands the role list in profile popouts",
authors: [Devs.surgedevs],
patches: [
{
find: "}renderTimeBar(",
find: 'action:"EXPAND_ROLES"',
replacement: {
match: /renderTimeBar\((.{1,3})\){.{0,50}?let/,
replace: "renderTimeBar($1){let"
match: /(roles:\i(?=.+?(\i)\(!0\)[,;]\i\({action:"EXPAND_ROLES"}\)).+?\[\i,\2\]=\i\.useState\()!1\)/,
replace: (_, rest, setExpandedRoles) => `${rest}!0)`
}
}
],
]
});

View file

@ -60,13 +60,6 @@ export default definePlugin({
replace: ""
}
},
{
find: "notosans-400-normalitalic",
replacement: {
match: /,"notosans-.+?"/g,
replace: ""
}
},
{
find: 'console.warn("[DEPRECATED] Please use `subscribeWithSelector` middleware");',
all: true,

View file

@ -26,6 +26,11 @@ interface IgnoredActivity {
type: ActivitiesTypes;
}
const enum FilterMode {
Whitelist,
Blacklist
}
const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getUserSettingLazy("status", "showCurrentGame")!;
@ -70,14 +75,17 @@ function handleActivityToggle(e: React.MouseEvent<HTMLButtonElement, MouseEvent>
if (ignoredActivityIndex === -1) settings.store.ignoredActivities = getIgnoredActivities().concat(activity);
else settings.store.ignoredActivities = getIgnoredActivities().filter((_, index) => index !== ignoredActivityIndex);
// Trigger activities recalculation
recalculateActivities();
}
function recalculateActivities() {
ShowCurrentGame.updateSetting(old => old);
}
function ImportCustomRPCComponent() {
return (
<Flex flexDirection="column">
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the allowed list</Forms.FormText>
<Forms.FormText type={Forms.FormText.Types.DESCRIPTION}>Import the application id of the CustomRPC plugin to the filter list</Forms.FormText>
<div>
<Button
onClick={() => {
@ -86,7 +94,7 @@ function ImportCustomRPCComponent() {
return showToast("CustomRPC application ID is not set.", Toasts.Type.FAILURE);
}
const isAlreadyAdded = allowedIdsPushID?.(id);
const isAlreadyAdded = idsListPushID?.(id);
if (isAlreadyAdded) {
showToast("CustomRPC application ID is already added.", Toasts.Type.FAILURE);
}
@ -99,39 +107,39 @@ function ImportCustomRPCComponent() {
);
}
let allowedIdsPushID: ((id: string) => boolean) | null = null;
let idsListPushID: ((id: string) => boolean) | null = null;
function AllowedIdsComponent(props: { setValue: (value: string) => void; }) {
const [allowedIds, setAllowedIds] = useState<string>(settings.store.allowedIds ?? "");
function IdsListComponent(props: { setValue: (value: string) => void; }) {
const [idsList, setIdsList] = useState<string>(settings.store.idsList ?? "");
allowedIdsPushID = (id: string) => {
const currentIds = new Set(allowedIds.split(",").map(id => id.trim()).filter(Boolean));
idsListPushID = (id: string) => {
const currentIds = new Set(idsList.split(",").map(id => id.trim()).filter(Boolean));
const isAlreadyAdded = currentIds.has(id) || (currentIds.add(id), false);
const ids = Array.from(currentIds).join(", ");
setAllowedIds(ids);
setIdsList(ids);
props.setValue(ids);
return isAlreadyAdded;
};
useEffect(() => () => {
allowedIdsPushID = null;
idsListPushID = null;
}, []);
function handleChange(newValue: string) {
setAllowedIds(newValue);
setIdsList(newValue);
props.setValue(newValue);
}
return (
<Forms.FormSection>
<Forms.FormTitle tag="h3">Allowed List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to allow (Useful for allowing RPC activities and CustomRPC)</Forms.FormText>
<Forms.FormTitle tag="h3">Filter List</Forms.FormTitle>
<Forms.FormText className={Margins.bottom8} type={Forms.FormText.Types.DESCRIPTION}>Comma separated list of activity IDs to filter (Useful for filtering specific RPC activities and CustomRPC</Forms.FormText>
<TextInput
type="text"
value={allowedIds}
value={idsList}
onChange={handleChange}
placeholder="235834946571337729, 343383572805058560"
/>
@ -145,40 +153,62 @@ const settings = definePluginSettings({
description: "",
component: () => <ImportCustomRPCComponent />
},
allowedIds: {
listMode: {
type: OptionType.SELECT,
description: "Change the mode of the filter list",
options: [
{
label: "Whitelist",
value: FilterMode.Whitelist,
default: true
},
{
label: "Blacklist",
value: FilterMode.Blacklist,
}
],
onChange: recalculateActivities
},
idsList: {
type: OptionType.COMPONENT,
description: "",
default: "",
onChange(newValue: string) {
const ids = new Set(newValue.split(",").map(id => id.trim()).filter(Boolean));
settings.store.allowedIds = Array.from(ids).join(", ");
settings.store.idsList = Array.from(ids).join(", ");
recalculateActivities();
},
component: props => <AllowedIdsComponent setValue={props.setValue} />
component: props => <IdsListComponent setValue={props.setValue} />
},
ignorePlaying: {
type: OptionType.BOOLEAN,
description: "Ignore all playing activities (These are usually game and RPC activities)",
default: false
default: false,
onChange: recalculateActivities
},
ignoreStreaming: {
type: OptionType.BOOLEAN,
description: "Ignore all streaming activities",
default: false
default: false,
onChange: recalculateActivities
},
ignoreListening: {
type: OptionType.BOOLEAN,
description: "Ignore all listening activities (These are usually spotify activities)",
default: false
default: false,
onChange: recalculateActivities
},
ignoreWatching: {
type: OptionType.BOOLEAN,
description: "Ignore all watching activities",
default: false
default: false,
onChange: recalculateActivities
},
ignoreCompeting: {
type: OptionType.BOOLEAN,
description: "Ignore all competing activities (These are normally special game activities)",
default: false
default: false,
onChange: recalculateActivities
}
}).withPrivateSettings<{
ignoredActivities: IgnoredActivity[];
@ -189,8 +219,8 @@ function getIgnoredActivities() {
}
function isActivityTypeIgnored(type: number, id?: string) {
if (id && settings.store.allowedIds.includes(id)) {
return false;
if (id && settings.store.idsList.includes(id)) {
return settings.store.listMode === FilterMode.Blacklist;
}
switch (type) {
@ -206,7 +236,7 @@ function isActivityTypeIgnored(type: number, id?: string) {
export default definePlugin({
name: "IgnoreActivities",
authors: [Devs.Nuckyz],
authors: [Devs.Nuckyz, Devs.Kylie],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
dependencies: ["UserSettingsAPI"],
@ -253,6 +283,12 @@ export default definePlugin({
],
async start() {
// Migrate allowedIds
if (Settings.plugins.IgnoreActivities.allowedIds) {
settings.store.idsList = Settings.plugins.IgnoreActivities.allowedIds;
delete Settings.plugins.IgnoreActivities.allowedIds; // Remove allowedIds
}
const oldIgnoredActivitiesData = await DataStore.get<Map<IgnoredActivity["id"], IgnoredActivity>>("IgnoreActivities_ignoredActivities");
if (oldIgnoredActivitiesData != null) {
@ -279,7 +315,7 @@ export default definePlugin({
if (isActivityTypeIgnored(props.type, props.application_id)) return false;
if (props.application_id != null) {
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || settings.store.allowedIds.includes(props.application_id);
return !getIgnoredActivities().some(activity => activity.id === props.application_id) || (settings.store.listMode === FilterMode.Whitelist && settings.store.idsList.includes(props.application_id));
} else {
const exePath = RunningGameStore.getRunningGames().find(game => game.name === props.name)?.exePath;
if (exePath) {

View file

@ -1,5 +1,6 @@
# MentionAvatars
Shows user avatars inside mentions
Shows user avatars and role icons inside mentions
![](https://github.com/user-attachments/assets/fc76ea47-5e19-4063-a592-c57785a75cc7)
![](https://github.com/user-attachments/assets/76c4c3d9-7cde-42db-ba84-903cbb40c163)

View file

@ -10,21 +10,42 @@ import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { SelectedGuildStore, useState } from "@webpack/common";
import { GuildStore, 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",
description: "Whether the the @ symbol should be displayed on user mentions",
default: true
}
});
function DefaultRoleIcon() {
return (
<svg
className="vc-mentionAvatars-icon vc-mentionAvatars-role-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M14 8.00598C14 10.211 12.206 12.006 10 12.006C7.795 12.006 6 10.211 6 8.00598C6 5.80098 7.794 4.00598 10 4.00598C12.206 4.00598 14 5.80098 14 8.00598ZM2 19.006C2 15.473 5.29 13.006 10 13.006C14.711 13.006 18 15.473 18 19.006V20.006H2V19.006Z"
/>
<path
d="M20.0001 20.006H22.0001V19.006C22.0001 16.4433 20.2697 14.4415 17.5213 13.5352C19.0621 14.9127 20.0001 16.8059 20.0001 19.006V20.006Z"
/>
<path
d="M14.8834 11.9077C16.6657 11.5044 18.0001 9.9077 18.0001 8.00598C18.0001 5.96916 16.4693 4.28218 14.4971 4.0367C15.4322 5.09511 16.0001 6.48524 16.0001 8.00598C16.0001 9.44888 15.4889 10.7742 14.6378 11.8102C14.7203 11.8418 14.8022 11.8743 14.8834 11.9077Z"
/>
</svg>
);
}
export default definePlugin({
name: "MentionAvatars",
description: "Shows user avatars inside mentions",
authors: [Devs.Ven],
description: "Shows user avatars and role icons inside mentions",
authors: [Devs.Ven, Devs.SerStars],
patches: [{
find: ".USER_MENTION)",
@ -32,6 +53,13 @@ export default definePlugin({
match: /children:"@"\.concat\((null!=\i\?\i:\i)\)(?<=\.useName\((\i)\).+?)/,
replace: "children:$self.renderUsername({username:$1,user:$2})"
}
},
{
find: ".ROLE_MENTION)",
replacement: {
match: /children:\[\i&&.{0,50}\.RoleDot.{0,300},\i(?=\])/,
replace: "$&,$self.renderRoleIcon(arguments[0])"
}
}],
settings,
@ -47,12 +75,31 @@ export default definePlugin({
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<img src={user.getAvatarURL(SelectedGuildStore.getGuildId(), 16, isHovering)} className="vc-mentionAvatars-avatar" />
<img
src={user.getAvatarURL(SelectedGuildStore.getGuildId(), 16, isHovering)}
className="vc-mentionAvatars-icon"
style={{ borderRadius: "50%" }}
/>
{getUsernameString(username)}
</span>
);
}, { noop: true })
}, { noop: true }),
renderRoleIcon: ErrorBoundary.wrap(({ roleId, guildId }: { roleId: string, guildId: string; }) => {
// Discord uses Role Mentions for uncached users because .... idk
if (!roleId) return null;
const role = GuildStore.getRole(guildId, roleId);
if (!role?.icon) return <DefaultRoleIcon />;
return (
<img
className="vc-mentionAvatars-icon vc-mentionAvatars-role-icon"
src={`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/role-icons/${roleId}/${role.icon}.webp?size=24&quality=lossless`}
/>
);
}),
});
function getUsernameString(username: string) {

View file

@ -1,8 +1,11 @@
.vc-mentionAvatars-avatar {
.vc-mentionAvatars-icon {
vertical-align: middle;
width: 1em !important; /* insane discord sets width: 100% in channel topic */
height: 1em;
margin: 0 4px 0.2rem 2px;
border-radius: 50%;
box-sizing: border-box;
}
.vc-mentionAvatars-role-icon {
margin: 0 2px 0.2rem 4px;
}

View file

@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { Margins } from "@utils/margins";
import definePlugin, { OptionType } from "@utils/types";
import { findByCodeLazy, findLazy } from "@webpack";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip, useState } from "@webpack/common";
import { Card, ChannelStore, Forms, GuildStore, PermissionsBits, Switch, TextInput, Tooltip } from "@webpack/common";
import type { Permissions, RC } from "@webpack/types";
import type { Channel, Guild, Message, User } from "discord-types/general";
@ -107,14 +107,8 @@ const defaultSettings = Object.fromEntries(
tags.map(({ name, displayName }) => [name, { text: displayName, showInChat: true, showInNotChat: true }])
) as TagSettings;
function SettingsComponent(props: { setValue(v: any): void; }) {
settings.store.tagSettings ??= defaultSettings;
const [tagSettings, setTagSettings] = useState(settings.store.tagSettings as TagSettings);
const setValue = (v: TagSettings) => {
setTagSettings(v);
props.setValue(v);
};
function SettingsComponent() {
const tagSettings = settings.store.tagSettings ??= defaultSettings;
return (
<Flex flexDirection="column">
@ -137,19 +131,13 @@ function SettingsComponent(props: { setValue(v: any): void; }) {
type="text"
value={tagSettings[t.name]?.text ?? t.displayName}
placeholder={`Text on tag (default: ${t.displayName})`}
onChange={v => {
tagSettings[t.name].text = v;
setValue(tagSettings);
}}
onChange={v => tagSettings[t.name].text = v}
className={Margins.bottom16}
/>
<Switch
value={tagSettings[t.name]?.showInChat ?? true}
onChange={v => {
tagSettings[t.name].showInChat = v;
setValue(tagSettings);
}}
onChange={v => tagSettings[t.name].showInChat = v}
hideBorder
>
Show in messages
@ -157,10 +145,7 @@ function SettingsComponent(props: { setValue(v: any): void; }) {
<Switch
value={tagSettings[t.name]?.showInNotChat ?? true}
onChange={v => {
tagSettings[t.name].showInNotChat = v;
setValue(tagSettings);
}}
onChange={v => tagSettings[t.name].showInNotChat = v}
hideBorder
>
Show in member list and profiles
@ -183,7 +168,7 @@ const settings = definePluginSettings({
tagSettings: {
type: OptionType.COMPONENT,
component: SettingsComponent,
description: "fill me",
description: "fill me"
}
});

View file

@ -33,7 +33,7 @@ interface URLReplacementRule {
// Do not forget to add protocols to the ALLOWED_PROTOCOLS constant
const UrlReplacementRules: Record<string, URLReplacementRule> = {
spotify: {
match: /^https:\/\/open\.spotify\.com\/(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
match: /^https:\/\/open\.spotify\.com\/(?:intl-[a-z]{2}\/)?(track|album|artist|playlist|user|episode)\/(.+)(?:\?.+?)?$/,
replace: (_, type, id) => `spotify://${type}/${id}`,
description: "Open Spotify links in the Spotify app",
shortlinkMatch: /^https:\/\/spotify\.link\/.+$/,

View file

@ -37,7 +37,7 @@ type UserPermissions = Array<UserPermission>;
const { RoleRootClasses, RoleClasses, RoleBorderClasses } = proxyLazyWebpack(() => {
const [RoleRootClasses, RoleClasses, RoleBorderClasses] = findBulk(
filters.byProps("root", "showMoreButton", "collapseButton"),
filters.byProps("root", "expandButton", "collapseButton"),
filters.byProps("role", "roleCircle", "roleName"),
filters.byProps("roleCircle", "dot", "dotBorderColor")
) as Record<string, string>[];

View file

@ -172,7 +172,7 @@ export default definePlugin({
{
find: ".VIEW_ALL_ROLES,",
replacement: {
match: /children:"\+"\.concat\(\i\.length-\i\.length\).{0,20}\}\),/,
match: /\.collapseButton,.+?}\)}\),/,
replace: "$&$self.ViewPermissionsButton(arguments[0]),"
}
}

View file

@ -0,0 +1,3 @@
# StickerPaste
Makes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending.

View file

@ -0,0 +1,24 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "StickerPaste",
description: "Makes picking a sticker in the sticker picker insert it into the chatbox instead of instantly sending",
authors: [Devs.ImBanana],
patches: [
{
find: ".stickers,previewSticker:",
replacement: {
match: /if\(\i\.\i\.getUploadCount/,
replace: "return true;$&",
}
}
]
});

View file

@ -22,10 +22,10 @@ export const settings = definePluginSettings({
},
superReactionPlayingLimit: {
description: "Max Super Reactions to play at once",
description: "Max Super Reactions to play at once. 0 to disable playing Super Reactions",
type: OptionType.SLIDER,
default: 20,
markers: [5, 10, 20, 40, 60, 80, 100],
markers: [0, 5, 10, 20, 40, 60, 80, 100],
stickToMarkers: true,
},
}, {
@ -58,6 +58,7 @@ export default definePlugin({
shouldPlayBurstReaction(playingCount: number) {
if (settings.store.unlimitedSuperReactionPlaying) return true;
if (settings.store.superReactionPlayingLimit === 0) return false;
if (playingCount <= settings.store.superReactionPlayingLimit) return true;
return false;
},

View file

@ -0,0 +1,5 @@
# TimeBarAllActivities
Adds the Spotify time bar to all activities if they have start and end timestamps.
![](https://github.com/user-attachments/assets/9fbbe33c-8218-43c9-8b8d-f907a4e809fe)

View file

@ -0,0 +1,76 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import { findComponentByCodeLazy } from "@webpack";
interface Activity {
timestamps?: ActivityTimestamps;
}
interface ActivityTimestamps {
start?: string;
end?: string;
}
const ActivityTimeBar = findComponentByCodeLazy<ActivityTimestamps>(".Millis.HALF_SECOND", ".bar", ".progress");
export const settings = definePluginSettings({
hideActivityDetailText: {
type: OptionType.BOOLEAN,
description: "Hide the large title text next to the activity",
default: true,
},
hideActivityTimerBadges: {
type: OptionType.BOOLEAN,
description: "Hide the timer badges next to the activity",
default: true,
}
});
export default definePlugin({
name: "TimeBarAllActivities",
description: "Adds the Spotify time bar to all activities if they have start and end timestamps",
authors: [Devs.fawn, Devs.niko],
settings,
patches: [
{
find: ".Messages.USER_ACTIVITY_PLAYING",
replacement: [
// Insert Spotify time bar component
{
match: /\(0,.{0,30}activity:(\i),className:\i\.badges\}\)/g,
replace: "$&,$self.getTimeBar($1)"
},
// Hide the large title on listening activities, to make them look more like Spotify (also visible from hovering over the large icon)
{
match: /(\i).type===(\i\.\i)\.WATCHING/,
replace: "($self.settings.store.hideActivityDetailText&&$self.isActivityTimestamped($1)&&$1.type===$2.LISTENING)||$&"
}
]
},
// Hide the "badge" timers that count the time since the activity starts
{
find: ".TvIcon).otherwise",
replacement: {
match: /null!==\(\i=null===\(\i=(\i)\.timestamps\).{0,50}created_at/,
replace: "($self.settings.store.hideActivityTimerBadges&&$self.isActivityTimestamped($1))?null:$&"
}
}
],
isActivityTimestamped(activity: Activity) {
return activity.timestamps != null && activity.timestamps.start != null && activity.timestamps.end != null;
},
getTimeBar(activity: Activity) {
if (this.isActivityTimestamped(activity)) {
return <ActivityTimeBar start={activity.timestamps!.start} end={activity.timestamps!.end} />;
}
}
});

View file

@ -0,0 +1,9 @@
# Volume Booster
Allows you to boost the volume over 200% on desktop and over 100% on other clients.
Works on users, bots, and streams!
![the volume being moved up to 270% on vesktop](https://github.com/user-attachments/assets/793e012e-c069-4fa4-a3d5-61c2f55edd3e)
![the volume being moved up to 297% on a stream](https://github.com/user-attachments/assets/77463eb9-2537-4821-a3ab-82f60633ccbc)

View file

@ -31,10 +31,27 @@ const settings = definePluginSettings({
}
});
interface StreamData {
audioContext: AudioContext,
audioElement: HTMLAudioElement,
emitter: any,
// added by this plugin
gainNode?: GainNode,
id: string,
levelNode: AudioWorkletNode,
sinkId: string,
stream: MediaStream,
streamSourceNode?: MediaStreamAudioSourceNode,
videoStreamId: string,
_mute: boolean,
_speakingFlags: number,
_volume: number;
}
export default definePlugin({
name: "VolumeBooster",
authors: [Devs.Nuckyz],
description: "Allows you to set the user and stream volume above the default maximum.",
authors: [Devs.Nuckyz, Devs.sadan],
description: "Allows you to set the user and stream volume above the default maximum",
settings,
patches: [
@ -45,12 +62,28 @@ export default definePlugin({
].map(find => ({
find,
replacement: {
match: /(?<=maxValue:\i\.\i)\?(\d+?):(\d+?)(?=,)/,
replace: (_, higherMaxVolume, minorMaxVolume) => ""
+ `?${higherMaxVolume}*$self.settings.store.multiplier`
+ `:${minorMaxVolume}*$self.settings.store.multiplier`
match: /(?<=maxValue:)\i\.\i\?(\d+?):(\d+?)(?=,)/,
replace: (_, higherMaxVolume, minorMaxVolume) => `${higherMaxVolume}*$self.settings.store.multiplier`
}
})),
// Patches needed for web/vesktop
{
find: "streamSourceNode",
predicate: () => IS_WEB,
group: true,
replacement: [
// Remove rounding algorithm
{
match: /Math\.max.{0,30}\)\)/,
replace: "arguments[0]"
},
// Patch the volume
{
match: /\.volume=this\._volume\/100;/,
replace: ".volume=0.00;$self.patchVolume(this);"
}
]
},
// Prevent Audio Context Settings sync from trying to sync with values above 200, changing them to 200 before we send to Discord
{
find: "AudioContextSettingsMigrated",
@ -83,4 +116,20 @@ export default definePlugin({
]
}
],
patchVolume(data: StreamData) {
if (data.stream.getAudioTracks().length === 0) return;
data.streamSourceNode ??= data.audioContext.createMediaStreamSource(data.stream);
if (!data.gainNode) {
const gain = data.gainNode = data.audioContext.createGain();
data.streamSourceNode.connect(gain);
gain.connect(data.audioContext.destination);
}
data.gainNode.gain.value = data._mute
? 0
: data._volume / 100;
}
});

View file

@ -538,6 +538,14 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Joona",
id: 297410829589020673n
},
sadan: {
name: "sadan",
id: 521819891141967883n,
},
Kylie: {
name: "Cookie",
id: 721853658941227088n
},
AshtonMemer: {
name: "AshtonMemer",
id: 373657230530052099n
@ -550,6 +558,18 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "Lumap",
id: 585278686291427338n,
},
Obsidian: {
name: "Obsidian",
id: 683171006717755446n,
},
SerStars: {
name: "SerStars",
id: 861631850681729045n,
},
niko: {
name: "niko",
id: 341377368075796483n,
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly

View file

@ -309,6 +309,8 @@ export interface DefinedSettings<
> {
/** Shorthand for `Vencord.Settings.plugins.PluginName`, but with typings */
store: SettingsStore<Def> & PrivateSettings;
/** Shorthand for `Vencord.PlainSettings.plugins.PluginName`, but with typings */
plain: SettingsStore<Def> & PrivateSettings;
/**
* React hook for getting the settings for this plugin
* @param filter optional filter to avoid rerenders for irrelevent settings

View file

@ -49,6 +49,11 @@ export const moment: typeof import("moment") = findByPropsLazy("parseTwoDigitYea
export const hljs: typeof import("highlight.js") = findByPropsLazy("highlight", "registerLanguage");
export const { match, P }: Pick<typeof import("ts-pattern"), "match" | "P"> = mapMangledModuleLazy("@ts-pattern/matcher", {
match: filters.byCode("return new"),
P: filters.byProps("when")
});
export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
export const i18n: t.i18n = findLazy(m => m.Messages?.["en-US"]);