mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-07 13:43:03 -04:00
Merge branch 'dev' into strict-csp
This commit is contained in:
commit
00539b7a96
72 changed files with 832 additions and 713 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "vencord",
|
||||
"private": "true",
|
||||
"version": "1.11.9",
|
||||
"version": "1.12.0",
|
||||
"description": "The cutest Discord client mod",
|
||||
"homepage": "https://github.com/Vendicated/Vencord#readme",
|
||||
"bugs": {
|
||||
|
|
|
@ -33,7 +33,7 @@ import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
|
|||
import { StartAt } from "@utils/types";
|
||||
|
||||
import { get as dsGet } from "./api/DataStore";
|
||||
import { showNotification } from "./api/Notifications";
|
||||
import { NotificationData, showNotification } from "./api/Notifications";
|
||||
import { PlainSettings, Settings } from "./api/Settings";
|
||||
import { patches, PMLogger, startAllPlugins } from "./plugins";
|
||||
import { localStorage } from "./utils/localStorage";
|
||||
|
@ -86,6 +86,46 @@ async function syncSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
let notifiedForUpdatesThisSession = false;
|
||||
|
||||
async function runUpdateCheck() {
|
||||
const notify = (data: NotificationData) => {
|
||||
if (notifiedForUpdatesThisSession) return;
|
||||
notifiedForUpdatesThisSession = true;
|
||||
|
||||
setTimeout(() => showNotification({
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
...data
|
||||
}), 10_000);
|
||||
};
|
||||
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (!isOutdated) return;
|
||||
|
||||
if (Settings.autoUpdate) {
|
||||
await update();
|
||||
if (Settings.autoUpdateNotification) {
|
||||
notify({
|
||||
title: "Vencord has been updated!",
|
||||
body: "Click here to restart",
|
||||
onClick: relaunch
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
notify({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
onClick: openUpdaterModal!
|
||||
});
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await onceReady;
|
||||
startAllPlugins(StartAt.WebpackReady);
|
||||
|
@ -93,33 +133,8 @@ async function init() {
|
|||
syncSettings();
|
||||
|
||||
if (!IS_WEB && !IS_UPDATER_DISABLED) {
|
||||
try {
|
||||
const isOutdated = await checkForUpdates();
|
||||
if (!isOutdated) return;
|
||||
|
||||
if (Settings.autoUpdate) {
|
||||
await update();
|
||||
if (Settings.autoUpdateNotification)
|
||||
setTimeout(() => showNotification({
|
||||
title: "Vencord has been updated!",
|
||||
body: "Click here to restart",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: relaunch
|
||||
}), 10_000);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => showNotification({
|
||||
title: "A Vencord update is available!",
|
||||
body: "Click here to view the update",
|
||||
permanent: true,
|
||||
noPersist: true,
|
||||
onClick: openUpdaterModal!
|
||||
}), 10_000);
|
||||
} catch (err) {
|
||||
UpdateLogger.error("Failed to check for updates", err);
|
||||
}
|
||||
runUpdateCheck();
|
||||
setInterval(runUpdateCheck, 1000 * 60 * 30); // 30 minutes
|
||||
}
|
||||
|
||||
if (IS_DEV) {
|
||||
|
|
|
@ -21,25 +21,14 @@ import { Channel, User } from "discord-types/general/index.js";
|
|||
import { JSX } from "react";
|
||||
|
||||
interface DecoratorProps {
|
||||
activities: any[];
|
||||
channel: Channel;
|
||||
/**
|
||||
* Only for DM members
|
||||
*/
|
||||
channelName?: string;
|
||||
/**
|
||||
* Only for server members
|
||||
*/
|
||||
currentUser?: User;
|
||||
guildId?: string;
|
||||
isMobile: boolean;
|
||||
isOwner?: boolean;
|
||||
isTyping: boolean;
|
||||
selected: boolean;
|
||||
status: string;
|
||||
type: "guild" | "dm";
|
||||
user: User;
|
||||
[key: string]: any;
|
||||
/** only present when this is a DM list item */
|
||||
channel: Channel;
|
||||
/** only present when this is a guild list item */
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
export type MemberListDecoratorFactory = (props: DecoratorProps) => JSX.Element | null;
|
||||
type OnlyIn = "guilds" | "dms";
|
||||
|
||||
|
@ -53,18 +42,16 @@ export function removeMemberListDecorator(identifier: string) {
|
|||
decoratorsFactories.delete(identifier);
|
||||
}
|
||||
|
||||
export function __getDecorators(props: DecoratorProps): JSX.Element {
|
||||
const isInGuild = !!(props.guildId);
|
||||
|
||||
export function __getDecorators(props: DecoratorProps, type: "guild" | "dm"): JSX.Element {
|
||||
const decorators = Array.from(
|
||||
decoratorsFactories.entries(),
|
||||
([key, { render: Decorator, onlyIn }]) => {
|
||||
if ((onlyIn === "guilds" && !isInGuild) || (onlyIn === "dms" && isInGuild))
|
||||
if ((onlyIn === "guilds" && type !== "guild") || (onlyIn === "dms" && type !== "dm"))
|
||||
return null;
|
||||
|
||||
return (
|
||||
<ErrorBoundary noop key={key} message={`Failed to render ${key} Member List Decorator`}>
|
||||
<Decorator {...props} />
|
||||
<Decorator {...props} type={type} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Queue } from "@utils/Queue";
|
||||
import { ReactDOM } from "@webpack/common";
|
||||
import { createRoot } from "@webpack/common";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
|
@ -35,7 +35,7 @@ function getRoot() {
|
|||
const container = document.createElement("div");
|
||||
container.id = "vc-notification-container";
|
||||
document.body.append(container);
|
||||
reactRoot = ReactDOM.createRoot(container);
|
||||
reactRoot = createRoot(container);
|
||||
}
|
||||
return reactRoot;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { LazyComponent, LazyComponentWrapper } from "@utils/react";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
import { ErrorCard } from "./ErrorCard";
|
||||
|
@ -107,9 +107,9 @@ const ErrorBoundary = LazyComponent(() => {
|
|||
}
|
||||
};
|
||||
}) as
|
||||
React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||
LazyComponentWrapper<React.ComponentType<React.PropsWithChildren<Props>> & {
|
||||
wrap<T extends object = any>(Component: React.ComponentType<T>, errorBoundaryProps?: Omit<Props<T>, "wrappedProps">): React.FunctionComponent<T>;
|
||||
};
|
||||
}>;
|
||||
|
||||
ErrorBoundary.wrap = (Component, errorBoundaryProps) => props => (
|
||||
<ErrorBoundary {...errorBoundaryProps} wrappedProps={props}>
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
|
||||
import { CodeBlock } from "@components/CodeBlock";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
||||
import { makeCodeblock } from "@utils/text";
|
||||
import { Patch, ReplaceFn } from "@utils/types";
|
||||
import { search } from "@webpack";
|
||||
import { Button, Clipboard, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
|
||||
import { Button, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
|
||||
|
||||
import { SettingsTab, wrapTab } from "./shared";
|
||||
|
||||
|
@ -381,8 +382,8 @@ 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>
|
||||
<Button onClick={() => copyToClipboard(code)}>Copy to Clipboard</Button>
|
||||
<Button className={Margins.top8} onClick={() => copyToClipboard("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
|
||||
</>
|
||||
)}
|
||||
</SettingsTab>
|
||||
|
|
|
@ -87,7 +87,7 @@ async function runReporter() {
|
|||
result = Webpack[method](...args);
|
||||
}
|
||||
|
||||
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail");
|
||||
if (result == null || (result.$$vencordGetWrappedComponent != null && result.$$vencordGetWrappedComponent() == null)) throw new Error("Webpack Find Fail");
|
||||
} catch (e) {
|
||||
let logMessage = searchType;
|
||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
||||
|
|
|
@ -67,13 +67,7 @@ export async function installExt(id: string) {
|
|||
try {
|
||||
await access(extDir, fsConstants.F_OK);
|
||||
} catch (err) {
|
||||
const url = id === "fmkadmapgofadopljbjfkapdkoienihi"
|
||||
// React Devtools v4.25
|
||||
// v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843
|
||||
// Unfortunately, Google does not serve old versions, so this is the only way
|
||||
// This zip file is pinned to long commit hash so it cannot be changed remotely
|
||||
? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip"
|
||||
: `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
|
||||
const url = `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=${process.versions.chrome}`;
|
||||
|
||||
const buf = await get(url, {
|
||||
headers: {
|
||||
|
|
|
@ -46,8 +46,6 @@ const ContributorBadge: ProfileBadge = {
|
|||
let DonorBadges = {} as Record<string, Array<Record<"tooltip" | "badge", string>>>;
|
||||
|
||||
async function loadBadges(noCache = false) {
|
||||
DonorBadges = {};
|
||||
|
||||
const init = {} as RequestInit;
|
||||
if (noCache)
|
||||
init.cache = "no-cache";
|
||||
|
@ -56,6 +54,8 @@ async function loadBadges(noCache = false) {
|
|||
.then(r => r.json());
|
||||
}
|
||||
|
||||
let intervalId: any;
|
||||
|
||||
export default definePlugin({
|
||||
name: "BadgeAPI",
|
||||
description: "API to add badges to users.",
|
||||
|
@ -63,7 +63,7 @@ export default definePlugin({
|
|||
required: true,
|
||||
patches: [
|
||||
{
|
||||
find: ".FULL_SIZE]:26",
|
||||
find: ".MODAL]:26",
|
||||
replacement: {
|
||||
match: /(?=;return 0===(\i)\.length\?)(?<=(\i)\.useMemo.+?)/,
|
||||
replace: ";$1=$2.useMemo(()=>[...$self.getBadges(arguments[0].displayProfile),...$1],[$1])"
|
||||
|
@ -89,6 +89,11 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
// for access from the console or other plugins
|
||||
get DonorBadges() {
|
||||
return DonorBadges;
|
||||
},
|
||||
|
||||
toolboxActions: {
|
||||
async "Refetch Badges"() {
|
||||
await loadBadges(true);
|
||||
|
@ -104,6 +109,13 @@ export default definePlugin({
|
|||
|
||||
async start() {
|
||||
await loadBadges();
|
||||
|
||||
clearInterval(intervalId);
|
||||
intervalId = setInterval(loadBadges, 1000 * 60 * 30); // 30 minutes
|
||||
},
|
||||
|
||||
async stop() {
|
||||
clearInterval(intervalId);
|
||||
},
|
||||
|
||||
getBadges(props: { userId: string; user?: User; guildId: string; }) {
|
||||
|
|
|
@ -33,19 +33,16 @@ export default definePlugin({
|
|||
find: ".lostPermission)",
|
||||
replacement: [
|
||||
{
|
||||
match: /let\{[^}]*lostPermissionTooltipText:\i[^}]*\}=(\i),/,
|
||||
replace: "$&vencordProps=$1,"
|
||||
}, {
|
||||
match: /#{intl::GUILD_OWNER}(?=.+?decorators:(\i)\(\)).+?\1=?\(\)=>.+?children:\[/,
|
||||
replace: "$&(typeof vencordProps=='undefined'?null:Vencord.Api.MemberListDecorators.__getDecorators(vencordProps)),"
|
||||
match: /children:\[(?=.{0,300},lostPermissionTooltipText:)/,
|
||||
replace: "children:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'guild'),"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
find: "PrivateChannel.renderAvatar",
|
||||
replacement: {
|
||||
match: /decorators:(\i\.isSystemDM\(\))\?(.+?):null/,
|
||||
replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0]),$1?$2:null]"
|
||||
match: /decorators:(\i\.isSystemDM\(\)\?.+?:null)/,
|
||||
replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'dm'),$1]"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -32,9 +32,9 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
{
|
||||
find: "#{intl::SERVERS}),children",
|
||||
find: ".setGuildsTree(",
|
||||
replacement: {
|
||||
match: /(?<=#{intl::SERVERS}\),children:)\i\.map\(\i\)/,
|
||||
match: /(?<=#{intl::SERVERS}\),gap:"xs",children:)\i\.map\(.{0,50}\.length\)/,
|
||||
replace: "Vencord.Api.ServerList.renderAll(Vencord.Api.ServerList.ServerListRenderPosition.In).concat($&)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import ErrorBoundary from "@components/ErrorBoundary";
|
|||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentChannel } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findComponentByCodeLazy } from "@webpack";
|
||||
import { findComponentByCodeLazy } from "@webpack";
|
||||
import { ContextMenuApi, Menu, useEffect, useRef } from "@webpack/common";
|
||||
import { User } from "discord-types/general";
|
||||
|
||||
|
@ -19,8 +19,7 @@ interface UserProfileProps {
|
|||
originalRenderPopout: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const UserProfile = findComponentByCodeLazy("UserProfilePopoutWrapper: user cannot be undefined");
|
||||
const styles = findByPropsLazy("accountProfilePopoutWrapper");
|
||||
const UserProfile = findComponentByCodeLazy(".POPOUT,user");
|
||||
|
||||
let openAlternatePopout = false;
|
||||
let accountPanelRef: React.RefObject<Record<PropertyKey, any> | null> = { current: null };
|
||||
|
@ -77,7 +76,7 @@ export default definePlugin({
|
|||
replace: "$self.useAccountPanelRef();$&"
|
||||
},
|
||||
{
|
||||
match: /(\.AVATAR,children:.+?renderPopout:(\i)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
|
||||
match: /(\.AVATAR,children:.+?renderPopout:\((\i),\i\)=>){(.+?)}(?=,position)(?<=currentUser:(\i).+?)/,
|
||||
replace: (_, rest, popoutProps, originalPopout, currentUser) => `${rest}$self.UserProfile({popoutProps:${popoutProps},currentUser:${currentUser},originalRenderPopout:()=>{${originalPopout}}})`
|
||||
},
|
||||
{
|
||||
|
@ -121,14 +120,18 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
const currentChannel = getCurrentChannel();
|
||||
if (currentChannel?.getGuildId() == null) {
|
||||
if (currentChannel?.getGuildId() == null || !UserProfile.$$vencordGetWrappedComponent()) {
|
||||
return originalRenderPopout();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.accountProfilePopoutWrapper}>
|
||||
<UserProfile {...popoutProps} userId={currentUser.id} guildId={currentChannel.getGuildId()} channelId={currentChannel.id} />
|
||||
</div>
|
||||
<UserProfile
|
||||
{...popoutProps}
|
||||
user={currentUser}
|
||||
currentUser={currentUser}
|
||||
guildId={currentChannel.getGuildId()}
|
||||
channelId={currentChannel.id}
|
||||
/>
|
||||
);
|
||||
}, { noop: true })
|
||||
});
|
||||
|
|
|
@ -43,7 +43,7 @@ export default definePlugin({
|
|||
// Status emojis
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(\.CUSTOM_STATUS.+?animate:)\i/,
|
||||
match: /(\.CUSTOM_STATUS.+?animateEmoji:)\i/,
|
||||
replace: "$1!0"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -36,7 +36,7 @@ export default definePlugin({
|
|||
settings,
|
||||
patches: [
|
||||
{
|
||||
find: "#{intl::BAN_CONFIRM_TITLE}",
|
||||
find: "#{intl::jeKpoq::raw}", // BAN_CONFIRM_TITLE
|
||||
replacement: {
|
||||
match: /src:\i\("?\d+"?\)/g,
|
||||
replace: "src:$self.source"
|
||||
|
|
|
@ -42,15 +42,14 @@ export default ErrorBoundary.wrap(guildsBarProps => {
|
|||
const guilds = document.querySelector(guildsBarProps.className.split(" ").map(c => `.${c}`).join(""));
|
||||
|
||||
// We need to display none if we are in fullscreen. Yes this seems horrible doing with css, but it's literally how Discord does it.
|
||||
// Also display flex otherwise to fix scrolling
|
||||
const barStyle = {
|
||||
display: isFullscreen ? "none" : "flex",
|
||||
gridArea: "betterFoldersSidebar"
|
||||
// Also display flex otherwise to fix scrolling.
|
||||
const sidebarStyle = {
|
||||
display: isFullscreen ? "none" : "flex"
|
||||
} satisfies CSSProperties;
|
||||
|
||||
if (!guilds || !settings.store.sidebarAnim) {
|
||||
return visible
|
||||
? <div style={barStyle}>{Sidebar}</div>
|
||||
? <div className="vc-betterFolders-sidebar" style={sidebarStyle}>{Sidebar}</div>
|
||||
: null;
|
||||
}
|
||||
|
||||
|
@ -62,9 +61,9 @@ export default ErrorBoundary.wrap(guildsBarProps => {
|
|||
leave={{ width: 0 }}
|
||||
config={{ duration: 200 }}
|
||||
>
|
||||
{(animationStyle, show) =>
|
||||
{(animationStyle: any, show: any) =>
|
||||
show && (
|
||||
<Animations.animated.div style={{ ...animationStyle, ...barStyle }}>
|
||||
<Animations.animated.div className="vc-betterFolders-sidebar" style={{ ...animationStyle, ...sidebarStyle }}>
|
||||
{Sidebar}
|
||||
</Animations.animated.div>
|
||||
)
|
||||
|
|
|
@ -16,14 +16,15 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./sidebarFix.css";
|
||||
import "./style.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
|
||||
import { FluxDispatcher, useMemo } from "@webpack/common";
|
||||
import { FluxDispatcher } from "@webpack/common";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import FolderSideBar from "./FolderSideBar";
|
||||
|
||||
|
@ -50,6 +51,34 @@ function closeFolders() {
|
|||
FolderUtils.toggleGuildFolderExpand(id);
|
||||
}
|
||||
|
||||
// Nuckyz: Unsure if this should be a general utility or not
|
||||
function filterTreeWithTargetNode(children: any, predicate: (node: any) => boolean) {
|
||||
if (children == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(children)) {
|
||||
if (predicate(children)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return filterTreeWithTargetNode(children.props?.children, predicate);
|
||||
}
|
||||
|
||||
let childIsTargetChild = false;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const shouldKeep = filterTreeWithTargetNode(children[i], predicate);
|
||||
if (shouldKeep) {
|
||||
childIsTargetChild = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
children.splice(i--, 1);
|
||||
}
|
||||
|
||||
return childIsTargetChild;
|
||||
}
|
||||
|
||||
export const settings = definePluginSettings({
|
||||
sidebar: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -101,6 +130,10 @@ export const settings = definePluginSettings({
|
|||
}
|
||||
});
|
||||
|
||||
const IS_BETTER_FOLDERS_VAR = "typeof isBetterFolders!=='undefined'?isBetterFolders:arguments[0]?.isBetterFolders";
|
||||
const BETTER_FOLDERS_EXPANDED_IDS_VAR = "typeof betterFoldersExpandedIds!=='undefined'?betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds";
|
||||
const GRID_STYLE_NAME = "vc-betterFolders-sidebar-grid";
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterFolders",
|
||||
description: "Shows server folders on dedicated sidebar and adds folder related improvements",
|
||||
|
@ -113,30 +146,41 @@ export default definePlugin({
|
|||
find: '("guildsnav")',
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: [
|
||||
// Create the isBetterFolders variable in the GuildsBar component
|
||||
// Create the isBetterFolders and betterFoldersExpandedIds variables in the GuildsBar component
|
||||
// Needed because we access this from a non-arrow closure so we can't use arguments[0]
|
||||
{
|
||||
match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/,
|
||||
replace: "$&,isBetterFolders"
|
||||
replace: "$&,isBetterFolders,betterFoldersExpandedIds"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders
|
||||
// Export the isBetterFolders and betterFoldersExpandedIds variable to the Guild List component
|
||||
{
|
||||
match: /\[(\i)\]=(\(0,\i\.\i\).{0,40}getGuildsTree\(\).+?}\))(?=,)/,
|
||||
replace: (_, originalTreeVar, rest) => `[betterFoldersOriginalTree]=${rest},${originalTreeVar}=$self.getGuildTree(!!arguments[0]?.isBetterFolders,betterFoldersOriginalTree,arguments[0]?.betterFoldersExpandedIds)`
|
||||
match: /,{guildDiscoveryButton:\i,/g,
|
||||
replace: "$&isBetterFolders:arguments[0]?.isBetterFolders,betterFoldersExpandedIds:arguments[0]?.betterFoldersExpandedIds,"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the servers and folders from the GuildsBar Guild List children
|
||||
// Wrap the guild node (guild or folder) component in a div with display: none if it's not an expanded folder or a guild in an expanded folder
|
||||
{
|
||||
match: /switch\((\i)\.type\){.+?default:return null}/,
|
||||
replace: `return $self.wrapGuildNodeComponent($1,()=>{$&},${IS_BETTER_FOLDERS_VAR},${BETTER_FOLDERS_EXPANDED_IDS_VAR});`
|
||||
},
|
||||
// Export the isBetterFolders variable to the folder component
|
||||
{
|
||||
match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
|
||||
replace: `$&isBetterFolders:${IS_BETTER_FOLDERS_VAR},`
|
||||
},
|
||||
// Make the callback for returning the guild node component depend on isBetterFolders and betterFoldersExpandedIds
|
||||
{
|
||||
match: /switch\(\i\.type\).+?,\i,\i\.setNodeRef/,
|
||||
replace: "$&,arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the guilds and folders from the Guild List children
|
||||
{
|
||||
match: /lastTargetNode:\i\[\i\.length-1\].+?}\)(?::null)?\](?=}\))/,
|
||||
replace: "$&.filter($self.makeGuildsBarGuildListFilter(!!arguments[0]?.isBetterFolders))"
|
||||
},
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the scroller for the guild list from the GuildsBar Tree children
|
||||
// If we are rendering the Better Folders sidebar, we filter out everything but the Guild List from the Sidebar children
|
||||
{
|
||||
match: /unreadMentionsIndicatorBottom,.+?}\)\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarTreeFilter(!!arguments[0]?.isBetterFolders))"
|
||||
},
|
||||
// Export the isBetterFolders variable to the folders component
|
||||
{
|
||||
match: /switch\(\i\.type\){case \i\.\i\.FOLDER:.+?folderNode:\i,/,
|
||||
replace: '$&isBetterFolders:typeof isBetterFolders!=="undefined"?isBetterFolders:false,'
|
||||
match: /unreadMentionsFixedFooter\].+?\]/,
|
||||
replace: "$&.filter($self.makeGuildsBarSidebarFilter(!!arguments[0]?.isBetterFolders))"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -161,7 +205,7 @@ export default definePlugin({
|
|||
]
|
||||
},
|
||||
{
|
||||
find: ".expandedFolderBackground,",
|
||||
find: ".FOLDER_ITEM_ANIMATION_DURATION),",
|
||||
predicate: () => settings.store.sidebar,
|
||||
replacement: [
|
||||
// We use arguments[0] to access the isBetterFolders variable in this nested folder component (the parent exports all the props so we don't have to patch it)
|
||||
|
@ -181,27 +225,20 @@ export default definePlugin({
|
|||
// If we are rendering the normal GuildsBar sidebar, we avoid rendering guilds from folders that are expanded
|
||||
{
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /expandedFolderBackground,.+?,(?=\i\(\(\i,\i,\i\)=>{let{key.{0,45}ul)(?<=selected:\i,expanded:(\i),.+?)/,
|
||||
match: /folderGroupBackground.+?,(?=\i\(\(\i,\i,\i\)=>{let{key:.{0,70}"ul")(?<=selected:\i,expanded:(\i),.+?)/,
|
||||
replace: (m, isExpanded) => `${m}$self.shouldRenderContents(arguments[0],${isExpanded})?null:`
|
||||
},
|
||||
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
|
||||
{
|
||||
// Decide if we should render the expanded folder background if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /\.isExpanded\),.{0,30}children:\[/,
|
||||
match: /\.isExpanded\].{0,110}children:\[/,
|
||||
replace: "$&$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)&&"
|
||||
},
|
||||
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
|
||||
{
|
||||
// Decide if we should render the expanded folder icon if we are rendering the Better Folders sidebar
|
||||
predicate: () => settings.store.showFolderIcon !== FolderIconDisplay.Always,
|
||||
match: /(?<=\.expandedFolderBackground.+?}\),)(?=\i,)/,
|
||||
match: /(?<=\.folderGroupBackground.*?}\),)(?=\i,)/,
|
||||
replace: "!$self.shouldShowFolderIconAndBackground(!!arguments[0]?.isBetterFolders,arguments[0]?.betterFoldersExpandedIds)?null:"
|
||||
},
|
||||
{
|
||||
// Discord adds a slight bottom margin of 4px when it's expanded
|
||||
// Which looks off when there's nothing open in the folder
|
||||
predicate: () => !settings.store.keepIcons,
|
||||
match: /(?=className:.{0,50}folderIcon)/,
|
||||
replace: "style:arguments[0]?.isBetterFolders?{}:{marginBottom:0},"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -221,8 +258,8 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Add grid styles to fix aligment with other visual refresh elements
|
||||
match: /(?<=className:)(\i\.base)(?=,)/,
|
||||
replace: "`${$self.gridStyle} ${$1}`"
|
||||
match: /(?<=className:)\i\.base(?=,)/,
|
||||
replace: `"${GRID_STYLE_NAME} "+$&`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -278,55 +315,60 @@ export default definePlugin({
|
|||
}
|
||||
},
|
||||
|
||||
gridStyle: "vc-betterFolders-sidebar-grid",
|
||||
FolderSideBar,
|
||||
closeFolders,
|
||||
|
||||
getGuildTree(isBetterFolders: boolean, originalTree: any, expandedFolderIds?: Set<any>) {
|
||||
return useMemo(() => {
|
||||
if (!isBetterFolders || expandedFolderIds == null) return originalTree;
|
||||
|
||||
const newTree = new GuildsTree();
|
||||
// Children is every folder and guild which is not in a folder, this filters out only the expanded folders
|
||||
newTree.root.children = originalTree.root.children.filter(guildOrFolder => expandedFolderIds.has(guildOrFolder.id));
|
||||
// Nodes is every folder and guild, even if it's in a folder, this filters out only the expanded folders and guilds inside them
|
||||
newTree.nodes = Object.fromEntries(
|
||||
Object.entries(originalTree.nodes)
|
||||
.filter(([_, guildOrFolder]: any[]) => expandedFolderIds.has(guildOrFolder.id) || expandedFolderIds.has(guildOrFolder.parentId))
|
||||
);
|
||||
wrapGuildNodeComponent(node: any, originalComponent: () => ReactNode, isBetterFolders: boolean, expandedFolderIds?: Set<any>) {
|
||||
if (
|
||||
!isBetterFolders ||
|
||||
node.type === "folder" && expandedFolderIds?.has(node.id) ||
|
||||
node.type === "guild" && expandedFolderIds?.has(node.parentId)
|
||||
) {
|
||||
return originalComponent();
|
||||
}
|
||||
|
||||
return newTree;
|
||||
}, [isBetterFolders, originalTree, expandedFolderIds]);
|
||||
return (
|
||||
<div style={{ display: "none" }}>
|
||||
{originalComponent()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
makeGuildsBarGuildListFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (!isBetterFolders) return true;
|
||||
return (child: any) => {
|
||||
if (!isBetterFolders) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return child?.props?.["aria-label"] === getIntlMessage("SERVERS");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
},
|
||||
|
||||
makeGuildsBarTreeFilter(isBetterFolders: boolean) {
|
||||
return child => {
|
||||
if (!isBetterFolders) return true;
|
||||
|
||||
if (child?.props?.className?.includes("itemsContainer") && child.props.children != null) {
|
||||
// Filter out everything but the scroller for the guild list
|
||||
child.props.children = child.props.children.filter(child => child?.props?.onScroll != null);
|
||||
makeGuildsBarSidebarFilter(isBetterFolders: boolean) {
|
||||
return (child: any) => {
|
||||
if (!isBetterFolders) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
try {
|
||||
return filterTreeWithTargetNode(child, child => child?.props?.renderTreeNode != null);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
shouldShowFolderIconAndBackground(isBetterFolders: boolean, expandedFolderIds?: Set<any>) {
|
||||
if (!isBetterFolders) return true;
|
||||
if (!isBetterFolders) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (settings.store.showFolderIcon) {
|
||||
case FolderIconDisplay.Never:
|
||||
|
@ -352,8 +394,5 @@ export default definePlugin({
|
|||
if (props?.folderNode?.id === 1) return false;
|
||||
|
||||
return !props?.isBetterFolders && isExpanded;
|
||||
},
|
||||
|
||||
FolderSideBar,
|
||||
closeFolders,
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */
|
||||
.vc-betterFolders-sidebar {
|
||||
grid-area: betterFoldersSidebar
|
||||
}
|
||||
|
||||
/* These area names need to be hardcoded. Only betterFoldersSidebar is added by the plugin. */
|
||||
.visual-refresh .vc-betterFolders-sidebar-grid {
|
||||
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end]; /* stylelint-disable-line value-keyword-case */
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
grid-template-columns: [start] min-content [guildsEnd] min-content [sidebarEnd] min-content [channelsEnd] 1fr [end];
|
||||
grid-template-areas:
|
||||
"titleBar titleBar titleBar titleBar"
|
||||
"guildsList betterFoldersSidebar notice notice"
|
|
@ -7,11 +7,12 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getUserSettingLazy } from "@api/UserSettings";
|
||||
import { ImageIcon } from "@components/Icons";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { getCurrentGuild, openImageModal } from "@utils/discord";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { Clipboard, GuildStore, Menu, PermissionStore } from "@webpack/common";
|
||||
import { GuildStore, Menu, PermissionStore } from "@webpack/common";
|
||||
|
||||
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
|
||||
|
||||
|
@ -87,7 +88,7 @@ export default definePlugin({
|
|||
<Menu.MenuItem
|
||||
id="vc-copy-role-color"
|
||||
label="Copy Role Color"
|
||||
action={() => Clipboard.copy(role.colorString!)}
|
||||
action={() => copyToClipboard(role.colorString!)}
|
||||
icon={AppearanceIcon}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
import { Settings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { copyWithToast } from "@utils/misc";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Clipboard, Toasts } from "@webpack/common";
|
||||
|
||||
export default definePlugin({
|
||||
name: "BetterRoleDot",
|
||||
|
@ -84,15 +84,6 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
copyToClipBoard(color: string) {
|
||||
Clipboard.copy(color);
|
||||
Toasts.show({
|
||||
message: "Copied to Clipboard!",
|
||||
type: Toasts.Type.SUCCESS,
|
||||
id: Toasts.genId(),
|
||||
options: {
|
||||
duration: 1000,
|
||||
position: Toasts.Position.BOTTOM
|
||||
}
|
||||
});
|
||||
copyWithToast(color);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -142,8 +142,7 @@ export default definePlugin({
|
|||
// Thus, we sanity check webpack modules
|
||||
Layer(props: LayerProps) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
[FocusLock.$$vencordInternal(), ComponentDispatch, Classes].forEach(e => e.test);
|
||||
[FocusLock.$$vencordGetWrappedComponent(), ComponentDispatch, Classes].forEach(e => e.test);
|
||||
} catch {
|
||||
new Logger("BetterSettings").error("Failed to find some components");
|
||||
return props.children;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Classic Client Theme
|
||||
# Client Theme
|
||||
|
||||
Revival of the old client theme experiment (The one that came before the sucky one that we actually got)
|
||||
|
||||
|
|
|
@ -19,16 +19,8 @@
|
|||
border: thin solid var(--background-modifier-accent) !important;
|
||||
}
|
||||
|
||||
.vc-clientTheme-warning-text {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
||||
.vc-clientTheme-contrast-warning {
|
||||
background-color: var(--background-primary);
|
||||
padding: 0.5rem;
|
||||
border-radius: .5rem;
|
||||
.vc-clientTheme-buttons-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
|
104
src/plugins/clientTheme/components/Settings.tsx
Normal file
104
src/plugins/clientTheme/components/Settings.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { ErrorCard } from "@components/ErrorCard";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { settings } from "..";
|
||||
import { relativeLuminance } from "../utils/colorUtils";
|
||||
import { createOrUpdateThemeColorVars } from "../utils/styleUtils";
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
||||
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
|
||||
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
||||
|
||||
const cl = classNameFactory("vc-clientTheme-");
|
||||
|
||||
const colorPresets = [
|
||||
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
|
||||
"#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
|
||||
"#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d",
|
||||
"#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb",
|
||||
];
|
||||
|
||||
function onPickColor(color: number) {
|
||||
const hexColor = color.toString(16).padStart(6, "0");
|
||||
|
||||
settings.store.color = hexColor;
|
||||
createOrUpdateThemeColorVars(hexColor);
|
||||
}
|
||||
|
||||
function setDiscordTheme(theme: string) {
|
||||
saveClientTheme({ theme });
|
||||
}
|
||||
|
||||
export function ThemeSettingsComponent() {
|
||||
const currentTheme = useStateFromStores([ThemeStore], () => ThemeStore.theme);
|
||||
const isLightTheme = currentTheme === "light";
|
||||
const oppositeTheme = isLightTheme ? "Dark" : "Light";
|
||||
|
||||
const nitroThemeEnabled = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset != null);
|
||||
|
||||
const selectedLuminance = relativeLuminance(settings.store.color);
|
||||
|
||||
let contrastWarning = false;
|
||||
let fixableContrast = true;
|
||||
|
||||
if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12) {
|
||||
contrastWarning = true;
|
||||
}
|
||||
|
||||
if (selectedLuminance < 0.26 && selectedLuminance > 0.12) {
|
||||
fixableContrast = false;
|
||||
}
|
||||
|
||||
// Light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels
|
||||
if (isLightTheme && selectedLuminance > 0.65) {
|
||||
contrastWarning = true;
|
||||
fixableContrast = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cl("settings")}>
|
||||
<div className={cl("container")}>
|
||||
<div className={cl("settings-labels")}>
|
||||
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
|
||||
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
|
||||
</div>
|
||||
<ColorPicker
|
||||
color={parseInt(settings.store.color, 16)}
|
||||
onChange={onPickColor}
|
||||
showEyeDropper={false}
|
||||
suggestedColors={colorPresets}
|
||||
/>
|
||||
</div>
|
||||
{(contrastWarning || nitroThemeEnabled) && (<>
|
||||
<ErrorCard className={Margins.top8}>
|
||||
<Forms.FormTitle tag="h2">Your theme won't look good!</Forms.FormTitle>
|
||||
|
||||
{contrastWarning && <Forms.FormText>{">"} Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText>{">"} Nitro themes aren't supported</Forms.FormText>}
|
||||
|
||||
<div className={cl("buttons-container")}>
|
||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setDiscordTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
||||
{(nitroThemeEnabled) && <Button onClick={() => setDiscordTheme(currentTheme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
|
||||
</div>
|
||||
</ErrorCard>
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResetThemeColorComponent() {
|
||||
return (
|
||||
<Button onClick={() => onPickColor(0x313338)}>
|
||||
Reset Theme Color
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -7,104 +7,21 @@
|
|||
import "./clientTheme.css";
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Margins } from "@utils/margins";
|
||||
import { classes } from "@utils/misc";
|
||||
import definePlugin, { OptionType, StartAt } from "@utils/types";
|
||||
import { findByCodeLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
||||
import { Button, Forms, ThemeStore, useStateFromStores } from "@webpack/common";
|
||||
|
||||
const cl = classNameFactory("vc-clientTheme-");
|
||||
import { ResetThemeColorComponent, ThemeSettingsComponent } from "./components/Settings";
|
||||
import { disableClientTheme, startClientTheme } from "./utils/styleUtils";
|
||||
|
||||
const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", ".BACKGROUND_PRIMARY)");
|
||||
|
||||
const colorPresets = [
|
||||
"#1E1514", "#172019", "#13171B", "#1C1C28", "#402D2D",
|
||||
"#3A483D", "#344242", "#313D4B", "#2D2F47", "#322B42",
|
||||
"#3C2E42", "#422938", "#b6908f", "#bfa088", "#d3c77d",
|
||||
"#86ac86", "#88aab3", "#8693b5", "#8a89ba", "#ad94bb",
|
||||
];
|
||||
|
||||
function onPickColor(color: number) {
|
||||
const hexColor = color.toString(16).padStart(6, "0");
|
||||
|
||||
settings.store.color = hexColor;
|
||||
updateColorVars(hexColor);
|
||||
}
|
||||
|
||||
const saveClientTheme = findByCodeLazy('type:"UNSYNCED_USER_SETTINGS_UPDATE', '"system"===');
|
||||
|
||||
function setTheme(theme: string) {
|
||||
saveClientTheme({ theme });
|
||||
}
|
||||
|
||||
const NitroThemeStore = findStoreLazy("ClientThemesBackgroundStore");
|
||||
|
||||
function ThemeSettings() {
|
||||
const theme = useStateFromStores([ThemeStore], () => ThemeStore.theme);
|
||||
const isLightTheme = theme === "light";
|
||||
const oppositeTheme = isLightTheme ? "dark" : "light";
|
||||
|
||||
const nitroTheme = useStateFromStores([NitroThemeStore], () => NitroThemeStore.gradientPreset);
|
||||
const nitroThemeEnabled = nitroTheme !== undefined;
|
||||
|
||||
const selectedLuminance = relativeLuminance(settings.store.color);
|
||||
|
||||
let contrastWarning = false, fixableContrast = true;
|
||||
if ((isLightTheme && selectedLuminance < 0.26) || !isLightTheme && selectedLuminance > 0.12)
|
||||
contrastWarning = true;
|
||||
if (selectedLuminance < 0.26 && selectedLuminance > 0.12)
|
||||
fixableContrast = false;
|
||||
// light mode with values greater than 65 leads to background colors getting crushed together and poor text contrast for muted channels
|
||||
if (isLightTheme && selectedLuminance > 0.65) {
|
||||
contrastWarning = true;
|
||||
fixableContrast = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cl("settings")}>
|
||||
<div className={cl("container")}>
|
||||
<div className={cl("settings-labels")}>
|
||||
<Forms.FormTitle tag="h3">Theme Color</Forms.FormTitle>
|
||||
<Forms.FormText>Add a color to your Discord client theme</Forms.FormText>
|
||||
</div>
|
||||
<ColorPicker
|
||||
color={parseInt(settings.store.color, 16)}
|
||||
onChange={onPickColor}
|
||||
showEyeDropper={false}
|
||||
suggestedColors={colorPresets}
|
||||
/>
|
||||
</div>
|
||||
{(contrastWarning || nitroThemeEnabled) && (<>
|
||||
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom8)} />
|
||||
<div className={`client-theme-contrast-warning ${contrastWarning ? (isLightTheme ? "theme-dark" : "theme-light") : ""}`}>
|
||||
<div className={cl("warning")}>
|
||||
<Forms.FormText className={cl("warning-text")}>Warning, your theme won't look good:</Forms.FormText>
|
||||
{contrastWarning && <Forms.FormText className={cl("warning-text")}>Selected color won't contrast well with text</Forms.FormText>}
|
||||
{nitroThemeEnabled && <Forms.FormText className={cl("warning-text")}>Nitro themes aren't supported</Forms.FormText>}
|
||||
</div>
|
||||
{(contrastWarning && fixableContrast) && <Button onClick={() => setTheme(oppositeTheme)} color={Button.Colors.RED}>Switch to {oppositeTheme} mode</Button>}
|
||||
{(nitroThemeEnabled) && <Button onClick={() => setTheme(theme)} color={Button.Colors.RED}>Disable Nitro Theme</Button>}
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
export const settings = definePluginSettings({
|
||||
color: {
|
||||
type: OptionType.COMPONENT,
|
||||
default: "313338",
|
||||
component: ThemeSettings
|
||||
component: ThemeSettingsComponent
|
||||
},
|
||||
resetColor: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<Button onClick={() => onPickColor(0x313338)}>
|
||||
Reset Theme Color
|
||||
</Button>
|
||||
)
|
||||
component: ResetThemeColorComponent
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -115,185 +32,6 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
startAt: StartAt.DOMContentLoaded,
|
||||
async start() {
|
||||
updateColorVars(settings.store.color);
|
||||
|
||||
const styles = await getStyles();
|
||||
generateColorOffsets(styles);
|
||||
generateLightModeFixes(styles);
|
||||
},
|
||||
|
||||
stop() {
|
||||
document.getElementById("clientThemeVars")?.remove();
|
||||
document.getElementById("clientThemeOffsets")?.remove();
|
||||
document.getElementById("clientThemeLightModeFixes")?.remove();
|
||||
}
|
||||
start: () => startClientTheme(settings.store.color),
|
||||
stop: disableClientTheme
|
||||
});
|
||||
|
||||
const visualRefreshVariableRegex = /(--neutral-\d{1,3}-hsl):.*?(\S*)%;/g;
|
||||
const oldVariableRegex = /(--primary-\d{3}-hsl):.*?(\S*)%;/g;
|
||||
const lightVariableRegex = /^--primary-[1-5]\d{2}-hsl/g;
|
||||
const darkVariableRegex = /^--primary-[5-9]\d{2}-hsl/g;
|
||||
|
||||
// generates variables per theme by:
|
||||
// - matching regex (so we can limit what variables are included in light/dark theme, otherwise text becomes unreadable)
|
||||
// - offset from specified center (light/dark theme get different offsets because light uses 100 for background-primary, while dark uses 600)
|
||||
function genThemeSpecificOffsets(variableLightness: Record<string, number>, regex: RegExp | null, centerVariable: string): string {
|
||||
return Object.entries(variableLightness).filter(([key]) => regex == null || key.search(regex) > -1)
|
||||
.map(([key, lightness]) => {
|
||||
const lightnessOffset = lightness - variableLightness[centerVariable];
|
||||
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
|
||||
return `${key}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function generateColorOffsets(styles) {
|
||||
const oldVariableLightness = {} as Record<string, number>;
|
||||
const visualRefreshVariableLightness = {} as Record<string, number>;
|
||||
|
||||
// Get lightness values of --primary variables
|
||||
for (const [, variable, lightness] of styles.matchAll(oldVariableRegex)) {
|
||||
oldVariableLightness[variable] = parseFloat(lightness);
|
||||
}
|
||||
|
||||
for (const [, variable, lightness] of styles.matchAll(visualRefreshVariableRegex)) {
|
||||
visualRefreshVariableLightness[variable] = parseFloat(lightness);
|
||||
}
|
||||
|
||||
createStyleSheet("clientThemeOffsets", [
|
||||
`.theme-light {\n ${genThemeSpecificOffsets(oldVariableLightness, lightVariableRegex, "--primary-345-hsl")} \n}`,
|
||||
`.theme-dark {\n ${genThemeSpecificOffsets(oldVariableLightness, darkVariableRegex, "--primary-600-hsl")} \n}`,
|
||||
`.visual-refresh.theme-light {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-2-hsl")} \n}`,
|
||||
`.visual-refresh.theme-dark {\n ${genThemeSpecificOffsets(visualRefreshVariableLightness, null, "--neutral-69-hsl")} \n}`,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function generateLightModeFixes(styles: string) {
|
||||
const groupLightUsesW500Regex = /\.theme-light[^{]*\{[^}]*var\(--white-500\)[^}]*}/gm;
|
||||
// get light capturing groups that mention --white-500
|
||||
const relevantStyles = [...styles.matchAll(groupLightUsesW500Regex)].flat();
|
||||
|
||||
const groupBackgroundRegex = /^([^{]*)\{background:var\(--white-500\)/m;
|
||||
const groupBackgroundColorRegex = /^([^{]*)\{background-color:var\(--white-500\)/m;
|
||||
// find all capturing groups that assign background or background-color directly to w500
|
||||
const backgroundGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundRegex)).join(",\n");
|
||||
const backgroundColorGroups = mapReject(relevantStyles, entry => captureOne(entry, groupBackgroundColorRegex)).join(",\n");
|
||||
// create css to reassign them to --primary-100
|
||||
const reassignBackgrounds = `${backgroundGroups} {\n background: var(--primary-100) \n}`;
|
||||
const reassignBackgroundColors = `${backgroundColorGroups} {\n background-color: var(--primary-100) \n}`;
|
||||
|
||||
const groupBgVarRegex = /\.theme-light\{([^}]*--[^:}]*(?:background|bg)[^:}]*:var\(--white-500\)[^}]*)\}/m;
|
||||
const bgVarRegex = /^(--[^:]*(?:background|bg)[^:]*):var\(--white-500\)/m;
|
||||
// get all global variables used for backgrounds
|
||||
const lightVars = mapReject(relevantStyles, style => captureOne(style, groupBgVarRegex)) // get the insides of capture groups that have at least one background var with w500
|
||||
.map(str => str.split(";")).flat(); // captureGroupInsides[] -> cssRule[]
|
||||
const lightBgVars = mapReject(lightVars, variable => captureOne(variable, bgVarRegex)); // remove vars that aren't for backgrounds or w500
|
||||
// create css to reassign every var
|
||||
const reassignVariables = `.theme-light {\n ${lightBgVars.map(variable => `${variable}: var(--primary-100);`).join("\n")} \n}`;
|
||||
|
||||
createStyleSheet("clientThemeLightModeFixes", [
|
||||
reassignBackgrounds,
|
||||
reassignBackgroundColors,
|
||||
reassignVariables,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function captureOne(str, regex) {
|
||||
const result = str.match(regex);
|
||||
return (result === null) ? null : result[1];
|
||||
}
|
||||
|
||||
function mapReject(arr, mapFunc) {
|
||||
return arr.map(mapFunc).filter(Boolean);
|
||||
}
|
||||
|
||||
function updateColorVars(color: string) {
|
||||
const { hue, saturation, lightness } = hexToHSL(color);
|
||||
|
||||
let style = document.getElementById("clientThemeVars");
|
||||
if (!style)
|
||||
style = createStyleSheet("clientThemeVars");
|
||||
|
||||
style.textContent = `:root {
|
||||
--theme-h: ${hue};
|
||||
--theme-s: ${saturation}%;
|
||||
--theme-l: ${lightness}%;
|
||||
}`;
|
||||
}
|
||||
|
||||
function createStyleSheet(id, content = "") {
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("id", id);
|
||||
style.textContent = content.split("\n").map(line => line.trim()).join("\n");
|
||||
document.body.appendChild(style);
|
||||
return style;
|
||||
}
|
||||
|
||||
// returns all of discord's native styles in a single string
|
||||
async function getStyles(): Promise<string> {
|
||||
let out = "";
|
||||
const styleLinkNodes = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (const styleLinkNode of styleLinkNodes) {
|
||||
const cssLink = styleLinkNode.getAttribute("href");
|
||||
if (!cssLink) continue;
|
||||
|
||||
const res = await fetch(cssLink);
|
||||
out += await res.text();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// https://css-tricks.com/converting-color-spaces-in-javascript/
|
||||
function hexToHSL(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
const r = parseInt(hexCode.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
|
||||
|
||||
// RGB => HSL
|
||||
const cMax = Math.max(r, g, b);
|
||||
const cMin = Math.min(r, g, b);
|
||||
const delta = cMax - cMin;
|
||||
|
||||
let hue: number, saturation: number, lightness: number;
|
||||
|
||||
lightness = (cMax + cMin) / 2;
|
||||
|
||||
if (delta === 0) {
|
||||
// If r=g=b then the only thing that matters is lightness
|
||||
hue = 0;
|
||||
saturation = 0;
|
||||
} else {
|
||||
// Magic
|
||||
saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
||||
|
||||
if (cMax === r)
|
||||
hue = ((g - b) / delta) % 6;
|
||||
else if (cMax === g)
|
||||
hue = (b - r) / delta + 2;
|
||||
else
|
||||
hue = (r - g) / delta + 4;
|
||||
hue *= 60;
|
||||
if (hue < 0)
|
||||
hue += 360;
|
||||
}
|
||||
|
||||
// Move saturation and lightness from 0-1 to 0-100
|
||||
saturation *= 100;
|
||||
lightness *= 100;
|
||||
|
||||
return { hue, saturation, lightness };
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
||||
function relativeLuminance(hexCode: string) {
|
||||
const normalize = (x: number) =>
|
||||
x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
||||
|
||||
const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255);
|
||||
const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255);
|
||||
const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255);
|
||||
|
||||
return r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
}
|
||||
|
|
65
src/plugins/clientTheme/utils/colorUtils.ts
Normal file
65
src/plugins/clientTheme/utils/colorUtils.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
// https://css-tricks.com/converting-color-spaces-in-javascript/
|
||||
export function hexToHSL(hexCode: string) {
|
||||
// Hex => RGB normalized to 0-1
|
||||
const r = parseInt(hexCode.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexCode.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexCode.substring(4, 6), 16) / 255;
|
||||
|
||||
// RGB => HSL
|
||||
const cMax = Math.max(r, g, b);
|
||||
const cMin = Math.min(r, g, b);
|
||||
const delta = cMax - cMin;
|
||||
|
||||
let hue: number;
|
||||
let saturation: number;
|
||||
let lightness: number;
|
||||
|
||||
lightness = (cMax + cMin) / 2;
|
||||
|
||||
if (delta === 0) {
|
||||
// If r=g=b then the only thing that matters is lightness
|
||||
hue = 0;
|
||||
saturation = 0;
|
||||
} else {
|
||||
// Magic
|
||||
saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
||||
|
||||
if (cMax === r) {
|
||||
hue = ((g - b) / delta) % 6;
|
||||
} else if (cMax === g) {
|
||||
hue = (b - r) / delta + 2;
|
||||
} else {
|
||||
hue = (r - g) / delta + 4;
|
||||
}
|
||||
|
||||
hue *= 60;
|
||||
if (hue < 0) {
|
||||
hue += 360;
|
||||
}
|
||||
}
|
||||
|
||||
// Move saturation and lightness from 0-1 to 0-100
|
||||
saturation *= 100;
|
||||
lightness *= 100;
|
||||
|
||||
return { hue, saturation, lightness };
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
||||
export function relativeLuminance(hexCode: string) {
|
||||
const normalize = (x: number) => (
|
||||
x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4
|
||||
);
|
||||
|
||||
const r = normalize(parseInt(hexCode.substring(0, 2), 16) / 255);
|
||||
const g = normalize(parseInt(hexCode.substring(2, 4), 16) / 255);
|
||||
const b = normalize(parseInt(hexCode.substring(4, 6), 16) / 255);
|
||||
|
||||
return r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
}
|
90
src/plugins/clientTheme/utils/styleUtils.ts
Normal file
90
src/plugins/clientTheme/utils/styleUtils.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { hexToHSL } from "./colorUtils";
|
||||
|
||||
const VARS_STYLE_ID = "vc-clientTheme-vars";
|
||||
const OVERRIDES_STYLE_ID = "vc-clientTheme-overrides";
|
||||
|
||||
export function createOrUpdateThemeColorVars(color: string) {
|
||||
const { hue, saturation, lightness } = hexToHSL(color);
|
||||
|
||||
createOrUpdateStyle(VARS_STYLE_ID, `:root {
|
||||
--theme-h: ${hue};
|
||||
--theme-s: ${saturation}%;
|
||||
--theme-l: ${lightness}%;
|
||||
}`);
|
||||
}
|
||||
|
||||
export async function startClientTheme(color: string) {
|
||||
createOrUpdateThemeColorVars(color);
|
||||
createColorsOverrides(await getDiscordStyles());
|
||||
}
|
||||
|
||||
export function disableClientTheme() {
|
||||
document.getElementById(VARS_STYLE_ID)?.remove();
|
||||
document.getElementById(OVERRIDES_STYLE_ID)?.remove();
|
||||
}
|
||||
|
||||
function getOrCreateStyle(styleId: string) {
|
||||
const existingStyle = document.getElementById(styleId);
|
||||
if (existingStyle) {
|
||||
return existingStyle as HTMLStyleElement;
|
||||
}
|
||||
|
||||
const newStyle = document.createElement("style");
|
||||
newStyle.id = styleId;
|
||||
|
||||
return document.head.appendChild(newStyle);
|
||||
}
|
||||
|
||||
function createOrUpdateStyle(styleId: string, css: string) {
|
||||
const style = getOrCreateStyle(styleId);
|
||||
style.textContent = css;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A string containing all the CSS styles from the Discord client.
|
||||
*/
|
||||
async function getDiscordStyles(): Promise<string> {
|
||||
const styleLinkNodes = document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]');
|
||||
|
||||
const cssTexts = await Promise.all(Array.from(styleLinkNodes, async node => {
|
||||
if (!node.href)
|
||||
return null;
|
||||
|
||||
return fetch(node.href).then(res => res.text());
|
||||
}));
|
||||
|
||||
return cssTexts.filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
const VISUAL_REFRESH_COLORS_VARIABLES_REGEX = /(--neutral-\d{1,3}?-hsl):.+?([\d.]+?)%;/g;
|
||||
|
||||
function createColorsOverrides(styles: string) {
|
||||
const visualRefreshColorsLightness = {} as Record<string, number>;
|
||||
|
||||
for (const [, colorVariableName, lightness] of styles.matchAll(VISUAL_REFRESH_COLORS_VARIABLES_REGEX)) {
|
||||
visualRefreshColorsLightness[colorVariableName] = parseFloat(lightness);
|
||||
}
|
||||
|
||||
const lightThemeBaseLightness = visualRefreshColorsLightness["--neutral-2-hsl"];
|
||||
const darkThemeBaseLightness = visualRefreshColorsLightness["--neutral-69-hsl"];
|
||||
|
||||
createOrUpdateStyle(OVERRIDES_STYLE_ID, [
|
||||
`.visual-refresh.theme-light {\n ${generateNewColorVars(visualRefreshColorsLightness, lightThemeBaseLightness)} \n}`,
|
||||
`.visual-refresh.theme-dark {\n ${generateNewColorVars(visualRefreshColorsLightness, darkThemeBaseLightness)} \n}`,
|
||||
].join("\n\n"));
|
||||
}
|
||||
|
||||
function generateNewColorVars(colorsLightess: Record<string, number>, baseLightness: number) {
|
||||
return Object.entries(colorsLightess).map(([colorVariableName, lightness]) => {
|
||||
const lightnessOffset = lightness - baseLightness;
|
||||
const plusOrMinus = lightnessOffset >= 0 ? "+" : "-";
|
||||
|
||||
return `${colorVariableName}: var(--theme-h) var(--theme-s) calc(var(--theme-l) ${plusOrMinus} ${Math.abs(lightnessOffset).toFixed(2)}%);`;
|
||||
}).join("\n");
|
||||
}
|
|
@ -197,7 +197,7 @@ export default definePlugin({
|
|||
},
|
||||
// Patches Discord generic logger function
|
||||
{
|
||||
find: "Σ:",
|
||||
find: '"file-only"!==',
|
||||
predicate: () => settings.store.disableLoggers,
|
||||
replacement: {
|
||||
match: /(?<=&&)(?=console)/,
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Devs } from "@utils/constants";
|
|||
import { getCurrentChannel, getCurrentGuild } from "@utils/discord";
|
||||
import { runtimeHashMessageKey } from "@utils/intlHash";
|
||||
import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy";
|
||||
import { sleep } from "@utils/misc";
|
||||
import { ModalAPI } from "@utils/modal";
|
||||
import { relaunch } from "@utils/native";
|
||||
import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches";
|
||||
|
@ -135,7 +136,7 @@ function makeShortcuts() {
|
|||
});
|
||||
}
|
||||
|
||||
const root = Common.ReactDOM.createRoot(doc.body.appendChild(document.createElement("div")));
|
||||
const root = Common.createRoot(doc.body.appendChild(document.createElement("div")));
|
||||
root.render(Common.React.createElement(component, props));
|
||||
|
||||
doc.addEventListener("close", () => root.unmount(), { once: true });
|
||||
|
@ -173,8 +174,8 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
|
|||
function unwrapProxy(value: any) {
|
||||
if (value[SYM_LAZY_GET]) {
|
||||
forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED];
|
||||
} else if (value.$$vencordInternal) {
|
||||
return forceLoad ? value.$$vencordInternal() : value;
|
||||
} else if (value.$$vencordGetWrappedComponent) {
|
||||
return forceLoad ? value.$$vencordGetWrappedComponent() : value;
|
||||
}
|
||||
|
||||
return value;
|
||||
|
@ -206,10 +207,13 @@ function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) {
|
|||
return value;
|
||||
}
|
||||
|
||||
const webpackModulesProbablyLoaded = Webpack.onceReady.then(() => sleep(1000));
|
||||
|
||||
export default definePlugin({
|
||||
name: "ConsoleShortcuts",
|
||||
description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.",
|
||||
authors: [Devs.Ven],
|
||||
startAt: StartAt.Init,
|
||||
|
||||
patches: [
|
||||
{
|
||||
|
@ -221,7 +225,7 @@ export default definePlugin({
|
|||
}
|
||||
],
|
||||
|
||||
startAt: StartAt.Init,
|
||||
|
||||
start() {
|
||||
const shortcuts = makeShortcuts();
|
||||
window.shortcutList = {};
|
||||
|
@ -242,18 +246,16 @@ export default definePlugin({
|
|||
}
|
||||
|
||||
// unproxy loaded modules
|
||||
Webpack.onceReady.then(() => {
|
||||
setTimeout(() => this.eagerLoad(false), 1000);
|
||||
this.eagerLoad(false);
|
||||
|
||||
if (!IS_WEB) {
|
||||
const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative<typeof import("./native")>;
|
||||
Native.initDevtoolsOpenEagerLoad();
|
||||
}
|
||||
});
|
||||
if (!IS_WEB) {
|
||||
const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative<typeof import("./native")>;
|
||||
Native.initDevtoolsOpenEagerLoad();
|
||||
}
|
||||
},
|
||||
|
||||
async eagerLoad(forceLoad: boolean) {
|
||||
await Webpack.onceReady;
|
||||
await webpackModulesProbablyLoaded;
|
||||
|
||||
const shortcuts = makeShortcuts();
|
||||
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
|
||||
import { LinkIcon } from "@components/Icons";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Clipboard, Menu } from "@webpack/common";
|
||||
import { Menu } from "@webpack/common";
|
||||
import type { Channel, User } from "discord-types/general";
|
||||
|
||||
interface UserContextProps {
|
||||
|
@ -36,7 +37,7 @@ const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: U
|
|||
<Menu.MenuItem
|
||||
id="vc-copy-user-url"
|
||||
label="Copy User URL"
|
||||
action={() => Clipboard.copy(`<https://discord.com/users/${user.id}>`)}
|
||||
action={() => copyToClipboard(`<https://discord.com/users/${user.id}>`)}
|
||||
icon={LinkIcon}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import { CopyIcon, DeleteIcon } from "@components/Icons";
|
||||
import { Alerts, Clipboard, ContextMenuApi, Menu, UserStore } from "@webpack/common";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { Alerts, ContextMenuApi, Menu, UserStore } from "@webpack/common";
|
||||
|
||||
import { Decoration } from "../../lib/api";
|
||||
import { useCurrentUserDecorationsStore } from "../../lib/stores/CurrentUserDecorationsStore";
|
||||
|
@ -23,7 +24,7 @@ export default function DecorationContextMenu({ decoration }: { decoration: Deco
|
|||
id={cl("decoration-context-menu-copy-hash")}
|
||||
label="Copy Decoration Hash"
|
||||
icon={CopyIcon}
|
||||
action={() => Clipboard.copy(decoration.hash)}
|
||||
action={() => copyToClipboard(decoration.hash)}
|
||||
/>
|
||||
{decoration.authorId === UserStore.getCurrentUser().id &&
|
||||
<Menu.MenuItem
|
||||
|
|
|
@ -31,6 +31,10 @@ import hideBugReport from "./hideBugReport.css?managed";
|
|||
const KbdStyles = findByPropsLazy("key", "combo");
|
||||
const BugReporterExperiment = findLazy(m => m?.definition?.id === "2024-09_bug_reporter");
|
||||
|
||||
const isMacOS = navigator.platform.includes("Mac");
|
||||
const modKey = isMacOS ? "cmd" : "ctrl";
|
||||
const altKey = isMacOS ? "opt" : "alt";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
toolbarDevMenu: {
|
||||
type: OptionType.BOOLEAN,
|
||||
|
@ -48,7 +52,7 @@ export default definePlugin({
|
|||
Devs.Ven,
|
||||
Devs.Nickyux,
|
||||
Devs.BanTheNons,
|
||||
Devs.Nuckyz
|
||||
Devs.Nuckyz,
|
||||
],
|
||||
|
||||
settings,
|
||||
|
@ -75,9 +79,9 @@ export default definePlugin({
|
|||
replace: "$&$self.WarningCard(),"
|
||||
}
|
||||
},
|
||||
// change top right chat toolbar button from the help one to the dev one
|
||||
// Change top right chat toolbar button from the help one to the dev one
|
||||
{
|
||||
find: "toolbar:function",
|
||||
find: ".CONTEXTLESS,isActivityPanelMode:",
|
||||
replacement: {
|
||||
match: /hasBugReporterAccess:(\i)/,
|
||||
replace: "_hasBugReporterAccess:$1=true"
|
||||
|
@ -85,7 +89,7 @@ export default definePlugin({
|
|||
predicate: () => settings.store.toolbarDevMenu
|
||||
},
|
||||
|
||||
// makes the Favourites Server experiment allow favouriting DMs and threads
|
||||
// Make the Favourites Server experiment allow favouriting DMs and threads
|
||||
{
|
||||
find: "useCanFavoriteChannel",
|
||||
replacement: {
|
||||
|
@ -93,23 +97,36 @@ export default definePlugin({
|
|||
replace: "false",
|
||||
}
|
||||
},
|
||||
// enable option to always record clips even if you are not streaming
|
||||
// Enable option to always record clips even if you are not streaming
|
||||
{
|
||||
find: "isDecoupledGameClippingEnabled(){",
|
||||
replacement: {
|
||||
match: /\i\.isStaff\(\)/,
|
||||
replace: "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Enable experiment embed on sent experiment links
|
||||
{
|
||||
find: "dev://experiment/",
|
||||
replacement: [
|
||||
{
|
||||
match: /\i\.isStaff\(\)/,
|
||||
replace: "true"
|
||||
},
|
||||
// Fix some tricky experiments name causing a client crash
|
||||
{
|
||||
match: /.getRegisteredExperiments\(\)(?<=(\i)=.+?).+?if\(null==(\i)(?=\)return null;)/,
|
||||
replace: "$&||!Object.hasOwn($1,$2)"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
|
||||
start: () => !BugReporterExperiment.getCurrentConfig().hasBugReporterAccess && enableStyle(hideBugReport),
|
||||
stop: () => disableStyle(hideBugReport),
|
||||
|
||||
settingsAboutComponent: () => {
|
||||
const isMacOS = navigator.platform.includes("Mac");
|
||||
const modKey = isMacOS ? "cmd" : "ctrl";
|
||||
const altKey = isMacOS ? "opt" : "alt";
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Forms.FormTitle tag="h3">More Information</Forms.FormTitle>
|
||||
|
|
|
@ -29,8 +29,8 @@ export default definePlugin({
|
|||
{
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /,isOwner:(\i),/,
|
||||
replace: ",_isOwner:$1=$self.isGuildOwner(e),"
|
||||
match: /(?<=decorators:.{0,200}?isOwner:)\i/,
|
||||
replace: "$self.isGuildOwner(arguments[0])"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -24,7 +24,7 @@ export default definePlugin({
|
|||
patches: [
|
||||
// DM User Sidebar
|
||||
{
|
||||
find: ".PANEL}),nicknameIcons",
|
||||
find: ".SIDEBAR}),nicknameIcons",
|
||||
replacement: {
|
||||
match: /#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id)}\)}\)/,
|
||||
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:true})"
|
||||
|
@ -32,7 +32,15 @@ export default definePlugin({
|
|||
},
|
||||
// User Profile Modal
|
||||
{
|
||||
find: "action:\"PRESS_APP_CONNECTION\"",
|
||||
find: ".connections,userId:",
|
||||
replacement: {
|
||||
match: /#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
|
||||
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),"
|
||||
}
|
||||
},
|
||||
// User Profile Modal v2
|
||||
{
|
||||
find: ".MODAL_V2,onClose:",
|
||||
replacement: {
|
||||
match: /#{intl::USER_PROFILE_MEMBER_SINCE}\),.{0,100}userId:(\i\.id),.{0,100}}\)}\),/,
|
||||
replace: "$&,$self.FriendsSinceComponent({userId:$1,isSidebar:false}),"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { FluxDispatcher, useLayoutEffect, useRef, useState } from "@webpack/common";
|
||||
import { FluxDispatcher, useLayoutEffect, useMemo, useRef, useState } from "@webpack/common";
|
||||
|
||||
import { ELEMENT_ID } from "../constants";
|
||||
import { settings } from "../index";
|
||||
|
@ -160,6 +160,16 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
|
|||
}
|
||||
});
|
||||
|
||||
const imageSrc = useMemo(() => {
|
||||
try {
|
||||
const imageUrl = new URL(instance.props.src);
|
||||
imageUrl.searchParams.set("animated", "true");
|
||||
return imageUrl.toString();
|
||||
} catch {
|
||||
return instance.props.src;
|
||||
}
|
||||
}, [instance.props.src]);
|
||||
|
||||
if (!ready) return null;
|
||||
|
||||
const box = element.current?.getBoundingClientRect();
|
||||
|
@ -203,7 +213,7 @@ export const Magnifier = ErrorBoundary.wrap<MagnifierProps>(({ instance, size: i
|
|||
}}
|
||||
width={`${box.width * zoom.current}px`}
|
||||
height={`${box.height * zoom.current}px`}
|
||||
src={instance.props.src}
|
||||
src={imageSrc}
|
||||
alt=""
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -21,8 +21,9 @@ import { definePluginSettings } from "@api/Settings";
|
|||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { debounce } from "@shared/debounce";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Menu, ReactDOM } from "@webpack/common";
|
||||
import { createRoot, Menu } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
import type { Root } from "react-dom/client";
|
||||
|
||||
|
@ -237,12 +238,16 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
renderMagnifier(instance) {
|
||||
if (instance.props.id === ELEMENT_ID) {
|
||||
if (!this.currentMagnifierElement) {
|
||||
this.currentMagnifierElement = <Magnifier size={settings.store.size} zoom={settings.store.zoom} instance={instance} />;
|
||||
this.root = ReactDOM.createRoot(this.element!);
|
||||
this.root.render(this.currentMagnifierElement);
|
||||
try {
|
||||
if (instance.props.id === ELEMENT_ID) {
|
||||
if (!this.currentMagnifierElement) {
|
||||
this.currentMagnifierElement = <Magnifier size={settings.store.size} zoom={settings.store.zoom} instance={instance} />;
|
||||
this.root = createRoot(this.element!);
|
||||
this.root.render(this.currentMagnifierElement);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
new Logger("ImageZoom").error("Failed to render magnifier:", error);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -66,20 +66,29 @@ export default definePlugin({
|
|||
{
|
||||
find: '="SYSTEM_TAG"',
|
||||
replacement: {
|
||||
match: /\i.gradientClassName]\),style:/,
|
||||
replace: "$&{color:$self.calculateNameColorForMessageContext(arguments[0])},_style:"
|
||||
// Override colorString with our custom color and disable gradients if applying the custom color.
|
||||
match: /&&null!=\i\.secondaryColor,(?<=colorString:(\i).+?(\i)=.+?)/,
|
||||
replace: (m, colorString, hasGradientColors) => `${m}` +
|
||||
`vcIrcColorsDummy=[${colorString},${hasGradientColors}]=$self.getMessageColorsVariables(arguments[0],${hasGradientColors}),`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: "#{intl::GUILD_OWNER}),children:",
|
||||
replacement: {
|
||||
match: /(typingIndicatorRef:.+?},)(\i=.+?)color:null!=.{0,50}?(?=,)/,
|
||||
replace: (_, rest1, rest2) => `${rest1}ircColor=$self.calculateNameColorForListContext(arguments[0]),${rest2}color:ircColor`
|
||||
match: /(?<=roleName:\i,)color:/,
|
||||
replace: "color:$self.calculateNameColorForListContext(arguments[0]),originalColor:"
|
||||
},
|
||||
predicate: () => settings.store.memberListColors
|
||||
}
|
||||
],
|
||||
|
||||
getMessageColorsVariables(context: any, hasGradientColors: boolean) {
|
||||
const colorString = this.calculateNameColorForMessageContext(context);
|
||||
const originalColorString = context?.author?.colorString;
|
||||
|
||||
return [colorString, hasGradientColors && colorString === originalColorString];
|
||||
},
|
||||
|
||||
calculateNameColorForMessageContext(context: any) {
|
||||
const userId: string | undefined = context?.message?.author?.id;
|
||||
const colorString = context?.author?.colorString;
|
||||
|
@ -97,6 +106,7 @@ export default definePlugin({
|
|||
? color
|
||||
: colorString;
|
||||
},
|
||||
|
||||
calculateNameColorForListContext(context: any) {
|
||||
const id = context?.user?.id;
|
||||
const colorString = context?.colorString;
|
||||
|
|
|
@ -23,7 +23,7 @@ import { updateMessage } from "@api/MessageUpdater";
|
|||
import { Settings } from "@api/Settings";
|
||||
import { disableStyle, enableStyle } from "@api/Styles";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { Devs, SUPPORT_CATEGORY_ID, VENBOT_USER_ID } from "@utils/constants";
|
||||
import { getIntlMessage } from "@utils/discord";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { classes } from "@utils/misc";
|
||||
|
@ -295,8 +295,8 @@ export default definePlugin({
|
|||
ignoreChannels.includes(ChannelStore.getChannel(message.channel_id)?.parent_id) ||
|
||||
(isEdit ? !logEdits : !logDeletes) ||
|
||||
ignoreGuilds.includes(ChannelStore.getChannel(message.channel_id)?.guild_id) ||
|
||||
// Ignore Venbot in the support channel
|
||||
(message.channel_id === "1026515880080842772" && message.author?.id === "1017176847865352332");
|
||||
// Ignore Venbot in the support channels
|
||||
(message.author?.id === VENBOT_USER_ID && ChannelStore.getChannel(message.channel_id)?.parent_id === SUPPORT_CATEGORY_ID);
|
||||
},
|
||||
|
||||
EditMarker({ message, className, children, ...props }: any) {
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isNonNullish } from "@utils/guards";
|
||||
|
@ -30,8 +32,8 @@ const SelectedChannelActionCreators = findByPropsLazy("selectPrivateChannel");
|
|||
const UserUtils = findByPropsLazy("getGlobalName");
|
||||
|
||||
const ProfileListClasses = findByPropsLazy("emptyIconFriends", "emptyIconGuilds");
|
||||
const ExpandableList = findComponentByCodeLazy('"PRESS_SECTION"');
|
||||
const GuildLabelClasses = findByPropsLazy("guildNick", "guildAvatarWithoutIcon");
|
||||
const MutualsListClasses = findByPropsLazy("row", "icon", "name", "nick");
|
||||
const ExpandableList = findComponentByCodeLazy('"PRESS_SECTION"', ".header");
|
||||
|
||||
function getGroupDMName(channel: Channel) {
|
||||
return channel.name ||
|
||||
|
@ -57,7 +59,7 @@ function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
|
|||
return mutualDms.map(c => (
|
||||
<Clickable
|
||||
key={c.id}
|
||||
className={ProfileListClasses.listRow}
|
||||
className={MutualsListClasses.row}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
SelectedChannelActionCreators.selectPrivateChannel(c.id);
|
||||
|
@ -66,12 +68,12 @@ function renderClickableGDMs(mutualDms: Channel[], onClose: () => void) {
|
|||
<Avatar
|
||||
src={IconUtils.getChannelIconURL({ id: c.id, icon: c.icon, size: 32 })}
|
||||
size="SIZE_40"
|
||||
className={ProfileListClasses.listAvatar}
|
||||
className={MutualsListClasses.icon}
|
||||
>
|
||||
</Avatar>
|
||||
<div className={ProfileListClasses.listRowContent}>
|
||||
<div className={ProfileListClasses.listName}>{getGroupDMName(c)}</div>
|
||||
<div className={GuildLabelClasses.guildNick}>{c.recipients.length + 1} Members</div>
|
||||
<div className={MutualsListClasses.details}>
|
||||
<div className={MutualsListClasses.name}>{getGroupDMName(c)}</div>
|
||||
<div className={MutualsListClasses.nick}>{c.recipients.length + 1} Members</div>
|
||||
</div>
|
||||
</Clickable>
|
||||
));
|
||||
|
@ -85,8 +87,9 @@ export default definePlugin({
|
|||
authors: [Devs.amia],
|
||||
|
||||
patches: [
|
||||
// User Profile Modal
|
||||
{
|
||||
find: ".MUTUAL_FRIENDS?(",
|
||||
find: ".BOT_DATA_ACCESS?(",
|
||||
replacement: [
|
||||
{
|
||||
match: /\i\.useEffect.{0,100}(\i)\[0\]\.section/,
|
||||
|
@ -95,6 +98,36 @@ export default definePlugin({
|
|||
{
|
||||
match: /\(0,\i\.jsx\)\(\i,\{items:\i,section:(\i)/,
|
||||
replace: "$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):$&"
|
||||
},
|
||||
// Discord adds spacing between each item which pushes our tab off screen.
|
||||
// set the gap to zero to ensure ours stays on screen
|
||||
{
|
||||
match: /className:\i\.tabBar/,
|
||||
replace: '$& + " vc-mutual-gdms-modal-tab-bar"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// User Profile Modal v2
|
||||
{
|
||||
find: ".tabBarPanel,children:",
|
||||
replacement: [
|
||||
{
|
||||
match: /items:(\i),.+?(?=return\(0,\i\.jsxs?\)\("div)/,
|
||||
replace: "$&$self.pushSection($1,arguments[0].user);"
|
||||
},
|
||||
{
|
||||
match: /\.tabBarPanel,children:(?=.+?section:(\i))/,
|
||||
replace: "$&$1==='MUTUAL_GDMS'?$self.renderMutualGDMs(arguments[0]):"
|
||||
},
|
||||
// Make the gap between each item smaller so our tab can fit.
|
||||
{
|
||||
match: /className:\i\.tabBar/,
|
||||
replace: '$& + " 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"'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -130,8 +163,8 @@ export default definePlugin({
|
|||
|
||||
sections[IS_PATCHED] = true;
|
||||
sections.push({
|
||||
text: getMutualGDMCountText(user),
|
||||
section: "MUTUAL_GDMS",
|
||||
text: getMutualGDMCountText(user)
|
||||
});
|
||||
} catch (e) {
|
||||
new Logger("MutualGroupDMs").error("Failed to push mutual group dms section:", e);
|
||||
|
|
7
src/plugins/mutualGroupDMs/style.css
Normal file
7
src/plugins/mutualGroupDMs/style.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.vc-mutual-gdms-modal-tab-bar {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.vc-mutual-gdms-modal-v2-tab-bar {
|
||||
gap: 12px;
|
||||
}
|
|
@ -19,53 +19,17 @@
|
|||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
|
||||
export default definePlugin({
|
||||
name: "NoUnblockToJump",
|
||||
description: "Allows you to jump to messages of blocked users without unblocking them",
|
||||
authors: [Devs.dzshn],
|
||||
patches: [
|
||||
{
|
||||
// Clicking on search results to jump
|
||||
find: '.id,"Search Results"',
|
||||
replacement: [
|
||||
{
|
||||
match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,
|
||||
replace: "if(false)$1"
|
||||
},
|
||||
{
|
||||
match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNIGNORE_TO_JUMP_TITLE})/,
|
||||
replace: "if(false)$1"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
// Jump buttton in top right corner of messages
|
||||
find: "renderJumpButton()",
|
||||
replacement: [
|
||||
{
|
||||
match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,
|
||||
replace: "if(false)$1"
|
||||
},
|
||||
{
|
||||
match: /if\(.{1,10}\)(.{1,10}\.show\({.{1,50}#{intl::UNIGNORE_TO_JUMP_TITLE})/,
|
||||
replace: "if(false)$1"
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
// Clicking on replied messages to jump
|
||||
find: '("interactionUsernameProfile',
|
||||
replacement: [
|
||||
{
|
||||
match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNBLOCK_TO_JUMP_TITLE})/,
|
||||
replace: "false?$1"
|
||||
},
|
||||
{
|
||||
match: /.\?(.{1,10}\.show\({.{1,50}#{intl::UNIGNORE_TO_JUMP_TITLE})/,
|
||||
replace: "false?$1"
|
||||
},
|
||||
]
|
||||
find: "#{intl::UNIGNORE_TO_JUMP_BODY}",
|
||||
replacement: {
|
||||
match: /return \i\.\i\.isBlockedForMessage\(/,
|
||||
replace: "return true;$&"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2025 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: "NSFWGateBypass",
|
||||
description: "Allows you to access NSFW channels without setting/verifying your age",
|
||||
authors: [Devs.Commandtechno],
|
||||
patches: [
|
||||
{
|
||||
find: ".nsfwAllowed=null",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=\.nsfwAllowed=)null!=.+?(?=[,;])/,
|
||||
replace: "true",
|
||||
},
|
||||
{
|
||||
match: /(?<=\.ageVerificationStatus=)null!=.+?(?=[,;])/,
|
||||
replace: "3", // VERIFIED_ADULT
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
});
|
|
@ -19,10 +19,11 @@
|
|||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Flex } from "@components/Flex";
|
||||
import { InfoIcon, OwnerCrownIcon } from "@components/Icons";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { getIntlMessage, getUniqueUsername } from "@utils/discord";
|
||||
import { ModalCloseButton, ModalContent, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { findByCodeLazy } from "@webpack";
|
||||
import { Clipboard, ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { ContextMenuApi, FluxDispatcher, GuildMemberStore, GuildStore, i18n, Menu, PermissionsBits, ScrollerThin, Text, Tooltip, useEffect, useMemo, UserStore, useState, useStateFromStores } from "@webpack/common";
|
||||
import { UnicodeEmoji } from "@webpack/types";
|
||||
import type { Guild, Role, User } from "discord-types/general";
|
||||
|
||||
|
@ -228,7 +229,7 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
|
|||
id={cl("copy-role-id")}
|
||||
label={getIntlMessage("COPY_ID_ROLE")}
|
||||
action={() => {
|
||||
Clipboard.copy(roleId);
|
||||
copyToClipboard(roleId);
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -269,7 +270,7 @@ function UserContextMenu({ userId }: { userId: string; }) {
|
|||
id={cl("copy-user-id")}
|
||||
label={getIntlMessage("COPY_ID_USER")}
|
||||
action={() => {
|
||||
Clipboard.copy(userId);
|
||||
copyToClipboard(userId);
|
||||
}}
|
||||
/>
|
||||
</Menu.Menu>
|
||||
|
|
|
@ -16,28 +16,27 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import "./style.css";
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
|
||||
export default definePlugin({
|
||||
name: "PlainFolderIcon",
|
||||
description: "Doesn't show the small guild icons in folders",
|
||||
description: "Dont show the small guild icons in folders",
|
||||
authors: [Devs.botato],
|
||||
patches: [{
|
||||
find: ".expandedFolderIconWrapper",
|
||||
replacement: [
|
||||
// there are two elements, the first one is the plain folder icon
|
||||
// the second is the four guild preview icons
|
||||
// always show this one (the plain icons)
|
||||
{
|
||||
match: /\(\i\|\|\i\)&&(\(.{0,40}\(\i\.animated)/,
|
||||
replace: "$1",
|
||||
},
|
||||
// and never show this one (the guild preview icons)
|
||||
{
|
||||
match: /\(\i\|\|!\i\)&&(\(.{0,40}\(\i\.animated)/,
|
||||
replace: "false&&$1",
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
||||
patches: [
|
||||
{
|
||||
find: ".folderPreviewGuildIconError",
|
||||
replacement: [
|
||||
{
|
||||
// Discord always renders both plain and guild icons folders and uses a css transtion to switch between them
|
||||
match: /(?<=.folderButtonContent]:(!\i))/,
|
||||
replace: (_, hasFolderButtonContentClass) => `,"vc-plainFolderIcon-plain":${hasFolderButtonContentClass}`
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
10
src/plugins/plainFolderIcon/style.css
Normal file
10
src/plugins/plainFolderIcon/style.css
Normal file
|
@ -0,0 +1,10 @@
|
|||
.vc-plainFolderIcon-plain {
|
||||
/* Without this, they are a bit laggier */
|
||||
transition: none !important;
|
||||
|
||||
/* Don't show the mini guild icons */
|
||||
transform: translateZ(0);
|
||||
|
||||
/* The new icons are fully transparent. Add a sane default to match the old behavior */
|
||||
background-color: color-mix(in oklab, var(--custom-folder-color, var(--bg-brand)) 40%, transparent);
|
||||
}
|
|
@ -20,7 +20,7 @@ import { Devs } from "@utils/constants";
|
|||
import definePlugin from "@utils/types";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
let ERROR_CODES: any;
|
||||
let ERROR_CODES: Record<string, string> | undefined;
|
||||
|
||||
export default definePlugin({
|
||||
name: "ReactErrorDecoder",
|
||||
|
@ -28,13 +28,12 @@ export default definePlugin({
|
|||
authors: [Devs.Cyn, Devs.maisymoe],
|
||||
patches: [
|
||||
{
|
||||
find: '"https://reactjs.org/docs/error-decoder.html?invariant="',
|
||||
find: "React has blocked a javascript: URL as a security precaution.",
|
||||
replacement: {
|
||||
match: /(function .\(.\)){(for\(var .="https:\/\/reactjs\.org\/docs\/error-decoder\.html\?invariant="\+.,.=1;.<arguments\.length;.\+\+\).\+="&args\[\]="\+encodeURIComponent\(arguments\[.\]\);return"Minified React error #"\+.\+"; visit "\+.\+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings.")}/,
|
||||
replace: (_, func, original) =>
|
||||
`${func}{var decoded=$self.decodeError.apply(null, arguments);if(decoded)return decoded;${original}}`,
|
||||
},
|
||||
},
|
||||
match: /"https:\/\/react.dev\/errors\/"\+\i;/,
|
||||
replace: "$&const vcDecodedError=$self.decodeError(...arguments);if(vcDecodedError)return vcDecodedError;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
async start() {
|
||||
|
@ -56,5 +55,5 @@ export default definePlugin({
|
|||
index++;
|
||||
return arg;
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -77,23 +77,23 @@ export default definePlugin({
|
|||
|
||||
patches: [
|
||||
{
|
||||
find: ".BITE_SIZE,user:",
|
||||
find: ".POPOUT,user:",
|
||||
replacement: {
|
||||
match: /{profileType:\i\.\i\.BITE_SIZE,children:\[/,
|
||||
match: /children:\[(?=[^[]+?shouldShowTooltip:)/,
|
||||
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: ".FULL_SIZE,user:",
|
||||
find: ".MODAL,user:",
|
||||
replacement: {
|
||||
match: /{profileType:\i\.\i\.FULL_SIZE,children:\[/,
|
||||
match: /children:\[(?=[^[]+?shouldShowTooltip:)/,
|
||||
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'location:"UserProfilePanel"',
|
||||
find: ".SIDEBAR,shouldShowTooltip:",
|
||||
replacement: {
|
||||
match: /{profileType:\i\.\i\.PANEL,children:\[/,
|
||||
match: /children:\[(?=[^[]+?shouldShowTooltip:)/,
|
||||
replace: "$&$self.BiteSizeReviewsButton({user:arguments[0].user}),"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,8 +84,8 @@ export default definePlugin({
|
|||
find: ".USER_MENTION)",
|
||||
replacement: [
|
||||
{
|
||||
match: /(?<=onContextMenu:\i,color:)\i(?=,onClick)(?<=user:(\i),channel:(\i).+?)/,
|
||||
replace: "$self.getColorInt($1?.id,$2?.id)",
|
||||
match: /(?<=onContextMenu:\i,color:)\i(?<=\.getNickname\((\i),\i,(\i).+?)/,
|
||||
replace: "$self.getColorInt($2?.id,$1)",
|
||||
}
|
||||
],
|
||||
predicate: () => settings.store.chatMentions
|
||||
|
|
|
@ -16,7 +16,7 @@ import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, GuildStore,
|
|||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||
const FriendRow = findComponentByCodeLazy(".listName,discriminatorClass");
|
||||
const FriendRow = findComponentByCodeLazy("discriminatorClass:", ".isMobileOnline", "getAvatarURL");
|
||||
|
||||
const cl = classNameFactory("vc-gp-");
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
border-bottom: 2px solid transparent;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
height: 39px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Clipboard } from "@webpack/common";
|
||||
import { JSX } from "react";
|
||||
|
||||
import { cl } from "../utils/misc";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
|
@ -28,20 +25,14 @@ export interface ButtonRowProps {
|
|||
}
|
||||
|
||||
export function ButtonRow({ content, theme }: ButtonRowProps) {
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
if (Clipboard.SUPPORTS_COPY) {
|
||||
buttons.push(
|
||||
<CopyButton
|
||||
content={content}
|
||||
className={cl("btn")}
|
||||
style={{
|
||||
backgroundColor: theme.accentBgColor,
|
||||
color: theme.accentFgColor,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cl("btns")}>{buttons}</div>;
|
||||
return <div className={cl("btns")}>
|
||||
<CopyButton
|
||||
content={content}
|
||||
className={cl("btn")}
|
||||
style={{
|
||||
backgroundColor: theme.accentBgColor,
|
||||
color: theme.accentFgColor,
|
||||
}}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -16,13 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Clipboard, React } from "@webpack/common";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { React } from "@webpack/common";
|
||||
|
||||
export function useCopyCooldown(cooldown: number) {
|
||||
const [copyCooldown, setCopyCooldown] = React.useState(false);
|
||||
|
||||
function copy(text: string) {
|
||||
Clipboard.copy(text);
|
||||
copyToClipboard(text);
|
||||
setCopyCooldown(true);
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import React from "react";
|
||||
|
||||
const handleClick = async () =>
|
||||
console.log((await import("@webpack/common")).Clipboard.copy("\u200b"));
|
||||
console.log((await import("@utils/clipboard")).copyToClipboard("\u200b"));
|
||||
|
||||
export const Example: React.FC<{
|
||||
real: boolean,
|
||||
|
|
|
@ -325,7 +325,7 @@ export default definePlugin({
|
|||
]
|
||||
},
|
||||
{
|
||||
find: '})},"overflow"))',
|
||||
find: '="interactive-normal",overflowCountClassName:',
|
||||
replacement: [
|
||||
{
|
||||
// Create a variable for the channel prop
|
||||
|
@ -334,18 +334,21 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
// Make Discord always render the plus button if the component is used inside the HiddenChannelLockScreen
|
||||
match: /\i>0(?=&&.{0,60}renderPopout)/,
|
||||
match: /\i>0(?=&&.{0,30}Math.min)/,
|
||||
replace: m => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)?true:${m})`
|
||||
},
|
||||
{
|
||||
// Prevent Discord from overwriting the last children with the plus button if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||
match: /(?<=\.value\(\),(\i)=.+?length-)1(?=\]=.{0,60}renderPopout)/,
|
||||
// Prevent Discord from overwriting the last children with the plus button
|
||||
// if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||
match: /(?<=\i\.length-)1(?=\]=.{0,60}renderPopout)(?<=(\i)=\i\.length-\i.+?)/,
|
||||
replace: (_, amount) => `($self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?0:1)`
|
||||
},
|
||||
{
|
||||
// Show only the plus text without overflowed children amount if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||
match: /(?<="\+",)(\i)\+1/,
|
||||
replace: (m, amount) => `$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&${amount}<=0?"":${m}`
|
||||
// Show only the plus text without overflowed children amount
|
||||
// if the overflow amount is <= 0 and the component is used inside the HiddenChannelLockScreen
|
||||
match: /(?<="\+"\.concat\()\i/,
|
||||
replace: overflowTextAmount => "" +
|
||||
`$self.isHiddenChannel(typeof shcChannel!=="undefined"?shcChannel:void 0,true)&&(${overflowTextAmount}-1)<=0?"":${overflowTextAmount}`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -48,9 +48,10 @@ export default definePlugin({
|
|||
authors: [Devs.Rini, Devs.TheKodeToad],
|
||||
patches: [
|
||||
{
|
||||
find: '?"@":""',
|
||||
find: '"BaseUsername"',
|
||||
replacement: {
|
||||
match: /(?<=onContextMenu:\i,children:)\i\+\i/,
|
||||
/* TODO: remove \i+\i once change makes it to stable */
|
||||
match: /(?<=onContextMenu:\i,children:)(?:\i\+\i|\i)/,
|
||||
replace: "$self.renderUsername(arguments[0])"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -28,6 +28,7 @@ import { openImageModal } from "@utils/discord";
|
|||
import { classes, copyWithToast } from "@utils/misc";
|
||||
import { ContextMenuApi, FluxDispatcher, Forms, Menu, React, useEffect, useState, useStateFromStores } from "@webpack/common";
|
||||
|
||||
import { SeekBar } from "./SeekBar";
|
||||
import { SpotifyStore, Track } from "./SpotifyStore";
|
||||
|
||||
const cl = classNameFactory("vc-spotify-");
|
||||
|
@ -160,7 +161,7 @@ const seek = debounce((v: number) => {
|
|||
SpotifyStore.seek(v);
|
||||
});
|
||||
|
||||
function SeekBar() {
|
||||
function SpotifySeekBar() {
|
||||
const { duration } = SpotifyStore.track!;
|
||||
|
||||
const [storePosition, isSettingPosition, isPlaying] = useStateFromStores(
|
||||
|
@ -181,6 +182,12 @@ function SeekBar() {
|
|||
}
|
||||
}, [storePosition, isSettingPosition, isPlaying]);
|
||||
|
||||
const onChange = (v: number) => {
|
||||
if (isSettingPosition) return;
|
||||
setPosition(v);
|
||||
seek(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={cl("progress-bar")}>
|
||||
<Forms.FormText
|
||||
|
@ -190,16 +197,13 @@ function SeekBar() {
|
|||
>
|
||||
{msToHuman(position)}
|
||||
</Forms.FormText>
|
||||
<Menu.MenuSliderControl
|
||||
<SeekBar
|
||||
initialValue={position}
|
||||
minValue={0}
|
||||
maxValue={duration}
|
||||
value={position}
|
||||
onChange={(v: number) => {
|
||||
if (isSettingPosition) return;
|
||||
setPosition(v);
|
||||
seek(v);
|
||||
}}
|
||||
renderValue={msToHuman}
|
||||
onValueChange={onChange}
|
||||
asValueChanges={onChange}
|
||||
onValueRender={msToHuman}
|
||||
/>
|
||||
<Forms.FormText
|
||||
variant="text-xs/medium"
|
||||
|
@ -382,7 +386,7 @@ export function Player() {
|
|||
return (
|
||||
<div id={cl("player")} style={exportTrackImageStyle}>
|
||||
<Info track={track} />
|
||||
<SeekBar />
|
||||
<SpotifySeekBar />
|
||||
<Controls />
|
||||
</div>
|
||||
);
|
||||
|
|
25
src/plugins/spotifyControls/SeekBar.ts
Normal file
25
src/plugins/spotifyControls/SeekBar.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { LazyComponent } from "@utils/lazyReact";
|
||||
import { Slider } from "@webpack/common";
|
||||
|
||||
export const SeekBar = LazyComponent(() => {
|
||||
const SliderClass = Slider.$$vencordGetWrappedComponent();
|
||||
|
||||
// Discord's Slider does not update `state.value` when `props.initialValue` changes if state.value is not nullish.
|
||||
// We extend their class and override their `getDerivedStateFromProps` to update the value
|
||||
return class SeekBar extends SliderClass {
|
||||
static getDerivedStateFromProps(props: any, state: any) {
|
||||
const newState = super.getDerivedStateFromProps!(props, state);
|
||||
if (newState) {
|
||||
newState.value = props.initialValue;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
};
|
||||
});
|
|
@ -55,7 +55,7 @@ export default definePlugin({
|
|||
settings,
|
||||
|
||||
patches: [
|
||||
// User Popout, Full Size Profile, Direct Messages Side Profile
|
||||
// User Popout, User Profile Modal, Direct Messages Side Profile
|
||||
{
|
||||
find: "#{intl::USER_PROFILE_LOAD_ERROR}",
|
||||
replacement: {
|
||||
|
|
|
@ -190,7 +190,7 @@ export default definePlugin({
|
|||
},
|
||||
|
||||
patches: [
|
||||
// Avatar component used in User DMs "User Profile" popup in the right and Profiles Modal pfp
|
||||
// Avatar component used in User DMs "User Profile" popup in the right and User Profile Modal pfp
|
||||
{
|
||||
find: ".overlay:void 0,status:",
|
||||
replacement: [
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { copyToClipboard } from "@utils/clipboard";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { saveFile } from "@utils/web";
|
||||
import { filters, mapMangledModuleLazy } from "@webpack";
|
||||
import { Clipboard, ComponentDispatch } from "@webpack/common";
|
||||
import { ComponentDispatch } from "@webpack/common";
|
||||
|
||||
const ctxMenuCallbacks = mapMangledModuleLazy('.tagName)==="TEXTAREA"||', {
|
||||
contextMenuCallbackWeb: filters.byCode('.tagName)==="INPUT"||'),
|
||||
|
@ -114,11 +115,18 @@ export default definePlugin({
|
|||
// Fix silly Discord calling the non web support copy
|
||||
{
|
||||
match: /\i\.\i\.copy/,
|
||||
replace: "Vencord.Webpack.Common.Clipboard.copy"
|
||||
replace: "Vencord.Util.copyToClipboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
find: "Copy image not supported",
|
||||
replacement: {
|
||||
match: /(?<=(?:canSaveImage|canCopyImage)\(\i?\)\{.{0,50})!\i\.isPlatformEmbedded/g,
|
||||
replace: "false"
|
||||
}
|
||||
},
|
||||
// Add back Copy & Save Image
|
||||
{
|
||||
find: 'id:"copy-image"',
|
||||
|
@ -129,7 +137,7 @@ export default definePlugin({
|
|||
replace: "false"
|
||||
},
|
||||
{
|
||||
match: /return\s*?\[\i\.\i\.canCopyImage\(\)/,
|
||||
match: /return\s*?\[.{0,50}?(?=\?.{0,100}?id:"copy-image")/,
|
||||
replace: "return [true"
|
||||
},
|
||||
{
|
||||
|
@ -223,7 +231,7 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
match: /\i\.\i\.copy(?=\(\i)/,
|
||||
replace: "Vencord.Webpack.Common.Clipboard.copy"
|
||||
replace: "Vencord.Util.copyToClipboard"
|
||||
}
|
||||
],
|
||||
all: true,
|
||||
|
@ -288,7 +296,7 @@ export default definePlugin({
|
|||
const selection = document.getSelection();
|
||||
if (!selection) return;
|
||||
|
||||
Clipboard.copy(selection.toString());
|
||||
copyToClipboard(selection.toString());
|
||||
},
|
||||
|
||||
cut() {
|
||||
|
|
9
src/utils/clipboard.ts
Normal file
9
src/utils/clipboard.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export function copyToClipboard(text: string): Promise<void> {
|
||||
return IS_DISCORD_DESKTOP ? DiscordNative.clipboard.copy(text) : navigator.clipboard.writeText(text);
|
||||
}
|
|
@ -19,6 +19,7 @@
|
|||
export * from "../shared/debounce";
|
||||
export * from "../shared/onceDefined";
|
||||
export * from "./ChangeList";
|
||||
export * from "./clipboard";
|
||||
export * from "./constants";
|
||||
export * from "./discord";
|
||||
export * from "./guards";
|
||||
|
|
|
@ -4,26 +4,28 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { ComponentType } from "react";
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { makeLazy } from "./lazy";
|
||||
|
||||
const NoopComponent = () => null;
|
||||
|
||||
export type LazyComponentWrapper<ComponentType> = ComponentType & { $$vencordGetWrappedComponent(): ComponentType; };
|
||||
|
||||
/**
|
||||
* A lazy component. The factory method is called on first render.
|
||||
* @param factory Function returning a Component
|
||||
* @param attempts How many times to try to get the component before giving up
|
||||
* @returns Result of factory function
|
||||
*/
|
||||
export function LazyComponent<T extends object = any>(factory: () => React.ComponentType<T>, attempts = 5) {
|
||||
export function LazyComponent<T extends object = any>(factory: () => ComponentType<T>, attempts = 5): LazyComponentWrapper<ComponentType<T>> {
|
||||
const get = makeLazy(factory, attempts);
|
||||
const LazyComponent = (props: T) => {
|
||||
const Component = get() ?? NoopComponent;
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
LazyComponent.$$vencordInternal = get;
|
||||
LazyComponent.$$vencordGetWrappedComponent = get;
|
||||
|
||||
return LazyComponent as ComponentType<T>;
|
||||
return LazyComponent;
|
||||
}
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Clipboard, Toasts } from "@webpack/common";
|
||||
import { Toasts } from "@webpack/common";
|
||||
|
||||
import { copyToClipboard } from "./clipboard";
|
||||
import { DevsById } from "./constants";
|
||||
|
||||
/**
|
||||
|
@ -35,12 +36,8 @@ export function sleep(ms: number): Promise<void> {
|
|||
return new Promise(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") {
|
||||
if (Clipboard.SUPPORTS_COPY) {
|
||||
Clipboard.copy(text);
|
||||
} else {
|
||||
toastMessage = "Your browser does not support copying to clipboard";
|
||||
}
|
||||
export async function copyWithToast(text: string, toastMessage = "Copied to clipboard!") {
|
||||
await copyToClipboard(text);
|
||||
Toasts.show({
|
||||
message: toastMessage,
|
||||
id: Toasts.genId(),
|
||||
|
|
|
@ -140,7 +140,7 @@ export type MediaModalProps = {
|
|||
shouldHideMediaOptions?: boolean;
|
||||
};
|
||||
|
||||
// modal key: "Media Viewer Modal"
|
||||
// Modal key: "Media Viewer Modal"
|
||||
export const openMediaModal: (props: MediaModalProps) => void = findByCodeLazy("hasMediaOptions", "shouldHideMediaOptions");
|
||||
|
||||
interface ModalAPI {
|
||||
|
|
|
@ -39,10 +39,15 @@ async function Unwrap<T>(p: Promise<IpcRes<T>>) {
|
|||
|
||||
export async function checkForUpdates() {
|
||||
changes = await Unwrap(VencordNative.updater.getUpdates());
|
||||
if (changes.some(c => c.hash === gitHash)) {
|
||||
isNewer = true;
|
||||
return (isOutdated = false);
|
||||
|
||||
// we only want to check this for the git updater, not the http updater
|
||||
if (!IS_STANDALONE) {
|
||||
if (changes.some(c => c.hash === gitHash)) {
|
||||
isNewer = true;
|
||||
return (isOutdated = false);
|
||||
}
|
||||
}
|
||||
|
||||
return (isOutdated = changes.length > 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -16,19 +16,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import { LazyComponent, LazyComponentWrapper } from "@utils/react";
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
|
||||
|
||||
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]): T {
|
||||
export function waitForComponent<T extends React.ComponentType<any> = React.ComponentType<any> & Record<string, any>>(name: string, filter: FilterFn | string | string[]) {
|
||||
if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
|
||||
|
||||
let myValue: T = function () {
|
||||
throw new Error(`Vencord could not find the ${name} Component`);
|
||||
} as any;
|
||||
|
||||
const lazyComponent = LazyComponent(() => myValue) as T;
|
||||
const lazyComponent = LazyComponent(() => myValue) as LazyComponentWrapper<T>;
|
||||
waitFor(filter, (v: any) => {
|
||||
myValue = v;
|
||||
Object.assign(lazyComponent, v);
|
||||
|
|
|
@ -24,12 +24,19 @@ export const Menu = {} as t.Menu;
|
|||
|
||||
// Relies on .name properties added by the MenuItemDemanglerAPI
|
||||
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
|
||||
// we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
|
||||
const module = wreq(id);
|
||||
// We have to do this manual require by ID because m in this case is the MenuCheckBoxItem instead of the entire module
|
||||
const exports = wreq(id);
|
||||
|
||||
for (const e of Object.values(module)) {
|
||||
if (typeof e === "function" && e.name.startsWith("Menu")) {
|
||||
Menu[e.name] = e;
|
||||
for (const exportKey in exports) {
|
||||
// Some exports might have not been initialized yet due to circular imports, so try catch it.
|
||||
try {
|
||||
var exportValue = exports[exportKey];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof exportValue === "function" && exportValue.name.startsWith("Menu")) {
|
||||
Menu[exportValue.name] = exportValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line path-alias/no-relative
|
||||
import { findByPropsLazy, waitFor } from "../webpack";
|
||||
import { findByCodeLazy, findByPropsLazy, waitFor } from "../webpack";
|
||||
|
||||
export let React: typeof import("react");
|
||||
export let useState: typeof React.useState;
|
||||
|
@ -28,7 +28,9 @@ export let useRef: typeof React.useRef;
|
|||
export let useReducer: typeof React.useReducer;
|
||||
export let useCallback: typeof React.useCallback;
|
||||
|
||||
export const ReactDOM: typeof import("react-dom") & typeof import("react-dom/client") = findByPropsLazy("createPortal", "render");
|
||||
export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPortal");
|
||||
// 299 is an error code used in createRoot and createPortal
|
||||
export const createRoot: typeof import("react-dom/client").createRoot = findByCodeLazy("(299));", ".onRecoverableError");
|
||||
|
||||
waitFor("useState", m => {
|
||||
React = m;
|
||||
|
|
4
src/webpack/common/types/components.d.ts
vendored
4
src/webpack/common/types/components.d.ts
vendored
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
|
||||
import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref } from "react";
|
||||
|
||||
|
||||
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
|
||||
|
@ -356,7 +356,7 @@ export type SearchableSelect = ComponentType<PropsWithChildren<{
|
|||
"aria-labelledby"?: boolean;
|
||||
}>>;
|
||||
|
||||
export type Slider = ComponentType<PropsWithChildren<{
|
||||
export type Slider = ComponentClass<PropsWithChildren<{
|
||||
initialValue: number;
|
||||
defaultValue?: number;
|
||||
keyboardStep?: number;
|
||||
|
|
|
@ -149,11 +149,6 @@ export const ApplicationAssetUtils = mapMangledModuleLazy("getAssetImage: size m
|
|||
getAssets: filters.byCode(".assets")
|
||||
});
|
||||
|
||||
export const Clipboard: t.Clipboard = mapMangledModuleLazy('queryCommandEnabled("copy")', {
|
||||
copy: filters.byCode(".copy("),
|
||||
SUPPORTS_COPY: e => typeof e === "boolean"
|
||||
});
|
||||
|
||||
export const NavigationRouter: t.NavigationRouter = mapMangledModuleLazy("Transitioning to ", {
|
||||
transitionTo: filters.byCode("transitionTo -"),
|
||||
transitionToGuild: filters.byCode("transitionToGuild -"),
|
||||
|
|
|
@ -442,7 +442,12 @@ function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unkno
|
|||
}
|
||||
|
||||
for (const exportKey in exports) {
|
||||
const exportValue = exports[exportKey];
|
||||
// Some exports might have not been initialized yet due to circular imports, so try catch it.
|
||||
try {
|
||||
var exportValue = exports[exportKey];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (exportValue != null && filter(exportValue)) {
|
||||
waitForSubscriptions.delete(filter);
|
||||
|
|
|
@ -156,7 +156,14 @@ export function _blacklistBadModules(requireCache: NonNullable<AnyWebpackRequire
|
|||
|
||||
let hasOnlyBadProperties = true;
|
||||
for (const exportKey in exports) {
|
||||
if (shouldIgnoreValue(exports[exportKey])) {
|
||||
// Some exports might have not been initialized yet due to circular imports, so try catch it.
|
||||
try {
|
||||
var exportValue = exports[exportKey];
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldIgnoreValue(exportValue)) {
|
||||
makePropertyNonEnumerable(exports, exportKey);
|
||||
} else {
|
||||
hasOnlyBadProperties = false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue