Compare commits
31 Commits
v1.4.5
...
serverProf
Author | SHA1 | Date | |
---|---|---|---|
|
95b12f109a | ||
|
f40ab912eb | ||
|
700301ffbb | ||
|
d92894697b | ||
|
47569d2ffa | ||
|
d3b18bbcd2 | ||
|
eeedc531a3 | ||
|
59a2b834a6 | ||
|
1cfeaf77c1 | ||
|
1757d17661 | ||
|
9485d2457a | ||
|
3fd2fc1d61 | ||
|
49bc6b8fd6 | ||
|
c165725297 | ||
|
29fbe3701a | ||
|
0b7c0e9587 | ||
|
d88524e8cf | ||
|
d6efd99849 | ||
|
fe6be987fd | ||
|
d688075c0a | ||
|
c752be45b2 | ||
|
07c1f5eed1 | ||
|
4df01b1e62 | ||
|
ebe10d3fad | ||
|
eca4af829f | ||
|
60458cdf1f | ||
|
714d87241c | ||
|
f628aa7a70 | ||
|
0f0282551d | ||
|
0335e1ca59 | ||
|
817f0f7473 |
@ -4,8 +4,7 @@
|
||||
|
||||
The cutest Discord client mod
|
||||
|
||||
![](https://user-images.githubusercontent.com/45497981/235015332-0453d3eb-1da6-4601-963e-ef5e454123a1.png)
|
||||
*A screenshot of Vencord featuring the [ClearVision-v6 theme](https://github.com/ClearVision/ClearVision-v6) (Vencord does not come with it pre-installed, it is only an example)*
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -19,13 +19,14 @@
|
||||
|
||||
import esbuild from "esbuild";
|
||||
|
||||
import { commonOpts, globPlugins, isStandalone, VERSION, watch } from "./common.mjs";
|
||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
|
||||
|
||||
const defines = {
|
||||
IS_STANDALONE: isStandalone,
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_UPDATER_DISABLED: updaterDisabled,
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP: Date.now(),
|
||||
BUILD_TIMESTAMP,
|
||||
};
|
||||
if (defines.IS_STANDALONE === "false")
|
||||
// If this is a local build (not standalone), optimise
|
||||
|
@ -24,7 +24,7 @@ import { readFileSync } from "fs";
|
||||
import { appendFile, mkdir, readFile, rm, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
import { commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
||||
import { BUILD_TIMESTAMP, commonOpts, globPlugins, VERSION, watch } from "./common.mjs";
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
@ -46,8 +46,9 @@ const commonOptions = {
|
||||
IS_DEV: JSON.stringify(watch),
|
||||
IS_DISCORD_DESKTOP: "false",
|
||||
IS_VESKTOP: "false",
|
||||
IS_UPDATER_DISABLED: "true",
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
BUILD_TIMESTAMP: Date.now(),
|
||||
BUILD_TIMESTAMP,
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -30,15 +30,18 @@ import PackageJSON from "../../package.json" assert { type: "json" };
|
||||
import { getPluginTarget } from "../utils.mjs";
|
||||
|
||||
export const VERSION = PackageJSON.version;
|
||||
export const BUILD_TIMESTAMP = Date.now();
|
||||
// https://reproducible-builds.org/docs/source-date-epoch/
|
||||
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
|
||||
export const watch = process.argv.includes("--watch");
|
||||
export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
|
||||
export const gitHash = execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||
export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
|
||||
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
||||
export const banner = {
|
||||
js: `
|
||||
// Vencord ${gitHash}
|
||||
// Standalone: ${isStandalone}
|
||||
// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
|
||||
// Updater disabled: ${updaterDisabled}
|
||||
`.trim()
|
||||
};
|
||||
|
||||
@ -133,11 +136,14 @@ export const gitRemotePlugin = {
|
||||
namespace: "git-remote", path: args.path
|
||||
}));
|
||||
build.onLoad({ filter, namespace: "git-remote" }, async () => {
|
||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||
const remote = res.stdout.trim()
|
||||
.replace("https://github.com/", "")
|
||||
.replace("git@github.com:", "")
|
||||
.replace(/.git$/, "");
|
||||
let remote = process.env.VENCORD_REMOTE;
|
||||
if (!remote) {
|
||||
const res = await promisify(exec)("git remote get-url origin", { encoding: "utf-8" });
|
||||
remote = res.stdout.trim()
|
||||
.replace("https://github.com/", "")
|
||||
.replace("git@github.com:", "")
|
||||
.replace(/.git$/, "");
|
||||
}
|
||||
|
||||
return { contents: `export default "${remote}"` };
|
||||
});
|
||||
|
@ -161,7 +161,7 @@ async function parseFile(fileName: string) {
|
||||
|
||||
const target = getPluginTarget(fileName);
|
||||
if (target) {
|
||||
if (!["web", "discordDesktop", "vencordDesktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
|
||||
if (!["web", "discordDesktop", "vencordDesktop", "desktop", "dev"].includes(target)) throw fail(`invalid target ${target}`);
|
||||
data.target = target as any;
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
*/
|
||||
export function getPluginTarget(filePath) {
|
||||
const pathParts = filePath.split(/[/\\]/);
|
||||
if (/^index\.tsx?$/.test(filePath.at(-1))) pathParts.pop();
|
||||
if (/^index\.tsx?$/.test(pathParts.at(-1))) pathParts.pop();
|
||||
|
||||
const identifier = pathParts.at(-1).replace(/\.tsx?$/, "");
|
||||
const identiferBits = identifier.split(".");
|
||||
|
@ -48,7 +48,7 @@ export default {
|
||||
},
|
||||
|
||||
addThemeChangeListener(cb: () => void) {
|
||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, cb);
|
||||
ipcRenderer.on(IpcEvents.THEME_UPDATE, () => cb());
|
||||
},
|
||||
|
||||
openFile: () => invoke<void>(IpcEvents.OPEN_QUICKCSS),
|
||||
|
@ -33,11 +33,10 @@ export function SettingTextComponent({ option, pluginSettings, definedSettings,
|
||||
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
|
||||
if (typeof isValid === "string") setError(isValid);
|
||||
else if (!isValid) setError("Invalid input provided.");
|
||||
else {
|
||||
setError(null);
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
else setError(null);
|
||||
|
||||
setState(newValue);
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -231,7 +231,7 @@ function ThemesTab() {
|
||||
ref={fileInputRef}
|
||||
onChange={onFileUpload}
|
||||
multiple={true}
|
||||
filters={[{ extensions: ["*.css"] }]}
|
||||
filters={[{ extensions: ["css"] }]}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
|
@ -249,4 +249,4 @@ function Updater() {
|
||||
);
|
||||
}
|
||||
|
||||
export default IS_WEB ? null : wrapTab(Updater, "Updater");
|
||||
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
|
||||
|
1
src/globals.d.ts
vendored
1
src/globals.d.ts
vendored
@ -35,6 +35,7 @@ declare global {
|
||||
export var IS_WEB: boolean;
|
||||
export var IS_DEV: boolean;
|
||||
export var IS_STANDALONE: boolean;
|
||||
export var IS_UPDATER_DISABLED: boolean;
|
||||
export var IS_DISCORD_DESKTOP: boolean;
|
||||
export var IS_VESKTOP: boolean;
|
||||
export var VERSION: string;
|
||||
|
@ -22,6 +22,28 @@ import { readFile } from "fs/promises";
|
||||
import { request } from "https";
|
||||
import { basename, normalize } from "path";
|
||||
|
||||
import { getSettings } from "./ipcMain";
|
||||
|
||||
// FixSpotifyEmbeds
|
||||
app.on("browser-window-created", (_, win) => {
|
||||
win.webContents.on("frame-created", (_, { frame }) => {
|
||||
frame.once("dom-ready", () => {
|
||||
if (frame.url.startsWith("https://open.spotify.com/embed/")) {
|
||||
const settings = getSettings().plugins?.FixSpotifyEmbeds;
|
||||
if (!settings?.enabled) return;
|
||||
|
||||
frame.executeJavaScript(`
|
||||
const original = Audio.prototype.play;
|
||||
Audio.prototype.play = function() {
|
||||
this.volume = ${(settings.volume / 100) || 0.1};
|
||||
return original.apply(this, arguments);
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// #region OpenInApp
|
||||
// These links don't support CORS, so this has to be native
|
||||
const validRedirectUrls = /^https:\/\/(spotify\.link|s\.team)\/.+$/;
|
||||
|
@ -20,18 +20,6 @@ import { app } from "electron";
|
||||
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs";
|
||||
import { basename, dirname, join } from "path";
|
||||
|
||||
const { setAppUserModelId } = app;
|
||||
|
||||
// Apparently requiring Discords updater too early leads into issues,
|
||||
// copied this workaround from powerCord
|
||||
app.setAppUserModelId = function (id: string) {
|
||||
app.setAppUserModelId = setAppUserModelId;
|
||||
|
||||
setAppUserModelId.call(this, id);
|
||||
|
||||
patchUpdater();
|
||||
};
|
||||
|
||||
function isNewer($new: string, old: string) {
|
||||
const newParts = $new.slice(4).split(".").map(Number);
|
||||
const oldParts = old.slice(4).split(".").map(Number);
|
||||
@ -77,25 +65,6 @@ function patchLatest() {
|
||||
}
|
||||
}
|
||||
|
||||
// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we
|
||||
// need to reinject
|
||||
function patchUpdater() {
|
||||
// Array of autoStart paths to try
|
||||
const autoStartPaths = [
|
||||
join(require.main!.filename, "..", "autoStart", "win32.js"), // Vanilla
|
||||
join(require.main!.filename, "..", "autoStart.js") // OpenAsar
|
||||
];
|
||||
|
||||
for (const path of autoStartPaths) {
|
||||
try {
|
||||
const { update } = require(path);
|
||||
|
||||
require.cache[path]!.exports.update = function () {
|
||||
update.apply(this, arguments);
|
||||
patchLatest();
|
||||
};
|
||||
} catch {
|
||||
// Ignore as non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try to patch latest on before-quit
|
||||
// Discord's Win32 updater will call app.quit() on restart and open new version on will-quit
|
||||
app.on("before-quit", patchLatest);
|
||||
|
@ -16,4 +16,5 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import(IS_STANDALONE ? "./http" : "./git");
|
||||
if (!IS_UPDATER_DISABLED)
|
||||
import(IS_STANDALONE ? "./http" : "./git");
|
||||
|
@ -103,7 +103,7 @@ export default definePlugin({
|
||||
element: require("@components/VencordSettings/ThemesTab").default,
|
||||
className: "vc-themes"
|
||||
},
|
||||
!IS_WEB && {
|
||||
!IS_UPDATER_DISABLED && {
|
||||
section: "VencordUpdater",
|
||||
label: "Updater",
|
||||
element: require("@components/VencordSettings/UpdaterTab").default,
|
||||
|
@ -127,6 +127,7 @@ export const defaultRules = [
|
||||
"redircnt@yandex.*",
|
||||
"feature@youtube.com",
|
||||
"kw@youtube.com",
|
||||
"si@youtu.be",
|
||||
"wt_zmc",
|
||||
"utm_source",
|
||||
"utm_content",
|
||||
|
55
src/plugins/copyUserURLs.tsx
Normal file
55
src/plugins/copyUserURLs.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Vencord, a modification for Discord's desktop app
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { LinkIcon } from "@components/Icons";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Clipboard, Menu } from "@webpack/common";
|
||||
import type { Channel, User } from "discord-types/general";
|
||||
|
||||
interface UserContextProps {
|
||||
channel: Channel;
|
||||
guildId?: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
const UserContextMenuPatch: NavContextMenuPatchCallback = (children, { user }: UserContextProps) => () => {
|
||||
children.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-copy-user-url"
|
||||
label="Copy User URL"
|
||||
action={() => Clipboard.copy(`<https://discord.com/users/${user.id}>`)}
|
||||
icon={LinkIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "CopyUserURLs",
|
||||
authors: [Devs.castdrian],
|
||||
description: "Adds a 'Copy User URL' option to the user context menu.",
|
||||
|
||||
start() {
|
||||
addContextMenuPatch("user-context", UserContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("user-context", UserContextMenuPatch);
|
||||
},
|
||||
});
|
@ -23,10 +23,12 @@ import { Logger } from "@utils/Logger";
|
||||
import { closeAllModals } from "@utils/modal";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { maybePromptToUpdate } from "@utils/updater";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, NavigationRouter } from "@webpack/common";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
const CrashHandlerLogger = new Logger("CrashHandler");
|
||||
const ModalStack = findByPropsLazy("pushLazy", "popAll");
|
||||
|
||||
const settings = definePluginSettings({
|
||||
attemptToPreventCrashes: {
|
||||
@ -51,8 +53,6 @@ export default definePlugin({
|
||||
authors: [Devs.Nuckyz],
|
||||
enabledByDefault: true,
|
||||
|
||||
popAllModals: undefined as (() => void) | undefined,
|
||||
|
||||
settings,
|
||||
|
||||
patches: [
|
||||
@ -62,13 +62,6 @@ export default definePlugin({
|
||||
match: /(?=this\.setState\()/,
|
||||
replace: "$self.handleCrash(this)||"
|
||||
}
|
||||
},
|
||||
{
|
||||
find: 'dispatch({type:"MODAL_POP_ALL"})',
|
||||
replacement: {
|
||||
match: /"MODAL_POP_ALL".+?};(?<=(\i)=function.+?)/,
|
||||
replace: (m, popAll) => `${m}$self.popAllModals=${popAll};`
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@ -128,7 +121,7 @@ export default definePlugin({
|
||||
CrashHandlerLogger.debug("Failed to close open context menu.", err);
|
||||
}
|
||||
try {
|
||||
this.popAllModals?.();
|
||||
ModalStack?.popAll();
|
||||
} catch (err) {
|
||||
CrashHandlerLogger.debug("Failed to close old modals.", err);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { definePluginSettings, Settings } from "@api/Settings";
|
||||
import { Link } from "@components/Link";
|
||||
import { Devs } from "@utils/constants";
|
||||
import { isTruthy } from "@utils/guards";
|
||||
@ -86,8 +86,7 @@ const settings = definePluginSettings({
|
||||
appID: {
|
||||
type: OptionType.STRING,
|
||||
description: "Application ID (required)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (!value) return "Application ID is required.";
|
||||
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
|
||||
@ -97,8 +96,7 @@ const settings = definePluginSettings({
|
||||
appName: {
|
||||
type: OptionType.STRING,
|
||||
description: "Application name (required)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (!value) return "Application name is required.";
|
||||
if (value.length > 128) return "Application name must be not longer than 128 characters.";
|
||||
@ -108,8 +106,7 @@ const settings = definePluginSettings({
|
||||
details: {
|
||||
type: OptionType.STRING,
|
||||
description: "Details (line 1)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -118,8 +115,7 @@ const settings = definePluginSettings({
|
||||
state: {
|
||||
type: OptionType.STRING,
|
||||
description: "State (line 2)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -128,8 +124,7 @@ const settings = definePluginSettings({
|
||||
type: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Activity type",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
options: [
|
||||
{
|
||||
label: "Playing",
|
||||
@ -157,16 +152,14 @@ const settings = definePluginSettings({
|
||||
streamLink: {
|
||||
type: OptionType.STRING,
|
||||
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
isDisabled: isStreamLinkDisabled,
|
||||
onChange: onChange,
|
||||
disabled: isStreamLinkDisabled,
|
||||
isValid: isStreamLinkValid
|
||||
},
|
||||
timestampMode: {
|
||||
type: OptionType.SELECT,
|
||||
description: "Timestamp mode",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
options: [
|
||||
{
|
||||
label: "None",
|
||||
@ -190,9 +183,8 @@ const settings = definePluginSettings({
|
||||
startTime: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Start timestamp (only for custom timestamp mode)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
isDisabled: isTimestampDisabled,
|
||||
onChange: onChange,
|
||||
disabled: isTimestampDisabled,
|
||||
isValid: (value: number) => {
|
||||
if (value && value < 0) return "Start timestamp must be greater than 0.";
|
||||
return true;
|
||||
@ -201,9 +193,8 @@ const settings = definePluginSettings({
|
||||
endTime: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "End timestamp (only for custom timestamp mode)",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
isDisabled: isTimestampDisabled,
|
||||
onChange: onChange,
|
||||
disabled: isTimestampDisabled,
|
||||
isValid: (value: number) => {
|
||||
if (value && value < 0) return "End timestamp must be greater than 0.";
|
||||
return true;
|
||||
@ -211,16 +202,14 @@ const settings = definePluginSettings({
|
||||
},
|
||||
imageBig: {
|
||||
type: OptionType.STRING,
|
||||
description: "Big image key",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
description: "Big image key/link",
|
||||
onChange: onChange,
|
||||
isValid: isImageKeyValid
|
||||
},
|
||||
imageBigTooltip: {
|
||||
type: OptionType.STRING,
|
||||
description: "Big image tooltip",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -228,16 +217,14 @@ const settings = definePluginSettings({
|
||||
},
|
||||
imageSmall: {
|
||||
type: OptionType.STRING,
|
||||
description: "Small image key",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
description: "Small image key/link",
|
||||
onChange: onChange,
|
||||
isValid: isImageKeyValid
|
||||
},
|
||||
imageSmallTooltip: {
|
||||
type: OptionType.STRING,
|
||||
description: "Small image tooltip",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
|
||||
return true;
|
||||
@ -246,8 +233,7 @@ const settings = definePluginSettings({
|
||||
buttonOneText: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 1 text",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
|
||||
return true;
|
||||
@ -256,14 +242,12 @@ const settings = definePluginSettings({
|
||||
buttonOneURL: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 1 URL",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc
|
||||
onChange: onChange
|
||||
},
|
||||
buttonTwoText: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 2 text",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc,
|
||||
onChange: onChange,
|
||||
isValid: (value: string) => {
|
||||
if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
|
||||
return true;
|
||||
@ -272,26 +256,29 @@ const settings = definePluginSettings({
|
||||
buttonTwoURL: {
|
||||
type: OptionType.STRING,
|
||||
description: "Button 2 URL",
|
||||
restartNeeded: true,
|
||||
onChange: setRpc
|
||||
onChange: onChange
|
||||
}
|
||||
});
|
||||
|
||||
function isStreamLinkDisabled(): boolean {
|
||||
function onChange() {
|
||||
setRpc(true);
|
||||
if (Settings.plugins.CustomRPC.enabled) setRpc();
|
||||
}
|
||||
|
||||
function isStreamLinkDisabled() {
|
||||
return settings.store.type !== ActivityType.STREAMING;
|
||||
}
|
||||
|
||||
function isStreamLinkValid(): boolean | string {
|
||||
if (settings.store.type === ActivityType.STREAMING && settings.store.streamLink && !/(https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+)/.test(settings.store.streamLink)) return "Streaming link must be a valid URL.";
|
||||
function isStreamLinkValid(value: string) {
|
||||
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";
|
||||
return true;
|
||||
}
|
||||
|
||||
function isTimestampDisabled(): boolean {
|
||||
function isTimestampDisabled() {
|
||||
return settings.store.timestampMode !== TimestampMode.CUSTOM;
|
||||
}
|
||||
|
||||
function isImageKeyValid(value: string) {
|
||||
if (!/https?:\/\//.test(value)) return true;
|
||||
if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
|
||||
if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
|
||||
return true;
|
||||
@ -342,13 +329,10 @@ async function createActivity(): Promise<Activity | undefined> {
|
||||
};
|
||||
break;
|
||||
case TimestampMode.CUSTOM:
|
||||
if (startTime) {
|
||||
activity.timestamps = {
|
||||
start: startTime,
|
||||
};
|
||||
if (endTime) {
|
||||
activity.timestamps.end = endTime;
|
||||
}
|
||||
if (startTime || endTime) {
|
||||
activity.timestamps = {};
|
||||
if (startTime) activity.timestamps.start = startTime;
|
||||
if (endTime) activity.timestamps.end = endTime;
|
||||
}
|
||||
break;
|
||||
case TimestampMode.NONE:
|
||||
|
26
src/plugins/fixSpotifyEmbeds.desktop.ts
Normal file
26
src/plugins/fixSpotifyEmbeds.desktop.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { makeRange } from "@components/PluginSettings/components";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
|
||||
// The entire code of this plugin can be found in ipcPlugins
|
||||
export default definePlugin({
|
||||
name: "FixSpotifyEmbeds",
|
||||
description: "Fixes spotify embeds being incredibly loud by letting you customise the volume",
|
||||
authors: [Devs.Ven],
|
||||
settings: definePluginSettings({
|
||||
volume: {
|
||||
type: OptionType.SLIDER,
|
||||
description: "The volume % to set for spotify embeds. Anything above 10% is veeeery loud",
|
||||
markers: makeRange(0, 100, 10),
|
||||
stickToMarkers: false,
|
||||
default: 10
|
||||
}
|
||||
})
|
||||
});
|
@ -32,12 +32,18 @@ function makeIcon(showCurrentGame?: boolean) {
|
||||
return function () {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 96 960 960"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor" d="M182 856q-51 0-79-35.5T82 734l42-300q9-60 53.5-99T282 296h396q60 0 104.5 39t53.5 99l42 300q7 51-21 86.5T778 856q-21 0-39-7.5T706 826l-90-90H344l-90 90q-15 15-33 22.5t-39 7.5Zm498-240q17 0 28.5-11.5T720 576q0-17-11.5-28.5T680 536q-17 0-28.5 11.5T640 576q0 17 11.5 28.5T680 616Zm-80-120q17 0 28.5-11.5T640 456q0-17-11.5-28.5T600 416q-17 0-28.5 11.5T560 456q0 17 11.5 28.5T600 496ZM310 616h60v-70h70v-60h-70v-70h-60v70h-70v60h70v70Z" />
|
||||
{!showCurrentGame && <line x1="920" y1="280" x2="40" y2="880" stroke="var(--status-danger)" stroke-width="80" />}
|
||||
<path fill="currentColor" mask="url(#gameActivityMask)" d="M3.06 20.4q-1.53 0-2.37-1.065T.06 16.74l1.26-9q.27-1.8 1.605-2.97T6.06 3.6h11.88q1.8 0 3.135 1.17t1.605 2.97l1.26 9q.21 1.53-.63 2.595T20.94 20.4q-.63 0-1.17-.225T18.78 19.5l-2.7-2.7H7.92l-2.7 2.7q-.45.45-.99.675t-1.17.225Zm14.94-7.2q.51 0 .855-.345T19.2 12q0-.51-.345-.855T18 10.8q-.51 0-.855.345T16.8 12q0 .51.345 .855T18 13.2Zm-2.4-3.6q.51 0 .855-.345T16.8 8.4q0-.51-.345-.855T15.6 7.2q-.51 0-.855.345T14.4 8.4q0 .51.345 .855T15.6 9.6ZM6.9 13.2h1.8v-2.1h2.1v-1.8h-2.1v-2.1h-1.8v2.1h-2.1v1.8h2.1v2.1Z" />
|
||||
{!showCurrentGame && <>
|
||||
<mask id="gameActivityMask" >
|
||||
<rect fill="white" x="0" y="0" width="24" height="24" />
|
||||
<path fill="black" d="M23.27 4.54 19.46.73 .73 19.46 4.54 23.27 23.27 4.54Z"/>
|
||||
</mask>
|
||||
<path fill="var(--status-danger)" d="M23 2.27 21.73 1 1 21.73 2.27 23 23 2.27Z" />
|
||||
</>}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@ -60,7 +66,7 @@ function GameActivityToggleButton() {
|
||||
export default definePlugin({
|
||||
name: "GameActivityToggle",
|
||||
description: "Adds a button next to the mic and deafen button to toggle game activity.",
|
||||
authors: [Devs.Nuckyz],
|
||||
authors: [Devs.Nuckyz, Devs.RuukuLada],
|
||||
dependencies: ["SettingsStoreAPI"],
|
||||
|
||||
patches: [
|
||||
|
@ -18,6 +18,7 @@
|
||||
|
||||
import { addAccessory } from "@api/MessageAccessories";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { getSettingStoreLazy } from "@api/SettingsStore";
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Devs } from "@utils/constants.js";
|
||||
import { classes } from "@utils/misc";
|
||||
@ -318,9 +319,10 @@ function ChannelMessageEmbedAccessory({ message, channel, guildID }: MessageEmbe
|
||||
/>;
|
||||
}
|
||||
|
||||
const compactModeEnabled = getSettingStoreLazy<boolean>("textAndImages", "messageDisplayCompact")!;
|
||||
|
||||
function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||
const { message, channel, guildID } = props;
|
||||
|
||||
const isDM = guildID === "@me";
|
||||
const images = getImages(message);
|
||||
const { parse } = Parser;
|
||||
@ -336,7 +338,7 @@ function AutomodEmbedAccessory(props: MessageEmbedProps): JSX.Element | null {
|
||||
<span>{isDM ? " - Direct Message" : " - " + GuildStore.getGuild(channel.guild_id)?.name}</span>
|
||||
</Text>
|
||||
}
|
||||
compact={false}
|
||||
compact={compactModeEnabled.getSetting()}
|
||||
content={
|
||||
<>
|
||||
{message.content || message.attachments.length <= images.length
|
||||
@ -363,7 +365,7 @@ export default definePlugin({
|
||||
name: "MessageLinkEmbeds",
|
||||
description: "Adds a preview to messages that link another message",
|
||||
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
|
||||
dependencies: ["MessageAccessoriesAPI"],
|
||||
dependencies: ["MessageAccessoriesAPI", "SettingsStoreAPI"],
|
||||
patches: [
|
||||
{
|
||||
find: ".embedCard",
|
||||
|
@ -54,6 +54,8 @@ interface IVoiceChannelEffectSendEvent {
|
||||
const MOYAI = "🗿";
|
||||
const MOYAI_URL =
|
||||
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai.mp3";
|
||||
const MOYAI_URL_HD =
|
||||
"https://raw.githubusercontent.com/MeguminSama/VencordPlugins/main/plugins/moyai/moyai_hd.wav";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
volume: {
|
||||
@ -63,6 +65,14 @@ const settings = definePluginSettings({
|
||||
default: 0.5,
|
||||
stickToMarkers: false
|
||||
},
|
||||
quality: {
|
||||
description: "Quality of the 🗿🗿🗿",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Normal", value: "Normal", default: true },
|
||||
{ label: "HD", value: "HD" }
|
||||
],
|
||||
},
|
||||
triggerWhenUnfocused: {
|
||||
description: "Trigger the 🗿 even when the window is unfocused",
|
||||
type: OptionType.BOOLEAN,
|
||||
@ -157,7 +167,11 @@ function getMoyaiCount(message: string) {
|
||||
function boom() {
|
||||
if (!settings.store.triggerWhenUnfocused && !document.hasFocus()) return;
|
||||
const audioElement = document.createElement("audio");
|
||||
audioElement.src = MOYAI_URL;
|
||||
|
||||
audioElement.src = settings.store.quality === "HD"
|
||||
? MOYAI_URL_HD
|
||||
: MOYAI_URL;
|
||||
|
||||
audioElement.volume = settings.store.volume;
|
||||
audioElement.play();
|
||||
}
|
||||
|
@ -55,8 +55,8 @@ export default definePlugin({
|
||||
{
|
||||
find: '"MaskedLinkStore"',
|
||||
replacement: {
|
||||
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,200}\.trusted)/,
|
||||
replace: "return $self.handleLink(...arguments).then(handled => handled || $1)"
|
||||
match: /return ((\i)\.apply\(this,arguments\))(?=\}function \i.{0,250}\.trusted)/,
|
||||
replace: "return $self.handleLink(...arguments).then(handled => handled||$1)"
|
||||
}
|
||||
},
|
||||
// Make Spotify profile activity links open in app on web
|
||||
|
@ -178,12 +178,12 @@ export default definePlugin({
|
||||
start() {
|
||||
addContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||
addContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||
addContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
||||
addContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch("user-context", this.userContextMenuPatch);
|
||||
removeContextMenuPatch("channel-context", this.channelContextMenuPatch);
|
||||
removeContextMenuPatch("guild-context", this.guildContextMenuPatch);
|
||||
removeContextMenuPatch(["guild-context", "guild-header-popout"], this.guildContextMenuPatch);
|
||||
},
|
||||
});
|
||||
|
247
src/plugins/serverProfile/GuildProfileModal.tsx
Normal file
247
src/plugins/serverProfile/GuildProfileModal.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { classNameFactory } from "@api/Styles";
|
||||
import { openImageModal, openUserProfile } from "@utils/discord";
|
||||
import { classes } from "@utils/misc";
|
||||
import { ModalRoot, ModalSize, openModal } from "@utils/modal";
|
||||
import { LazyComponent, useAwaiter } from "@utils/react";
|
||||
import { findByCode, findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, Forms, GuildChannelStore, GuildMemberStore, moment, Parser, PresenceStore, RelationshipStore, ScrollerThin, SnowflakeUtils, TabBar, Timestamp, useEffect, UserStore, UserUtils, useState, useStateFromStores } from "@webpack/common";
|
||||
import { Guild, User } from "discord-types/general";
|
||||
|
||||
const IconUtils = findByPropsLazy("getGuildBannerURL");
|
||||
const IconClasses = findByPropsLazy("icon", "acronym", "childWrapper");
|
||||
const UserRow = LazyComponent(() => findByCode(".listDiscriminator"));
|
||||
|
||||
const cl = classNameFactory("vc-gp-");
|
||||
|
||||
export function openGuildProfileModal(guild: Guild) {
|
||||
openModal(props =>
|
||||
<ModalRoot {...props} size={ModalSize.MEDIUM}>
|
||||
<GuildProfileModal guild={guild} />
|
||||
</ModalRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const enum Tabs {
|
||||
ServerInfo,
|
||||
Friends,
|
||||
BlockedUsers
|
||||
}
|
||||
|
||||
interface GuildProps {
|
||||
guild: Guild;
|
||||
}
|
||||
|
||||
interface RelationshipProps extends GuildProps {
|
||||
setCount(count: number): void;
|
||||
}
|
||||
|
||||
const fetched = {
|
||||
friends: false,
|
||||
blocked: false
|
||||
};
|
||||
|
||||
function renderTimestamp(timestamp: number) {
|
||||
return (
|
||||
<Timestamp timestamp={moment(timestamp)} />
|
||||
);
|
||||
}
|
||||
|
||||
function GuildProfileModal({ guild }: GuildProps) {
|
||||
const [friendCount, setFriendCount] = useState<number>();
|
||||
const [blockedCount, setBlockedCount] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
fetched.friends = false;
|
||||
fetched.blocked = false;
|
||||
}, []);
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(Tabs.ServerInfo);
|
||||
|
||||
const bannerUrl = guild.banner && IconUtils.getGuildBannerURL({
|
||||
id: guild.id,
|
||||
banner: guild.banner
|
||||
}, true).replace(/\?size=\d+$/, "?size=1024");
|
||||
|
||||
const iconUrl = guild.icon && IconUtils.getGuildIconURL({
|
||||
id: guild.id,
|
||||
icon: guild.icon,
|
||||
canAnimate: true,
|
||||
size: 512
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cl("root")}>
|
||||
{bannerUrl && currentTab === Tabs.ServerInfo && (
|
||||
<img
|
||||
className={cl("banner")}
|
||||
src={bannerUrl}
|
||||
alt=""
|
||||
onClick={() => openImageModal(bannerUrl)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={cl("header")}>
|
||||
{guild.icon
|
||||
? <img
|
||||
src={iconUrl}
|
||||
alt=""
|
||||
onClick={() => openImageModal(iconUrl)}
|
||||
/>
|
||||
: <div aria-hidden className={classes(IconClasses.childWrapper, IconClasses.acronym)}>{guild.acronym}</div>
|
||||
}
|
||||
|
||||
<div className={cl("name-and-description")}>
|
||||
<Forms.FormTitle tag="h5" className={cl("name")}>{guild.name}</Forms.FormTitle>
|
||||
{guild.description && <Forms.FormText>{guild.description}</Forms.FormText>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabBar
|
||||
type="top"
|
||||
look="brand"
|
||||
className={cl("tab-bar")}
|
||||
selectedItem={currentTab}
|
||||
onItemSelect={setCurrentTab}
|
||||
>
|
||||
<TabBar.Item
|
||||
className={cl("tab", { selected: currentTab === Tabs.ServerInfo })}
|
||||
id={Tabs.ServerInfo}
|
||||
>
|
||||
Server Info
|
||||
</TabBar.Item>
|
||||
<TabBar.Item
|
||||
className={cl("tab", { selected: currentTab === Tabs.Friends })}
|
||||
id={Tabs.Friends}
|
||||
>
|
||||
Friends{friendCount !== undefined ? ` (${friendCount})` : ""}
|
||||
</TabBar.Item>
|
||||
<TabBar.Item
|
||||
className={cl("tab", { selected: currentTab === Tabs.BlockedUsers })}
|
||||
id={Tabs.BlockedUsers}
|
||||
>
|
||||
Blocked Users{blockedCount !== undefined ? ` (${blockedCount})` : ""}
|
||||
</TabBar.Item>
|
||||
</TabBar>
|
||||
|
||||
<div className={cl("tab-content")}>
|
||||
{currentTab === Tabs.ServerInfo && <ServerInfoTab guild={guild} />}
|
||||
{currentTab === Tabs.Friends && <FriendsTab guild={guild} setCount={setFriendCount} />}
|
||||
{currentTab === Tabs.BlockedUsers && <BlockedUsersTab guild={guild} setCount={setBlockedCount} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Owner(guildId: string, owner: User) {
|
||||
const guildAvatar = GuildMemberStore.getMember(guildId, owner.id)?.avatar;
|
||||
const ownerAvatarUrl =
|
||||
guildAvatar
|
||||
? IconUtils.getGuildMemberAvatarURLSimple({
|
||||
userId: owner!.id,
|
||||
avatar: guildAvatar,
|
||||
guildId,
|
||||
canAnimate: true
|
||||
}, true)
|
||||
: IconUtils.getUserAvatarURL(owner, true);
|
||||
|
||||
return (
|
||||
<div className={cl("owner")}>
|
||||
<img src={ownerAvatarUrl} alt="" onClick={() => openImageModal(ownerAvatarUrl)} />
|
||||
{Parser.parse(`<@${owner.id}>`)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerInfoTab({ guild }: GuildProps) {
|
||||
const [owner] = useAwaiter(() => UserUtils.fetchUser(guild.ownerId), {
|
||||
deps: [guild.ownerId],
|
||||
fallbackValue: null
|
||||
});
|
||||
|
||||
const Fields = {
|
||||
"Server Owner": owner ? Owner(guild.id, owner) : "Loading...",
|
||||
"Created At": renderTimestamp(SnowflakeUtils.extractTimestamp(guild.id)),
|
||||
"Joined At": renderTimestamp(guild.joinedAt.getTime()),
|
||||
"Vanity Link": guild.vanityURLCode ? (<a>{`discord.gg/${guild.vanityURLCode}`}</a>) : "-", // Making the anchor href valid would cause Discord to reload
|
||||
"Preferred Locale": guild.preferredLocale || "-",
|
||||
"Verification Level": ["None", "Low", "Medium", "High", "Highest"][guild.verificationLevel] || "?",
|
||||
"Nitro Boosts": `${guild.premiumSubscriberCount ?? 0} (Level ${guild.premiumTier ?? 0})`,
|
||||
"Channels": GuildChannelStore.getChannels(guild.id)?.count - 1 || "?", // - null category
|
||||
"Roles": Object.keys(guild.roles).length - 1, // - @everyone
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cl("info")}>
|
||||
{Object.entries(Fields).map(([name, node]) =>
|
||||
<div className={cl("server-info-pair")} key={name}>
|
||||
<Forms.FormTitle tag="h5">{name}</Forms.FormTitle>
|
||||
{typeof node === "string" ? <span>{node}</span> : node}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FriendsTab({ guild, setCount }: RelationshipProps) {
|
||||
return UserList("friends", guild, RelationshipStore.getFriendIDs(), setCount);
|
||||
}
|
||||
|
||||
function BlockedUsersTab({ guild, setCount }: RelationshipProps) {
|
||||
const blockedIds = Object.keys(RelationshipStore.getRelationships()).filter(id => RelationshipStore.isBlocked(id));
|
||||
return UserList("blocked", guild, blockedIds, setCount);
|
||||
}
|
||||
|
||||
function UserList(type: "friends" | "blocked", guild: Guild, ids: string[], setCount: (count: number) => void) {
|
||||
const missing = [] as string[];
|
||||
const members = [] as string[];
|
||||
|
||||
for (const id of ids) {
|
||||
if (GuildMemberStore.isMember(guild.id, id))
|
||||
members.push(id);
|
||||
else
|
||||
missing.push(id);
|
||||
}
|
||||
|
||||
// Used for side effects (rerender on member request success)
|
||||
useStateFromStores(
|
||||
[GuildMemberStore],
|
||||
() => GuildMemberStore.getMemberIds(guild.id),
|
||||
null,
|
||||
(old, curr) => old.length === curr.length
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetched[type] && missing.length) {
|
||||
fetched[type] = true;
|
||||
FluxDispatcher.dispatch({
|
||||
type: "GUILD_MEMBERS_REQUEST",
|
||||
guildIds: [guild.id],
|
||||
userIds: missing
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => setCount(members.length), [members.length]);
|
||||
|
||||
return (
|
||||
<ScrollerThin fade className={cl("scroller")}>
|
||||
{members.map(id =>
|
||||
<UserRow
|
||||
user={UserStore.getUser(id)}
|
||||
status={PresenceStore.getStatus(id) || "offline"}
|
||||
onSelect={() => openUserProfile(id)}
|
||||
onContextMenu={() => { }}
|
||||
/>
|
||||
)}
|
||||
</ScrollerThin>
|
||||
);
|
||||
}
|
7
src/plugins/serverProfile/README.md
Normal file
7
src/plugins/serverProfile/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# ServerProfile
|
||||
|
||||
Allows you to view info about servers and see friends and blocked users
|
||||
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)
|
40
src/plugins/serverProfile/index.tsx
Normal file
40
src/plugins/serverProfile/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { Menu } from "@webpack/common";
|
||||
import { Guild } from "discord-types/general";
|
||||
|
||||
import { openGuildProfileModal } from "./GuildProfileModal";
|
||||
|
||||
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => () => {
|
||||
const group = findGroupChildrenByChildId("privacy", children);
|
||||
|
||||
group?.push(
|
||||
<Menu.MenuItem
|
||||
id="vc-server-profile"
|
||||
label="Server Profile"
|
||||
action={() => openGuildProfileModal(guild)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default definePlugin({
|
||||
name: "ServerProfile",
|
||||
description: "Allows you to view info about a server by right clicking it in the server list",
|
||||
authors: [Devs.Ven, Devs.Nuckyz],
|
||||
tags: ["guild", "info"],
|
||||
|
||||
start() {
|
||||
addContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
|
||||
},
|
||||
|
||||
stop() {
|
||||
removeContextMenuPatch(["guild-context", "guild-header-popout"], Patch);
|
||||
}
|
||||
});
|
97
src/plugins/serverProfile/styles.css
Normal file
97
src/plugins/serverProfile/styles.css
Normal file
@ -0,0 +1,97 @@
|
||||
.vc-gp-root {
|
||||
height: 100%;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.vc-gp-banner {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-gp-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.vc-gp-header img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-gp-name-and-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.vc-gp-name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vc-gp-tab-bar {
|
||||
border-bottom: 2px solid var(--background-modifier-accent);
|
||||
margin: 20px 12px 0;
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: stretch;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.vc-gp-tab {
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
height: 39px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.vc-gp-tab-content {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.vc-gp-tab:where(.vc-gp-selected, :hover, :focus) {
|
||||
border-bottom-color: var(--interactive-active);
|
||||
}
|
||||
|
||||
.vc-gp-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.vc-gp-server-info-pair {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.vc-gp-server-info-pair [class^="timestamp"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.vc-gp-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.vc-gp-owner img {
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vc-gp-scroller {
|
||||
width: 100%;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.vc-gp-scroller [class^="listRow"] {
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.vc-gp-scroller [class^="listRow"]:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
@ -21,13 +21,14 @@ import "./styles.css";
|
||||
import { definePluginSettings } from "@api/Settings";
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin, { OptionType } from "@utils/types";
|
||||
import { Message } from "discord-types/general";
|
||||
import { Message, User } from "discord-types/general";
|
||||
|
||||
interface UsernameProps {
|
||||
author: { nick: string; };
|
||||
message: Message;
|
||||
withMentionPrefix?: boolean;
|
||||
isRepliedMessage: boolean;
|
||||
userOverride?: User;
|
||||
}
|
||||
|
||||
const settings = definePluginSettings({
|
||||
@ -67,11 +68,12 @@ export default definePlugin({
|
||||
],
|
||||
settings,
|
||||
|
||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix }: UsernameProps) => {
|
||||
renderUsername: ({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => {
|
||||
try {
|
||||
let { username } = message.author;
|
||||
const user = userOverride ?? message.author;
|
||||
let { username } = user;
|
||||
if (settings.store.displayNames)
|
||||
username = (message.author as any).globalName || username;
|
||||
username = (user as any).globalName || username;
|
||||
|
||||
const { nick } = author;
|
||||
const prefix = withMentionPrefix ? "@" : "";
|
||||
|
20
src/plugins/themeAttributes/README.md
Normal file
20
src/plugins/themeAttributes/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# ThemeAttributes
|
||||
|
||||
This plugin adds data attributes to various elements inside Discord
|
||||
|
||||
This allows themes to more easily theme those elements or even do things that otherwise wouldn't be possible
|
||||
|
||||
## Available Attributes
|
||||
|
||||
### All Tab Bars (User Settings, Server Settings, etc)
|
||||
|
||||
`data-tab-id` contains the id of that tab
|
||||
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/1263b782-f673-4f09-820c-4cc366d062ad)
|
||||
|
||||
### Chat Messages
|
||||
|
||||
- `data-author-id` contains the id of the author
|
||||
- `data-is-self` is a boolean indicating whether this is the current user's message
|
||||
|
||||
![image](https://github.com/Vendicated/Vencord/assets/45497981/34bd5053-3381-402f-82b2-9c812cc7e122)
|
45
src/plugins/themeAttributes/index.ts
Normal file
45
src/plugins/themeAttributes/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2023 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { Devs } from "@utils/constants";
|
||||
import definePlugin from "@utils/types";
|
||||
import { UserStore } from "@webpack/common";
|
||||
import { Message } from "discord-types/general";
|
||||
|
||||
export default definePlugin({
|
||||
name: "ThemeAttributes",
|
||||
description: "Adds data attributes to various elements for theming purposes",
|
||||
authors: [Devs.Ven],
|
||||
|
||||
patches: [
|
||||
// Add data-tab-id to all tab bar items
|
||||
// This for examples applies to the User and Server settings sidebars
|
||||
{
|
||||
find: ".tabBarRef",
|
||||
replacement: {
|
||||
match: /style:this\.getStyle\(\),role:"tab"/,
|
||||
replace: "$&,'data-tab-id':this.props.id"
|
||||
}
|
||||
},
|
||||
|
||||
// Add data-author-id and data-is-self to all messages
|
||||
{
|
||||
find: ".messageListItem",
|
||||
replacement: {
|
||||
match: /\.messageListItem(?=,"aria)/,
|
||||
replace: "$&,...$self.getMessageProps(arguments[0])"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
getMessageProps(props: { message: Message; }) {
|
||||
const authorId = props.message?.author?.id;
|
||||
return {
|
||||
"data-author-id": authorId,
|
||||
"data-is-self": authorId && authorId === UserStore.getCurrentUser()?.id
|
||||
};
|
||||
}
|
||||
});
|
@ -343,6 +343,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
name: "Rini",
|
||||
id: 1079479184478441643n
|
||||
},
|
||||
castdrian: {
|
||||
name: "castdrian",
|
||||
id: 224617799434108928n
|
||||
},
|
||||
Arrow: {
|
||||
name: "arrow",
|
||||
id: 958158495302176778n
|
||||
@ -351,6 +355,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
|
||||
name: "bb010g",
|
||||
id: 72791153467990016n,
|
||||
},
|
||||
RuukuLada: {
|
||||
name: "RuukuLada",
|
||||
id: 119705748346241027n,
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
@ -63,7 +63,7 @@ export async function update() {
|
||||
export const getRepo = () => Unwrap(VencordNative.updater.getRepo());
|
||||
|
||||
export async function maybePromptToUpdate(confirmMessage: string, checkForDev = false) {
|
||||
if (IS_WEB) return;
|
||||
if (IS_WEB || IS_UPDATER_DISABLED) return;
|
||||
if (checkForDev && IS_DEV) return;
|
||||
|
||||
try {
|
||||
|
Reference in New Issue
Block a user